Repository navigation
app: unsaved work survives a crash - #361
Merged
Merged
Conversation
4 of 5 tasks
foxnne
added a commit
that referenced
this pull request
Oct 10, 2026
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: - **its events,** less the pointer `position` event dvui adds to every frame; - **else the `dvui.refresh` calls before it,** by file and line; - **else something else:** a timer or an animation coming due. `core.profile.report` gains a `.causes` section with the counts and the places that asked for frames, most first. Idle under the agent's profiler, for example: ```zig .causes = .{ .frames = 252, .by_events = 0, .by_refresh = 251, .by_other = 1, .refreshed_from = .{ .{ .place = "Host.refresh (a plugin):0", .count = 252 }, }, }, ``` That first reading found a bug of ours: the agent's own `fizzy_profile` keeps fizzy awake while it measures. The fix is in the agent plugin, next. **Where the places come from:** - **fizzy and the built-in plugins:** dvui already records each refresh's file and line as a debug log line while `dvui.debug.logRefresh` is on, which is what `FIZZY_LOG_REFRESH` turns on. fizzy turns it on while the profiler records. The app's log function hands those lines to `core.profile.interceptLog`, matched by format at compile time, so no other log call costs anything, and they're counted instead of printed. - For release builds, dvui's debug level is compiled in (`log_scope_levels`), and its other debug lines are dropped in `logFn` as before. A ReleaseFast run logged none of them. - **A plugin dylib:** its refresh lines reach the host as text through `Host.logLine` and 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 as `Host.refresh (a plugin)`. Which plugin asked, it doesn't say. **No layout change:** `FrameCauses` is its own allocation, made the first frame the profiler records and published under its own key, as `Lookback` is (#353). The profiler's `abi` stays 4. It starts over each time the profiler starts recording, so it describes what happened while someone was looking. ## SDK impact - [x] Core-only or additive: reaches plugins at the next SDK release (`core.profile`: `FrameCauses`, `frameCauses`, `interceptLog`, `ReportOptions.causes`). No fingerprint move (`test-sdk-version` passes). ## Verified - [x] macOS, Debug and ReleaseFast, a sandbox profiled through the agent plugin: - **Idle:** all frames put down to `Host.refresh`, as above. The same in ReleaseFast, with no dvui debug lines in its log. - **With `FIZZY_LOG_REFRESH=1`:** the intercept fires for every refresh record dvui prints (234 in a run), and the lines still print. - [x] `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. - [x] `zig build`, `zig build -Doptimize=ReleaseFast`, `zig build test` (533/534, 1 skipped), `test-integration` (375/375), `check-web`, `test-sdk-version`. - [ ] Windows, Linux: CI. - **Not covered:** a test that holds dvui to the format strings it logs. The integration tests take their log function from Zig's test runner, so the intercept can't run there. If dvui changes the format, refresh places stop being recorded, and frames show up as "other". ## Follow-ups - **Which plugin called `Host.refresh`:** a source location through it, which moves the SDK boundary, so it waits for a release that moves it anyway. - **A dylib plugin's own `dvui.refresh`** calls are counted only in its Debug builds, and only once its dvui copy has refresh records on. Syncing that flag through `dvui_context` is the seam. - **Agent:** `fizzy_profile` should measure without driving frames, and a step to compare before and after. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
While a document has unsaved changes, fizzy keeps every open document as a restart keeps them (KeptDocuments) in <config>/checkpoint/: a second or so after the person types, and at least every half minute regardless, so an agent's edits are kept too. A clean quit deletes it. At the next launch a checkpoint still there means the last run ended without quitting (a crash, a kill, a power cut): its documents open from it, unsaved changes, caret and all, and a toast says so. State is captured on the UI thread and written on a thread of its own, to a folder that replaces the last one only once whole. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foxnne
force-pushed
the
app/crash-checkpoint
branch
from
October 10, 2026 22:40
14e3c12 to
dd26181
Compare
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/RESTART_AND_CRASHES_PLAN.md, step 4.What changes
A crash no longer loses unsaved work. While anything is unsaved, fizzy keeps a checkpoint of every open document in
<config>/checkpoint/. It uses the same format a restart keeps them in (#346: contents, caret, scroll, dirty). A clean quit deletes it. So a checkpoint found at launch means the last run ended without quitting: a crash, akill -9, a power cut. Its documents open from it as a restart's do, and a toast says "fizzy did not quit cleanly last time. Its unsaved changes are back."src/editor/Checkpoint.zig:KeptDocuments.saveGeneration): each checkpoint's files are named<generation>-<index>.state. They're written beside the last one's, thensession.zonis written to.partand renamed over the last, which is the moment the new checkpoint counts. Then the last generation's files are removed. A crash at any point leaves one whole generation.checkpoint/,session/,handover/, matched as top-level folders of the config folder). A checkpoint write no longer wakes the app or re-readssettings.zon. That was a cost on every platform, found by the crash-reports session.Checkpoint.recover, inEditor.init): a restart's session is newer and wins, and the checkpoint is dropped. Otherwise the checkpoint becomeseditor.session, read once.KeptDocuments.recoveredFromCheckpoint()says this run recovered unsaved work, so a crash and its recovery can show as one message.SDK impact
Verified
kill -9, relaunch: the typed line was back, unsaved, the caret where it was, and the toast showed (screenshot of the relaunched window).test-integration(371/371) has a new one: two generations leave only the second's files; a checkpoint is read once; a folder with nosession.zonis neither read nor kept; a restart's session wins.SettingsWatcher'sclassifytest covers the state folders, and that a plugin namedcheckpointis still a plugin.1-*then only2-*files;kill -9then relaunch recovered both edits, unsaved; saving left the folder empty.zig build,zig build test(531/532, 1 skipped),check-web,test-sdk-version.renameof the folder is the one OS call that differs.check-web; not built there, since a page has no crash to recover from that way.Follow-ups
recoveredFromCheckpoint().🤖 Generated with Claude Code