Skip to content

perf(messages): virtualize top-level thread replies - #514

Draft
kalvinnchau wants to merge 3 commits into
mainfrom
peon/virtualize-threads
Draft

kalvinnchau wants to merge 3 commits into
mainfrom
peon/virtualize-threads

Conversation

@kalvinnchau

@kalvinnchau kalvinnchau commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

What

Thread history mounted every loaded reply (up to 460 strict / 500 legacy). Top-level thread replies now render through the same Virtua Virtualizer as the channel timeline (virtua@0.51.0 + existing patch). A nested branch stays whole inside its top-level item.

How

  • ThreadPanel.tsx: top-level replies are virtual items. The exact target, an own pending send, the focused row and rows that request it stay mounted.
  • Virtua mounts and measures rows over later frames, so initial position, exact-target reveal and follow-latest hold their place until the mounted range is measured, instead of positioning once in a single layout effect.
  • The exact-target anchor is viewport-relative and refreshes on reader scroll, so a later row update does not pull the reader back.
  • Messages.module.css: overflow-anchor: none on the thread scroller; a focused or exact-target item stays visible while Virtua re-measures it.

Behavior changes

  • Jump to latest in a thread is immediate. The smooth scroll and its 1 s fallback timer are removed. This matches the channel timeline.
  • Offscreen thread replies are no longer in the DOM.

Browser test changes

Row-count assertions cannot hold once offscreen replies unmount. Each was replaced, not dropped:

spec removed replacement
thread-window.spec.mjs (legacy continuation) mounted count 50, then 61 newest loaded reply in viewport before and after retry
thread-window.spec.mjs (scrollback) mounted count per older page; 305 mounted and unique one relay read per demand; walk of the virtualized range asserting no duplicate among mounted rows and exactly 305 distinct replies
navigation-thread-history.spec.mjs mounted count 123, 125/126 scroll range grows when the held page applies; unread count as the arrival barrier
messages.spec.mjs mounted count 64/65 the delivered reply's own row is visible
channel-opening.spec.mjs 301 rows mounted ends the traversal newest reply's row mounted ends the traversal

Added: message-navigation.spec.mjs "live updates do not snap it back to the target" now scrolls the reader away programmatically before the live edit. Browser-only because it asserts real scroll geometry under virtualization.

Fail-then-pass:

  • Scroll-away regression: without the anchor refresh, fails in chromium and webkit (expected 903, received 1503); passes with it.
  • 305-reply walk: in its first form (a time-bounded poll, at 4fe221a7) it failed at 63 of 305 with the scroll step disabled (chromium). That form ran out of time on hosted runners (237 and 262 of 305), so the third commit replaced it with a loop that settles, samples and steps one viewport until the bottom. The fail-first check was not repeated on the loop form.

Vitest: thread suites mock virtua with a mount-everything stand-in (virtua.testing.tsx), since jsdom has no layout. That stand-in does not exercise eviction; eviction is covered by the browser specs.

Evidence

Hosted CI at the PR head d97719a9 (run 36920559245, executed on merge 759c0a56 with main 69a9af23)

  • Every browser spec this PR changes passes in chromium and webkit: thread-window 7/7, message-navigation 21/21, messages 7/7, navigation-thread-history 3/3, nested-replies 8/8 per engine; the large-thread channel-opening case this PR changes (:240) passes in both measurement projects. No retries.
  • Vitest: 491 files / 6525 tests passed. Lint, types, build, Rust, native fixture, browser measurements: pass.
  • CI required is red for two reasons, both also present on main's own run at 69a9af23 (run 36919997681):
    • channel-tabs.spec.mjs:281 fails in both engines (tab count expected 2, received 5). Also fails on main at e697aa7a. This PR does not touch it.
    • The JavaScript job was cancelled by its 10-minute timeout after Vitest finished green (561 s). main's run was cancelled at the same step. This PR does not touch it.

Local, macOS, headless

At d97719a9 (test-only delta from 4fe221a7, thread-window.spec.mjs only): thread-window.spec.mjs in chromium and webkit, 42 passed including repeated runs of the walk; biome clean.

At 4fe221a7 (rebased on e697aa7a):

  • tsc --noEmit, biome check --error-on-warnings ., vitest run (489 files / 6504 tests): pass.
  • Thread browser specs in chromium and webkit (thread-window, navigation-thread-history, nested-replies, message-navigation, messages, thread-opening, thread-unread, thread-video, image-scroll): 122 passed, 0 failed.

At the pre-rebase head 4af30376 on cc0c47c7 (identical patches per git range-diff):

  • Full browser suite: 1087 passed, 5 failed, 1 skipped. The 5 (profiles:524 both engines, reactions-polish:6 both engines, channel-completion:7 webkit) fail identically in a full run on clean cc0c47c7 and pass when run alone on either side.
  • Three-lane agent review (integration, security, static/lifecycle) found one defect, the scroll-away regression above, fixed in the second commit; the re-review reported no actionable findings. The third, test-only commit was reviewed at d97719a9 with no actionable findings; its runtime evidence is the hosted merge-tree run above, verified by the reviewers from the downloaded reports, not a standalone-head run.

The full browser suite has not been rerun locally at the rebased head.

300-reply thread, one full-suite run per side:

engine phase metric cc0c47c7 4af30376
chromium reopened first visible row 481 ms 48 ms
chromium cold traversal painted 918 ms 431 ms
webkit reopened first visible row 742 ms 74 ms
webkit cold traversal painted 1074 ms 635 ms

"Traversal painted" ends on a different condition after this change (see table above); "first visible row" is the same measurement.

Deferred

  • Not exercised in the native app.
  • Human test not done.
  • Nested portaled report retention under eviction is untested.

peon added 3 commits October 1, 2026 12:53
Thread history mounted every loaded reply (up to 460 strict / 500
legacy). Top-level replies now render through the same Virtua
Virtualizer as the channel timeline; a nested branch stays whole inside
its top-level item.

Virtua mounts and measures rows over later frames, so initial
positioning, exact-target reveal and follow-latest hold their place
until the mounted range is measured instead of positioning once in a
single layout effect. The exact target, an own pending send, the focused
row and rows with an open report stay mounted. Jump to latest is now
immediate rather than a smooth scroll.

Browser specs that counted mounted rows now assert on visible content,
scroll range or relay reads instead.

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
Signed-off-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
The exact-target anchor is viewport-relative while history is
incomplete. A scroll that bypassed the wheel, touch, key and pointer
handlers left it stale, so the next row update pulled the reader back to
the target. Refresh the anchor on scroll outside positioning.

Cover it in the existing live-update case, and restore the 305 unique
reply check as a walk over the virtualized range.

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
Signed-off-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
The 305-reply walk advanced on a 50 ms poll inside a 10 s expectation.
Hosted Linux runners timed out partway (237 and 262 of 305). Step one
viewport at a time, wait for geometry to settle before each sample, and
stop at the end of the scroll range.

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
Signed-off-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
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