dsh-large-proj-perf
DSH(DeepSeek Harness)大会话性能插件:零拷贝 fork、投影分片预热、分片 materialize, 一次装齐,消除 fork/历史加载对超大会话的事件循环阻塞。
⚠️ 版本兼容性警告:本插件通过 monkey-patch dsh 内部方法实现(
SessionStore.fork、PersistenceCoordinator.initFor、JsonlSessionPersistence.encodeMaterialization、SessionPreparations等),与 dsh 版本高度耦合,当前针对0.1.0-rc.6开发并验证。 dsh 升级后这些内部方法签名可能变化——所有补丁都带源码特征校验,不匹配时自动跳过 优化并回退官方行为(不会导致崩溃),但优化会静默失效。升级 dsh 后请确认启动日志 无signature mismatch告警,必要时重新适配本插件。
⚠️ 能力边界(重要):本插件只能缓解超大对话的性能/内存问题(分片、零拷贝、 LRU 裁剪、预热等),无法彻底解决。根本原因在 dsh 的架构——live 会话事件树全量 驻留内存(每个超大会话 ~700MB)、历史加载全量解码 + 逐事件深拷贝。这些是上游架构 问题,插件 monkey-patch 触及不到。真正的根治依赖 dsh 上游支持事件分页加载/按需驻留, 或主动控制会话规模(见下文「避免超长对话」)。
问题
dsh 0.1.0-rc.6 在大会话(数十万事件)上存在三类同步阻塞,导致 fork 卡顿、
历史加载报 signal timed out (internal)、严重时 OOM:
| 问题 | 环节 | 实测 |
|---|---|---|
| A. fork 深拷贝 | Session 构造器逐事件 snapshotJsonValue(纯 JS 深拷贝)+ persistence initFor 的 structuredClone(seed) |
18.2MB/20k 事件合计 ~480ms 同步阻塞 |
| B. projection 冷折叠 | SessionProjectionRegistry.cellFor() 冷时同步 buildCell 全量折叠 |
74 万事件冷折叠阻塞 20+ 分钟(100% 单核) |
| C. fork 全量序列化 | fork 子会话首次落盘 encodeMaterialization → eventLines = map(JSON.stringify).join("\n") 一次性序列化整个 seed |
60 万事件 = 501MB 单字符串;74 万事件直接 RangeError: Invalid string length |
为什么「单个会话没事、fork 后出事」:普通会话持久化走增量 appendLines
(每次序列化几十个事件),而 fork 子会话是全新 id,走 materialize 全量序列化
整个 seed——这是唯一会一次性序列化整条日志的路径。
方案
- 零拷贝 fork(
zeroCopyFork,A):fork 的 seed 事件本就是deepFreeze不可变纯 JSON 树。补丁改走Session.prepare(..., { seedSource: 'persistence' })的fromRestore通道——原地冻结复用引用,跳过整树深拷贝(346ms → 19ms)。 子会话 header(parentSession/seedLength/cwd)与官方 fork 逐字段一致。 - fast init-for(
fastInitFor,A):PersistenceCoordinator.initFor里那次structuredClone(seed)替换为冻结引用复用(135ms → ~0ms)。带 rc.6 源码特征 校验(structuredClone(e)标记),内部结构不匹配时自动跳过并告警。 - 投影分片预热(
warmupEnabled,B):会话进入(created/resume)且事件数超过 阈值时,抢在首次同步冷折叠前,分片重放 cells——每chunkSize个事件setImmediate让出事件循环,折叠完成后直写registration.cells(WeakMap), 此后snapshot()/drive()全部命中热 cell。可用时从投影缓存行取基线跳过已折叠 前缀。实测 74 万事件:冷折叠 20 分钟 → 预热 200ms。 - fork 缓存回填(B,随预热自动执行):fork 子会话(
header.parentSession存在) 预热完成后立即cache.write(child)建立投影缓存行——否则它被放弃时永远没有 缓存行,下次打开历史coldSnapshot走readFrom(0)全量读。 - 分片 materialize(
chunkedMaterialize,C):encodeMaterialization每materializeChunkEvents个事件一个 zstd frame(多帧是 dsh 解码端scanZstdFrames的原生格式,字节兼容),消除单巨字符串与 RangeError。 - 冷会话补行(
backfillOnBoot,B 辅助,默认关):磁盘缺缓存行的大会话流式 补写。默认关的原因:readRaw的 zstd 全量解码是同步的、插件层不可分片, 大文件仍会冻结事件循环数秒~数十秒。 - 冷会话 LRU 裁剪(
preparedCacheTrim/preparedCacheSize,D):persistence 的冷会话 LRU 默认缓存 5 个完整事件树(每个大会话 ~700MB,5×700MB 叠加是 OOM 主因之一)。插件运行时把容量降到preparedCacheSize(默认 1)并淘汰最旧的 ready 条目,主动释放冷会话事件树——省 ~2.8GB。无需手动改cordis.patch.yml;config.set运行时改这两个键即时生效;dispose 恢复原 capacity(已淘汰条目 不复活)。 - heap 上限检测(
heapWarnBytes,D):--max-old-space-size是 V8 启动期 参数、进程内改不了。插件检测 heap 上限低于阈值(默认 6GB)时告警,并引导用scripts/start-dsh.ps1(内置--max-old-space-size=8192)重启。
安全性
- 共享引用等价于深拷贝:事件在进入源会话时已通过完整 JSON 边界与 surface 验证并 深冻结,任何代码都无法修改。
- 所有补丁带 rc.6 源码特征校验,内部结构不符自动跳过并告警,绝不盲补。
- 三层回退:(a) 调用时能力探测缺失 → 官方实现;(b) 补丁内运行时异常 → try/catch 回退官方实现;(c) 配置开关 → 永远官方路径。
- dispose 完整还原所有补丁(冷会话 LRU 裁剪恢复 capacity 原值,已淘汰的缓存 条目不复活)。
安装
# 从 GitHub 安装(推荐)
dsh plugin --profile web add github:orangeofcarl0-sys/dsh-large-proj-perf
# 或
dsh plugin --profile web add https://github.com/orangeofcarl0-sys/dsh-large-proj-perf
# 本地开发
dsh plugin --profile web add file:<本仓库路径>
注意:每次修改仓库代码后,需把
lib/、cordis.patch.yml、package.json同步到<DSH_HOME>/profiles/web/node_modules/dsh-large-proj-perf/(file:安装 不会自动跟随源文件更新),或重新执行dsh plugin add。
重启 dsh web 生效。日志出现 [dsh-perf] installed (...) 即成功。
环境要求:Node ≥ 22.15.0(
node:zlib的 zstd 接口所需,低版本加载插件 会直接失败)。package.json已声明engines。
大会话内存(推荐启动方式)
多个超大会话(数十万事件)的 live 事件树每个 ~700MB,默认 V8 heap 上限 ~4GB 会让 dsh 在内存叠加时 OOM。插件已自动做冷会话 LRU 裁剪(省 ~2.8GB),但 heap 上限是 V8 启动期参数、进程内改不了,推荐用仓库自带脚本启动:
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\start-dsh.ps1
它等价于 node --max-old-space-size=8192 .../dsh/lib/bin.js web(停止旧进程 →
重启 → 打开浏览器)。若不使用该脚本,插件启动时会打 V8 heap limit ... < ...
告警提醒你加参数。
避免超长对话(主动规避)
本插件做的是「被动优化」——把 fork/历史加载/落盘的大会话阻塞降下来,但无法削减 正在使用的 live 会话事件树(每个 ~700MB,dsh 架构决定全量驻留)。要从根上避免 超长对话的内存/卡顿,推荐配套使用:
- dsh-fresh-start:
输入
/fresh一键「总结当前对话 → 开启新对话 → 归档老对话」。在一个会话工作 一段时间后用它归档,释放 live 事件树内存,而不是让会话无限增长到 70 万+ 事件。
dsh plugin --profile web add github:orangeofcarl0-sys/dsh-fresh-start
两者配合:dsh-large-proj-perf 兜底性能,dsh-fresh-start 主动控制会话规模。
API
POST http://127.0.0.1:3080/dsh-large-proj-perf/api/<method>:
stats.get— fork 次数/零拷贝占比/回退、预热计数、补行计数、最近记录stats.reset— 清零config.get/config.set— 运行时开关,config.set同时写 settings 持久化; 数值项带下限钳制(如materializeChunkEvents ≥ 1000、chunkSize ≥ 1), 非法值(类型不符/NaN/低于下限取下限)被拒绝或收敛,不会进入危险区间
curl -X POST http://127.0.0.1:3080/dsh-large-proj-perf/api/stats.get
curl -X POST http://127.0.0.1:3080/dsh-large-proj-perf/api/config.set \
-d '{"zeroCopyFork": false}'
验证
测试依赖真实的 dsh 内部包(@deepseek-ai/dsh-session 等),它们不在本仓库依赖里,
而是全局 dsh 安装的嵌套依赖,Node 解析不到。先链接再跑:
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\link-deps.ps1
npm test # 或单独跑:node tests/smoke_fork.mjs
tests/smoke_fork.mjs(17 断言):官方 fork 基线 / 零拷贝 fork 功能等价 / header 平价(含 origin 不继承)/ seed 前缀逐字节等价 / dispose 还原 / 版本漂移回退 —— ALL PASStests/test_fast_initfor.mjs(8 断言):initFor 补丁安装 / 源码特征漂移跳过 / 无 persistence 服务存活 —— ALL PASStests/smoke_warmup.mjs(11 断言):分片预热与同步折叠一致 / checkpoint 基线 / 并发 drive 让位 / dispose 中止 —— ALL PASStests/test_backfill.mjs(8 断言):fork 回填 / 磁盘冷会话补行 —— ALL PASStests/test_chunked_materialize.mjs(5 断言):分片多帧解码等价 / 阈值不变 —— ALL PASStests/test_cache_trim.mjs(17 断言):LRU 裁剪 / dispose 恢复 capacity / 运行时 开关 / config.set 数值钳制 —— ALL PASS
局限
- 版本高度耦合(重要):补丁绑定 rc.6 内部结构(
_forkSeed、initFor/encodeMaterialization源码特征、SessionPreparations.capacity等)。 dsh 大版本升级后,特征校验会自动跳过优化并回退官方行为(不崩溃、不误补), 但优化会静默失效——升级后务必确认启动日志无signature mismatch,并按需 重新适配。本插件不适合在 dsh 版本频繁变动时依赖其优化。 enqueue的逐事件structuredClone(fork 第三次拷贝)在插件层无法安全消除—— 它在 write-behind 闭包内部,且承担"persistence 独立于生产者"的所有权语义。根治需 上游改为按需快照。- 冷会话
coldSnapshot的全量readFrom(0)(zstd 解码 + 逐事件snapshotStoredEvents深拷贝)无法在插件层安全分片——readRaw的同步解码是硬伤。 根治需上游把readFromCore/loadStored改成分片让出事件循环。 - 超大会话(70 万+ 事件)加载本身有内存 OOM 风险,与插件无关;建议配合 dsh-fresh-start 主动归档。
No comments yet. Be the first to write one.