dsh-large-proj-perf:消除 DSH 超大会话的 fork、历史加载与落盘阻塞

前言

DSH(DeepSeek Harness)的设计理念是「一切皆插件」,社区里已有一些性能方向的插件:@linxin666/dsh-perf 做观测与前端渲染降载,dsh-pref-kit 从源头做流式增量合并。但存量大会话有一类问题它们管不到:单个会话事件数累积到几十万之后,fork 子会话、重开历史会话、把 fork 结果落盘,都会变成事件循环上的同步重活——轻则几百毫秒的卡顿,重则单核跑满 20 分钟,甚至直接 RangeError。

本文介绍的 dsh-large-proj-perf(v1.2.0,MIT 许可证,作者 orangeofcarl0-sys)针对的就是这三类阻塞:零拷贝 fork、分片投影预热、分片 materialize、冷会话内存治理,一次装齐。下面按问题定位、实现方案、安装运维的顺序展开。

它解决什么问题

dsh 0.1.x 在大会话上有三类同步阻塞,README 给出了源码级定位和实测数据:

问题 环节 实测
fork 深拷贝 Session 构造器逐事件 snapshotJsonValue,加上 persistence initForstructuredClone(seed) 18.2MB / 20k 事件合计 ~480ms
projection 冷折叠 cellFor() 缓存冷时同步 buildCell 全量折叠 74 万事件阻塞 20 分钟以上(单核 100%)
fork 全量序列化 encodeMaterialization 一次性序列化整个 seed 60 万事件 501MB 单串;74 万直接 RangeError

根因都在 dsh 自身实现里,插件层能做的是绕开或分片。下面逐项说明插件的做法。

核心功能

零拷贝 fork

fork 出来的 seed 本身是 deepFreeze 过的不可变 JSON 树,逐事件深拷贝并非语义必需。插件把 fork 的 seed 改走 Session.prepare(seedSource:'persistence') 的 fromRestore 通道,原地冻结、复用引用,子会话 header 与官方实现逐字段一致。实测单次 fork 从 346ms 降到 19ms。

分片投影预热与 fork 缓存回填

会话进入且事件数超过阈值(minEvents,默认 20000)时,插件抢在首次冷折叠之前分片重放 cells,每片之间用 setImmediate 让出事件循环,结果直写 registration.cells;磁盘上已有投影缓存行时,取基线跳过已折叠前缀。74 万事件的会话从阻塞 20 分钟降到约 200ms。

fork 出的子会话原本没有投影缓存行,重开会走全量读取(分钟级);预热完成后插件回填缓存行,重开时间降到秒级。

分片 materialize

fork 落盘原本一次性序列化整个 seed,60 万事件就是一条 501MB 的巨字符串。插件改为每 materializeChunkEvents(默认 50000)个事件写一个 zstd frame;多帧格式是解码端 scanZstdFrames 的原生格式,字节兼容,解码侧无需改动。

冷会话 LRU 裁剪与 heap 检测

每个超大会话的 live 事件树约 700MB,多个冷会话叠加时默认 V8 heap 上限(约 4GB)会 OOM。插件在运行时把 SessionPreparations.capacity 降到 preparedCacheSize(默认 1),并淘汰最旧的 ready 条目,实测省出约 2.8GB;config.set 即时生效,dispose 时恢复原 capacity。

配套的 heap 检测会在 heap 上限低于阈值(默认 6GB)时告警,并提示加 --max-old-space-size

fast initFor:按项退役的样板

persistence 的 initFor 会对 seed 做一次 structuredClone,插件改为冻结引用复用,实测 135ms 降到约 0ms。dsh 0.1.0-rc.8 起上游已原生实现同样的零拷贝形态,该补丁自动退役——特征缺失但检测到上游零拷贝形态时只打 info,不误报漂移。

这体现了插件的整体策略:上游吸收某项能力后,对应补丁按项退役而非整体下线。各补丁的实际状态经 stats.getpatches 字段暴露(active / retired / inactive / off)。

dsh-std 标准兼容

插件提供 dsh-std Community v0.15 兼容面:dsh-plugin.json 清单加 facets.host.entry(指向 lib/std-host.js)。当前 dsh 0.1.x 走 cordis.patch.yml 加载 lib/index.js,行为不变;未来标准宿主按清单加载,双轨并行。

补丁安全机制

所有补丁通过 monkey-patch 内部方法实现,与 dsh 版本高度耦合。安全设计有三层:

1、每个补丁带源码特征校验,签名不符自动跳过并告警,绝不盲补;
2、三层回退:能力探测 / try-catch / 配置开关,失败自动回退官方实现;
3、dispose 完整还原,不残留修改。

安装与启用

运行要求:Node ≥ 22.15.0(依赖 node:zlib 的 zstd 接口);dsh 0.1.0-rc.6 ~ 0.1.2-rc.1(package.jsonengines 已声明)。

先执行安装命令,再重启 dsh web,日志出现 [dsh-perf] installed (...) 即成功:

dsh plugin --profile web add github:orangeofcarl0-sys/dsh-large-proj-perf

