DSH / Atlas
2026-08-03implementedtesting

Frame-coalesced reasoning-chunk publication and browser stress validation

推理(reasoning)分片的逐帧累计发布与浏览器压力验证

Long reasoning streams continuously produce large numbers of process-local `assistant/live-chunk` updates before one durable settlement. Each update must remain ordered and be folded into the Assistant Definition to preserve live completeness, while the settlement embeds the exact stream for replay; React, however, needs only the current accumulated result, not every intermediate state within one browser frame. Each

English

Problem

Long reasoning streams continuously produce large numbers of process-local assistant/live-chunk updates before one durable settlement. Each update must remain ordered and be folded into the Assistant Definition to preserve live completeness, while the settlement embeds the exact stream for replay; React, however, needs only the current accumulated result, not every intermediate state within one browser frame.

Each yield in an async stream can create a new microtask boundary, so Notifier.markDirty() backed only by microtask batching degrades into rebuilding a ConversationSnapshot, notifying useSyncExternalStore, and running a React render for every chunk. Even with the live Think row collapsed, 100,000 reasoning chunks can overwhelm the main thread with reconciliation, commit, and layout work. The performance boundary must sit between session ingestion and React publication; the test must preserve every raw event and distinguish one-batch rendering cost from an unbounded transport backlog.

Decision

The Session Controller appends every Client-only live chunk to its event source, and Conversation immediately folds it into each matching Definition State. Chat and Trajectory Definitions request animation-frame publication for visible block-start, text-delta, reasoning-delta, tool-call-delta, and block-end chunks; the first change schedules one requestAnimationFrame, later chunks continue updating State, and the frame callback materializes one accumulated snapshot from the latest State. usage and finish request no publication. The durable assistant/message or assistant/attempt settlement publishes immediately and reproduces the same final stream during history replay.

BoundConversation owns one pending frame per Session. Ordinary structural events and durable settlements request immediate publication, flush the latest assembled State, and make a later frame callback harmless because no dirty Context remains. Environments without requestAnimationFrame publish immediately. A settlement may skip one intermediate partial that has not yet appeared, while the published final content and durable embedded stream remain complete.

Keeping the live Think row horizontally pinned to the end of the accumulated text is purely visual alignment and does not require synchronous layout reads on every React commit. An in-component scheduler coalesces consecutive requests into one update every three frames, reads scrollWidth and clientWidth from the latest DOM, and updates scrollLeft directly to the latest position; the fixed visual cadence keeps summary changes readable without allowing browser smooth-scroll animations to accumulate. This throttling applies only to Think's horizontal summary and does not delay Chat body scrolling, history-prepend anchoring, or user-triggered scrollIntoView.

pnpm run test:web:stress remains keyless, opt-in browser performance evidence. A test-owned model adapter emits 100,000 reasoning-delta chunks through the real Host, Gateway, WebSocket, Session reduction, and live Think row. Each 128-chunk batch waits for one browser timer turn before the next batch, so a 50-millisecond heartbeat and a pre-scheduled DOM event measure one bounded ingestion/render interval instead of accumulated socket backlog against a 250-millisecond budget. A terminal marker proves that all chunks reached the UI. DSH_WEB_STRESS_HEADFUL=1 lets developers profile the same scenario in a visible browser with the Performance panel. The stress lane is evidence for manual performance diagnosis and fix acceptance, not a default CI gate or a substitute for deterministic scheduling unit tests.

Focused tests pin Notifier's per-frame coalescing, structural-event preemption, invalidated callbacks, and no-rAF fallback, and prove at the Session layer that a frame publishes the latest accumulated text only once and that finalization is not followed by a duplicate notification from a stale frame callback. The opt-in browser case owns adapter pacing, exact event count, and terminal-marker delivery without bringing the 100,000-chunk workload into the default test suites.

Alternatives considered

React transitions, deferred values, or component throttling applied to snapshots. Rejected: the session source would still notify useSyncExternalStore for every chunk, the React render has already occurred before a component decides to defer display, and multiple components consuming the same snapshot would each need to implement the strategy. Visual tail-following throttling for the Think summary occurs after snapshot publication and only reduces the frequency of synchronous layout; it does not implement the data-publication policy.

