Repository navigation
core: the profiler says why each frame happened - #364
Merged
Merged
Conversation
fizzy draws only when something asks, so the frame-loop bugs are a frame that never comes and frames that never stop. While the profiler records, each frame is now put down to its events, else to the dvui.refresh calls before it by file and line, else to something else (a timer, an animation), and the report gains a .causes section: the counts, and the places that asked for frames, most first. Kept beside the profiler under its own key, so its abi stays 4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 of 3 tasks
Part of `plans/DASHBOARD_PLAN.md`, "What performance work lacked": startup and handover phases. Stacked on #364 (it shares the report code); retarget to `main` once that merges. ## What changes **fizzy times its own startup.** `core.profile.phase(io, name)` stamps a moment of the app's start. fizzy stamps: - **at launch:** `main`, `lock` (the single-instance lock), `window` (the backend up: GPU, fonts), `editor`, `plugins`, `shown`, and `first frame`; - **in an instance a restart's handover started (#356):** `lock deferred (handover)`, `handover ready` and `handover lock`. Each run's first frame logs one line: ``` info: startup: up 1288 ms after main (window shown at 1198 ms) ``` **The phases in the report:** `core.profile.report` includes them with `.startup = true`, and they're published like `Lookback` and `FrameCauses`: their own data key, no change to the profiler's layout or `abi`. The new instance of a handover, read that way: ```zig .startup = .{ .{ .phase = "main", .ms = 0.0 }, .{ .phase = "lock deferred (handover)", .ms = 0.4 }, .{ .phase = "window", .ms = 151.6 }, .{ .phase = "editor", .ms = 175.6 }, .{ .phase = "plugins", .ms = 208.4 }, .{ .phase = "handover ready", .ms = 211.7 }, .{ .phase = "shown", .ms = 219.5 }, .{ .phase = "handover lock", .ms = 232.1 }, .{ .phase = "first frame", .ms = 301.9 }, }, ``` It already shows something to act on: **the window is shown about 80 ms before its first frame is drawn.** That's true of every launch, and in a handover the new window covers the old one for those 80 ms before it has drawn anything. A follow-up can show it after the first frame. Measuring a restart used to take a window sampler and a pid poller running outside fizzy, on macOS only. ## SDK impact - [x] Core-only or additive: `core.profile.phase`, `phases`, `Phases`, `ReportOptions.startup`. No fingerprint move. ## Verified - [x] **macOS:** - **A cold launch:** the startup line above. - **A handover:** the table above, read from the new instance through the agent plugin. That used a local build of it against this tree; the agent's own tool for this waits for the SDK release, since it's pinned to 0.2.22. - **The first version of this crashed at `main`:** the clock went through `dvui.io`, which the backend hadn't set yet. So `phase` takes the `io`. - **Gates:** `zig build`, `zig build test` (533/534, 1 skipped), `test-integration` (375/375), `check-web`, `test-sdk-version`. - [ ] Windows, Linux: CI. ## Follow-ups - **Show a window after its first frame is drawn,** not before, starting with a handover's. - **The real process start** (macOS `proc_pidinfo`, Windows `GetProcessTimes`, Linux `/proc/self/stat`), so `main` isn't time zero and dyld and static initialisation are counted. - **The agent's `fizzy_build`** on fizzy's repo reports the new instance's phases, once the SDK is released and the agent repinned. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
foxnne
enabled auto-merge (squash)
October 10, 2026 22:20
foxnne
added a commit
that referenced
this pull request
Oct 11, 2026
## What changes `sdk_version` goes from 0.2.22 to 0.2.23. Merging tags `sdk-v0.2.23`, publishes its tarball, and asks the store plugins to repin. **Why now.** The plugin boundary's fingerprint moved since `sdk-v0.2.22` (`0x1bf417f6…` → `0x65096e94…`, by #360), so store plugins built against 0.2.22 don't load in a fizzy built from `main`, and fizzyed.it/app is built from `main`. Also, #355 moved the `plugins` service to version 2, and the agent plugin's `fizzy_build` asks for version 1 until it repins. ## SDK impact - [ ] None - [ ] Core-only or additive: reaches plugins at the next SDK release - [ ] Fingerprint moved: recorded in `sdk/src/version.zig`, `sdk_version` untouched, PR labelled `sdk` - [x] SDK release: bumps `sdk_version`, lists the `sdk` PRs since the last `sdk-v*` tag **The `sdk` PRs since `sdk-v0.2.22`:** - #360: a plugin's panic message reaches its crash report. This moves the fingerprint. **Additive, reaching plugins with this release:** - #355: the `plugins` service is version 2, with `list` (each plugin's identity, link, version, registration counts and status). A caller built for version 1 is refused until it rebuilds, which this release's repin does. - #351: a crash report says whose code crashed (`sdk/plugin_sdk.zig`). **Also reaching plugins (`core/`):** - `core.viz`, graphics for live numbers: `line`, `bar`, `stat`, `Table` (#345); `Pie`, `keyColor`, `palette` (#347); the table's rounded header and shaded rows (#359). - `core.profile`: - `lookback()`: 30 s of costs and the frames over budget, plus `ReportOptions.over_ms` / `.hitches` and `selfTimes` (#353). - `frameCauses()` and `ReportOptions.causes`: why each frame happened (#364). - `phase` / `Phases` / `ReportOptions.startup`: startup timing (#365, which landed inside #364's squash commit). - The profiler's `abi` stays 4. ## Verified - [x] macOS: - `zig build` and `test-sdk-version` on this head. - `scripts/pack-sdk.sh` packs `fizzy-sdk-v0.2.23.tar.gz`. - Every store plugin (fresh pulls of pixi, zig, drive, atlas and ghostty `main`) builds against this tree's `sdk/` with `zig build --fork=<sdk>` into a throwaway profile (31/31, and 28/28 for each of the other four). The fingerprint moved, so each repin PR is a real rebuild, but none should open as a draft for a broken build. The agent and chat plugins build too (22/22, 19/19). - [ ] Windows: - [ ] Linux: CI. - [ ] Web: ## Follow-ups - Each repin PR merged and its plugin tagged (the plugin repos' tags stay the maintainer's). - `fizzyedit/agent` and `fizzyedit/chat` repin to the `sdk-v0.2.23` tarball. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of
plans/DASHBOARD_PLAN.md, "What performance work lacked": frame causes. Coordinated with the session doing the profiler; it takes spans.What changes
The profiler says why each frame happened. fizzy draws only when something asks for a frame, so the frame-loop bugs are a frame that never comes (work waiting for the next unrelated event) and frames that never stop (something asking every frame). A profile showed where a frame's time went, but not who asked for the frame. Two of those bugs came up this week, the crash checkpoint (#361) and the restart handover (#356), and each cost a round of guessing.
While the profiler records, each frame is put down to one of three causes:
positionevent dvui adds to every frame;dvui.refreshcalls before it, by file and line;core.profile.reportgains a.causessection with the counts and the places that asked for frames, most first. Idle under the agent's profiler, for example:That first reading found a bug of ours: the agent's own
fizzy_profilekeeps fizzy awake while it measures. The fix is in the agent plugin, next.Where the places come from:
dvui.debug.logRefreshis on, which is whatFIZZY_LOG_REFRESHturns on. fizzy turns it on while the profiler records. The app's log function hands those lines tocore.profile.interceptLog, matched by format at compile time, so no other log call costs anything, and they're counted instead of printed.log_scope_levels), and its other debug lines are dropped inlogFnas before. A ReleaseFast run logged none of them.Host.logLineand are read the same way (interceptPluginLine). Only its Debug builds produce them.Host.refresh, how every plugin asks for a frame: it wakes the backend directly, so it's recorded at that point asHost.refresh (a plugin). Which plugin asked, it doesn't say.No layout change:
FrameCausesis its own allocation, made the first frame the profiler records and published under its own key, asLookbackis (#353). The profiler'sabistays 4. It starts over each time the profiler starts recording, so it describes what happened while someone was looking.SDK impact
core.profile:FrameCauses,frameCauses,interceptLog,ReportOptions.causes). No fingerprint move (test-sdk-versionpasses).Verified
Host.refresh, as above. The same in ReleaseFast, with no dvui debug lines in its log.FIZZY_LOG_REFRESH=1: the intercept fires for every refresh record dvui prints (234 in a run), and the lines still print.fizzy-profile-causes-tests(new, std-only): the three causes; the busiest place first; a long path keeping its tail; places past the 32 kept counted together; reset.zig build,zig build -Doptimize=ReleaseFast,zig build test(533/534, 1 skipped),test-integration(375/375),check-web,test-sdk-version.Follow-ups
Host.refresh: a source location through it, which moves the SDK boundary, so it waits for a release that moves it anyway.dvui.refreshcalls are counted only in its Debug builds, and only once its dvui copy has refresh records on. Syncing that flag throughdvui_contextis the seam.fizzy_profileshould measure without driving frames, and a step to compare before and after.🤖 Generated with Claude Code