Repository navigation
Agent: stop weighing the whole transcript on every streamed chunk - #1566
Open
AnthonyRonning wants to merge 1 commit into
Open
AnthonyRonning wants to merge 1 commit into
AnthonyRonning wants to merge 1 commit into
Conversation
Every streamed event bounded the transcript window by adding up the weight of every loaded row, cloning each row's id for the lookup. The window now keeps a running byte total (RowWeights): a row that arrives or changes adjusts it, and only a bulk change weighs every row. The limits and what gets evicted are unchanged. With application Vim on, following the stream also collected every row's id to read the last one; it now searches from the end. Co-Authored-By: Claude <noreply@anthropic.com>
This branch has not been deployed
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.
Every streamed chunk made the chat add up the size of every loaded row, copying each row's id on the way. A chunk now costs about a tenth as much in a long task (59.5 µs → 5.6 µs at 2,000 rows). This is the first fix from the UI performance audit (code-docs#123, item 2).
Cause
The transcript keeps at most 2,000 rows and 32 MiB loaded.
apply_timeline_itemenforces that after every event by callingbound_transcript_window, which summed the weight of every loaded row and cloned each row's id to look its weight up. So one chunk cost more the more rows were loaded, and a queued batch of 256 chunks paid that 256 times in one UI-thread update.With application Vim on,
follow_application_streamdid similar work a second time: it collected the id of every row to read the last one.What changes
RowWeightsholds each loaded row's weight and their sum. A row that arrives or changes adjusts the sum (bound_after_row_change). A bulk change, such as a page of history or another task, still weighs every row (bound_transcript_window, as before).Nothing a user sees changes. The 2,000-row and 32 MiB limits, which rows are dropped and from which end are all as before.
One scan per event remains: the search for the row that carries an event's live identity. It compares without allocating and costs about 2 µs per 1,000 rows, which is the growth left in the "after" column below. Replacing it needs a second index kept in step with the rows, so it is left alone.
Measured
A throwaway probe (not in this PR) built a tool-heavy transcript, applied 2,000 streamed chunks to its last row and timed
apply_timeline_item. Release profile, Mac Studio, median of three runs, alternating the two binaries on an idle machine:With application Vim on, through the whole event path (
apply_incoming_item, dev profile): 107.5 µs → 6.4 µs per chunk at 1,990 rows, 30.8 µs → 2.9 µs at 500.What this is worth: at ordinary transcript sizes a user will not feel the difference. It matters when chunks queue up in a long task, where one full batch held the UI thread for 15 ms (a frame at 60 Hz, two at 120 Hz) and now takes 1.4 ms. A slow batch lets more chunks queue behind it, and a full queue forces the chat to reload, so this is the path where falling behind compounds.
It does not change what a frame costs. The same probe put one redraw of the transcript at 0.14 ms with a 1 KB message on screen and 2.0 ms with a 50 KB one (the test window does not shape text, so the real figures are higher). That is the larger cost and the audit's later items cover it.
Remote
No protocol, capability or host change. A paired computer's chat runs the same code and gets the same saving.
Verification
just cipasses (format, the four clippy configurations, every test), and the pre-commit lint and test lanes passed on the commit.row_weights_follow_streamed_rekeyed_and_evicted_rows: the running total equals a fresh sum after a bulk load, a row that grows and shrinks, a live row that takes its stored key, and rows pushed out past the 2,000-row cap.a_streamed_row_past_the_byte_budget_evicts_the_oldest_row: a row whose growth takes the window past 32 MiB still drops the oldest row, through the path that no longer rescans.test_application_vim_follows_a_stream_to_its_newest_selectable_row: the Vim row follows each streamed row and skips rows the transcript does not show.🤖 Generated with Claude Code
Open in [code]smith · Need help on this PR? Tag
@codesmith-botwith what you need.