本地开发用 file: 协议安装:

dsh plugin --profile web add file:<本仓库路径>

注意 file: 安装不会自动跟随仓库改动,修改代码后需把 lib/cordis.patch.ymlpackage.jsondsh-plugin.json 同步到 profile 目录,或重新执行 dsh plugin add

推荐启动方式

LRU 裁剪能省内存,但 heap 上限是启动期参数,多个超大会话并存时默认的约 4GB 不够。建议用仓库自带脚本启动,内置 --max-old-space-size=8192

powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\start-dsh.ps1

配置与运行时 API

全部配置可经 Settings 卡片、config.set API 或 settings 持久化修改,数值项带下限钳制(如 materializeChunkEvents ≥ 1000、chunkSize ≥ 1、preparedCacheSize ≥ 1)。主要配置项:

默认 说明
zeroCopyFork true 零拷贝 fork
fastInitFor true fast initFor(rc.8+ 自动退役)
slowForkWarnMs 100 fork 耗时告警阈值
warmupEnabled true 大会话投影分片预热总开关
minEvents 20000 低于此事件数不预热
chunkSize 5000 每片折叠事件数(下限 1)
chunkYieldMs 0 片间让出方式:0=setImmediate,>0=setTimeout
warmOnCreated true session/created 即预热
backfillOnBoot false 磁盘冷会话补投影缓存行(默认关)
backfillMaxSessions / MinBytes / MaxBytes 8 / 1MB / 32MB 补行扫描范围
chunkedMaterialize true 分片落盘
materializeChunkEvents 50000 每帧事件数(下限 1000)
preparedCacheTrim true 冷会话 LRU 裁剪总开关
preparedCacheSize 1 裁剪目标容量(下限 1)
keepRecent 50 内存保留的最近记录数
heapWarnBytes 6GB heap 上限告警阈值

API 端点挂在 http://127.0.0.1:3080/dsh-large-proj-perf/api/<method>,仅接受回环地址并有同源校验。stats.get 返回 dshVersion 版本探针、patches 各补丁状态、fork / 预热 / 补行计数;stats.reset 清零计数;config.get / config.set 做运行时开关,config.set 同时写 settings 持久化。

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 0.1.0-rc.6 / rc.7 / rc.8、0.1.1-rc.1 / rc.2、0.1.2-alpha.5、0.1.2-rc.1 上开发验证。升级 dsh 后先做两件事:

1、跑 node tests/verify_compat.mjs,对真实安装的源码做 16 项结构断言;
2、启动后确认日志没有 signature mismatch 告警——补丁不匹配时不会崩溃,但优化会静默失效。

上游 rc.7 修复了历史分页栈溢出、rc.8 优化了 SQLite 后端,均与本插件不重叠,也没触碰根因(历史加载全量解码、live 事件树全量驻留)。

与其他性能插件共存

插件 层面 与本插件关系
@linxin666/dsh-perf 观测 + 写批频控 + 前端渲染降载 零方法重叠,它不 patch 方法
dsh-pref-kit 源头流式增量合并(事件量 −11~56%) 上下游互补:它减少新事件,本插件治理存量与 fork

三者可同时安装。唯一注意点:行管理实验项的白名单都含 session-projection-cache,不要禁用该行,否则投影缓存回填与基线读取失效(有防御回退,不崩溃,但功能打折)。

适用场景与注意事项

适合的场景很明确:DSH 会话事件数上到几十万、fork 或重开历史会话明显变慢、或遇到过 OOM。会话规模小的用户感知不大,预热有 minEvents 阈值兜底。

安装前有几点必须知道:

1、插件以当前 dsh 进程的权限运行,且通过 monkey-patch 修改内部方法。装之前建议先看一遍 GitHub 仓库源码和许可证(MIT)。
2、本插件是被动优化,无法削减正在使用的 live 事件树。推荐配合同作者的 dsh-fresh-start:/fresh 一键「总结 → 开新会话 → 归档老会话」,主动控制规模;一个兜底性能,一个控制规模。
3、缓解不根治:live 事件树全量驻留(约 700MB/超大会话)、历史加载全量解码的根因在 dsh 架构,根治依赖上游支持事件分页加载 / 按需驻留。enqueue 的逐事件 structuredClone 和冷会话 coldSnapshot 的全量 readFrom(0) 也无法在插件层安全消除。

开发侧如果想自己验证:先运行 scripts/link-deps.ps1 链接 dsh 内部包,再 npm test(8 套件、112 断言)。

小结

经过上面的步骤,fork、历史加载、落盘三类阻塞都有了对应当法,各项独立开关、带特征校验与回退,上游吸收后按项退役。对被超大会话拖慢的 DSH 实例来说,这是一个成本很低、可随时回退的缓解方案。

源码与文档见 GitHub 仓库:https://github.com/orangeofcarl0-sys/dsh-large-proj-perf;社区目录页:https://www.skillhub.cn/plugins/orangeofcarl0-sys/dsh-large-proj-perf。社区目录为独立站点,与 DeepSeek、幻方无官方从属关系,收录信息以目录站为准。

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

小夜