Skip to content

zero-swift: measure only the virtual list's rows near the viewport - #25

Merged
tmm merged 4 commits into
mainfrom
swift-lazy-measure
Oct 23, 2026
Merged

tmm merged 4 commits into
mainfrom
swift-lazy-measure

Conversation

@tmm

@tmm tmm commented Oct 22, 2026

Copy link
Copy Markdown
Member

After zbugs' first sync, the list's first layout pass created and measured all 50 loaded rows through SwiftUI, when 11 were visible: 460–680 ms on the main thread (iPhone simulator, Debug, low CPU priority). Together with applying the first poke, that stall makes XCTest's accessibility queries time out on slow CI runners.

What changed:

  • LayoutHost creates and measures only rows within one viewport height above and below the viewport. Other rows keep their last measured height, or the estimated row height, until they come near.
  • On-screen rows stay put: when an estimated row above the anchor gets its real height, the existing measureAndCompensate path keeps them still in the same pass, as for any row changing height. Mid-list it adjusts the space before the rows; near the start it scrolls.
  • Restores and permalink jumps still measure every row while landing, because where they land depends on the rows above.
  • Jumps to loaded rows: in VirtualizerCore.land(), on a host that lays out with its scroll, a row jump that scrolled lands again in the next pass with the row measured. Without this, .end/.center jumps drifted.
  • Width changes clear the cached heights (invalidateHeights()).

Tests: 4 new LayoutHostTests. The first fails on the old code:

  • only rows near the viewport are measured;
  • scrolling measures rows as they come near, and on-screen rows move by exactly the scroll;
  • rows above measured taller than estimated don't move what's on screen, both at the start and mid-list;
  • jumps to unmeasured rows land on them.

One existing test now compares against the user's scroll target rather than the post-compensation offset.

Not yet verified: the UIKit and AppKit parts (RowsScrollView, VirtualList) were changed without running them. The virtual-list UI suites run in CI, and they'll also run on a Mac before merging.

Tested on Linux: 215 Swift tests, swift format lint --strict, and the zbugs-swift build.

ampagent and others added 4 commits October 22, 2026 18:52
A layout pass made and measured a cell for every loaded row: after the
first sync, 50 rows while 11 were visible (460-682 ms on zbugs-swift).
LayoutHost now measures only rows within one viewport height above and
below the viewport. Other rows use their last measured height, or the
estimated row height, until they come that near. While a jump or a
restore is landing, it measures every row, because where it lands
depends on the rows above.

A row above the viewport that is measured at a height other than its
estimate goes through the same compensation as a row changing height,
in the same pass, so rows on screen hold still. A scroll to rows not
yet measured lays out again in the same pass. A jump to a loaded row
that scrolled is lined up again in that pass, now measured, before it
lands.

Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859
Co-authored-by: tmm <tmm@tmm.dev>
… 0.2 s

On a slow CI runner the layout pass that places the highlight can run
after 0.2 s; the test now polls up to 5 s, then checks as before.

Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859
Co-authored-by: tmm <tmm@tmm.dev>
When a stalled frame holds a jump's landing past the script's pause, the
next phase used to end the unmoved jump and check its landing frame as an
ordinary frame of that phase, flagging it as wrong. A jump that hasn't
moved when a phase begins now takes the next frame and ends with it. The
wrong-frame checks are unchanged.

Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859
Co-authored-by: tmm <tmm@tmm.dev>
…t, not 0

Resized from 0 high at a fractional list offset, as when lazy measuring
makes a view mid-scroll, a row view's autoresizing subviews came out that
fraction of a point above it, which a 1x display rounds to a whole point
for the view's life.

Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859
Co-authored-by: tmm <tmm@tmm.dev>
@tmm
tmm merged commit f564a2c into main Oct 23, 2026
19 checks passed
@tmm
tmm deleted the swift-lazy-measure branch October 23, 2026 02:40
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