Skip to content

feat(#380): runtime profiling-capture toggle + setup/plugin-drawGui phase timing - #784

Merged
apotema merged 2 commits into
mainfrom
feat/380-debug-profiling
Jul 19, 2026
Merged

feat(#380): runtime profiling-capture toggle + setup/plugin-drawGui phase timing#784
apotema merged 2 commits into
mainfrom
feat/380-debug-profiling

Conversation

@apotema

@apotema apotema commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

What

Ships the last engine-side pieces of the debug-inspector profiling issue. Most of #380 already landed earlier (per-script/per-plugin LABELLE_PROFILE profiler in v1.6-era, FPS FrameProfiler + overlay data API in #733); this PR closes the remaining gaps the live inspector panel needs:

  • profiler.setRecording(?bool) — runtime override of the LABELLE_PROFILE env gate. true/false force per-unit capture on/off; null defers back to the env var. This is what lets the debug plugin arm live capture while its Performance section is open — and hand the gate back on close without killing a user's env-enabled headless dump.
  • Game.setProfilingCapture / Game.profilingCaptureActive / Game.frameHistory (MiscMixin + re-exports) — the accessor surface the debug plugin drives/reads. frameHistory exposes the frame-time ring (ms, oldest-first) for the mini-graph.
  • New timed phases: ScriptRow/PluginRow gain a one-shot setup Stat (timed unconditionally at boot — two clock reads per unit, once; never window-reset) and PluginRow gains draw_gui (rec-gated in SystemRegistry.drawGui, mirroring script drawGui timing). The overlay can now show the full setup/tick/postTick/drawGui breakdown requested in the issue discussion.

Zero-cost-when-off

Unchanged: dispatch loops still branch on one cached bool per frame. Measured (ReleaseFast, macOS, same clock_gettime primitive nowNs uses):

  • capture ON: 53.1 ns per dispatched unit (2× clock read + Stat.record)
  • capture OFF: 2.1 ns per dispatched unit (cached-bool branch)
  • at flying-platform scale (~45 dispatched units/frame) capture costs ~2.3 µs/frame, ~0.014% of a 60 FPS budget

Tests

test/inspector_overlay_test.zig: override round-trip (restores env baseline), Game capture accessors, frameHistory passthrough, new phase-field defaults. zig build test green (exit-checked).

Companion PR

The inspector UI that consumes this lands in the labelle-assembler debug plugin (linked in comments): engine-fed FPS header + mini-graph, sorted per-unit ms tables with severity markers, panel-driven capture arming, LABELLE_DEBUG_OPEN=1 boot-open. Verified live on flying-platform-labelle via local: overrides. The plugin is @hasDecl/@hasField-gated, so it degrades gracefully against engines without this PR; the full panel needs an engine release cut from this (next minor, 2.5.0).

Follow-ups (not this PR):

  • Colored severity (green/yellow/red) needs a labelColored on core's GuiInterface + imgui adapter; text markers (*/!) until then.
  • Per-language-plugin script breakdown (labelle-scripting Controller tick) if per-script granularity inside a language plugin's single Systems.tick row is wanted.

Closes #380

https://claude.ai/code/session_011szWvquoss1yNX7KWSKCaM


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

…hase timing

Completes the engine side of the debug-inspector profiling issue:

- profiler.setRecording(?bool): runtime override of the LABELLE_PROFILE
  env gate. true/false force capture; null defers back to the env var,
  so the debug inspector can arm live capture while its Performance
  section is open without killing a user's env-enabled headless dump.
- Game.setProfilingCapture / profilingCaptureActive / frameHistory
  accessors (MiscMixin) — the surface the debug plugin reads/drives.
- New timed phases: ScriptRow/PluginRow gain a one-shot 'setup' Stat
  (recorded unconditionally at boot — two clock reads per unit, once)
  and PluginRow gains 'draw_gui' (rec-gated, mirrors script drawGui),
  so the overlay can show the full setup/tick/postTick/drawGui
  breakdown the issue asked for. dumpProfile resets the new draw_gui
  window; setup is never reset (boot cost).

Zero-cost-when-off is unchanged: dispatch loops still branch on one
cached bool per frame; the only unconditional additions are the
one-shot setup timings.

Tests: inspector_overlay_test — override round-trip (env-restoring),
Game capture accessors, frameHistory passthrough, new phase fields.

Closes #380

Claude-Session: https://claude.ai/code/session_011szWvquoss1yNX7KWSKCaM
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 33 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2151cf60-2a12-45a4-bae2-246ba4dfed12

📥 Commits

Reviewing files that changed from the base of the PR and between 0a87bb2 and da9225c.

📒 Files selected for processing (6)
  • scene/src/profiler.zig
  • scene/src/system.zig
  • src/game.zig
  • src/game/misc_mixin.zig
  • src/script_runner.zig
  • test/inspector_overlay_test.zig
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/380-debug-profiling

Comment @coderabbitai help to get the list of available commands.

@apotema

apotema commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

Companion UI PR: labelle-toolkit/labelle-assembler#640 (debug plugin panel consuming this API).

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a runtime override mechanism for the LABELLE_PROFILE environment variable gate, allowing the debug inspector to toggle profiling capture dynamically. It also adds unconditional tracking for boot-time setup costs in both game scripts and plugin systems, as well as tracking for the drawGui phase in plugins. Additionally, new helper methods for retrieving frame history and managing profiling state are exposed, accompanied by comprehensive unit tests. I have no feedback to provide as there are no review comments.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 31d5ee253f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scene/src/profiler.zig
Comment on lines +177 to +180
setup: Stat = .{},
tick: Stat = .{},
post_tick: Stat = .{},
draw_gui: Stat = .{},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include the new plugin phases in the overlay collector

The newly recorded plugin setup and draw_gui stats are dropped for callers using the documented overlay API: src/root.zig points consumers at game.pluginProfileRows() plus profiler.collect*Rows, but collectPluginRows() still flattens only tick and post_tick into OverlayRow. In a debug panel built on that public helper, plugin drawGui/setup will always be invisible even though SystemRegistry now records them, so the advertised full phase breakdown only works for consumers that bypass the collector.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in da9225c. OverlayRow now carries one named field per lifecycle phase — setup_ms/tick_ms/post_tick_ms/draw_gui_ms (each + severity) — so collectScriptRows/collectPluginRows surface the new plugin setup and draw_gui stats to any collector consumer. Scripts fill setup/tick/draw_gui (post_tick stays 0); plugins fill all four. The old aux_* secondary view is retained for back-compat, and totalMs now sums the recurring phases (tick+post_tick+draw_gui), still excluding one-shot setup. Tests assert the new named fields on both collectors.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-verified — the collector DOES surface both new plugin phases; this re-anchors onto the PluginRow.draw_gui field decl (line 180), but the collector path is complete:

  • OverlayRow (scene/src/profiler.zig ~202) has named fields setup_ms/setup_severity, post_tick_ms/post_tick_severity, draw_gui_ms/draw_gui_severity (plus tick_*).
  • collectPluginRows populates ALL four: .setup_ms = nsToMs(e.setup.last_ns), .tick_ms, .post_tick_ms = nsToMs(e.post_tick.last_ns), .draw_gui_ms = nsToMs(e.draw_gui.last_ns) (~lines 258–267).
  • collectScriptRows populates setup/tick/draw_gui (scripts have no postTick).
  • Test collectPluginRows: flattens setup + tick + postTick + drawGui asserts the debug row's draw_gui_ms == 0.08 and setup_ms == 0.5 flow through.

No source drop remains — the plugin setup/draw_gui stats reach any collector consumer. (da9225c)

@apotema

apotema commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

Verified on a real render

Ran a sokol game (three busy-work scripts + the debug plugin) headless-surfaceless with the overlay open, captured via the engine screenshot facility. Panel shows the engine-fed FPS: 60 | Frame: 16.7ms header + mini-graph, a sorted Scripts table (physics 0.75 / worker 0.16 / needs 0.03 ms, total 0.94ms), and a Plugins table (debug gui 0.26ms). Crucially the per-unit profiler log dump fired with no LABELLE_PROFILE env var — driven entirely by the panel calling setProfilingCapture(true) on open, exercising this PR's runtime gate end to end.

Where the timing hooks landed

  • Frame time / FPS: src/game/loop_mixin.zig tick()frame_profiler.record(dt) (real unscaled dt), ungated. FrameProfiler in src/frame_profiler.zig (shipped Debug inspector: FPS counter + live profiler-overlay data API (#380) #733).
  • Per-script: src/script_runner.zigtick/drawGui already rec-gated (shipped); this PR adds unconditional setup timing.
  • Per-plugin: scene/src/system.zig SystemRegistrytick/postTick already rec-gated; this PR adds unconditional setup + rec-gated drawGui timing.
  • Runtime gate: scene/src/profiler.zig setRecording(?bool); surfaced as Game.setProfilingCapture / profilingCaptureActive / frameHistory in src/game/misc_mixin.zig.

Overhead (ReleaseFast, macOS, profiler's own clock_gettime)

capture ON 53.1 ns/unit · OFF 2.1 ns/unit · delta ~51 ns/unit/frame while capturing (~2.3 µs/frame at ~45 units — ~0.01% of a 60 FPS budget).

Follow-up (assembler codegen, filed on the companion PR): per-plugin rows currently show the tuple index, not the plugin name, because the assembler emits SystemRegistry as an anonymous tuple. Per-script names are correct.

Round-1 review (codex P2, scene/src/profiler.zig): the newly-recorded
plugin `setup` and `draw_gui` Stats were dropped by the documented
OverlayRow collector — it only exposed `tick` + one `aux` secondary
phase, so any consumer reading collectScriptRows/collectPluginRows
missed them.

OverlayRow now carries one named field per lifecycle phase
(setup/tick/post_tick/draw_gui, each ms + severity); scripts populate
setup/tick/draw_gui (post_tick stays 0), plugins populate all four. The
old `aux_*` secondary view is kept for back-compat (drawGui for
scripts, postTick for plugins). totalMs now sums the recurring phases
(tick+post_tick+draw_gui) and still excludes one-shot setup.

Tests updated to assert the new named fields and the setup-excluded
total.

Claude-Session: https://claude.ai/code/session_011szWvquoss1yNX7KWSKCaM
@apotema
apotema merged commit c4b9de7 into main Jul 19, 2026
3 checks passed
@apotema
apotema deleted the feat/380-debug-profiling branch July 19, 2026 18:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Debug inspector: FPS counter and per-script time profiling

1 participant