TUI 性能报告
oh-my-dsh 将响应速度视为终端架构的一部分,而不是开发结束前的表面打磨。TUI 对实时状态使用不可变更新,通过私有的线性回放器恢复持久化日志,直接消费 Harness Projection 而不重复推导聚合状态,缓存已经格式化的对话区块,并且只向终端写入发生变化的行。
性能亮点
以下数据记录于 2026-08-20,使用 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 仍然同步刷新。settlement 也共用这个计时器:来自并行调用或进程内子代理的 step、tool 与 assistant settlement 每个间隔只付出一帧,而不是每个事件一帧;命令生命周期与 inbox splice 等控制事件仍然立即绘制。
原生 scrollback 与终端差分输出
在跟随模式下,MainScreenRenderer 追加已最终化的行,并对可变视口执行行级差分。物理边界可防止已最终化行在终端尺寸稳定的 epoch 内被重复写入。运行中的整个回合在结束前保持可变,因此过程折叠无需改写原生历史。普通更新期间,原生 scrollback 保持为追加式冻结视觉记录。生产启动只输出一个初始 session frame。idle replacement 会完整重放,running replacement 将活跃回合保持为可变状态,直到结束。任何 profile 都不会擦除原生 scrollback,因此转录替换是追加而不是清除;alternate-screen overlay 仅用于 direct terminal,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 仍然是发布门槛。