Dropping or sampling live chunks before Definition folding. Rejected: the live accumulated state would diverge from the durable embedded stream and could omit visible intermediate content. Compact durable storage and frame-coalesced React publication solve different costs.

Microtask batching alone. Rejected: consecutive asynchronous yield operations can drain the microtask queue between adjacent chunks, making microtask batching approximate one notification per chunk.

Pacing the test producer by animation frames. Rejected: the producer would slow whenever rendering slowed, giving the page implicit backpressure absent from a real network stream and masking main-thread starvation.

Sending the former browser-local rate through the WebSocket without an acknowledgment. Rejected: queued transport work accumulates faster than a model can produce it and turns the result into a socket-backlog measurement. A browser timer acknowledgment bounds each batch while the heartbeat still detects a batch that starves the page.

A live external model or recorded HTTP byte stream. Rejected: an external model is nondeterministic, and an HTTP recording would add a second fixture format without improving the target assertion. The test-owned adapter preserves individual chunks through the real Host and browser transport while controlling the workload.

Consequences

The publication rate of streaming ConversationSnapshot objects is bounded by the browser's paint rate, so React handles at most one accumulated partial containing all received text per frame; structural events can still publish sooner. Ingestion, ordering, logging, string concatenation, and accumulator updates still run for every raw chunk, so this decision reduces snapshot rebuilding and React work without pretending to solve raw-stream parsing cost.

Horizontal layout reads and writes for the collapsed Think summary run at most once every three frames, and each update moves the summary directly to the latest position; React still commits accumulated snapshots normally, and the summary returns to the first line at finalization. This local visual policy does not change the immediacy of body scrolling or user interactions.

The browser stress lane continues to provide a responsiveness signal from the real assembled application and carrier plus an entry point for visible profiling, but hardware and scheduling differences make it suitable only as explicit performance evidence. Deterministic focused tests guard publication counts, accumulated content, and preemption order, while the default test lanes remain fast.

中文

问题

长 reasoning stream 会在一个持久 settlement 前连续产生大量进程本地 assistant/live-chunk update。每个 update 都必须保持有序并折叠进 Assistant Definition,以保留实时完整性;settlement 则嵌入精确 stream 供 replay。React 只需要看到当前累计结果,不需要观察同一浏览器帧内每个中间态。

异步流的每次 yield 都可能形成新的微任务边界,因此仅靠微任务合批的 Notifier.markDirty() 会退化为每个分片重建一次 ConversationSnapshot、通知一次 useSyncExternalStore 并运行一次 React render。即使实时 Think 行保持折叠,100,000 个推理分片仍会让协调、提交和布局工作压住主线程。性能边界必须位于会话接收与 React 发布之间;测试必须保留每个原始事件,并区分单批渲染成本与无限累积的 transport backlog。

决策

Session Controller 把每个 Client-only live chunk 追加到 event source,Conversation 会立即把它折叠进每个匹配 Definition State。Chat 与 Trajectory Definition 为可见 block-start、text-delta、reasoning-delta、tool-call-delta 与 block-end chunk 请求 animation-frame publication;第一项变化调度一次 requestAnimationFrame,后续 chunk 继续更新 State,frame callback 再从最新 State materialize 一个累计 snapshot。usage 与 finish 不请求 publication。持久 assistant/message 或 assistant/attempt settlement 会立即发布,并在历史 replay 中复现同一最终 stream。

BoundConversation 为每个 Session 拥有一个 pending frame。普通结构 event 与持久 settlement 请求 immediate publication,flush 最新 assembled State,并让之后的 frame callback 因没有 dirty Context 而不产生影响。没有 requestAnimationFrame 的环境会立即发布。Settlement 可以跳过一个尚未显示的中间 partial,但发布的最终 content 与持久嵌入式 stream 保持完整。

