DeepSeek Harness 流式发布优化:reasoning 分片的帧级合并发布与浏览器压力验证
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
长推理流(reasoning stream)在运行时会连续产生海量assistant/chunk事件。在 DeepSeek Harness 的 Web 客户端中,这些事件既要被完整接收、排序、落日志并折叠进PartialAccumulator,又要避免让 React 为每个分片都重建快照、重跑渲染,把主线程压垮。本文基于仓库中的 Agent Note(.agents/notes/implemented/testing/2026-08-03-opt-in-reasoning-chunk-browser-stress.md)展开,讲解Notifier的逐帧(per-frame)合并发布机制、Think 横向摘要的视觉节流策略,以及一条无需密钥、可显式开启的浏览器压力测试车道。读完你将理解 DeepSeek Harness 如何在"保留完整原始事件"与"限制 React 发布频率"之间划定性能边界,并掌握pnpm run test:web:stress的用法与判定口径。
问题:100,000 个推理分片如何压垮主线程
当模型长时间输出推理内容时,会话层会收到大量assistant/chunk事件。按照会话正确性的要求,每一个原始事件都必须经历:
- 排序:保证事件序列与产生顺序一致;
- 日志记录:写入会话事件日志以便重放;
- 折叠进
PartialAccumulator:累积出当前已接收内容的完整快照,保证重放保真度与最终内容完整性。
但 React 并不需要看到同一浏览器帧内的每一个中间状态——它只需要"当前累计结果"。
关键在于异步流的特性:每次yield都可能形成一个新的微任务边界。因此,仅靠微任务合批(microtask batching)的Notifier.markDirty()在连续异步yield场景下会退化为每个分片一次通知:每来一个分片就重建一次ConversationSnapshot、通知一次useSyncExternalStore、运行一次 React render。即便实时 Think 行处于折叠状态,100,000 个推理分片产生的 reconciliation(协调)、commit(提交)与 layout(布局)工作依然会占满浏览器主线程,造成明显卡顿。
性能边界必须位于"会话接收(session ingestion)"与"React 发布(publication)"之间。不能靠减慢生产方、也不能靠丢弃原始事件来掩盖问题——这两条路都会破坏会话数据的权威性。
决策:帧级合并发布(Frame-Coalesced Publication)
接收端:原始事件一条不丢
会话入口Session.acceptLiveEvent()对每个原始事件立即追加,并同步更新 transcript(文本记录)、PartialAccumulator及其他会话派生状态。也就是说,接收、排序、日志、字符串拼接与累积器更新的成本始终按原始分片逐个执行,这一层不做任何降频或抽样。
发布端:每帧至多一次累计快照
真正被"合并"的是 React 可见的发布动作。可见分片——包括block-start、text-delta、reasoning-delta、tool-call-delta、block-end——通过Notifier.markFrameDirty()发布:
- 第一项变化到达时,调度一次
requestAnimationFrame; - 后续分片只继续更新累积器,不再追加调度;
- 帧回调从最新状态重建一个累计快照,并通知订阅者一次。
而usage、finish以及未知的不可见分片仍保留在事件窗口中,但不触发多余的 React 通知。会话与历史检查共用同一套"可见分片分类"逻辑,保证口径一致。
对应源码位于 packages/api/session-controller/src/client/sessions/notifier.ts。Notifier内部维护三种调度种类('none' | 'microtask' | 'frame')和一个代际标记(scheduleGeneration):
/** Mark the snapshot dirty and publish cumulative state at most once per frame. */ markFrameDirty(): void { this.dirty = true this.notifyPending = true if (this.scheduled !== 'none') return this.schedule(typeof globalThis.requestAnimationFrame === 'function' ? 'frame' : 'microtask') }schedule()每次分配新的generation,回调执行前会校验generation !== this.scheduleGeneration,不匹配则直接放弃——这正是"旧帧回调失效"机制的实现:
private schedule(kind: 'microtask' | 'frame'): void { const generation = ++this.scheduleGeneration this.scheduled = kind const publish = () => { if (generation !== this.scheduleGeneration) return this.scheduled = 'none' this.flush() } if (kind === 'frame') { globalThis.requestAnimationFrame(publish) } else { queueMicrotask(publish) } }flush()遵循"先重建、再通知"的顺序,并且在没有订阅者时保持惰性(dirty 状态留给下次getSnapshot时按需重建):
private flush(): void { if (!this.notifyPending) return if (this.listeners.size === 0) return // lazy this.notifyPending = false if (this.dirty) { this.dirty = false this.rebuild() } notifySubscribers(this.listeners, '[session-controller]') }结构事件的抢占:微任务优于帧
帧级合并不影响结构事件的及时性。普通结构事件继续通过markDirty()在微任务中发布;如果某个定稿消息、工具事件或错误在帧发布尚待执行时到达,微任务会取代它,旧帧回调因代际不匹配而失效。notifyNow()同样会使旧调度失效,从而为受控输入(如输入框 onChange)保留同步回响——否则 React 会把 DOM 回滚到过期值,导致光标跳到末尾。
一个值得注意的语义是:定稿事件到来时,可能跳过一个尚未展示的中间 partial。这是有意为之——发布的定稿内容与原始事件序列保持完整,只是略过了"注定会被覆盖"的中间累积态。
无 rAF 环境的回退
在没有requestAnimationFrame的环境(如部分测试环境、无头场景)中,markFrameDirty()自动退回微任务合批。这一点在源码中体现为:
this.schedule(typeof globalThis.requestAnimationFrame === 'function' ? 'frame' : 'microtask')Think 行横向跟尾:每三帧一次的纯视觉节流
除了数据发布,折叠状态下的实时 Think 行还有一个视觉细节:摘要文本要始终横向钉在累计文本的末尾(tail-following)。文档明确指出,这属于纯视觉对齐,不需要在每次 React 提交时同步读取布局。
具体策略由 Think 组件内部的调度器实现:
- 连续请求被合并为每三帧一次更新;
- 从最新 DOM读取
scrollWidth与clientWidth; - 将
scrollLeft直接更新到最新位置。
固定的视觉节奏(fixed visual cadence)让摘要变化保持可读,同时避免浏览器平滑滚动动画层层积压。这一节流只作用于 Think 的横向摘要,不会延迟 Chat 正文滚动、历史 prepend 锚定或用户触发的scrollIntoView;它发生在快照发布之后,只降低同步布局的频率,不承担数据发布策略。
浏览器压力验证:pnpm run test:web:stress
为什么需要这条车道
确定性调度单元测试能守住"一帧只发布一次""抢占顺序正确"等不变量,但无法回答"真实组装后的浏览器里,100,000 个分片到底卡不卡"。浏览器压力测试车道正是为此设计的显式性能证据(opt-in evidence):它跑在真实装配好的 Web 应用上,测量主线程停顿与交互延迟,供手动性能诊断与修复验收使用——它不是默认 CI 门禁,也不替代确定性单元测试。
运行方式与配置
在仓库根目录执行:
pnpm run test:web:stress对应脚本定义在根 package.json:
"test:web:stress": "npm run build && vitest run --config vitest.web-stress.config.ts"配置位于 vitest.web-stress.config.ts,要点包括:
- 只收集
apps/web/stress-tests/**/*.stress.ts(注释明确说明"no default Vitest config includes *.stress.ts",默认测试套件不会带上它); testTimeout: 600_000(10 分钟)、hookTimeout: 120_000;fileParallelism: false,压力场景串行执行。
需要在可见浏览器里用 Performance 面板分析同一场景时,设置环境变量:
DSH_WEB_STRESS_HEADFUL=1 pnpm run test:web:stress无头与有头模式的分支在测试源码中清晰可见:chromium.launch({ headless: process.env.DSH_WEB_STRESS_HEADFUL !== '1' })。
测试场景:100,000 个 reasoning-delta 分片
压力用例位于 apps/web/stress-tests/reasoning-chunks.stress.ts,核心常量如下:
| 常量 | 值 | 含义 |
|---|---|---|
CHUNK_COUNT | 100_000 | 分片总数 |
CHUNKS_PER_INTERVAL | 128 | 每个时间片注入的分片数 |
CHUNK_INTERVAL_MS | 16 | 分片批次间隔(毫秒) |
MAIN_THREAD_DELAY_BUDGET_MS | 250 | 主线程停顿 / 交互延迟预算(毫秒) |
测试流程(关键步骤):
- 通过
launchWebScaffold()启动 Web 脚手架,注入当前会话fx-alpha,以?fixture参数加载页面——?fixture会启用内存中的 fixture 会话(deterministic session); - 隐藏 fixture 的欢迎覆盖层(
[class*="onboardingOverlay"]),让其下方的生产聊天树保持挂载并运行生产渲染路径; - 调用页面注入的
__fxTiming.startReasoningChunkStorm('fx-alpha', 100_000, 128, 16)启动"分片风暴"; - 等待
[data-variant="think"][data-state="running"]的实时 Think 行出现; - 轮询直至
emitted === 100_000,并断言实时 Think 行的文本最终包含结尾标记REASONING_STRESS_COMPLETE:<turn>:<chunkCount>——这个标记证明事件经过生产会话归并、到达实时 Think 行,而不是只在测试桩层面被消化; - 汇总探针数据并断言。
两个测量探针:主线程停顿与交互延迟
测试在page.evaluate中预埋了两个探针:
- 50ms 心跳(heartbeat):
setInterval每 50ms 记录一次实际 tick 时间,maxDelayMs = max(tickAt - lastTickAt - 50),即事件循环被主线程工作占用的最大延迟; - 预调度 DOM 事件(interaction probe):1 秒后通过
setTimeout派发reasoning-stress-interaction自定义事件,测量从"应处理时刻"到"实际处理时刻"的间隔interactionDelayMs。
两份测量都以250ms 预算为判定线:maxMainThreadDelayMs < 250且interactionDelayMs < 250。同时测试断言:心跳样本数大于 0、无页面错误(pageErrors)、无警告(warnings)。测试尾部会把汇总报告以 JSON 形式写入 stdout:
reasoning-chunk stress report: {"chunkCount":100000,"chunksPerInterval":128,"intervalMs":16,"emitted":100000,...}生产方节奏:刻意独立于绘制
一个容易被忽略的设计是:测试生产方的节奏独立于动画帧。fixture 中的startReasoningChunkStorm(位于 packages/client/connection/src/client/fixture.ts)以setTimeout泵送分片:每个时间片按elapsedIntervals * chunksPerInterval计算"到期应发数",补齐到当前时刻应发的数量,直至发满chunkCount。这种按墙钟时间补发的节奏意味着:
- 渲染变慢时生产方不会同步减速(拒绝隐式背压),从而真实暴露主线程饥饿;
- 与真实网络流到达节奏更接近——真实流不会因为页面卡了而放慢。
同时 fixture 对入口参数做了严格校验:chunkCount、chunksPerInterval、intervalMs必须是正整数安全整数,且同一时刻只允许一个"分片风暴"运行(并发启动直接抛错)。结尾标记会被作为最后一个reasoning-delta的文本注入(index === chunkCount - 1时文本变为\nREASONING_STRESS_COMPLETE:...),这正是测试断言"事件到达实时 Think 行"的依据。
为什么用 fixture 而不是真实模型或录制流
测试场景的载体选择经过了明确权衡:
| 候选方案 | 被拒原因 |
|---|---|
| React transition / deferred value / 组件节流 | 会话源仍会逐分片通知useSyncExternalStore,React render 在组件决定延后展示之前已经发生;多个消费同一快照的组件要各自重复实现 |
| 在接收或日志层丢弃、抽样、拼接原始分片 | 原始assistant/chunk是可重放的会话事实,改动会损失诊断与 UI 保真度,把展示频率策略混入数据权威层 |
| 仅微任务合批 | 连续异步yield会在相邻分片间排空微任务队列,使合批近似退化为每分片一次通知 |
| 按动画帧控制测试生产方 | 生产方随渲染变慢而减速,形成真实网络流不存在的隐式背压,掩盖主线程饥饿 |
| 真实模型或录制的 HTTP/SSE 字节流 | 实时模型不具确定性;HTTP/SSE 录制不会改进目标断言 |
最终采用内存 fixture:它保留"逐个异步会话事件 → 生产客户端归并 → React 渲染路径"的完整链路,同时完全控制工作负载与到达节奏,兼具确定性与真实性。
分层测试策略:确定性与证据分离
整个方案遵循"确定性单元测试守住不变量,压力车道提供浏览器证据"的分层原则:
- 聚焦单元测试(packages/api/session-controller/tests/notifier.client.spec.ts)固定了
Notifier的关键行为:- N 次
markDirty()合并为一次 flush,且"先 rebuild 再 notify"; - 零监听者时保持惰性,
ensureFresh()恰好重建一次; notifyNow()同步通知(受控输入契约)且不会造成重复 rebuild;- 多次
markFrameDirty()只调度一个rAF 帧,帧回调执行一次累积发布; - 结构事件微任务可抢占待执行的帧,旧帧回调被代际失效后不再通知;
- 无
requestAnimationFrame时回退为微任务合批; - 退订后不再收到通知。
- N 次
- Session 层测试证明:一帧只发布一次最新累计文本,且定稿后不会被旧的过期帧回调重复通知(finalization 与 stale frame callback 的竞态被代际机制消除)。
- fixture 小型单元测试继续固定输入校验、外部到达节奏、并发拒绝、精确事件数与结尾标记交付——这些都不需要把 100,000 分片工作负载带进默认测试套件,因此默认测试车道保持快速。
后果与边界:这条优化到底解决(和没解决)什么
从后果维度看,这一决策带来的收益边界非常清晰:
它解决的是"发布频率"问题。流式ConversationSnapshot的发布频率被约束在浏览器绘制频率内——React 每帧至多处理一个包含全部已接收文本的累计 partial;结构事件仍可更快发布。水平布局读写方面,折叠 Think 摘要最多每三帧一次,每次直接跳到最新位置,React 仍按累计快照正常提交,定稿时摘要恢复到首行。正文滚动、用户交互的即时性不受影响。
它没有假装解决"解析成本"问题。接收、排序、日志记录、字符串拼接与累积器更新仍然按原始分片逐个执行——这部分原始流解析成本是会话正确性的刚需,不做任何降频。
它的证据定位。浏览器压力车道能提供真实组装应用上的响应性信号与可见 profiling 入口,但由于硬件与调度差异,它只适合作为显式性能证据(手动诊断与修复验收),不是默认 CI 门禁,也不替代确定性的调度单元测试。
小结
DeepSeek Harness 对推理流式发布的处理可以概括为三个层次:
- 数据层不妥协:
Session.acceptLiveEvent()逐事件同步处理,assistant/chunk全部保留,重放保真度与内容完整性不受影响; - 发布层做合并:
Notifier.markFrameDirty()用一帧一次requestAnimationFrame合并所有可见分片,代际标记保证结构事件与notifyNow()能抢占、失效旧帧回调,无 rAF 环境自动回退微任务; - 证据层分层:聚焦单元测试与 fixture 测试守住确定性不变量,
pnpm run test:web:stress提供 100,000 分片级别的真实浏览器响应性证据(50ms 心跳 + 预调度交互事件 + 250ms 预算),DSH_WEB_STRESS_HEADFUL=1开启可视化 Performance 面板分析。
对于任何需要处理高频率流式事件、同时又不想牺牲原始数据完整性的前端架构而言,"接收端全量处理、发布端按帧合并、测试端确定性单测与浏览器证据分离"这套划分方式,正是这份 Agent Note 留下的可复用范式。
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考