Skip to content

feat(zero-swift): let a VirtualList keep the views of rows that page out - #213

Open
tmm wants to merge 1 commit into
mainfrom
feat/virtual-list-kept-rows
Open

tmm wants to merge 1 commit into
mainfrom
feat/virtual-list-kept-rows

Conversation

@tmm

@tmm tmm commented Nov 14, 2026

Copy link
Copy Markdown
Member

Follows #202. Most of the macOS issue list's remaining scroll stalls were rows paging back in: each made its SwiftUI view again and measured it (about 1.4 ms a row, 4–6 ms for a frame's first) in the frame that brought it.

What it adds

  • VirtualList.maxKeptRows(_:) (a modifier, so the inits don't change) and VirtualListView.maxKeptRows: how many rows that leave the snapshot keep their views, hidden, for when they come back. The oldest are evicted first.
  • A kept row that comes back is shown in its own view again, with its height and @State. If its data, its selection, or the list's width changed while it was kept, it's shown or measured again.
  • Default 0: keeps none, the same as before. A kept row's own queries stay open while it's kept, so it's opt-in.
  • zbugs-swift's SwiftUI issue list keeps 200 rows (about 7 MB per 100 rows).

Measured

Release builds on a 60 Hz Mac satellite, 155 open issues, the same flings-and-drags script (measure.txt, ~100 s), three alternating runs, #202 → this:

#202 maxKeptRows(200)
frames dropped 5 / 4 / 4 6 / 2 / 5
main thread busy > 8.3 ms 238 / 259 / 302 171 / 157 / 175
busy > 16.7 ms 12 / 15 / 19 13 / 16 / 14

In Time Profiler traces, stalls ≥ 8.3 ms from rows coming into reach went from 200 spans (2.2 s) to 50 (0.6 s); all stalls ≥ 8.3 ms went from 3.5 s to 2.1 s. Row layouts a frame fell from 0.40 to 0.12, so hidden kept rows don't lay themselves out. The stalls over 16.7 ms and the dropped frames are mostly page changes (queries reopening, the window's toolbar re-rendering on each snapshot), which this doesn't change.

Tests

  • keepsTheViewsOfRowsThatLeave (0, 5, 100): drives RowsLayout with row views that count what they're asked to do. Rows that page out and come back are shown and measured again only beyond the cap. 0 keeps none.
  • aKeptRowComesBackAsItIsNow: data, selection, and width changed while a row was kept.
  • aKeptRowComesBackWithItsState: VirtualList(...).maxKeptRows(300) in a window; rows scrolled far away and back keep their @State.

Each test fails under a deliberate break: modifier not passed through, cap ignored, stale height after a width change, stale selection. Locally on macOS: all ZeroVirtual tests (122) and Apple tests (29), the iOS Simulator builds, the macOS FrameProbeUITests, and swift format lint --strict pass.

Most of the macOS issue list's remaining scroll stalls were rows paging
back in: each made its SwiftUI view again and measured it (about 1.4 ms
a row, 4 to 6 ms for a frame's first) in the frame that brought it.

`VirtualList.maxKeptRows(_:)` and `VirtualListView.maxKeptRows` keep the
views of up to that many rows that leave the snapshot, hidden, oldest
evicted first. A kept row that comes back is shown in its view again,
with its height and state, unless its data, selection or the list's
width changed meanwhile. Its own queries stay open while it's kept.
The default, 0, keeps none, as before. zbugs-swift's SwiftUI list keeps
200, about 14 MB.

On a 60 Hz satellite, the flings-and-drags benchmark over 155 open
issues (three alternating runs, #202 then this): main-thread stalls over
8.3 ms 238/259/302 then 171/157/175, over 16.7 ms 12/15/19 then
13/16/14, dropped frames 5/4/4 then 6/2/5. Traced, stalls of rows coming
into reach went from 200 spans (2.2 s) to 50 (0.6 s); the stalls over
16.7 ms are page changes, which this doesn't touch.

Amp-Thread-ID: https://ampcode.com/threads/T-02a95685-6d19-4dd4-b1de-8210ec5599df
Co-authored-by: tmm <tmm@tmm.dev>

This branch has not been deployed

No deployments
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.

2 participants