Skip to content

gpui: keep a list row's nodes lingering when a view in it is laid out again - #55

Merged
sunli829 merged 2 commits into
mainfrom
perf-chat-drag
Oct 12, 2026
Merged

sunli829 merged 2 commits into
mainfrom
perf-chat-drag

Conversation

@sunli829

Copy link
Copy Markdown
Collaborator

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's chat-drag scenario, 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.

  • Test: 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:: and cargo test -p gpui_perf pass.

🤖 Generated with Claude Code

… 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
sunli829 merged commit 0a66043 into main Oct 12, 2026
4 checks passed
@sunli829
sunli829 deleted the perf-chat-drag branch October 12, 2026 03:42
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)
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.

1 participant