Repository navigation
layout: opening the view picker no longer stalls a frame - #570
Merged
Merged
Conversation
foxnne
force-pushed
the
layout/picker-stall
branch
4 times, most recently
from
October 18, 2026 20:09
1b147eb to
eddf3b4
Compare
Opening a place's view picker drew every view that shows nowhere for the first time in one frame, and about ten frames later captured all of them in another: 19-26 ms of main-thread work each (Debug, loaded machine). Now those views are drawn offscreen one at a time. As the picker draws its cards it chooses the next: a card in view whose view drew nowhere and has no picture (`State.offscreen`: its registry index, checked against its id's hash). `captureUnplaced`, before the picker draws as before, warms that one view (ten frames and a reveal's fade) and captures it, one draw a frame, so what it opens stacks under the picker and its picture shows on its card that frame. A view whose card leaves the list early is dropped. A failed capture is remembered for the picker's session (`State.recordCaptureFailed`); three in a row that cannot start, or more failures than are remembered, end capture for the session. The picker's work is in the profiler: "picker", "picker: capture unplaced" and "picker: capture". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foxnne
force-pushed
the
layout/picker-stall
branch
from
October 18, 2026 20:22
eddf3b4 to
63418c7
Compare
Capture is given up for the picker's session only after three offscreen captures in a row cannot start (their target is small and fixed); an on-screen view whose capture fails is only marked failed. Failed views go in a per-session set (no cap, no global disable). Whether the chosen view's card left the list is decided from frame stamps (`card_ns` against `Picker.list_ns`), so a picker draw that returns early never drops it. A view on screen without its picture asks for a frame again, unless it failed or has no room. One predicate, `mayDrawOffscreen`, decides for both the picker and `captureUnplaced`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No conflicts, and nothing to port: the branch builds and passes its gates with Zig 0.17.0 as merged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foxnne
marked this pull request as ready for review
October 18, 2026 20:36
foxnne
enabled auto-merge (squash)
October 18, 2026 20:50
foxnne
added a commit
that referenced
this pull request
Oct 19, 2026
## What changes `sdk_version` goes from 0.2.30 to 0.2.31. Merging this tags `sdk-v0.2.31` and publishes its tarball. `sdk-repin` then asks each store plugin to repin. **Why now:** the SDK's shape has moved several times since 0.2.30. Store plugins and apps built on fizzy, Graphl among them, only get the new calls once they're released. Most of them come from Graphl's report. ## 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 **`sdk` PRs since `sdk-v0.2.30`:** - **#560: a plugin's own data folder.** `host.pluginDataFolder(plugin)` gives `<config>/plugin-data/<id>`, made on first ask. It's the plugin's own from `register` through `deinit`, and stays on disk after uninstall. - **#587: the data folder goes with the plugin.** `Host.retirePlugin` unregisters a plugin, runs its `deinit`, then tells the app, so a plugin can still write out state in `deinit`. On `EditorAPI`, `pluginUnregistered` became `pluginRetired`; that vtable is implemented by the app, not by plugins. - **#593: put a view away.** `host.hideSurface(id)` shuts the tray whose shown view is `id`, or closes a float that holds only `id`. Anything else returns false. Mid-frame it acts after the frame has drawn. A toggle is `if (!host.hideSurface(id)) _ = host.revealSurface(id);`. - **#603: times on recent folders.** `host.recentFolderOpened(i)` returns when recent folder `i` was opened, in Unix seconds, or null if unknown. `recentFolderAt` is unchanged. - **#599: a home page.** `sdk.keywords.ide.home` is new. While the workbench's pane is empty, it shows a surface keyworded `home` in place of its own page. The fingerprint didn't move; a plugin on an older SDK can write `&.{"home"}`. - **#581: undo is announced.** Undoing someone else's step is announced as `fizzy.history.undone`, with a payload type `events.StepTaken`. Additive; the fingerprint didn't move. - **#609: history restore.** An optional `restoreHistory` hook and `sdk.Plugin.RestoredHistory`. Plugins without the hook are unaffected. **Core change plugins pick up at this release** (no boundary change): #550, a replayed backdrop captures inside a front-to-back block. (`app/` changes since 0.2.30, such as #570 and #602, are not compiled into plugins.) ## Verified - [x] macOS: `scripts/check-fmt.sh` and `zig build test-sdk-version` pass. The fingerprint is main's recorded value; only `sdk_version` changed. - [ ] Windows: - [ ] Linux: CI - [ ] Web: ## Follow-ups - Repin and release the store plugins (pixi, zig, ghostty, drive, atlas; then agent and chat) when `sdk-repin` opens their PRs. - Make 0.2.31 live on fizzyed.it/app once the repinned plugins are in the store: `gh workflow run web.yml --ref main -f go_live=0.2.31`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.
Now on Zig 0.17:
main(with #575, the 0.17 switch, and #555) is merged in, and the gates pass with 0.17.0.Part of fizzyedit/work#29
What changes
Opening a place's view picker (its corner button, or Layout settings) no longer pays for every view's picture in one or two frames.
Picker.nextOffscreen,State.offscreen). The choice is a card the list laid out in view, whose view may be drawn offscreen (Layout.mayDrawOffscreen, the one rule the capture step checks too): capture works here, the view has no picture or failed capture, isn't hidden, and drew nowhere this frame. A card doesn't count on its own first frame, when every card packs into the top of a list without sizes. A view scrolled or filtered out of the list before it is chosen is never drawn for it.main.captureUnplacedwarms the chosen view and then captures it, one offscreen draw a frame. The warm-up is at least 10 frames and at least a reveal's fade at the interface's speed, so a fast display doesn't capture mid-fade.card_nsagainstPicker.list_ns, when the list last laid its cards out. A picker draw that returns before its list never drops a view.Stateand reset when it reopens.recordCaptureFailed,captureFailed) and is never retried. Nothing else is affected.State.capture_unavailable) only after three offscreen captures in a row couldn't start (noteOffscreenStart). Their target is small and fixed, so that means the backend has no texture targets. Once given up, nothing more is captured, on screen or off, and no frame is asked for one.main, unless capture has failed for them or here at all. They render that frame anyway, and in the measurements below they never showed in a frame over 9 ms.devtools.stats) aspicker,picker: capture unplacedandpicker: capture. They cost one branch each while the profiler is off.Nothing changes while the picker is closed:
captureUnplacedstill returns onsnapshots_wanted, anddrawSolidadds two checks behind it.What was measured
fizzyedit/work#29 was filed as a pixel flash: the on-screen views redrawn offscreen and blitted back for a frame. On macOS I could not reproduce that.
drawCapturedon and with it skipped. The frame where the on-screen views were captured matched the frame before to within one level out of 255 (65k pixels at ±1, 6 at ±2; the rest identical).What was there is a stall. These are profiler numbers per frame over 9 ms, with a temporary probe opening the Sidebar's picker. The machine was shared and loaded (1-minute load 10–18), Debug build, two runs each, before and after alternated.
The "after" column is an earlier version of this branch, so it is not this branch's final number. Since then:
None of these were re-measured in the app: no visible sandbox runs.
Why a capture costs so much: it is the one draw of an offscreen view that renders anything. The warm-up draws at its offscreen position are clipped away and lay out only. So a capture costs a full render of the view: 8–12 ms each for the devtools dashboard's pie and table in Debug.
Tried and dropped: photographing the on-screen views from the last frame (
core.FrameTarget.snapshot, asViewDrag.photographFromFramedoes) on the frame the picker opens. On macOS it removed no visible flash, and it needed per-frame state.SDK impact
Verified
devtools.statsover the control socket from a sandbox fizzy (--profile), picker opened by a temporary probe, never committed. New integration test, which fails with the in-view check removed. It uses only views no place shows, so the first capture is offscreen.mainunder Zig 0.17.0:scripts/check-fmt.sh,zig build,zig build test,zig build test-integration,zig build check-web,zig build check-tests-reached.check-web); not run.Follow-ups
PickerFrame, so it takes views out of the list instead. Worth a look on its own.🤖 Generated with Claude Code