perf(webui): bound the transcript — containment, block flow, windowing#755
Open
eous wants to merge 1 commit into
Open
perf(webui): bound the transcript — containment, block flow, windowing#755eous wants to merge 1 commit into
eous wants to merge 1 commit into
Conversation
The remaining steady-state cost after the wedge-proofing pass was structural: every row of an unbounded transcript participated in every layout, and full re-renders rebuilt all of it. CSS pair (shared scroller + the ui/static duplicate): - The messages scroller is BLOCK flow, not a column flexbox — flex relayouts all items when the streaming row's height changes, O(rows) per token; block flow dirties only the tail. The old per-child flex-shrink pin (and the min-height:auto squish hazard it suppressed) goes with it; inter-row rhythm moves to a sibling margin. - overflow-anchor: none — the pane owns bottom pinning, and native anchoring kept re-selecting an anchor inside the innerHTML-replaced live bubble every frame. - content-visibility: auto with contain-intrinsic-size: auto estimates on .msg (80px) and .conv-batch (200px) rows, exempting the last two children so the live tail never toggles skip-state mid-stream. The `auto` keyword memoizes each row's rendered size, keeping scrollHeight and the bottom pin stable once painted. Transcript windowing (interactive pane): - Full re-renders paint the most recent 300 messages, cut FORWARD to a user-turn boundary so an assistant tool_calls message is never split from the tool results that anchor to it. Hidden content sits behind a "Load earlier messages" pager; each click grows the window a step and refetches, restoring the scroll anchor by scrollHeight delta (the rAF pin re-checks the near-bottom flag at fire time, so no suppression is needed). Rewind/edit turn math is tail-relative and unaffected — pinned as such. - Live appends are bounded at the idle edge: past 900 rendered rows the oldest rows are trimmed (again to a turn boundary), only while pinned to the bottom — a scrolled-up user is reading the rows a trim would remove. Trimmed content stays in /history and returns through the pager; detached agent-card entries are swept. The perf page gains ?window= (and the runner --perf-extra) so windowing and containment effects can be isolated. Measured (n=3000 history + 20-turn storm; baseline -> previous branch -> this change): full replay 1060ms -> 238ms -> 28ms windowed / 107ms with the window grown to the full transcript; longtasks during the run 6/1080ms -> 4/495ms -> none windowed / 4/205ms unwindowed; per-turn live-storm cost at n=3000 now equals n=300 (~220ms harness floor) even with all 25k nodes live — the transcript-size tax is gone. Known degraded-mode cost: the chunk path at a fully-grown window measures ~1017ms vs the 833ms floor; the shipped windowed config sits at the floor.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR implements the “P2” tranche of the long-session Web UI performance plan by reducing layout/re-render cost in very long transcripts. It does so via scroller CSS containment/anchoring changes and by windowing the replayed transcript (with a pager to load older history), while extending the perf harness to measure the effects independently.
Changes:
- Switch the transcript scroller to block flow, disable native scroll anchoring, and add
content-visibilitycontainment plus pager styling (interactive + UI static duplicate). - Add transcript windowing (replay renders most recent window, pager grows it) and pinned-only live trimming to cap rendered DOM size.
- Extend
scripts/livepass.pyperf harness with?window=and--perf-extra, and add tests pinning these invariants.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| turnstone/ui/static/style.css | Align static UI transcript scroller behavior with interactive scroller (anchoring off; no flex column). |
| turnstone/shared_static/interactive.js | Implement transcript windowing, pager-driven history growth, and pinned-only live trimming + agent-card sweep. |
| turnstone/shared_static/interactive.css | Apply block-flow scroller, anchoring-off, content-visibility containment, and pager styling. |
| tests/test_interactive_pane_js.py | Add regression tests for block-flow/anchoring/containment and transcript windowing + trim invariants. |
| scripts/livepass.py | Add perf query override plumbing (?window= + --perf-extra) to measure containment vs windowing separately. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+55
to
+57
| .pane--embedded .pane-messages > * + * { | ||
| margin-top: 2px; | ||
| } |
Comment on lines
+1207
to
+1217
| if (this.busy) return; | ||
| this._historyWindow += _HISTORY_WINDOW_STEP; | ||
| const token = this._historyLoadToken; | ||
| const prevScrollHeight = this.messagesEl.scrollHeight; | ||
| const prevScrollTop = this.messagesEl.scrollTop; | ||
| this._beginReplayQuiesce(token); | ||
| this._refetchHistory(this.wsId, token).finally(() => { | ||
| if (token !== this._historyLoadToken) return; | ||
| this.messagesEl.scrollTop = | ||
| this.messagesEl.scrollHeight - prevScrollHeight + prevScrollTop; | ||
| }); |
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.
Follow-up to #754 (the P2 tranche of the long-session perf plan). #754 removed the per-event O(N) work and the wedge failure modes; this bounds the remaining structural cost — every row of an unbounded transcript participating in every layout, and full re-renders rebuilding all of it.
CSS pair (shared scroller + the ui/static duplicate)
min-height:autosquish hazard the old per-childflex-shrink: 0pin suppressed; row rhythm moves to a sibling margin at identical spacing.overflow-anchor: none— the pane owns bottom pinning, and native anchoring re-selected an anchor inside the innerHTML-replaced live bubble every frame.content-visibility: auto+contain-intrinsic-size: auto 80px/200pxon.msg/.conv-batchrows; the last two children are exempt so the live tail never toggles skip-state mid-stream. Theautokeyword memoizes rendered sizes, keepingscrollHeightand the pin stable.Transcript windowing
tool_callsmessage is never split from the tool results that anchor to it). A "Load earlier messages (N hidden)" pager grows the window a step and refetches, restoring scroll position by scrollHeight delta — no suppression logic needed because fix(webui): wedge-proof the live-session pipeline and de-O(N) hot paths #754's rAF pin re-checks the near-bottom flag at fire time./historyand returns via the pager; detached agent-card entries are swept.The perf page gains
?window=(and the runner--perf-extra) so windowing and containment effects stay separately measurable.Measurements (n=3000 history + 20-turn storm)
The window-grown column isolates the containment pair: even with all 25k nodes live, the per-turn transcript tax is gone. One noted degraded-mode cost: the chunk path at a fully-grown window measures ~1,017ms vs the 833ms harness floor (suspected content-visibility bookkeeping at 25k contained roots); the shipped windowed config sits at the floor.
Verification
422 frontend tests green, including new pins for block flow + anchor-off + containment with the live-tail exemption, the turn-boundary window cut, the pinned-only trim, and the tail-relative rewind invariant. A lightweight correctness pass over the diff found nothing.
Browser floor for
content-visibility: auto: Chrome 85+, Firefox 125+, Safari 18+ — older engines simply ignore the declarations and get the block-flow/windowing wins alone.