Skip to content

perf(tui): a streaming reply's settled blocks are not lexed and rendered again for each chunk - #75

Merged
Kushalkhemka merged 1 commit into
pi-boltfrom
perf/engine-streaming-096
Oct 20, 2026
Merged

Kushalkhemka merged 1 commit into
pi-boltfrom
perf/engine-streaming-096

Conversation

@Kushalkhemka

@Kushalkhemka Kushalkhemka commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

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)

  1. When a block is lexed again to the same token, compared field by field, the old token object is kept, so renderBlock() reuses its lines. Matching raw and 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.
  2. Once the last block is past its first non-blank line, and the block before it is one that marked tries before a paragraph (code, heading, hr, list, blockquote, table, html), only the last block is lexed again. This does not apply when the block before is a paragraph: a setext underline several lines later can turn that paragraph, together with the lines after it that the $$ 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.

  • The suite runs 400 documents. A longer run of 16,500 documents was also identical.
  • The fuzz is what found the paragraph case above and the "first non-blank line" case.

Also:

  • A test in packages/tui/test/markdown-incremental-lex.test.ts checks that a settled block is the same token from one chunk to the next.
  • The existing tests pass: 32 streaming-render cases against a full render, tui markdown (81) and incremental-lex (37).
  • npm run check is clean.

Numbers against 0.9.5

All builds use the 0.9.5 macOS runtime. Instruction counts are medians of 5 interleaved runs:

0.9.5 this PR
long_answer 20k: instructions 2069M 1823M -11.9%
per frame 1969K 1719K -12.7%
long_answer 60k: instructions 6590M 5810M -11.8%
per frame 2116K 1863K -12.0%
tmux_load md:20000: bytes Pi wrote / tmux sent 553-557 / 1206-1212 KB 557 / 1211 KB same within frame timing

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:

0.9.5 this PR
long_answer 20k 0.47 / 0.47 / 0.46 0.43 / 0.42 / 0.42 -9.6%
long_answer 60k 1.54 / 1.50 / 1.53 1.35 / 1.35 / 1.39 -11.1%
pauses write:50/200/400 0.21 / 0.50 / 0.93 0.21 / 0.52 / 0.91 unchanged (no Markdown)

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.write and fs.writeSync cost the same). The SSE/JSON path is too small a share of a TUI run to be worth a runtime build.

…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.
Copilot AI balanced review requested due to automatic review settings October 20, 2026 01:08

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-20T01:14:38.130239Z 7aad135 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Kushalkhemka
Kushalkhemka merged commit 953b50a into pi-bolt Oct 20, 2026
12 checks passed
@Kushalkhemka
Kushalkhemka deleted the perf/engine-streaming-096 branch October 20, 2026 07:37
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.

3 participants