dsh-large-proj-perf 0.0.0-stage → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -0
- package/LICENSE +21 -0
- package/README.md +267 -2
- package/cordis.patch.yml +9 -0
- package/lib/api.js +91 -0
- package/lib/backfill.js +227 -0
- package/lib/config.js +91 -0
- package/lib/index.js +123 -0
- package/lib/patches/chunked-materialize.js +120 -0
- package/lib/patches/fast-init-for.js +65 -0
- package/lib/patches/prepared-cache-trim.js +52 -0
- package/lib/patches/zero-copy-fork.js +185 -0
- package/lib/runtime.js +68 -0
- package/lib/stats.js +45 -0
- package/lib/warmup.js +120 -0
- package/package.json +58 -4
- package/scripts/link-deps.ps1 +85 -0
- package/scripts/set-npm-token.sh +46 -0
- package/scripts/start-dsh.ps1 +76 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
本插件的版本线:**插件版本**(`1.x.y`)与**支持的 dsh 版本区间**各自演进。每个条目
|
|
4
|
+
同时标注该版本验证过的 dsh 版本。
|
|
5
|
+
|
|
6
|
+
遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/) 与
|
|
7
|
+
[语义化版本](https://semver.org/lang/zh-CN/);补丁点的上游变化详见 README 的
|
|
8
|
+
「支持版本」表。
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
## [1.3.0] — 2026-10-11
|
|
13
|
+
|
|
14
|
+
面向 **dsh 0.2.0** 的发布准备版本。
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **0.2.0 插件契约**:`peerDependencies` 声明真实依赖的 dsh 包
|
|
19
|
+
(`@deepseek-ai/dsh`、`@deepseek-ai/dsh-session`,范围 `>=0.1.0-rc.6 <0.3.0-0`)。
|
|
20
|
+
dsh 0.2.0 起会据此做严格 peer 门禁,范围外会被跳过加载。
|
|
21
|
+
- **真实组件端到端测试**(`tests/e2e_real_components.mjs`):直接驱动 dsh 安装包内的
|
|
22
|
+
真实实现类(`JsonlSessionPersistence` / `SessionProjectionRegistry` /
|
|
23
|
+
`SessionStore`),逐点位验证「补丁安装 → 行为正确」;mock 套件继续覆盖逻辑与边界。
|
|
24
|
+
- `verify_compat` 增加 peer 门禁离线复算(用 dsh 自带 semver 复算同一规则)。
|
|
25
|
+
- 发布元数据:`publishConfig.access=public`、`prepublishOnly` 测试门禁、`.npmignore`、
|
|
26
|
+
本 CHANGELOG 与 `PUBLISHING.md`。
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **`zero-copy-fork` 缺少 `ctx.sessions` 服务防御**:服务缺失时会抛错并中断整个
|
|
31
|
+
`apply`(其余补丁全部装不上)。现改为记录日志并将 `zeroCopyFork` 标记为
|
|
32
|
+
`inactive` 后静默跳过。
|
|
33
|
+
- **投影缓存 identity 缺 `formatVersion`**(0.1.3 起的静默失效):导致 warmup 基线
|
|
34
|
+
无法复用上游缓存行、backfill 写入的行不能作折叠短路。两个 identity 构造器均已
|
|
35
|
+
补上该字段。
|
|
36
|
+
|
|
37
|
+
### Compatibility
|
|
38
|
+
|
|
39
|
+
- 新增支持 dsh `0.2.0-rc.2`(`dsh-session` 重构 tail-repair、新增 `ToolCallRecovery`;
|
|
40
|
+
`buildForkSeed`/`SESSION_FORMAT_VERSION` 等导出不变)。
|
|
41
|
+
- 登记 dsh `0.1.7-rc.1` / `0.1.7-rc.2`:fork 内部重构(`_forkSeed` →
|
|
42
|
+
`_forkBoundary` + `buildForkSeed`)、open-turn 边界改为注入 turn 闭合事件、
|
|
43
|
+
会话格式 v4。零拷贝补丁新增 0.1.7 协议分支(三协议自动探测)。
|
|
44
|
+
- 登记 dsh `0.1.5-alpha.1` / `0.1.5-rc.1` / `0.1.5-rc.2`(格式 v3、toolHistory)。
|
|
45
|
+
- 登记 dsh `0.1.2-rc.1`。
|
|
46
|
+
|
|
47
|
+
### Removed
|
|
48
|
+
|
|
49
|
+
- **dsh-std 支持**(`dsh-plugin.json` 清单、`lib/std-host.js` facet 入口、
|
|
50
|
+
`tests/test_manifest.mjs`)。标准宿主始终未落地,dsh 亦不消费该清单;dsh 自身的
|
|
51
|
+
插件加载链路(`package.json` 的 `dsh.bundle.patch` → `cordis.patch.yml`)不受影响。
|
|
52
|
+
|
|
53
|
+
## [1.2.0] — 2026-09-04
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- `stats.get` 暴露每个补丁的生命周期状态(`active`/`retired`/`inactive`/`off`),
|
|
58
|
+
实现「上游吸收某项能力后对应补丁自动退役」的可见化。
|
|
59
|
+
- dsh-std Community v0.15 清单与 facet 宿主入口(1.3.0 已移除)。
|
|
60
|
+
- 冷会话 LRU 裁剪(`preparedCacheTrim` / `preparedCacheSize`)与 heap 上限检测。
|
|
61
|
+
|
|
62
|
+
### Compatibility
|
|
63
|
+
|
|
64
|
+
- 适配 dsh `0.1.2-alpha.5` 破坏性变更:`Session.events` → `snapshotEvents()`;
|
|
65
|
+
fork meta `seedLength` → `isSeeded` + `options.inheritedEventCount`;
|
|
66
|
+
`encodeMaterialization(meta, events)` → `(storage, events)`;投影缓存 identity 新增
|
|
67
|
+
`isSeeded`/`inheritedEventCount`(`putSoft` → `put`)。
|
|
68
|
+
- 适配 dsh `0.1.2-rc.1`(补丁点源码零差异)。
|
|
69
|
+
|
|
70
|
+
## [1.1.1]
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
|
|
74
|
+
- 分片 materialize 在 cordis Proxy 环境下从不安装(`ctx.get` 返回的 shadow method
|
|
75
|
+
使签名校验永远失败);改为取原型方法做特征校验。
|
|
76
|
+
|
|
77
|
+
## [1.1.0]
|
|
78
|
+
|
|
79
|
+
### Added
|
|
80
|
+
|
|
81
|
+
- 冷会话 LRU 裁剪:运行时下调 `SessionPreparations.capacity` 并淘汰最旧 ready 条目
|
|
82
|
+
(官方默认 5 → 1,省约 2.8GB)。
|
|
83
|
+
- heap 上限检测与启动参数引导。
|
|
84
|
+
|
|
85
|
+
## [1.0.0]
|
|
86
|
+
|
|
87
|
+
### Added
|
|
88
|
+
|
|
89
|
+
- 合并 fork-perf 与 projection-warmup 两个插件为单一插件:
|
|
90
|
+
- 零拷贝 fork(fork 时跳过逐事件深拷贝)
|
|
91
|
+
- 投影分片预热(消除历史会话首次冷折叠的事件循环冻结)
|
|
92
|
+
- fork 缓存回填
|
|
93
|
+
- 分片 materialize(多帧 zstd,消除单巨字符串 / RangeError)
|
|
94
|
+
- 全部补丁带源码特征校验 + 三层回退(能力探测 / try-catch / 配置开关)。
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 orangeofcarl0
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,268 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-large-proj-perf
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[]()
|
|
4
|
+
[]()
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
DSH(DeepSeek Harness)大会话性能插件:零拷贝 fork、分片投影预热、fork 缓存回填、
|
|
8
|
+
分片 materialize、冷会话内存治理——一次装齐,消除 fork / 历史加载 / 落盘对超大会话
|
|
9
|
+
(数十万事件)的事件循环阻塞与 OOM。
|
|
10
|
+
|
|
11
|
+
所有优化带**源码特征校验 + 协议探测 + 三层回退**(能力探测 / try-catch / 配置开关),
|
|
12
|
+
不匹配自动跳过、绝不盲补;上游吸收了某项能力后对应补丁**自动从 `active` 转
|
|
13
|
+
`retired`**——插件按项逐项退役,最终全部 `retired` 之日即整体退役之时。
|
|
14
|
+
|
|
15
|
+
## 特性一览
|
|
16
|
+
|
|
17
|
+
| 能力 | 解决什么 | 0.1.3-alpha.2 实测 | 状态 |
|
|
18
|
+
|---|---|---|---|
|
|
19
|
+
| 零拷贝 fork | fork 时逐事件 `snapshotJsonValue` 深拷贝阻塞 | 49ms → 5.9ms(0.1.7 真实环境) | 活跃(0.1.7 走 `create(eventState:'shared-frozen')` + `buildForkSeed` 通道) |
|
|
20
|
+
| 分片投影预热 | 打开历史会话的同步冷折叠冻结事件循环 | 74 万事件 20min → ~200ms | 活跃 |
|
|
21
|
+
| fork 缓存回填 | fork 子会话无投影缓存行,重开走全量读 | 分钟级 → 秒级 | 活跃(0.1.3 直读 `session.vN.jsonl.zstd` 多帧) |
|
|
22
|
+
| 分片 materialize | fork 落盘单巨字符串:60 万事件 501MB、74 万直接 RangeError | 多帧 zstd,字节兼容 | 活跃(0.1.3 三参签名已适配) |
|
|
23
|
+
| 冷会话 LRU 裁剪 | 冷会话事件树 ~700MB/个 ×5 叠加 OOM | 省 ~2.8GB | 0.1.3 上游系统修复(`coldLogMemo=2`),补丁自动跳空 |
|
|
24
|
+
| fast initFor | persistence 的 `structuredClone(seed)` 深拷贝 | 135ms → ~0ms | 已退役(rc.8 起上游原生;0.1.3 coordinator 移除) |
|
|
25
|
+
| heap 检测 | V8 heap 上限过低告警 | 运维辅助 | 活跃 |
|
|
26
|
+
|
|
27
|
+
运行时各补丁的实际状态经 `stats.get` 的 `patches` 字段暴露:
|
|
28
|
+
`active`(已安装生效)/ `retired`(上游已原生实现)/ `inactive`(环境缺失或特征
|
|
29
|
+
漂移)/ `off`(配置关闭)。
|
|
30
|
+
|
|
31
|
+
## 工作原理
|
|
32
|
+
|
|
33
|
+
dsh 0.1.x 在大会话上有三类同步阻塞(源码级定位 + 实测):
|
|
34
|
+
|
|
35
|
+
| 问题 | 环节 | 实测 |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| A. fork 深拷贝 | `Session` 构造器逐事件 `snapshotJsonValue` + 旧 persistence `initFor` 的 `structuredClone(seed)` | 18.2MB / 20k 事件合计 ~480ms |
|
|
38
|
+
| B. projection 冷折叠 | `cellFor()` 冷时同步 `buildCell` 全量折叠 | 74 万事件阻塞 20+ 分钟(100% 单核) |
|
|
39
|
+
| C. fork 全量序列化 | `encodeMaterialization` 一次性序列化整个 seed | 60 万事件 501MB 单串;74 万 RangeError |
|
|
40
|
+
|
|
41
|
+
对应方案(全部独立开关、失败自动回退官方实现):
|
|
42
|
+
|
|
43
|
+
1. **零拷贝 fork**:fork 的 seed 是 live 会话的 `snapshotEvents()`(事件在 append 时
|
|
44
|
+
已 deepFreeze)。改走 fromRestore 通道原地冻结复用引用(三协议自动探测):
|
|
45
|
+
- **0.1.7+**:复用原生 `_resolveForkSource` + `_forkBoundary` + 模块级
|
|
46
|
+
`buildForkSeed`(slice + 注入 `session/end-seed {inherited:true}` marker +
|
|
47
|
+
`openTurnClosers` 补 turn 闭合),只把 `create` 的写入通道换成
|
|
48
|
+
`eventState:'shared-frozen'`;`buildForkSeed` 新建的事件未冻结,送入前
|
|
49
|
+
原地深冻结(满足「已冻结或独立拥有」前提);
|
|
50
|
+
- **0.1.3–0.1.5**:复用 `_forkSeed`,补丁补 `version/id/createdAt` 到 meta
|
|
51
|
+
并在 seed 尾部注入 inherited marker(磁盘校验要求 `cut == marker.seq`,
|
|
52
|
+
原生快照通道自动追加、restore 通道不追加);
|
|
53
|
+
- **alpha.5 / rc.1**:`prepare({seedSource:'persistence'})` 旧通道。
|
|
54
|
+
2. **分片投影预热**:会话进入且事件数超阈值时,抢在首次冷折叠前分片重放 cells
|
|
55
|
+
(每片间 `setImmediate`/`setTimeout` 让出),直写 `registration.cells`;有投影
|
|
56
|
+
缓存行时取基线跳过已折叠前缀;fork 子会话预热后回填缓存行。
|
|
57
|
+
3. **分片 materialize**:每 `materializeChunkEvents` 事件一个 zstd frame(多帧是
|
|
58
|
+
解码端 `scanZstdFrames` 的原生格式,字节兼容),消除单巨字符串。0.1.3 头帧
|
|
59
|
+
直接复用上游 `encodeCurrentHeader` 编码器(`original(meta, inheritedEventCount,
|
|
60
|
+
[])` 只产 header 帧),不重写格式层。
|
|
61
|
+
4. **冷会话 LRU 裁剪**:运行时把 `SessionPreparations.capacity` 降到
|
|
62
|
+
`preparedCacheSize`(默认 1)并淘汰最旧 ready 条目;`config.set` 即时生效,
|
|
63
|
+
dispose 恢复原容量。0.1.3 起上游以 `COLD_LOG_MEMO_MAX_ENTRIES = 2` + handle
|
|
64
|
+
模型懒 materialize 系统代偿,该补丁在上游 0.1.3 自动跳空(老版本仍活跃)。
|
|
65
|
+
5. **fast initFor**:`structuredClone(seed)` → 冻结引用复用;rc.8 起上游原生实现,
|
|
66
|
+
补丁自动退役(0.1.3 连 `PersistenceCoordinator` 本体也已移除)。
|
|
67
|
+
6. **heap 检测**:heap 上限低于阈值(默认 6GB)时告警并提示
|
|
68
|
+
`--max-old-space-size`。
|
|
69
|
+
|
|
70
|
+
安全性:共享冻结引用与深拷贝语义等价(事件进入源会话时已完整校验并深冻结);
|
|
71
|
+
三层回退保证任何不匹配都不破坏上游行为。
|
|
72
|
+
|
|
73
|
+
### 加载侧:上游已优化的部分与插件的作用点
|
|
74
|
+
|
|
75
|
+
长对话「打开」的成本分三段,0.1.3–0.1.7 期间上游已优化前两段:
|
|
76
|
+
|
|
77
|
+
| 阶段 | 上游现状(0.1.7) | 插件是否介入 |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| 磁盘解码 | 逐帧 zstd 解码 + 增量 `SessionLogScanner`(不拼整串),每 500ms `scheduler.yield()` 让出事件循环 | 否(上游已良好) |
|
|
80
|
+
| 老格式迁移 | 格式 < v4 的会话经 v0→v1→v2→v3→v4 链,**在 Worker Thread 内完成**并发布新世代文件(`session.vN.jsonl`) | 否(上游已良好) |
|
|
81
|
+
| 投影折叠 | `cellFor` 冷时**同步全量** `buildCell`(仍在主线程) | **是**:分片预热 + 缓存行基线(见下) |
|
|
82
|
+
| 落盘编码 | `encodeMaterialization` 单 body 帧(fork 子会话首次落盘可能 501MB 单串) | **是**:分片 zstd 多帧 |
|
|
83
|
+
|
|
84
|
+
插件写/读投影缓存行时必须使用与上游 `identityOf` 一致的键:`createdAt`/`cwd`/
|
|
85
|
+
`isSeeded`/`inheritedEventCount` 以及 **0.1.3 起新增的 `formatVersion`**
|
|
86
|
+
(= `header.version`)。上传 identity 缺 `formatVersion` 时,上游只把它当作
|
|
87
|
+
title 前驱提示,**不能作折叠短路**——补行与基线复用会静默失效(此坑已于 1.2.0
|
|
88
|
+
修复并有回归断言守护)。
|
|
89
|
+
|
|
90
|
+
## 支持版本
|
|
91
|
+
|
|
92
|
+
| 版本线 | 形态 |
|
|
93
|
+
|---|---|
|
|
94
|
+
| 0.1.0-rc.6 / rc.7 / rc.8 | fork `prepare(seedSource)` 通道;initFor `structuredClone` 形态 |
|
|
95
|
+
| 0.1.1-rc.1 / rc.2 | 同 alpha.5 形态;rc.2 为 `(meta, events)` 裸 meta 签名 |
|
|
96
|
+
| 0.1.2-alpha.5 / 0.1.2-rc.1 | `snapshotEvents()`;fork meta `isSeeded`+`inheritedEventCount`;`(storage, events)` 签名 |
|
|
97
|
+
| 0.1.3-alpha.2 | persistence 子系统重写(见下);投影缓存 identity 新增 `formatVersion`(插件已跟进) |
|
|
98
|
+
| 0.1.5-alpha.1 | 会话格式 **v3**(`SESSION_FORMAT_VERSION=3`);cut/header 语义与 v2 一致,文件名 `session.v3.jsonl`;补丁点零行级变更 |
|
|
99
|
+
| 0.1.5-rc.1 | 仅新增已知事件类型(`deliverables/presented`、`subagent/catalog`);补丁点零变化 |
|
|
100
|
+
| 0.1.5-rc.2 | 相对 rc.1 四个关键包源码零差异(仅版本号),无新增变更 |
|
|
101
|
+
| 0.1.7-rc.2 | 新增 `toolHistory()` 增量折叠(游标只消费新事件);persistence/format/projection-cache 零差异,补丁点无变化 |
|
|
102
|
+
| 0.2.0-rc.2 | 主版本升级。**插件管理引入严格 peer 门禁**(见下);`dsh-session` 重构 tail-repair 并新增 `ToolCallRecovery`(`buildForkSeed`/`SESSION_FORMAT_VERSION` 等导出不变);`llm-deepseek` 拆分为 `-api-key`/`-account`;四个补丁点与 projection-cache 零破坏 |
|
|
103
|
+
| 0.1.7-rc.1 | fork 内部重构:`_forkSeed` → `_forkBoundary` + 模块级 `buildForkSeed`;**open-turn 边界从「拒绝」改为「注入 turn 闭合事件」**;会话格式升 **v4**(cut/文件名语义同 v2/v3);事件词汇收紧(`tool/result` role=`tool`、新增 `system/message`)。插件已适配(三协议探测) |
|
|
104
|
+
|
|
105
|
+
### 0.1.3-alpha.2 关键变化(已全部适配)
|
|
106
|
+
|
|
107
|
+
- `dsh-session-persistence` 变为 contract 层(`SessionPersistence` 基类 + 错误 +
|
|
108
|
+
校验),`PersistenceCoordinator`/`initFor`/`SessionPreparations` 移除;JSONL
|
|
109
|
+
实现移入 `dsh-session-persistence-jsonl`。
|
|
110
|
+
- 格式 v0→v1→v2 迁移链(`dsh-session-format` 系列);物理 header 移除
|
|
111
|
+
`seedLength`,seed cut 由事件流尾 `session/end-seed {inherited:true}` marker
|
|
112
|
+
表达(`cut == marker.seq`);文件名变 `session.vN.jsonl[.zstd]`。
|
|
113
|
+
- `encodeMaterialization(meta, events)` → `(meta, inheritedEventCount, events)`;
|
|
114
|
+
事件词汇收紧(`assistant/message` 需 `data.stream`、消息 `id/source/role` 校验)。
|
|
115
|
+
|
|
116
|
+
升级 dsh 后:启动日志显示 `dsh version: x.y.z (verified)`(版本探针)或列表外
|
|
117
|
+
版本告警;跑 `node tests/verify_compat.mjs`(对真实安装源码做结构断言 + peer 门禁
|
|
118
|
+
离线复算,当前 30 项);确认无 `signature mismatch` 告警。
|
|
119
|
+
|
|
120
|
+
## 安装
|
|
121
|
+
|
|
122
|
+
要求:Node **≥ 22.15.0**(`node:zlib` zstd 接口);dsh `0.1.0-rc.6` ~ `0.2.0-rc.2`
|
|
123
|
+
(`package.json` 已声明 `engines`)。
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
# 从 GitHub 安装(推荐)
|
|
127
|
+
dsh plugin --profile web add github:orangeofcarl0-sys/dsh-large-proj-perf
|
|
128
|
+
|
|
129
|
+
# 本地开发
|
|
130
|
+
dsh plugin --profile web add file:<本仓库路径>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
> 修改仓库代码后需把 `lib/`、`cordis.patch.yml`、`package.json` 同步到
|
|
134
|
+
> `<DSH_HOME>/profiles/web/node_modules/dsh-large-proj-perf/`(`file:` 安装
|
|
135
|
+
> 不自动跟随),或重新 `dsh plugin add`。重启 `dsh web` 生效,日志出现
|
|
136
|
+
> `[dsh-perf] installed (...)` 即成功。
|
|
137
|
+
|
|
138
|
+
### 插件管理:0.2.0 的 peer 门禁(重要)
|
|
139
|
+
|
|
140
|
+
dsh 0.2.0 起,profile bundle 加载前会校验插件 `package.json` 的
|
|
141
|
+
`peerDependencies` 中**所有 `@deepseek-ai/dsh` / `@deepseek-ai/dsh-*` 项**:
|
|
142
|
+
运行时版本不满足 range 即**跳过整个插件**(启动日志打
|
|
143
|
+
`skipping profile bundle "<name>": ... incompatible with dsh <ver>`),可用
|
|
144
|
+
`dsh plugin allow-version <pkg>@<ver> --profile web` 做精确版本豁免。
|
|
145
|
+
|
|
146
|
+
本插件的声明(0.2.0 契约的显式兼容声明):
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
"peerDependencies": {
|
|
150
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
151
|
+
"@deepseek-ai/dsh": ">=0.1.0-rc.6 <0.3.0-0",
|
|
152
|
+
"@deepseek-ai/dsh-session": ">=0.1.0-rc.6 <0.3.0-0"
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- 只声明我们**真实依赖**的东西:`dsh-session`(`snapshotEvents`/`buildForkSeed`/
|
|
157
|
+
`SessionLogOffset`)与 `dsh`(版本探针);
|
|
158
|
+
- 上界 `<0.3.0-0` 覆盖整个 0.1.x–0.2.x 线:既让门禁能替我们守住兼容边界,
|
|
159
|
+
又不会在小版本推进时误杀自己。**升级 dsh 大版本(0.3+)前需先更新此范围**;
|
|
160
|
+
- `verify_compat.mjs` 会用 dsh 自带的 semver **离线复算**同一条门禁规则
|
|
161
|
+
(`plugin contract: declared dsh peers satisfy runtime version`)——离线报警
|
|
162
|
+
= 重启后必被跳过。
|
|
163
|
+
|
|
164
|
+
### 大会话内存(推荐启动方式)
|
|
165
|
+
|
|
166
|
+
多个超大会话的 live 事件树每个 ~700MB,默认 V8 heap 上限 ~4GB 会 OOM。插件已自动
|
|
167
|
+
做冷会话治理,但 heap 上限是**启动期参数**,进程内改不了——推荐用仓库脚本启动(内置
|
|
168
|
+
`--max-old-space-size=8192`):
|
|
169
|
+
|
|
170
|
+
```powershell
|
|
171
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\start-dsh.ps1
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 避免超长对话(主动规避)
|
|
175
|
+
|
|
176
|
+
本插件是「被动优化」,无法削减**正在使用的** live 事件树(每会话几百 MB 是它的
|
|
177
|
+
物理成本)。推荐配合 [dsh-fresh-start](https://github.com/orangeofcarl0-sys/dsh-fresh-start):
|
|
178
|
+
`/fresh` 一键「总结 → 开新会话 → 归档老会话」,主动控制会话规模。本插件兜底
|
|
179
|
+
性能,fresh-start 控制规模。
|
|
180
|
+
|
|
181
|
+
## 配置
|
|
182
|
+
|
|
183
|
+
全部配置可经 Settings 卡片、`config.set` API 或 settings 持久化修改;数值项带下限
|
|
184
|
+
钳制(非法值被拒绝或收敛到下限:`materializeChunkEvents ≥ 1000`、`chunkSize ≥ 1`、
|
|
185
|
+
`preparedCacheSize ≥ 1` 等),NaN/Infinity 直接拒绝。
|
|
186
|
+
|
|
187
|
+
| 键 | 默认 | 说明 |
|
|
188
|
+
|---|---|---|
|
|
189
|
+
| `zeroCopyFork` | `true` | 零拷贝 fork(三协议自动探测:0.1.7 fork-seed / 0.1.3 event-state / alpha.5 seed-source) |
|
|
190
|
+
| `fastInitFor` | `true` | fast initFor(rc.8+ 自动退役;0.1.3 无此形态) |
|
|
191
|
+
| `slowForkWarnMs` | `100` | fork 耗时告警阈值(ms) |
|
|
192
|
+
| `warmupEnabled` | `true` | 大会话投影分片预热总开关 |
|
|
193
|
+
| `minEvents` | `20000` | 低于此事件数不预热(同步冷折叠足够快) |
|
|
194
|
+
| `chunkSize` | `5000` | 每片折叠事件数(下限 1;片间让出事件循环) |
|
|
195
|
+
| `chunkYieldMs` | `0` | 让出方式:0=setImmediate;>0=setTimeout(ms) |
|
|
196
|
+
| `warmOnCreated` | `true` | session/created(含 resume 的大会话)即预热 |
|
|
197
|
+
| `backfillOnBoot` | `false` | 磁盘冷会话补投影缓存行(大文件解码同步,默认关) |
|
|
198
|
+
| `backfillMaxSessions` | `8` | 补行扫描会话数上限 |
|
|
199
|
+
| `backfillMinBytes` / `backfillMaxBytes` | `1MB` / `32MB` | 补行文件大小范围 |
|
|
200
|
+
| `chunkedMaterialize` | `true` | 分片落盘(0.1.3 头帧复用上游编码器) |
|
|
201
|
+
| `materializeChunkEvents` | `50000` | 每帧事件数(下限 1000) |
|
|
202
|
+
| `preparedCacheTrim` | `true` | 冷会话 LRU 裁剪总开关(false 恢复官方容量) |
|
|
203
|
+
| `preparedCacheSize` | `1` | 裁剪目标容量(官方默认 5 → 1,省 ~2.8GB) |
|
|
204
|
+
| `keepRecent` | `50` | 内存保留的最近记录数(fork/预热统计) |
|
|
205
|
+
| `heapWarnBytes` | `6GB` | heap 上限告警阈值 |
|
|
206
|
+
|
|
207
|
+
## API
|
|
208
|
+
|
|
209
|
+
`POST http://127.0.0.1:3080/dsh-large-proj-perf/api/<method>`(回环 + 同源校验):
|
|
210
|
+
|
|
211
|
+
| 端点 | 说明 |
|
|
212
|
+
|---|---|
|
|
213
|
+
| `stats.get` | `dshVersion` 版本探针、`patches` 各补丁状态(active/retired/inactive/off)、fork/预热/补行计数与最近记录 |
|
|
214
|
+
| `stats.reset` | 清零计数与历史(保留版本探针与补丁状态) |
|
|
215
|
+
| `config.get` / `config.set` | 运行时开关;`config.set` 同时写 settings 持久化(`backfillOnBoot` 等生效即补跑) |
|
|
216
|
+
|
|
217
|
+
```sh
|
|
218
|
+
curl -X POST http://127.0.0.1:3080/dsh-large-proj-perf/api/stats.get
|
|
219
|
+
curl -X POST http://127.0.0.1:3080/dsh-large-proj-perf/api/config.set \
|
|
220
|
+
-H 'Content-Type: application/json' -d '{"key":"backfillOnBoot","value":true}'
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## 开发与测试
|
|
224
|
+
|
|
225
|
+
```sh
|
|
226
|
+
# 一次到位:junction 链接全局 dsh 的嵌套依赖(仓库无 node_modules)
|
|
227
|
+
powershell -ExecutionPolicy Bypass -File .\scripts\link-deps.ps1
|
|
228
|
+
|
|
229
|
+
# 全套 8 套件 138 断言
|
|
230
|
+
# - verify_compat:对真实安装源码做特征断言 + peer 门禁离线复算
|
|
231
|
+
# - e2e_real_components:直接驱动 dsh 真实实现类(真实 JsonlSessionPersistence
|
|
232
|
+
# / SessionProjectionRegistry / SessionStore),逐点位验证补丁安装与行为
|
|
233
|
+
npm test
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
仓库布局:
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
lib/index.js 编排层(apply:配置/统计/日志/版本探针/补丁安装/定时器)
|
|
240
|
+
lib/runtime.js 日志、safeGet、sessionEventsOf、sessionLogOffset、版本探针
|
|
241
|
+
lib/config.js 默认值/下限钳制/settings schema 注册
|
|
242
|
+
lib/stats.js 统计与补丁生命周期状态(active/retired/inactive/off)
|
|
243
|
+
lib/api.js stats/config HTTP 端点
|
|
244
|
+
lib/warmup.js 分片投影预热 + fork 缓存回填
|
|
245
|
+
lib/backfill.js 磁盘冷会话补行(readRaw / 0.1.3 直读双通道)
|
|
246
|
+
lib/patches/*.js 零拷贝 fork / fast initFor / 冷会话 LRU / 分片 materialize
|
|
247
|
+
tests/ verify_compat(真实源码特征 + 门禁复算)/ e2e_real_components
|
|
248
|
+
(真实实现类端到端)/ smoke_*、test_*(mock 逻辑与边界)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
补丁安装原则:**特征校验先行**(`String(原型方法)` 含特征字面量,不匹配打
|
|
252
|
+
`signature mismatch` 后跳过);**协议探测**(如 fork 的 `eventState` vs
|
|
253
|
+
`seedSource` 双分支);**try-catch 兜底**(零拷贝失败自动回退官方实现并计入
|
|
254
|
+
`fallbacks`)。所有补丁 dispose 时按原形状还原(原型方法 delete 回落 / 自有属性
|
|
255
|
+
原样写回)。
|
|
256
|
+
|
|
257
|
+
## 已知限制
|
|
258
|
+
|
|
259
|
+
- `backfillOnBoot` 的解码在插件层同步执行(虽按 chunkSize 分片让出),超大文件
|
|
260
|
+
仍可能短暂冻结事件循环,故默认关闭,需要时手动开启。
|
|
261
|
+
- 0.1.3 上 `preparedCacheTrim` 自动跳空为 `inactive`(上游已系统代偿);若你仍
|
|
262
|
+
运行老版本 dsh,该补丁才真正生效。
|
|
263
|
+
- 插件不与 `dsh-perf`(配置订阅类)或 `dsh-pref-kit` 冲突,但行管理类插件禁用
|
|
264
|
+
`session-projection-cache` 会削弱本插件的缓存/预热功能。
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
MIT
|
package/cordis.patch.yml
ADDED
package/lib/api.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// webServer API:/dsh-large-proj-perf/api(stats / config),回环 + 同源校验
|
|
2
|
+
|
|
3
|
+
import { DEFAULT_CONFIG, NS } from './config.js'
|
|
4
|
+
|
|
5
|
+
// 仅回环地址 + 同源(Host/Origin/sec-fetch-site 三重校验),LAN 不可达
|
|
6
|
+
const isTrusted = (request) => {
|
|
7
|
+
try {
|
|
8
|
+
const host = request.headers && request.headers.host
|
|
9
|
+
if (typeof host !== 'string' || host === '') return false
|
|
10
|
+
const hostUrl = new URL('http://' + host)
|
|
11
|
+
const hostname = hostUrl.hostname
|
|
12
|
+
const loopback = hostname === 'localhost' || hostname === '[::1]'
|
|
13
|
+
|| (hostname.split('.').length === 4 && hostname.split('.')[0] === '127'
|
|
14
|
+
&& hostname.split('.').every((p) => /^\d{1,3}$/.test(p) && Number(p) <= 255))
|
|
15
|
+
if (!loopback) return false
|
|
16
|
+
if (request.headers['sec-fetch-site'] === 'cross-site') return false
|
|
17
|
+
const origin = request.headers.origin
|
|
18
|
+
if (origin === void 0) return true
|
|
19
|
+
return new URL(origin).host === hostUrl.host
|
|
20
|
+
} catch { return false }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const readBody = (req) => new Promise((resolve, reject) => {
|
|
24
|
+
let data = ''
|
|
25
|
+
req.setEncoding('utf8')
|
|
26
|
+
req.on('data', (chunk) => { data += chunk; if (data.length > 16384) { reject(new Error('body too large')); req.destroy() } })
|
|
27
|
+
req.on('end', () => { try { resolve(data ? JSON.parse(data) : {}) } catch (e) { reject(e) } })
|
|
28
|
+
req.on('error', reject)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const writeJson = (res, status, obj) => {
|
|
32
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
|
|
33
|
+
res.end(JSON.stringify(obj))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function installApi(pc, ictx) {
|
|
37
|
+
const { ctx, config, stats, logErr, safeGet, setConfigValue, retrimPreparedCache, backfill, disposers } = pc
|
|
38
|
+
const actx = ictx ?? ctx
|
|
39
|
+
const webServer = safeGet(actx, 'webServer')
|
|
40
|
+
if (!webServer || typeof webServer.register !== 'function') return
|
|
41
|
+
const off = webServer.register({
|
|
42
|
+
kind: 'prefix',
|
|
43
|
+
path: '/dsh-large-proj-perf/api',
|
|
44
|
+
handler: async (req, res) => {
|
|
45
|
+
if (!isTrusted(req)) return writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'forbidden' } })
|
|
46
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: { code: 'method-error', message: 'method not allowed' } })
|
|
47
|
+
let method = ''
|
|
48
|
+
try { method = new URL(req.url || '/', 'http://dsh.internal').pathname.slice('/dsh-large-proj-perf/api/'.length) } catch { method = '' }
|
|
49
|
+
if (!method || method.includes('/')) return writeJson(res, 404, { ok: false, error: { code: 'not-found', message: 'unknown method' } })
|
|
50
|
+
try {
|
|
51
|
+
if (method === 'stats.get') return writeJson(res, 200, { ok: true, value: stats })
|
|
52
|
+
if (method === 'stats.reset') {
|
|
53
|
+
// 只清计数器与历史;fastInitForInstalled 是安装状态标志(非测量值),刻意保留
|
|
54
|
+
stats.forks = 0; stats.zeroCopy = 0; stats.fallbacks = 0; stats.forkRecent = []
|
|
55
|
+
stats.warmed = 0; stats.skipped = 0; stats.aborted = 0; stats.backfilled = 0; stats.backfillSkipped = 0
|
|
56
|
+
stats.warmRecent = []; stats.backfill = []
|
|
57
|
+
return writeJson(res, 200, { ok: true })
|
|
58
|
+
}
|
|
59
|
+
if (method === 'config.get') return writeJson(res, 200, { ok: true, value: config })
|
|
60
|
+
if (method === 'config.set') {
|
|
61
|
+
const payload = await readBody(req)
|
|
62
|
+
const before = { ...config }
|
|
63
|
+
if (payload && typeof payload === 'object') {
|
|
64
|
+
for (const key of Object.keys(DEFAULT_CONFIG)) {
|
|
65
|
+
if (key in payload) setConfigValue(key, payload[key])
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (config.preparedCacheTrim !== before.preparedCacheTrim
|
|
69
|
+
|| config.preparedCacheSize !== before.preparedCacheSize) {
|
|
70
|
+
try { retrimPreparedCache?.() } catch (error) {
|
|
71
|
+
logErr(`prepared cache retrim failed: ${String(error?.message ?? error)}`)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// 开机定时器只触发一次;过后才打开 backfillOnBoot 的,立即补跑一次扫描
|
|
75
|
+
if (!before.backfillOnBoot && config.backfillOnBoot && backfill?.isFired()) {
|
|
76
|
+
Promise.resolve(backfill.backfillColdSessions()).catch((e) => logErr(`backfill scan failed: ${String(e?.message ?? e)}`))
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const settings = ctx.get('settings')
|
|
80
|
+
if (settings && typeof settings.update === 'function') await settings.update(NS, { ...config })
|
|
81
|
+
} catch { /* 内存态已生效 */ }
|
|
82
|
+
return writeJson(res, 200, { ok: true, value: config })
|
|
83
|
+
}
|
|
84
|
+
return writeJson(res, 404, { ok: false, error: { code: 'not-found', message: 'unknown method' } })
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return writeJson(res, 500, { ok: false, error: { code: 'rejected', message: String(error?.message ?? error) } })
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
if (typeof off === 'function') disposers.push(off)
|
|
91
|
+
}
|