TUI 性能报告
oh-my-dsh 将响应速度视为终端架构的一部分,而不是开发结束前的表面打磨。TUI 对实时状态使用不可变更新,通过私有的线性回放器恢复持久化日志,直接消费 Harness Projection 而不重复推导聚合状态,缓存已经格式化的对话区块,并且只向终端写入发生变化的行。
性能亮点
当前实现使用 10,000 轮模拟对话、10,000 次工具调用记录和包含 5,000 轮对话的缓存渲染界面进行测量。
| 工作负载 | 诊断基线 | 当前中位数 | 提升 |
|---|---|---|---|
| 恢复 10,000 轮对话 | 323.6 ms | 2.62 ms | 123.5× |
| 恢复 10,000 次工具调用 | 307.6 ms | 22.71 ms | 13.5× |
| 应用 10,000 次投影统计更新 | 81.5 ms | 0.43 ms | 189.5× |
| 在 5,000 轮对话上渲染 200 个缓存帧 | — | 总计 69.66 ms | 0.35 ms/帧 |
| 在 5,000 轮对话上渲染 200 个流式帧 | — | 总计 142.73 ms | 0.71 ms/帧 |
| 输出 200 个差分流式帧 | — | 200 次 write / 43.00 KiB | — |
诊断基线采集于引入线性回放和 Projection 快速路径之前,使用的是等价模拟工作负载。这些数据只测量 TUI 自身的 CPU 工作,不包含模型推理、网络延迟、文件系统延迟或物理终端吞吐。
测试环境
| 项目 | 配置 |
|---|---|
| 日期 | 2026-08-20 |
| 硬件 | Apple M5 Pro,arm64 |
| 操作系统 | macOS 26.5.2 |
| Node.js | 24.18.0 |
| pnpm | 11.20.0 |
| 仓库基线 | 测量优化改动前的 a71fe2a |
| 渲染视口 | 160 列 × 50 行 |
| 采样方式 | 预热 1 次后,取 7 次正式运行的中位数 |
为什么足够快
线性时间的持久化会话回放
实时事件继续通过不可变的 applyEvent 状态转换,从而保持更新逻辑可预测并方便缓存。恢复会话时使用私有回放器,它持有的可变区块数组在回放完成前不会逸出。这消除了对持续增长的对话数组进行反复复制的问题,使大型会话重建从二次增长变成线性增长。
回放器还维护一份私有的 callId → block index 映射。工具密集型会话可以直接定位部分工具调用、完整工具调用和结果,不必为每个事件扫描整段对话。
Harness Projection 快速路径
持久化的会话统计、Token 用量和上下文压力本来就由 DeepSeek Harness 负责。当这些 Projection 存在时,TUI 直接读取它们,只访问首尾事件来计算持续时间。缺少对应插件时仍然保留完整日志 Fold 作为降级方案,因此既能遵循“一切皆插件”的组合模型,也不会让默认组合在每个流式事件上承担一次无意义的历史扫描。
对话布局缓存
已经完成的对话区块会按照区块身份和渲染参数缓存格式化后的 Markdown 与工具行,稳定的对话正文则按照不可变区块数组缓存。编辑输入、更新状态、播放动画和滚动时都可以复用已经完成的内容,不需要重新格式化整段会话。密集到达且处于同一个 8 ms 帧窗口的 assistant delta 会立即折叠进状态,但共用一次终端渲染;用户交互、工具状态切换和 turn settlement 仍然同步刷新。
原生 scrollback 与终端差分输出
在跟随模式下,MainScreenRenderer 追加已最终化的行,并对可变视口执行行级差分。物理边界可防止已最终化行在终端尺寸稳定的 epoch 内被重复写入。仅追加的 assistant stream 在头部离开屏幕时可以推进该边界,从而保留终端的自然滚动;running-tool preview 因早期行可能折叠或变化而继续固定。普通更新期间,原生 scrollback 保持为追加式冻结视觉记录。生产启动只输出一个初始 session frame。idle replacement 会完整重放,running replacement 只把可变 preview 区域固定到完成为止。ED3 与 alternate-screen overlay 仅用于 direct terminal;multiplexer 与 ConPTY 保留宿主 scrollback,multiplexer 的 resize 突发会在重绘前合并。大型 resume transcript 会完整重放而不是截断。在终端宽度稳定时,已最终化行也构成 prepared prefix:frame fitting 只校验可变后缀,不会因 streaming、状态或 composer 更新而再次测量完整 transcript。
每个帧都会按可见行与上一帧比较。终端写入器只重写发生变化的行,只清理失效的旧行,并保持目标光标位置。所有宽度都按终端显示单元计算,因此 ANSI 样式、中文、Emoji 和组合字符不会因为布局错误触发额外的修正绘制。
复现性能测试
在项目根目录安装依赖并运行仓库内置 Benchmark:
pnpm install
pnpm benchmark:tui上述环境中的输出示例:
oh-my-dsh TUI microbenchmarks
Node v24.18.0 · darwin/arm64 · median of 7 measured runs
Resume 10,000 conversation turns 2.62 ms
Resume 10,000 tool calls 22.71 ms
Apply 10,000 projected stats updates 0.43 ms
Render 200 cached 5,000-turn frames 69.66 ms
Render 200 streaming 5,000-turn frames 142.73 ms
Terminal output for 200 streaming frames 200 writes · 43.00 KiB该 Benchmark 会直接导入源码实现并刻意排除物理终端 I/O。除了 CPU 耗时,它还会让真实差分 renderer 写入内存 sink,并报告流式工作负载的终端 write 次数和 ANSI 字节量。因此它既能发现算法复杂度回退,也能发现输出放大;绝对数值仍会随硬件、Node.js 版本、后台负载和运行时预热状态变化。
当前边界与后续方向
目前仍然可以测量到的增长来自正在流式生成的 Assistant Markdown 区块:文本追加后,后续语法可能改变此前内容的表现形式,因此活动区块需要重新参与格式化。在相同环境中,2,500 字符回复的一次更新约为 0.19 ms,5,000 字符约为 0.29 ms,极端的 25,000 字符约为 1.30 ms。这些数据仍低于当前帧预算。短合并窗口能在 token 密集到达时减少重复工作,又不会引入可感知的帧延迟。
如果真实使用中的性能追踪表明响应已经超过这些范围,下一步候选方案是增量解析语法已经稳定的 Markdown 前缀。这项优化应由真实终端 Trace 驱动,而不是只依据微基准数字。
回归保障
优化后的回放路径会与不可变实时 Fold 对比,覆盖用户消息、思考与正文 Delta、完整 Assistant 消息、部分及完整工具调用、工具结果、队列消息和轮次结束。另一个契约测试保证完整 Harness Projection 只读取事件日志边界,而不会遍历全部历史。常规单元测试、类型检查、构建、Markdown 检查、Happy Path Smoke 和 PTY Smoke 仍然是发布门槛。