perf(web-shell): keep streaming output responsive - #9405
Conversation
E2E performance reportThe deterministic Chromium scenario passed after rebasing onto the latest
Related verification also passed: SDK and Web Shell production builds, SDK/Web Shell/WebUI type checks, 419 Web Shell tests, 301 SDK transcript tests, and 217 WebUI session-provider tests. |
|
Thanks for the PR — this is a well-instrumented one. Template looks good ✓ Problem: observed and measured, not theoretical. The design doc carries browser profiling (the 104 ms long task dominated by Direction: aligned. Composer responsiveness during long streams is a real user-facing problem for Web Shell, and the area is actively invested in — the reference product's changelog carries a directly analogous item ("Improved fullscreen streaming: long sessions stay responsive because the whole conversation is no longer re-normalized on every update"). Size: this is a cross-package change ( Approach: the scope feels justified rather than padded — each mechanism maps to a measured hotspot (dispatch batching, 50 ms render throttle with an input-quiet window, deferred snapshots, COW index + WeakMap-normalized tool content, timer keyed on presence, bounded Markdown parsing, tail-only fast paths), and the design doc names explicit non-goals (no incremental projector, no worker, no daemon event-ordering changes). Two things I'll look at closely in code review: the unexplained Risk: no elevated risk signals — none of the changed files match the revert-correlated high-risk paths. Moving on to code review. 🔍 Heads-up: because Stage 0 flagged this for maintainer awareness, the final decision here caps at a defer to maintainer rather than an auto-approve, regardless of how the review goes. 中文说明感谢贡献!这个 PR 的证据非常充分。 模板完整 ✓ 问题:已观测且有实测数据,不是理论性问题。设计文档附有浏览器 profiling(104 ms 长任务主要由 方向:对齐。长流式输出期间输入框的响应性是 Web Shell 真实的用户体验问题,且该方向有持续投入——参考产品的 changelog 有直接类似的条目("Improved fullscreen streaming: long sessions stay responsive because the whole conversation is no longer re-normalized on every update")。 规模:这是跨包改动( 方案:范围合理而非堆砌——每个机制都对应一个实测热点(派发批处理、50 ms 渲染节流 + 输入静默窗口、deferred 快照、COW 索引 + WeakMap 规范化工具内容、按"是否有内容"挂载计时器、有界的 Markdown 解析、仅尾部快路径),设计文档也明确列出了非目标(不做增量投影器、不用 Worker、不改 daemon 事件顺序)。代码审查阶段会重点看两处:投影快路径中未加解释的 风险:无升级风险信号——变更文件均未命中与 revert 相关的高风险路径。 进入代码审查 🔍 提示:由于 Stage 0 已提请维护者关注,无论审查结果如何,最终决定上限为转交维护者确认,不会自动批准。 — Qwen Code · qwen3.8-max Reviewed at |
🖼️ web-shell visual previewRendered against a mock daemon (no real backend): the PR base vs this PR head Screenshots · before / afterFull-resolution recordings (.webm) are attached to the workflow run. — Qwen Code · web-shell visuals |
Code reviewMy independent take on this problem would be: coalesce store notifications with a time-based, input-aware throttle; use No correctness blockers found. Findings worth the author's and maintainer's attention, none blocking:
The test additions are genuinely pinning: identity preservation across streamed deltas, throttle window boundaries, input-quiet deferral with the 250 ms starvation cap, session-switch and same-session-reset staleness, abandoned concurrent renders, timer reuse, the plain-text → settled-Markdown transition, and the COW index sharing/copy contract at the SDK level. sequenceDiagram
participant P1 as Daemon SSE events
participant P2 as DaemonSessionProvider
participant P3 as Transcript store
participant P4 as Render throttle hook
participant P5 as useMessages projection
participant P6 as MessageList fast path
participant P7 as Composer input
P1->>P2: chunk arrives
P2->>P2: batch in a 16 ms window
P2->>P3: one dispatch per window
P3->>P4: notify
P4->>P4: 50 ms throttle plus input quiet window
P4->>P5: deferred snapshot, stale ones rejected
P5->>P6: stable history prefix plus new tail
P6->>P6: swap the tail row, keep virtualizer keys
P7->>P4: beforeinput defers notify, capped at 250 ms
Files changed (24 of 24 shown)
Testing evidenceThis is an unattended CI run — PR code is never executed here. Evidence is the PR's own CI on the reviewed commit, fetched via the API at review time; the main unit suite is still running and the table updates in place when CI settles. No red checks at review time. Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 What CI can and cannot settle here: the unit and DOM suites pin the mechanisms — identity preservation, throttle boundaries, staleness rejection, the plain-text → Markdown transition — and they run on the ubuntu job above. They do not pin the central claim, which is the performance win itself: the committed deterministic perf scenario is opt-in ( 中文说明代码审查我对这个问题的独立方案是:用带时间窗口、感知输入的节流合并 store 通知;用 未发现正确性阻塞问题。以下发现值得作者和维护者关注,均不阻塞:
新增测试确实在"钉住"行为:流式增量间的身份保持、节流窗口边界、输入静默延迟与 250 ms 饥饿上限、会话切换和同会话重置的过期处理、被放弃的并发渲染、计时器复用、纯文本到 Markdown 的收敛过渡,以及 SDK 层 COW 索引的共享/复制契约。 (时序图见英文正文,流程为:SSE 事件 → 16 ms 批派发 → store → 50 ms 节流 + 输入静默窗口 → deferred 快照 → 稳定前缀投影 → 仅替换尾行;输入事件可延迟通知但上限 250 ms。) 测试证据这是无人值守 CI 运行——此处绝不执行 PR 代码。证据为审查时通过 API 获取的该提交自身 CI 结果;主单测套件仍在运行,表格会在 CI 结束后原地更新。审查时无红色检查。 CI 能钉住的是各机制(身份保持、节流边界、过期拒绝、纯文本→Markdown 过渡,见 ubuntu 任务);钉不住的是核心主张即性能收益本身:确定性 perf 场景是可选的( — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 3/5 — clean review with no blocking findings, but the Stage 0 escalation (cross-package core surface at ~723 production lines) caps this at a maintainer sign-off rather than an auto-approve. Stepping back: this is a strong PR. The approach matches — and arguably exceeds — what I would have proposed independently, every mechanism traces to a measured hotspot, the design doc names its non-goals, and the test suite pins the actual invariants (identity, throttle bounds, staleness, fallbacks) rather than smoke. The author has a long, mostly-merged track record on exactly this surface, and the problem is real and user-facing, with direction corroborated by the reference product's changelog. Why not approve from here:
⏸️ Deferring to @chiga0 — needs a human call on two things: (a) sign-off on the daemon/ui contract change and the 16 ms dispatch batch for downstream SDK consumers, and (b) whether to require an A/B run first — No approval or rejection posted — the review stands on the two comments above. 中文说明置信度:3/5 —— 审查干净、无阻塞发现,但 Stage 0 升级(跨核心包、约 723 行生产逻辑)将本 PR 的最终决定上限设为维护者签核,而非自动批准。 退一步看:这是一个高质量的 PR。方案与我独立设想的一致甚至更完整——每个机制都能追溯到实测热点,设计文档明确了非目标,测试钉住的是真实不变量(身份、节流边界、过期处理、兜底)而非表面行为。作者在这一领域有大量已合并的记录,问题真实且面向用户,方向也有参考产品 changelog 的佐证。 不在此批准的原因:
⏸️ 转交 @chiga0 —— 需要人工决定两件事:(a) 对 daemon/ui 契约变化和 16 ms 派发批处理在下游 SDK 消费方上的影响签核;(b) 是否先要求一次 A/B 验证——对自带的 未发布批准或拒绝——审查结论见上面两条评论。 — Qwen Code · qwen3.8-max Reviewed at |
ytahdn
left a comment
There was a problem hiding this comment.
代码审查 / Code Review
结论 / Verdict: 💬 评论(无 blocking,倾向确认个别项后批准)/ Comment (no blockers; leaning approve after a couple of confirmations)
中文
总体印象:这是一个设计扎实、测试覆盖充分的性能优化。设计文档清晰说明了问题、方案与取舍,确定性负载下的前后对比数据可信(流式阶段 17.8s→8.0s、长任务 92→1)。优化手段(批量投递、useDeferredValue 延迟渲染、COW 身份稳定、Markdown 解析预算)都针对实测热点,而非凭直觉。
做得好的地方 🎉
useAnimationFrameTranscriptBlocks用useDeferredValue+ sessionId/blockIndexById 双身份兜底,既保输入流畅,又从根上杜绝“重置后显示已丢弃 transcript / 切会话串数据”,配套测试到位。Markdown大流降级为纯文本走 React 文本插值,自动转义、无 XSS;transformMarkdown自定义钩子在纯文本模式下仍被调用,可定制性没丢。- 边界覆盖用心:abandoned concurrent render 不污染缓存、undefined/空 content 安全降级、
ThinkingMessage计时器不再每 chunk 重建 interval。 - 已核实:
transcript.ts中 blocks 与 index 的所有权拆分是完整正确的——唯一直接写 index 的appendBlock已调用takeBlockIndexOwnership,其余路径均整体 rebuild 并注册所有权,getWritableBlockById的原地替换不改变 index。我最初担心的“遗漏写点导致 COW index 跨快照污染”不成立。
行内评论(见 line comments)
- 🟡
useMessages.ts—"insight_"魔法字符串需解释/具名化。 - 🟡
MessageList.tsx— render 期写 ref 的反模式,建议固化不变量/收敛为 hook。 - 🟢
package.json— 确认cross-env已在 devDependencies。
建议 💡
Markdown:流结束瞬间会对超大文档做一次完整解析,可能产生单次卡顿;设计文档已列为非目标,未来可考虑分片解析。- e2e perf spec:目前只做正确性断言 + 记录指标,不设性能阈值,无法在 CI 中作为回归门禁;可考虑加软阈值或趋势对比。
transcriptToMessages:Object.freeze(content)只冻结外层数组;WeakMap 按 block 引用缓存依赖“store 永不原地改 block”这一约定,建议在注释里写明。
English
Overall: A well-designed, thoroughly tested performance optimization. The design doc clearly states the problem, approach, and trade-offs, and the deterministic before/after numbers are credible (stream phase 17.8s→8.0s, long tasks 92→1). The techniques (batched delivery, useDeferredValue deferral, COW identity stability, Markdown parse budget) target measured hotspots rather than guesswork.
What's done well 🎉
useAnimationFrameTranscriptBlocksusesuseDeferredValuewith a sessionId/blockIndexById dual-identity fallback: keeps input fluid while structurally preventing "showing discarded transcript after reset" and cross-session bleed; tests cover both.- The
Markdownlarge-stream plain-text fallback uses React text interpolation (auto-escaped, no XSS), and still invokes thetransformMarkdowncustomization hook in plain-text mode. - Careful edge coverage: abandoned concurrent renders don't pollute caches, undefined/empty content degrades safely, and the
ThinkingMessagetimer no longer recreates its interval per chunk. - Verified: the blocks/index ownership split in
transcript.tsis complete and correct —appendBlock(the only direct index writer) callstakeBlockIndexOwnership, all other paths rebuild the index wholesale and register ownership, andgetWritableBlockById's in-place replacement doesn't change the index. My initial worry about a missed write site poisoning a shared COW index does not hold.
Inline comments (see line comments)
- 🟡
useMessages.ts— the"insight_"magic string needs an explanation / a named constant. - 🟡
MessageList.tsx— ref writes during render are an anti-pattern; please pin down the invariants / collapse into a hook. - 🟢
package.json— confirmcross-envis already a devDependency.
Suggestions 💡
Markdown: the one-time full parse of a very large document at stream end may jank; acknowledged as a non-goal, but consider chunked parsing later.- e2e perf spec: it only asserts correctness and records metrics, with no perf threshold, so it can't gate regressions in CI; consider a soft threshold or trend comparison.
transcriptToMessages:Object.freeze(content)freezes only the outer array; the block-keyed WeakMap cache relies on the "store never mutates a block in place" contract — worth spelling out in a comment.
chiga0
left a comment
There was a problem hiding this comment.
Review: perf(web-shell): keep streaming output responsive
This is a well-scoped performance PR applying several complementary techniques — 16 ms transcript-event batching, 50 ms render throttle with input-quiet window, useDeferredValue with session/blockIndex identity guard, stable projection-prefix identity, WeakMap tool-content caching, and a streaming Markdown size cap with plain-text fallback. The architecture is sound and the 258-line e2e perf spec (@perf) gives solid regression coverage.
Two minor findings (both flagged in the author's own self-review, confirmed below) need attention before merge.
Cross-Validation
| # | Finding | Author | My Assessment |
|---|---|---|---|
| C1 | useMessages.ts:107 — '"insight_' magic string; no comment explaining why it disqualifies prefix reuse |
ytahdn ✅ | Confirmed — inline comment posted. Named constant + explanation required; correctness-guard vs. heuristic distinction is unclear. |
| C2 | MessageList.tsx:3233 — multiple *Cache.current / reusedVisibleStreamingTailRef.current written during useMemo render phase |
ytahdn ✅ | Confirmed — inline comment posted. Safe via previousMessagesRef / useLayoutEffect guard (tests validate), but the invariant must be documented in-code for future maintainers. |
| C3 | web-shell/package.json:29 — cross-env used in test:e2e:perf without explicit dep declaration |
ytahdn ❓ (verify) | Non-issue — cross-env@^7.0.3 is declared in the monorepo root package.json#devDependencies and is hoisted. No per-package redeclaration needed. |
Additional Audit Coverage
blockIndexById COW ownership — Traced all mutation sites: appendBlock (calls takeBlockIndexOwnership ✓), discardToolBlock (rebuilds index + registers ownership ✓), trimTranscriptState (rebuilds + registers ✓), truncateTranscriptBeforeBlock (calls rebuildTranscriptIndexes + registers ✓). getWritableBlockById mutates only block content, leaving the index reference untouched (correct: no index entry changes on content-only edits). Object.freeze(result.blockIndexById) in FREEZE_TRANSCRIPT_COLLECTIONS seals the object for downstream consumers. Ownership accounting correct throughout.
useDeferredValue session/blockIndex guard — Verified the two-condition guard (deferred.sessionId === sessionId && deferred.blockIndexById === live.blockIndexById) covers all transitions:
- Session switch →
sessionIdmismatch → fall through tolive.blocksimmediately ✓ - Same-session store reset (e.g., truncation) → new
blockIndexByIdobject → mismatch → live ✓ - Streaming text growth on existing block →
takeBlocksOwnershiponly;blockIndexByIdreference unchanged → deferred path active (input stays responsive) ✓ - New block appended →
takeBlockIndexOwnershipcalled → new object → live ✓ (structural change, not just streaming text)
Markdown plain-text XSS safety — <pre>{renderedContent}</pre> renders renderedContent as a React text node (not dangerouslySetInnerHTML). React escapes all HTML entities. The transformMarkdown result is also text-interpolated, not injected as raw HTML. No XSS risk.
ThinkingMessage timer stabilization — useEffect dependency changed from content (string) to hasContent (boolean). Timer interval is now created/destroyed only when thinking starts or stops, not on every streaming token. Correct optimization; no timer leak path.
reuseUnchangedProjectedPrefix computational cost — transcriptBlocksToLocalizedMessages(blocks, t) is always called before the fast path can skip downstream work. The O(transcript) projection is paid on every render; what the prefix-reuse buys is stable React object identity for unchanged history messages (avoiding downstream re-renders) and cache hits in MessageList.tsx for mergeCompactToolGroups / attachTurnOutputs / applyTurnCollapse. Consistent with the PR's own profiling note that projection contributes only ~2.5% of main-thread time — the list-processing pipeline is the real target.
Verdict
COMMENT — two minor findings (C1 and C2), both non-blocking on correctness but important for long-term maintainability. Core perf strategy is architecturally sound; no correctness, security, or regression issues found.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
中文说明
已审查——无阻断问题。 建议见行内评论。
— qwen3.8-max via Qwen Code /review (v0.21.13)
chiga0
left a comment
There was a problem hiding this comment.
Code Review Overview (AI Generated)
PR: #9405 perf(web-shell): keep streaming output responsive
Type: Performance Optimization
Change size: +1680/-109 across 24 files
HEAD: 08ad3ab8d
Findings Summary
- Critical/Major: 0 (all prior findings resolved at HEAD)
- Minor/Nit: 2 (new, non-blocking)
Key Observations
A well-designed, thoroughly benchmarked performance PR. The stack of complementary techniques — 16 ms event batching, 50 ms render throttle with input-quiet window, useDeferredValue + session/blockIndexById identity guard, stable projection-prefix identity, WeakMap tool-content caching, and a 32 KB Markdown parse cap — collectively reduce the stream phase from 17.8 s to ~8 s and long-task count from 92 to 1 under a deterministic 5000-turn / 400-chunk workload. All critical findings from earlier rounds (timestamp guards disabling fast paths in production, E2E promptId mismatch, older-history anchor race, INSIGHT_CONTENT_MARKER undocumented, render-phase ref-write invariant undocumented, 16 ms batch untested) are confirmed fixed at HEAD.
Cross-Validation
| # | Finding | Prior Review | My Assessment |
|---|---|---|---|
| C1 | useMessages.ts — serverTimestamp guard in reuseUnchangedProjectedPrefix disabled prefix reuse on every live delta |
qwen-code-ci-bot R1-3 | ✅ Confirmed fixed — guard removed at HEAD; regression test explicitly varies serverTimestamp: 1_001→1_002 and verifies fast path still fires |
| C2 | MessageList.tsx — timestamp comparison in isStreamingTailContentOnlyUpdate disabled merged/display/visible fast paths on every live frame |
qwen-code-ci-bot R1-1 | ✅ Confirmed fixed — comparison removed; DOM regression test with timestamp 1001→1002 confirms fast path engages |
| C3 | web-shell.stream-performance.spec.ts — terminal event promptId: 'performance-prompt' mismatched mock daemon's assigned id; turn never settled |
qwen-code-ci-bot R1-2 | ✅ Confirmed fixed — promptId corrected to 'prompt-e2e'; streaming-plain-text-disappears assertion added to cover turn-settle render pass |
| C4 | DaemonSessionProvider.tsx:832 — 16 ms batch window untested; setTimeout(0) revert would be invisible |
qwen-code-ci-bot R1-4 | ✅ Confirmed fixed — fake-timer test added: two chunks 5 ms apart, no dispatch before 16 ms, single size-2 batch after window closes |
| C5 | MessageList.tsx — older-history anchor cleared after one frame before parent could commit prepended messages (race → wrong scroll position) |
wenshao CR | ✅ Confirmed fixed — waitForPrepend loop waits for message-count change, 30-frame upper bound, new-load cancels pending frame; regression test with 2-frame prepend delay added |
| C6 | useMessages.ts — INSIGHT_CONTENT_MARKER literal undocumented |
ytahdn self-review / chiga0 | ✅ Confirmed fixed — named constant extracted; comment explains insight JSON projects one growing text block into multiple messages, making prefix reuse unsafe |
| C7 | MessageList.tsx:3239 — render-phase cache writes (*Cache.current, reusedVisibleStreamingTailRef) lack invariant documentation |
ytahdn self-review / chiga0 | ✅ Confirmed fixed — inline comment explains these caches are keyed to previousMessagesRef (post-commit identity), so abandoned renders cannot advance the guard and their writes are harmlessly discarded by the next committed render |
| C8 | web-shell/package.json — cross-env used in new perf script without local dep declaration |
ytahdn self-review | ✅ Non-issue — cross-env@^7.0.3 declared in repo-root package.json devDependencies, hoisted across workspace |
New Findings (non-blocking)
Nit-1: mergedMessages fast path has an assistant-only role guard while isStreamingTailContentOnlyUpdate returns true for both assistant and thinking tails. For thinking streams, streamingTailContentOnly === true but mergedMessages and displayItems fast paths both gate on tail?.role === 'assistant' and fall through to full recomputation. The optimization is skipped for the thinking-stream case — correct behavior, but a minor performance gap that could be closed in a follow-up.
Nit-2: normalizedToolContentCache (module-level WeakMap) freezes only the outer array (Object.freeze(content)), not its DaemonMessageToolCallContent elements. The cache correctly relies on the invariant that the transcript store never mutates a block in place (COW discipline). The comment added in discardToolBlock/trimTranscriptState about ownership tracking is good; a parallel one-liner in transcriptToMessages.ts noting the shallow-freeze and the COW assumption would close the doc gap ytahdn flagged.
Additional Audit Coverage
Areas independently verified beyond existing findings:
blockIndexByIdCOW ownership completeness — traced all 4 mutation paths:appendBlock(callstakeBlockIndexOwnership✓),discardToolBlock(rebuilds + registers ✓),trimTranscriptState(rebuilds + registers ✓),truncateTranscriptBeforeBlock(callsrebuildTranscriptIndexesthen registers ✓).getWritableBlockByIdmutates only block fields, not the index — no ownership call needed. ✓hasPendingInput()API availability —navigator.scheduling.isInputPending()is Chromium-only; the optional-chaining fallback correctly treats absence asfalse(no pending input), which keeps the throttle conservative on Firefox/Safari. ✓renderStreamingPlainTextwithtransformMarkdown—renderedContentis the source-transformed text (not raw content). If a customtransformMarkdownmodifies the string, the<pre>displays the transformed version. This is intentional — thetransformMarkdownhook applies before the Markdown renderer and the plain-text path preserves that contract. ✓normalizeToolContentWeakMap GC safety — module-level WeakMap keyed by block objects. Blocks are garbage-collected with the transcript, so no permanent retention. Keys never collide because COW gives each mutated block a new identity. ✓pendingSinceTsmax-deferral reset — set when the first rAF is scheduled, cleared when dispatch fires; re-schedules insidedispatchWhenDuedo not reset it, so the 250 ms cap is measured from first deferral, not from each rescheduled frame. ✓clientReceivedAtvsserverTimestampstability —clientReceivedAtis set once when a block first arrives;serverTimestampwas updated on every SSE delta (now removed). The survivingclientReceivedAtguard is correct and stable. ✓
Final Verdict
✅ Approve. All critical and suggestion findings are confirmed resolved at HEAD. The performance improvements are substantial (2× stream-phase speedup, 92→1 long tasks), the test coverage is thorough, and the two remaining Nits are cosmetic follow-up candidates. Ready to merge.
This review was generated by QoderWork AI
What this PR does
Reduces Web Shell main-thread contention during long thinking and assistant streams by batching transcript delivery, prioritizing composer input, preserving stable history identities, limiting repeated tail derivations, and avoiding expensive Markdown parsing while very large content is still growing. Adds a deterministic browser performance scenario so the behavior can be reproduced without relying on model output.
Why it's needed
Long retained transcripts caused each stream delta to repeat whole-history projection, grouping, virtualization, and Markdown work, which competed with editor input. Under the fixed 5,000-turn and 400-chunk workload, current main measured a median 17.8s stream phase, 600ms typing overhead, and 92 long tasks; the optimized path measured about 8.0s, 327ms, and 1 long task across three runs.
Reviewer Test Plan
How to verify
Open a Web Shell session with a long transcript and stream a large thinking or assistant response while continuously typing in the composer. Verify typed characters are not dropped, the composer remains responsive, the final response sentinel appears, short streams keep live Markdown rendering, large streams restore full Markdown after settling, compact mode updates the active tail, and resetting a session never displays discarded transcript blocks. The opt-in deterministic performance scenario should complete with all correctness assertions and print timing metrics for comparison.
Evidence (Before & After)
The deterministic workload uses 5,000 retained turns, 400 chunks, and a 10ms chunk interval.
The final post-rebase verification completed in 6,950ms with 291ms typing overhead and one 56ms long task. These figures compare practical versions rather than a strict isolated ablation because the original worktree baseline and latest main were not identical commits.
Tested on
Environment (optional)
macOS, Node.js 22.14.0, Chromium via Playwright.
Risk & Scope
Linked Issues
N/A
中文说明
本 PR 做了什么
通过批量投递 transcript、优先保障输入框交互、维持历史消息对象身份稳定、减少尾部重复派生,并在超大内容持续增长时避免昂贵的 Markdown 重复解析,降低 Web Shell 在长 thinking 和 assistant 流式输出期间的主线程竞争。同时增加确定性的浏览器性能场景,使测试不再依赖模型输出的随机性。
为什么需要
当 transcript 保留大量历史记录时,每个流式增量都会重复执行整段历史的投影、分组、虚拟列表和 Markdown 工作,与编辑器输入争抢主线程。在固定的 5000 轮历史、400 个 chunk 负载下,当前 main 的中位结果为流式阶段 17.8 秒、输入额外开销 600 毫秒、92 个长任务;优化方案三次运行的中位结果约为 8.0 秒、327 毫秒和 1 个长任务。
Reviewer 测试计划
如何验证
打开包含很长 transcript 的 Web Shell 会话,在持续输出大型 thinking 或 assistant 内容时连续在输入框中输入。确认字符不会丢失、输入框保持响应、最终响应标记可见、短流仍实时渲染 Markdown、大型流结束后恢复完整 Markdown、紧凑模式持续更新活动尾部,并且同一会话重置后不会显示已丢弃的 transcript block。可选的确定性性能场景应通过全部正确性断言并输出可比较的时序指标。
证据(前后对比)
确定性负载固定为 5000 轮历史、400 个 chunk、每个 chunk 间隔 10 毫秒。
最终 rebase 后的一次验证结果为流式阶段 6,950 毫秒、输入额外开销 291 毫秒、1 个 56 毫秒长任务。由于最初 worktree 的基线与最新 main 并非完全相同的提交,上述数据是实际版本对比,不是严格的单变量消融实验。
测试平台
环境(可选)
macOS、Node.js 22.14.0、Playwright Chromium。
风险与范围
关联 Issue
无。