实时 Think 行对累计文本的横向跟尾属于纯视觉对齐,不需要在每次 React 提交中同步读取布局。组件内调度器将连续请求合并为每三帧一次,从最新 DOM 读取 scrollWidth 和 clientWidth 并将 scrollLeft 直接更新到最新位置;固定的视觉节奏让摘要变化可读,又不会积压浏览器平滑滚动动画。该节流只作用于 Think 的横向摘要,不延迟 Chat 正文滚动、历史 prepend 锚定或用户触发的 scrollIntoView。

pnpm run test:web:stress 保留为无密钥、需显式启用的浏览器性能证据。测试持有的模型 adapter 通过真实 Host、Gateway、WebSocket、Session 归并和实时 Think 行发出 100,000 个 reasoning-delta chunk。每批 128 个 chunk 会等待一次浏览器 timer 轮次后再发下一批,因此 50 毫秒心跳与预先调度的 DOM 事件按 250 毫秒预算测量一个有界接收/渲染区间,而不是累积的 socket backlog。结尾标记证明所有 chunk 都已到达 UI。DSH_WEB_STRESS_HEADFUL=1 允许开发者在可见浏览器中使用 Performance 面板分析同一场景。该压力车道是手动性能诊断与修复验收的证据,不是默认 CI 门禁,也不替代确定性的调度单元测试。

聚焦测试固定 Notifier 的逐帧合并、结构事件抢占、失效回调和无 rAF 回退,并在 Session 层证明一帧只发布一次最新累计文本且定稿不会被旧帧回调重复通知。按需启用的浏览器用例持有 adapter 节奏、精确事件数和结尾标记交付,不把 100,000 分片工作负载带入默认测试套件。

曾考虑的替代方案

在 React 内对快照使用 transition、deferred value 或组件节流。 不予采纳:会话源仍会逐分片通知 useSyncExternalStore,React render 在组件决定延后展示之前已经发生,且多个消费同一快照的组件需要重复实现策略。Think 摘要的视觉跟尾节流位于快照发布之后,只减少同步布局频率,不承担数据发布策略。

在 Definition fold 前丢弃或抽样 live chunk。 不予采纳:实时累计 State 会与持久嵌入式 stream 分歧,并可能省略可见中间内容。紧凑持久存储与逐 frame 合并 React publication 解决的是不同成本。

只使用微任务合批。 不予采纳:连续异步 yield 会在相邻分片间排空微任务队列,使微任务合批近似退化为每个分片通知一次。

按动画帧控制测试生产方节奏。 不予采纳:生产方会在渲染变慢时同步减速,使页面获得真实网络流不存在的隐式背压,并掩盖主线程饥饿。

不等待确认就把原有浏览器本地速率发入 WebSocket。 不予采纳:transport 工作的排队速度会超过模型可能达到的产出速度,使结果变成 socket backlog 测量。浏览器 timer 确认限制每批工作量,同时 heartbeat 仍能发现一批数据使页面饥饿。

真实外部模型或录制的 HTTP 字节流。 不予采纳:外部模型不具确定性,HTTP 录制还会增加第二种 fixture 格式,却不会改进目标断言。测试持有的 adapter 让每个 chunk 经过真实 Host 与浏览器 transport,同时控制工作负载。

后果

流式 ConversationSnapshot 的发布频率受浏览器绘制频率约束,React 每帧至多处理一个包含全部已接收文本的累计 partial;结构事件仍可更快发布。接收、排序、日志记录、字符串拼接和累积器更新仍按原始分片执行,因此该决策降低的是快照重建与 React 工作,不把原始流解析成本伪装成已解决。

折叠 Think 摘要的横向布局读写最多每三帧执行一次,并直接追上该时刻的最新位置;React 仍按累计快照正常提交,定稿时摘要恢复到首行。该局部视觉策略不会改变正文滚动和用户交互的即时性。

浏览器压力车道继续提供真实组装应用与载体上的响应性信号和可见 profiling 入口,但硬件与调度差异使其只适合作为显式性能证据。确定性的 focused tests 负责守住发布次数、累计内容与抢占顺序,默认测试车道保持快速。