Repository navigation
Cold-opening a long session costs seconds: the whole log is decoded before the tail page is served #12627
Replies: 3 comments
|
Adding a second consumer of the same root cause, with measurements: incremental reads from a plugin through the public persistence API. The frame index you propose would fix this case as well. Use caseA plugin keeps a derived view per session (goal history, todos, child-session summaries) and refreshes it periodically. Long-lived agent teams make logs grow without bound, so the plugin is written to be incremental:
The work is incremental at the logical level, but the physical cost still scales with total log size. Why (
|
| operation | time |
|---|---|
stat, revision unchanged (30×) |
mean 1.77 ms, no read handle opened |
| first cold full read | 1013 ms |
after appending 1 event, read(offset) returning that 1 event (3×) |
807 / 817 / 832 ms |
| same revision, empty tail read | 1.4–1.9 ms (memo hit) |
Returning one appended event costs as much as reading the whole log. Logs in long-running sessions grow, so this cost keeps rising.
What would help (for the maintainers to decide)
Any public way to read only what was appended, at a cost proportional to the new data:
- Make
read(offset)physically skip the confirmed prefix. Your per-frame "first event seq" index would do this. - Or support cursor-based resumption:
readreturns an opaque cursor (revision plus physical position), the next call reads only later appends, and the call fails clearly if the prefix was rewritten. - Or provide a cross-instance append notification, so subscribers don't need to re-read cold sessions at all.
Our current workaround: at most one full read per cold session per 15 s, triggered only when the revision changes, plus a persisted derived checkpoint so a restart doesn't refold. It works, but the cost grows with the log.
中文摘要:补充同一根因的另一种场景。插件通过公开的持久化接口做增量读取:冷会话按 revision 判断是否变化,变化后只调用 read(offset) 读新增部分。但任何追加都会改变 revision、让 memo 失效,read(offset) 仍会整份解码、校验、冻结后再切片。实测 37MB / 18,280 事件,只追加 1 条,读出这 1 条要约 0.81 秒。希望提供成本与新增数据量成正比的公开读取方式,例如本帖提出的分帧索引、基于游标的续读,或跨实例的追加通知。
|
Thanks for the detailed write-up. I measured the same frame-count effect synthetically and want to add one data point plus a scoping nuance. Synthetic benchmark. On a 1,000-frame JSONL+Zstd log, creating a fresh Zstd decoder per frame made a 1,000-event tail read take ~10 s. Reusing one decoder across a contiguous frame batch and extracting only the final N lines with native Buffer operations took ~9–18 ms. The frame count, not the payload size, dominated. Nuance. Current Missing capability. What upstream does not have is frame-indexed tail selection: use Would maintainers welcome a PR for the frame-indexed tail read plus checkpoint-served first page? Happy to prototype it against the current persistence seam and cover the index/checkpoint invariants with tests. |
|
Thanks — the code-point analysis matches current master. One correction and one addition. Correction on the decoder. Master already reuses a single decoder across the frame batch: What is still missing. Frame-indexed tail selection. The same restore runs elsewhere. Full materialization is not unique to opening a Session: the first prompt after a resume and model selection on a cold Session both go through resume and pay the same cost. A tail-oriented loader used by resume fixes those cases too. Would a PR that adds the frame index plus checkpoint seeding be welcome? I would keep the durable format unchanged, deriving the index on first read and caching it per generation. 中文说明感谢——对代码点的分析与当前 master 一致。这里有一处更正和一点补充。 关于 decoder 的更正。 Master 已经在整个帧批次上复用单个 decoder: 仍然缺失的部分。 按帧索引的尾部选择。 同样的恢复也发生在其他路径。 全量物化不只出现在打开 Session 时:恢复后的第一条 prompt,以及在冷 Session 上选择模型,都会走恢复流程并付出同样代价。让恢复使用面向尾部的 loader 也能修好这些场景。 是否欢迎一个加入帧索引并用检查点预填首页的 PR?我会保持持久化格式不变,在首次读取时推导索引并按 generation 缓存。 |
Uh oh!
There was an error while loading. Please reload this page.
Summary
Opening a cold long session (one not currently attached) in the Web GUI takes roughly 2–3 seconds here. From reading the shipped packages, the cold read path materializes and validates the entire event log before it can serve the bounded tail page the client asks for. For one representative session, pure decompress + JSON.parse alone is ≈1.5 s.
This is a report with measurements and one suggested direction, not a request for a specific design.
Environment
dsh,dsh-session-persistence-jsonl,dsh-session-persistence,dsh-session-query,dsh-api-session-controllerall 0.2.2-rc.1), Web profile, Linux.session.v4.jsonl.zstd, zstd frames).Measurements
Across 659 local session logs (690 MB total), the median log is 0.43 MB; 9 are ≥5 MB and 5 are ≥20 MB — i.e. only a subset is affected, matching the "some long sessions" observation.
Representative log:
zstd -dcto /dev/nullJSON.parse(stream)≈2 events per frame; per-frame overhead dominates the decode time.
Where the cost is (from the shipped code)
session/followandsession/eventsopen an observation; for a cold session the observation reads the whole artifact:readStableJsonlFile+readZstdPrefix(dsh-session-persistence-jsonl).validateStoredEvents(dsh-session-persistence),adoptSessionEvent+deepFreeze(dsh-session), plus seed validation and the surface fold when the session is restored.paginate(dsh-api-session-controller) slice the requested tail page.session-projection-cache) accelerates projection folds only; the prepared-observation cache (preparedSessionCacheSize, default 5) makes repeat opens of the same cold session fast, but evictions and restarts fall back to the full read.Suggested direction (one option)
structuredClonein query paths that return only a slice.Reproduction (read-only)
Caveats
中文摘要
冷启动打开一个“当前未附着”的超长会话约需 2–3 秒。从已安装包看,宿主在提供有界的尾部一页之前,会先把整份日志解码、逐事件校验并冻结;实测代表样本 68.8MB(解压 191.5MB、43,332 事件、22,113 个 zstd 帧),仅解压 0.40s + 逐行 JSON.parse 1.12s ≈ 1.5 秒。客户端首屏本来就限 ≤500 条消息,瓶颈在宿主侧全量物化。建议方向:写入时记录“每帧首事件 seq”小索引,读取只解码尾部所需帧;放大写入帧;只读页只校验切片;查询路径不要整数组深拷贝。以上为只读测量,未读取会话内容。
All reactions