Repository navigation
会话累积后启动慢/内存高:projcache 全量载入与 session 列表全量扫描(0.2.1-rc.5 实测,附复现脚本与补丁建议) #11856
Scorpiolol
started this conversation in
General
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
English short version: After sessions accumulate, (1) every host boot eagerly loads the entire
session_projcachedomain (all record documents read + parsed + schema-validated; 4923 records / 1011 MB here → ~87 s of single-thread CPU, ~700 MB retained heap), and (2) every session-list request walks the whole tree, opens one log per session, reads its 8 KiB header, and stats every log (~7.8 s for 5975 sessions). A third walk is repeated on the same request when any log predates the current format, becausehistoricalCorpusRevision()is not memoized. All three scale linearly with session count. Reproducer and a small patch below — posted as data, not as a demand.环境
latest)~/.dsh/sessions≈ 5975 个会话目录 / 5.2 GB(日志 5999 个,其中 4170 个是 v4 之前的老格式)~/.dsh/storages/session_projcache4923 条记录 / 1.0 GB观察到的现象
loadAll是open()里的await,会把宿主启动阻塞住)。phys_footprint2.4 GB / 峰值 3.3 GB,整机被拖进 swap。~/Library/Logs/DeepSeek Harness/crash-*.log里 5 次崩溃,全部是FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory,栈落在
node::fs::AfterOpenFileHandle→uv__work_done(异步 fs 回调),与上面第 1 条的全量读取吻合。三条机制(以 master 源码为准)
1)
open()内部 eager 全量loadAll()packages/storage/storage-domain/src/index.ts:119KvUnit接口只有loadAll(),没有 lazy / paged / stream 的替代读法。session-projection-cache在Service.init里无条件storageDomain.open(spec),于是每次宿主启动都会把整个 domain 拉进内存。2)per-record 后端无界并发读
packages/storage/storage-json/src/per-record-unit.ts:166上层
:87还有一层对 table 目录的Promise.all,两层相乘。每条记录都被readFile成字符串再JSON.parse,随后由 domain 层逐条做 schema 校验;一次 fan-out 会在首条记录进入records之前持有整张表的瞬时副本。记录数随会话数增长,峰值内存也就随会话数增长。3)会话列表全量扫描 + 语料 revision 未记忆化
packages/session/session-persistence-jsonl/src/index.tslistArtifacts()(:1138起):全树readdir→ 每个会话解析出一个 log generation →open()+ 读 8192 B 头(:1499)→ zstd 解帧 →JSON.parse。list()(:522):再对每个 artifactstat()一次。historicalCorpusRevision()(:1121-1136):每次调用都重新listGenerations()全树 walk + 对每个 logstat()+ 对[path, revision]做 sha256,结果没有缓存。只要存在一个sourceVersion < SESSION_FORMAT_VERSION的日志(:531),每次list()都要多付这一整份。本机 4170/5975 是老格式,所以这份税是每次列表请求都付的。
复现
脚本只读
~/.dsh,不碰任何运行中的进程;在会话较多的机器上跑,数字会随会话数变化。repro-dsh-session-scale.mjs
本机输出(脚本是串行的,真实代码
list侧是pLimit(3),墙钟时间会不同):清理前的实测数据
open()全量加载:读 +JSON.parseheapUsedlist():open/readdir/stat次数list()单次墙钟为什么这不是个例
两项成本的分母都是「会话数 × 单条大小」与「会话数」。会话只增不减(见下),所以这是一个必然随时间触顶的形状:在本机约 5000 条会话时就已经到 2–3 GB 峰值堆,再翻一倍就会撞上 V8 的堆上限——这也正是崩溃日志里的 OOM。
想确认的问题
storageDomain.open()里的 eagerloadAll()是有意的设计(比如为了在 open 后保证同步读一致),还是可以改成 lazy / paged / 按需?KvUnit目前只有loadAll()一个读法。session-persistence-jsonl的 README 明确写了 "Nothing deletes session files" 与 "No eviction or retention surface"。这个「累积由外部维护」的立场是有意的吗?如果是,社区侧能安全依赖的手工清理姿势是什么(尤其投影缓存里的孤儿记录——删掉会话目录并不会让它消失,下次启动仍会被全量加载)。historicalCorpusRevision()的结果能否按(log path, revision/size, mtime)记忆化?看起来它是纯函数式的,只是每次重算。附带一个最小补丁(供参考,不要求采纳)
把
loadTableRecords()的无界Promise.all改成有界并发(默认 16),不改任何语义:结果顺序、Promise.all的 rejection 行为、以及「单条坏记录读作缺失」都保持不变。已用一个独立 harness 验证 26 项(16 项等价性 + 4 项并发上限 + 6 项 rejection 语义):空表 / 单条 / 25 条 / 200 条 / falsy 与结构化值 / 坏记录读作缺失 / readdir 失败透传 / 实测 in-flight 由 64 降至 16 且小表不被串行化 / 首个拒绝胜出且无未处理 rejection。
0001-bounded-concurrency.patch
如果这三条里哪一条其实是预期行为,还请直接说,我就不再往这个方向推了。感谢。
All reactions