Repository navigation
gpui: keep a list row's nodes lingering when a view in it is laid out again - #55
Merged
Merged
Conversation
… again A view in a list row that moved is laid out again as the list prepaints the row, outside the row's own layout, and claiming its nodes there cleared the flag that keeps a row's nodes for 240 frames after the row leaves. Once the row scrolled out they were released, so scrolling back over it laid its views out, and shaped their text, afresh. The flag now stays set once a node is claimed laying out a list row. Adds gpui_perf's chat-drag, which drags a chat transcript's scrollbar from the end to the top and back, as a user does through a long session. With retained views it took 37 % longer per frame than drawing from scratch, shaping twice the lines; it now does the same work.
sunli829
marked this pull request as ready for review
October 12, 2026 02:36
trancong12102
added a commit
to aislopware/gpui-fast
that referenced
this pull request
Oct 12, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sunli829
added a commit
that referenced
this pull request
Oct 12, 2026
#56) Users of Allsum reported high CPU when dragging the scrollbar of a long conversation. Profiling Allsum (release build, a long "Greeting" session with charts, tables and markdown) showed the transcript list's scroll layer bought almost nothing: rows were rendered again on every frame, and the layer was demoted or kept off on most frames. Three fixes here, one commit each. **Stacked on #48.** The base branch `fast-robust-retention-on-main` is #48 merged into current `main` (conflict in `fast/tests/mod.rs` resolved), so this diff shows only these changes. ### 1. A list row's writes while it is drawn are its own Writes made while a row was laid out, prepainted or painted counted as someone else's. A chart filling its own path cache as it paints made its row (and every row like it) render again on every scrolled frame. Those writes are now the row's own, as a view's writes while it renders already are. The write generation is shared with the thread like the read logs, for the list to read without an app at hand. Test: `rows_writing_what_they_read_as_they_are_drawn_keep_the_layer` (fails without the fix: every shown row re-renders per scroll). ### 2. Replay a list's layer instead of compositing tiles A list's layer keeps its rows' paint operations and replays those reaching the viewport into the frame, moved by the scroll. No tiles are rasterized for lists, so: - rows painting paths, or drawing over earlier rows' paths, are replayed in order (no overlay, no keep-off); - a background that cannot be baked (translucent window) no longer matters; - a list's layer is not demoted: repainting costs what drawing without the layer costs. Overscan and the per-frame render budget are unchanged. `OverlayReach` (lists only) is removed. Tests that encoded tile, overlay or demotion behaviour for lists were rewritten to assert the new behaviour; every `matches_layers_off` comparison passes unchanged. `composites()` in the test helpers now also counts replayed frames. This is the change most worth discussing: it trades GPU-side tile compositing for CPU-side replay of display lists for lists. In Allsum it was the largest single win for scrollbar drag. ### 3. A list's scrollbar drag is a scroll `ListState::set_offset_from_scrollbar` bumped the list's state version, which counts content changes too, so every drag frame repainted the layer. It now notes the offset like other code-set offsets (`ScrollLog::take_offsets_set`, matched to the painted list by its state), which counts as a scroll. Without layers the version still moves. Test: `a_list_dragged_by_its_scrollbar_is_composited` (fails without the fix). ### Measured in Allsum (experimental build: these fixes, the hover freeze of the stacked PR and the app-side fixes; the zero-overscan variant of replay. Without hover freeze, wheel CPU was ~37 %; drag was unchanged) | | wheel CPU | drag CPU | |---|---|---| | main | 54 % | 36 % | | with fixes | ~22 % | ~19 % | ### Measured again with the code of these PRs Allsum release build against this branch stack (#56, #57 and the lingering-nodes fix of #55), GPUI Kit's draft fix and longbridge/ai-chat#438, same session, quiet machine: | | wheel CPU | drag CPU | |---|---|---| | main | 47.2 % | 32.4 % | | these PRs | 21.9 % | 19.7 % (~2.2 ms/frame) | Keeping the overscan unchanged (unlike the experimental build) lost nothing. The app-side fixes are in separate PRs (GPUI Kit, ai-chat). Hover freezing while the wheel scrolls is a separate draft stacked on this one. `script/check-upstream`: 0 violations. `cargo test -p gpui --features test-support --lib` (681 passed), `cargo test -p gpui_perf`, `cargo clippy -p gpui --features test-support --lib --tests` pass. 🤖 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.
A view in a list row that moved is laid out again as the list prepaints the row, outside the row's own layout. Claiming its nodes there cleared the flag that keeps a row's nodes for 240 frames after the row leaves. Once the row scrolled out its nodes were released, so scrolling back over it laid its views out, and shaped their text, from scratch. The flag now stays set once a node has been claimed laying out a list row (
fast/layout.rs,claim()).Adds
gpui_perf'schat-dragscenario, which drags a chat transcript's scrollbar from the end to the top and back, as a user does through a long session. With retained views it took 37 % longer per frame than drawing from scratch and shaped twice as many lines; it now does the same work.Found while investigating high CPU when dragging the transcript scrollbar in Allsum. In Allsum itself this change alone is a small part of the gain; the larger fixes are in a separate draft PR on top of #48.
a_moved_view_in_a_list_row_keeps_its_nodes_while_the_row_is_out_of_view(fast/tests/layout.rs).docs/scroll-layers.md: one sentence on the lingering nodes.script/check-upstream: 0 violations.cargo test -p gpui --features test-support --lib fast::andcargo test -p gpui_perfpass.🤖 Generated with Claude Code