perf(desktop): make scrollback paging paint from memory and stop page cascades - #6601
Open
tlongwell-block wants to merge 7 commits into
Open
perf(desktop): make scrollback paging paint from memory and stop page cascades#6601tlongwell-block wants to merge 7 commits into
tlongwell-block wants to merge 7 commits into
Conversation
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Signed-off-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Scrollback paging felt slow even with a fast relay because a fetched page could sit unpainted for seconds, and one gesture often pulled several pages. useSettleGatedPrependMessages held a landed page on ANY wheel/scroll input with a 4s deadline, and assumed motion at hold start — so even a scroller at rest paid a full quiet window, and a reader keeping fingers on the trackpad waited seconds for a 450ms page. Track motion for the scroller's whole life (lastMotionTsRef) so an at-rest hold admits after the stable-frame count, and cut SETTLE_HOLD_DEADLINE_MS to 1500: trackpad momentum — the only phase with the dropped-write hazard — decays well inside that bound. Retiring the hold directly on wheel was tried and rejected: it drifted the anchor ~30px under live input. loadOlderViaVirtualizer refused to re-fire only while isFetchingOlder or the settle hold was set. Between the fetch resolving and the prepend committing, WebKit's scroll events at the boundary started the next page, cascading one gesture into many. Add isRenderedTimelineBehindHistoryPrepend to the refuse predicate — the same signal the spinner already uses. Tests (each fails on the previous code): - useSettleGatedPrependMessages.test.mjs: at-rest hold admits without a charged quiet window; motion before the hold still charges it. - virtualization 09: commit under 60 wheel bursts is bounded (gapSinceInput < 80ms, sinceSpinner < deadline+slack) and the anchor holds within 5px at commit. - scroll-history "one gesture pages once": emulate WebKit's at-rest scroll events and tighten the ceiling from <=4 pages to <=1. Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz> Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
"fast middle-page scroll settles with continuous mounted coverage" drove the scroller with bare scrollTop writes right after a prepend. Those are not reader input, so the virtualizer's bottom intent stayed armed, and when the prepend's extent resize fired inside the burst window the settle hook legitimately re-pinned the floor — the coverage read then measured the wrong viewport (197px hole). The test used to win that race only because the at-rest settle hold took ~100ms longer to admit the page; bounding the hold made the race visible (4/10 on WebKit, 5/12 on Chromium; 0 on the previous commit). Dispatch one wheel event before the burst, as a real trackpad pass does. Proven fail-first: with cancel() no longer clearing bottom intent the test is red 4/4 at 198px; clean product 16/16 Chromium, 12/12 isolated. Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz> Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
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.
Summary
Scrollback in the desktop timeline felt slow even though the relay answers a history page in well under half a second: the second page lagged, and one upward gesture often pulled many pages. Research:
RESEARCH/BUZZ_DESKTOP_SCROLLBACK_PAGING_DEEP_DIVE_2026_08_22.md(workspace). Four independent causes, one commit each.What
Settle hold bounded; page cascades stopped —
c9a26d31fuseSettleGatedPrependMessagesheld a landed page on any wheel/scroll input with a 4s deadline and assumed motion at hold start, so an at-rest reader paid a quiet window on every page and a reader with fingers on the trackpad waited seconds for a 450ms page. Motion is now tracked for the scroller's whole life (lastMotionTsRef) so an at-rest hold admits after the stable-frame count, andSETTLE_HOLD_DEADLINE_MSdrops 4000 → 1500 (trackpad momentum — the only phase with the dropped-write hazard — decays well inside it). Retiring the hold directly onwheelwas tried and rejected: ~30px anchor drift under live input.loadOlderViaVirtualizernow also refuses whileisRenderedTimelineBehindHistoryPrepend(deferredMessages, messages)— the same predicate the spinner uses — closing the resolve→commit gap where WebKit's at-rest scroll events started page N+1.Page N+1 staged locally —
db4bf9d33,856b32b02,7442713de(Max)After a page commits,
pageOlderMessagesfetches exactly one successor into astagedPageslot onChannelWindowStore— outside projection, so nothing renders until the reader asks. The next pass consumes it when itsstartCursorequals the retained tail cursor, otherwise it is discarded;appendOlder/replaceNewestChannelWindowclear the slot so a head refresh can never serve stale chain data. In-flight stages are tracked per channel with aretainedTailIsidentity guard, so a reader who outruns the stage awaits the one request already in flight instead of issuing a duplicate. The trigger moves from a 200px sentinel tooffset <= viewportSize * 1.5, still wrapped byarmUpwardMomentumso layout scrolls near the top cannot poison the reader's next input. Net: from the second page on, scrollback paints from memory while the network runs one page ahead.Relay responses gzipped on the desktop leg —
c706a0308(Perci)reqwest
gzipfor the app relay client: the measured 74KB history page negotiates its 26KB representation. Client construction split intohttp_client.rs; both media-proxy paths use a dedicated.no_gzip()+ no-redirect client so upstreamContent-Length/Content-Rangesurvive for video seeking. No relay change: no seeded local benchmark exists to prove a ≥50ms win on the same REPEATABLE READ session.Retention eviction held at the history boundary —
9bb578146(Wren)useTimelineRetention.onScrollEndarms a 3s eviction guard when the reader ends a scroll within 1.5 viewports of the top, so rows are not evicted from under a fetch in flight (the −570/−590px mid-fetchscrollHeightshrink in traces B/C).Related issue
None found for scrollback paging latency specifically; sibling perf PR is #6572 (see below).
Testing
Each behavioral test shown failing on the previous code.
useSettleGatedPrependMessages.test.mjs+2: at-rest hold admits without a charged quiet window; motion before the hold still charges it. Red 2/2 on old hook.tests/e2e/virtualization.spec.ts09 rewritten: 60 wheel bursts → commitgapSinceInput < 80ms,sinceSpinner < 1500 + slack, anchor drift at commit< 5px. Old hook: red (gap 125ms). New: 10/10 repeats.tests/e2e/scroll-history.spec.ts"one gesture pages once": emulates WebKit's at-rest scroll events; ceiling≤ 4→≤ 1(→≤ 2fetches with L2 staging). Without the re-fire gate: fails at 2.useTimelineRetention.test.mjs+1 (Wren, fail-first).http_client.rsloopback contract tests (Perci): app client advertises+decodes gzip (red without the Cargo feature); media client does not advertise gzip.pageOlderMessages.test.mjs+4 /channelWindowStore.test.mjs+3 (Max): one successor staged outside projection; next pass consumes it with no network wait; trigger during staging awaits the same request (one request total); a replaced tail does not reuse its stale in-flight stage (red with the identity guard removed); newest refresh drops the staged successor.tests/e2e/scroll-history.spec.ts"one gesture pages once" extended: with the mock delay raised to 2s, the next gesture paints the staged page within 500ms and the request count rises by exactly one (the background stage).tests/e2e/virtualization.spec.ts08 rewritten: park at 2× viewport, wheel into the 1.5× band, pre-prepend rollback< 5pxon both the staged-local and network commit paths. Red withshift={isPrepend}→shift={false}(5020px).tests/e2e/timeline-no-shift.spec.tsestimate-stability fixture shrunk to one channel-window page so "ordinary scroll" cannot enter the lookahead band; a deliberate estimate call from the scroll path still fails it (61 vs 51).tests/e2e/scroll-history.spec.ts"fast middle-page scroll settles with continuous mounted coverage" now starts its burst with onewheelevent —d73ba9c19. The test drove the scroller with barescrollTopwrites, which are not reader input, so bottom intent stayed armed; once the settle hold admitted pages ~100ms sooner (c9a26d31f) the prepend's extent resize re-pinned the floor mid-burst (197px hole, 4/10 WebKit, 5/12 Chromium; 0 on the previous commit, 0 on main). A real trackpad pass always starts with a wheel event, so this is test fidelity, not a product change. Fail-first: withcancel()no longer clearing bottom intent the wheel-first test is red 4/4 at 198px; clean product 16/16 WebKit, 12/12 Chromium.7442713de;d73ba9c19is test-only on top (lefthook pre-commit clean). Tauri crate 2755 passed / 0 failed / 18 ignored atc706a0308; no Rust change since.7442713de(251 tests, local darwin): 248 pass. The 3 failures are pre-existing on main4baccd539:workflow-local-controlssnapshot (local baseline drift) andthread-unread07/11 (parallel-load flake: 4/40 on main vs 1/40 here, same assertion). Shards 1–3: this PR's CI.webkit-pagingproject (scroll-history, timeline-no-shift, virtualization) atd73ba9c19: green excepttimeline-no-shift"prepend plus late row reflow", whose reflow sampler returns 0 samples ~2/20 here and 1/20 on main4baccd539— pre-existing sampler timing, branch-independent.d73ba9c19(Wren,git rev-parse HEADasserted in the same shell): Minimalness 9.3 / Elegance 9.2 / Correctness 9.2 on code + automated evidence; L3 native contract tests 2/2, focused renderer/store/gate/retention 44/44,git diff --checkclean.<video>seek through the media proxy (range streaming on the.no_gzip()client). Agent sessions have no WindowServer/Accessibility (screencapturefails, Apple Events-1743), so this needs an interactive Desktop session; opened at Tyler's request with the gate called out rather than manufactured. Receipt to be added here before merge.Relationship to #6572
Disjoint TS files. One Rust conflict in
src-tauri/src/app_state.rs(its pool bump 10s/1 → 300s/2 vs thehttp_client.rsmove); whichever lands second keepscrate::http_client::build()and ports 300s/2 intobuild()only (notbase(), which also feeds the media client).Not in this PR
Relay aux-hop latency (~130ms/page on prod) — motivation recorded, no safe seeded benchmark to prove a change; follow-up if wanted.