perf(message-scroller): transcript perf fixes + keep identity continuous across session bind - #1210
Open
roxi3906 wants to merge 7 commits into
Open
perf(message-scroller): transcript perf fixes + keep identity continuous across session bind#1210roxi3906 wants to merge 7 commits into
roxi3906 wants to merge 7 commits into
Conversation
roxi3906
force-pushed
the
perf/message-scroller
branch
from
August 17, 2026 02:38
f394833 to
e5fcb9d
Compare
Measure long-transcript behavior at 500 and 2000 seeded messages: open TTI, DOM size, scroll frame health (rAF deltas + longtasks), and main-thread cost while the fake agent streams a reply. Logs one PERF_RESULT JSON line per scenario for run-to-run comparison.
…ent estimates Streaming into a long transcript rebuilt per-row data with O(n) scans of conversationGraph.messages (graph node lookup, revisions collection) for every row on every 33ms presentation tick — O(rows^2) per tick. At 2000 rows this produced multi-hundred-ms long tasks per streamed chunk (p95 frame 134-172ms, 16 longtasks/~1.9s per stream). Hoist the scans into per-render indexes memoized on conversationGraph identity (graphMessageNodeById, graphRuntimeSegmentById, revisionMessagesByRootId, messageCreatedAtById, subsequentTurnCountByMessageId), making the row map O(1) per row. Also let MessageScrollerItem take a containmentEstimate and pass per-row-type contain-intrinsic-size values (user 3rem, agent 21rem, from measured row heights), cutting scrollHeight drift while offscreen rows resolve from ~8.5% to ~1%. Benchmark (2000 rows, streaming): frame p95 134-172 -> 66-68ms, longtasks 16 -> 5, longtask total ~1.9s -> ~0.5s; scroll and TTI unchanged.
Lift the 3rem/21rem contain-intrinsic-size guesses into named constants with their measured provenance (user rows ~46px, agent rows ~336px per the performance benchmark), so future retunes know where the numbers came from.
…ters Expose window.__streamingMetrics in the e2e build (vite mode "e2e" keeps it out of production) and snapshot the counters around the benchmark's streaming leg. The counters validate the frame sampling against what the pipeline actually did — e.g. at 2000 rows 36 chunk events now land in 10 store commits (tick batching), and future regressions in commit cadence become visible in PERF_RESULT.
Lifecycle races such as pending Session binding only become observable when session creation is slow like a real agent spawn. Honor FAKE_OPENCODE_NEW_SESSION_DELAY_MS in the session/new handler.
…ssion bind Sending the first message (or branch-submitting) renders the transcript in a pending Session; bindPendingSession later rewrote the Session id and selectedSessionId to the backend id. Both the MessageScrollerProvider key (activeSession.id) and every row key ([scopeId, item.id], scope = [sessionId, activeBranchId]) flipped twice (real -> pending -> real), remounting the whole transcript (multi-second freeze in dev) and re-anchoring the viewport to the turn's scrollAnchor row via defaultScrollPosition=last-anchor. The graph's synthesized root ids (root-frame/message-branch/runtime-segment) also kept embedding the dead pending id after the bind. - bindPendingSession now records the pending-side presentation identities (boundFromPendingSessionId, boundFromPendingMessageBranchId, boundToMessageBranchId) and rebases the conversation graph's session-derived root ids onto the backend id via rebaseConversationGraphSessionId (shared/conversation-graph), so no graph id references the pending id anymore. - WorkspaceMessageScroller keys the provider and the presentation scope on the recorded pending identities, translating the rebased branch id back for the scope, so neither the id swap nor the rebase remounts rows; intentional branch switches still change the scope (aipoch#1124). - The identities stay in memory only (excluded from toPersistedSession). Regression: e2e/message-scroll-session-bind.spec.ts branch-submits into a seeded 500-message session with session creation slowed by FAKE_OPENCODE_NEW_SESSION_DELAY_MS and asserts the transcript rows are not remounted (first row DOM node survives) and the viewport never resets, staying at the live edge. Fails without the fix (firstRowConnected=false), passes with it.
roxi3906
force-pushed
the
perf/message-scroller
branch
from
August 18, 2026 05:57
ac2cdfe to
ee2bf72
Compare
…graph Review follow-ups for the pending-Session bind fix: - bindPendingSession computed boundToMessageBranchId with string surgery (endsWith + first-match replace) alongside the rebase's exact-id rename map — two mechanisms that could silently disagree, in which case the scope translation never fires and the original flip returns. Run the rebase first and read the before/after active branch ids off the graphs; a pair is recorded only when the rebase actually remapped it. - The session-bind e2e spec set FAKE_OPENCODE_NEW_SESSION_DELAY_MS on the worker process permanently, leaking a 4s session-creation delay into every later spec in the same worker. Save/restore around the file's tests instead.
roxi3906
marked this pull request as ready for review
August 18, 2026 10:06
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.
Background
Two transcript issues, diagnosed and fixed together (both reproduced with the benchmark/regression specs included here):
Changes
Performance
e2e/message-scroller-performance.spec.ts: seeds sessions with 500/2000 messages and measures open TTI, DOM size, scroll frame health (rAF deltas + longtasks), and main-thread cost while the fake agent streams. Emits onePERF_RESULTJSON line per scenario, cross-checked against the app's own streaming pipeline counters (window.__streamingMetrics, exposed in the e2e build via vite modee2e).conversationGraph.messagesper row — hoisted into per-render indexes memoized on conversationGraph identity (O(1) per row).MessageScrollerItemgainscontainmentEstimate; user rows 3rem / agent rows 21rem (measured heights, named constants), cutting scrollHeight drift from ~8.5% to ~1%.Session bind identity continuity (the freeze + scroll reset)
Sending a message opens a pending Session;
bindPendingSessionlater rewrites the sessionidandselectedSessionId. Both flips invalidated theMessageScrollerProviderkey and every row key ([scopeId, item.id]), causing two full-transcript remounts (multi-second main-thread block in dev) and a re-anchor to the turn's user row viadefaultScrollPosition="last-anchor".Fix (stable logical identity — bind-in-place was rejected because ACP assigns the session id and event routing keys on it):
bindPendingSessionrecordsboundFromPendingSessionId(+ branch id pair) as transient, non-persisted fields, and rebases the graph with the newrebaseConversationGraphSessionIdso no graph id embeds the dead pending id.e2e/message-scroll-session-bind.spec.ts(fake agent with opt-inFAKE_OPENCODE_NEW_SESSION_DELAY_MS): RED without the fix (row nodes replaced, scrollTop drops 336/496px), GREEN with it (zero scope flips, row nodes survive, stays at live edge).Benchmark (2000 rows, back-to-back same-machine runs)
Audit conclusions (plan items that needed no change)
Verification
Virtualization (@tanstack/react-virtual) was evaluated on a companion branch and deferred; archived as draft PR #1318 with the full A/B/A+B comparison.