Repository navigation
perf(tui): a streaming reply's settled blocks are not lexed and rendered again for each chunk - #75
Conversation
…red again for each chunk Each chunk lexed the last two top-level blocks again and rendered both to new lines, highlighting included. Now: - a block lexed again to the same token (compared field by field) is the token from before, so its rendered lines are kept; - when the last block is past its first non-blank line and the block before it is one marked tries before a paragraph (code, heading, hr, list, blockquote, table, html), only the last block is lexed again. What renders is unchanged: a test streams 400 random documents (16,500 in a longer run) in random chunks and widths and compares every frame with the previous algorithm. long_answer 20k/60k: 12% fewer instructions per frame.
There was a problem hiding this comment.
If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.
Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Problem
Streaming a long answer in the TUI costs about 2M instructions per frame (bench/long_answer.py md:20000: 2.07G instructions, 1,045 frames). The socket-to-agent path (fetch, decoder, SSE, JSON.parse, event stream, agent loop) is under 10% of that: a headless run of the same paced answer costs about 196K instructions per chunk. The rest is the frame, and the largest part of the frame is the streaming message's Markdown (about 60% of frame time in an instrumented AOT build: lexing 281 us, block rendering 298 us of a 950 us frame).
Cause
Each chunk lexed the last two top-level blocks again and rendered both. The second-to-last block came back as a new token object, so its render cache missed every time. For example, while a table streamed in after a code block, the code block was lexed, highlighted and wrapped again for every chunk.
Fix (packages/tui/src/components/markdown.ts)
renderBlock()reuses its lines. Matchingrawand type is not enough: a paragraph built from lazy lines can keep its lines' indentation in one case and drop it in the other. The last token is always the new one.$$extension took, into a heading.Same output
setMarkdownFastPathsForTest(false)restores the previous algorithm. A new fuzz test (packages/coding-agent/test/streaming-markdown-render.test.ts) builds random documents from 39 kinds of pieces: lists, "2" turning into "2.", lazy lines, open and closed fences, tables, quotes, setext underlines, html,$$,\[, tabs, CRLF, U+2028 and nbsp lines. It streams each one in random chunk sizes at widths 20, 47 and 160, and requires every frame and the final render to be identical under both settings.Also:
npm run checkis clean.Numbers against 0.9.5
All builds use the 0.9.5 macOS runtime. Instruction counts are medians of 5 interleaved runs:
CI, Linux x86-64, one CPU: bench run 39351715881, branch
bench/engine-streaming-096. It compares the 0.9.5 release with this PR built on the 0.9.5 runtimes. CPU seconds, 3 rounds:tmux_load did not run on CI: the 0.9.5 release's TUI did not start in tmux on the runner. That is a runner problem, not a result. The tmux byte check above was run locally.
No engine change. A write-path microbenchmark found nothing to gain in Bun's terminal write (
process.stdout.writeandfs.writeSynccost the same). The SSE/JSON path is too small a share of a TUI run to be worth a runtime build.