DSH / Atlas
2026-09-28implementedtesting

Performance baselines for large Session corpora

大型 Session 语料的性能基线

Existing gates measure one long Session: [opening it](2026-09-04-session-open-performance-gate.md) or [continuing it](2026-09-06-backend-continuation-performance.md). Listing, content search, and fork scale with the whole corpus or with the source Session's length, and no gate covered a user with many long Sessions. A corpus of uniform small Sessions would understate the work, because real corpora are heavy-tailed.

English

Problem

Existing gates measure one long Session: opening it or continuing it. Listing, content search, and fork scale with the whole corpus or with the source Session's length, and no gate covered a user with many long Sessions. A corpus of uniform small Sessions would understate the work, because real corpora are heavy-tailed.

Decision

The session-corpus benchmark measures three Web Host endpoints over synthetic current-generation Zstandard corpora. No product implementation changes.

Workload

On 2026-09-28, one developer's local DSH home held 1,650 Sessions in 88 project directories. Aggregate counts were extracted from the newest generation of each Session; no content, identity, or path left that machine. corpus-shape.ts records 22 upper-quantile anchors for three dimensions: logical events (compact delta rows expanded), decompressed bytes, and turns.

QuantileEventsLogical bytesTurns
p505386,1951
p907091,849,5033
p997,2298,479,55418
max84,46748,139,8772,181

Rank r of an n-Session corpus takes the first anchor at or above quantile (r + 1) / n, so every quantile of the synthetic corpus is at least the measured one. All dimensions share the rank: a long Session is long in every dimension. At 1,000 Sessions this upper-step sampling adds 16% events and 14% bytes over the measured quantile function; coarser anchors added up to 40%.

Other measured aggregates also set the fixture: 19 Sessions per project directory, one subagent child per six Sessions (measured 288 of 1,650), a 20,000-character system prompt (the measured median largest event is 21.9 KB), 3.9 characters per streamed delta, and 1.6 events per Zstandard frame. Turns contain tool steps (assistant message, tool call, tool result) until the event target is reached; a secant search over payload characters then reaches the byte target from below. Text comes from a Zipf-distributed synthetic vocabulary. Its stored compression ratio is 2.8, below the measured 3.4, so the fixture stores more compressed bytes than the source corpus.

Corpus construction

Two preparation processes, one per CPU of a standard hosted runner, split the anchors into byte-balanced groups. Each authors one event body per anchor, compresses it once, and saves it with the anchor's projection record. A seed process then writes each Session as its own header frame followed by its anchor's body. Frame compression and file writes run eight at a time: each pending compression holds a native Zstandard context, and unbounded compression peaked at 7.7 GB RSS, more than a standard hosted runner provides. Sessions sharing an anchor differ only in header identity, creation time, project directory, and parent link. Creation time is a fixed permutation of rank. List order follows each body's last prompt time, which Sessions sharing an anchor share; it does not affect list cost. Events carry the fields that Session.append records, compact streams come from the production AssistantStreamAccumulator, and rows are encoded with the persistence package's eventLines. Before saving, each preparation process replays its authored bodies through Session.create, as sessionQuery.readSession does.

The projection cache is part of the list endpoint: each cold row carries cached projection values. Preparation folds each anchor once through the public sessionProjectionCache.coldSnapshot write-back. The seed then writes per-record documents for every Session with serializeRecord, rebinding identity to each header. Writing through the domain API costs one fsync per record, which took 89 s for 10,000 Sessions. The list worker requires every returned row to carry projections, so a storage layout change fails the benchmark instead of silently removing that work.

Measured endpoints

Workers are compiled plain-Node processes, each started fresh against a seeded root. They compose the production Session store, projection registry, JSONL persistence, SQLite Session query with openAt: first-search on an in-memory index, JSON storage and projection cache, the Web projection set, AgentLoop, and the real SessionController. Transport lookups, upload registration, default model selection, and the Workspace registry are inert providers, because no measured path depends on them.

CaseCorpusSamplesEndpoint
List3,0003Host boot; first session.list; repeated session.list in the same process
Content search1,0001First session.search, including the index build; a second query with a different term
Fork1,000, separate root3, then 1session.fork return for rank midpoints of ten equal length strata and rank 988; then rank 999 once

The file must finish within five minutes on standard hosted CI, including seeding, and its last case asserts that limit. Search over 1,000 Sessions and one fork of the longest Session take about 170 s of it, so each has one sample. The list corpus is the largest that fits the remainder. Budgets enforce medians of list boot, first, and repeat, and of the fork median over strata and rank 990; search and the longest fork enforce their single samples. Search and fork use separate 1,000-Session roots, so fork children never reach the search index. Rank 988 is the highest top-level rank at the measured p99 (7,229 events); forking a subagent child adds a lineage lookup, and rank 990 already falls in the 99.3% anchor. Peak RSS of the list, search, and fork processes has a budget with the shared 1.25 headroom and no time scale. Each child's deadline equals the file limit, and each test's deadline outlasts its sequential children, so a failed case reaps every child before the scratch root is removed.

Calibration evidence

Budgets use standard two-CPU hosted CI expectations (Linux x64, Node 24.21, PR #5403), rounded above the higher recorded median across the AMD EPYC 7763 and EPYC 9V74 runners and multiplied by the shared 1.25 headroom. Samples varied by up to 15% within one run; the runner models differed by up to 25% per endpoint. The hosted-to-reference ratio is 2.4 to 3.4, above the shared reference scale of 2, so reference-machine scaling would reject ordinary hosted runs.

Endpoint (7763 / 9V74)Hosted mediansExpectationBudget
List 3,000: boot2,317.8 / 2,204.5 ms2,400 ms3,000 ms
List 3,000: first3,228.7 / 2,633.6 ms3,300 ms4,125 ms
List 3,000: repeat2,636.4 / 2,235.1 ms2,700 ms3,375 ms
Search 1,000: first (one sample)144,359 / 135,065 ms145,000 ms181,250 ms
Search 1,000: repeat (one sample)3,426.3 / 2,704 ms3,500 ms4,375 ms
Fork: strata median95.5 / 73.5 ms100 ms125 ms
Fork: p99, rank 988 (7763 only)1,066.2 ms1,100 ms1,375 ms
Fork: longest (one sample)19,162.1 / 20,326.7 ms20,400 ms25,500 ms
Peak RSS: list / search / fork742.7 / 1,197.8 / 745.1 MB (higher model)750 / 1,200 / 750 MB938 / 1,500 / 938 MB

With seeding in one process, the complete file took 294.5 s on the EPYC 7763 runner (85 s seeding, 26 s listing, 149 s searching, 34 s forking) and 266.6 s on the 9V74. With parallel preparation it took 276.8 s on the 7763: the two preparation processes took 59 s and 65 s, because they share two CPUs with Zstandard and garbage-collection threads, and writing the corpora took 4.6 s. The other cases varied by less than 3% between runs on one runner model. Each additional 1,000 listed Sessions costs about 9 s, so a larger list corpus would leave less than a tenth of the limit for runner variation.

On Apple M5 Pro with Node 26.5, isolated runs measured list 1,000 at 311–358 ms first, list 5,000 at 1,364–1,603 ms first, search at 57 s first, and fork of the longest Session at 6.0–6.4 s. A 10,000-Session list took 3.2–5.4 s for the first call with 1.9 GB peak RSS.

Alternatives considered

  • Sanitized copies of real Sessions. Rejected: benchmarks never use recorded user material.
  • Authoring every Session through Session.append. Rejected: per-append validation and freezing, repeated per Session, made seeding take minutes.
  • Omitting the projection cache. Rejected: rows would lack the values the Web list shows, which understates list work.
  • A separate CI job. Rejected: benchmarks share one idle standard runner. This file is limited to five minutes, and the job timeout rises from 15 to 20 minutes.
  • Listing 5,000 Sessions with five samples, and two list sizes. Rejected: with the other cases the file took 6.6 minutes on standard hosted CI.

Consequences

The gate covers corpus-scale list, search, and fork costs that single-Session gates cannot see. On standard hosted CI it already shows that content search spends 139 s building its index on first use, that the first list took 5.1 s at 5,000 Sessions in calibration, and that forking the longest Session takes 17.5 to 20.3 s. Seeding writes about 1.5 GB.

Exclusions: the corpus is current-generation only, although most real Sessions are stored in older generations, which list revision hashing and migration-on-read would add. Only 22 distinct bodies exist, so search indexes repeated text. Fork runs without Agent presets or Workspace attachment. Calls are in-process, without Typert transport or a browser. Filesystem caches are not evicted, so cold means a fresh process, not cold storage.

中文

问题

现有门禁只测量单个长 Session:打开或续聊。列表、内容搜索和 fork 的成本随整个语料或源 Session 长度增长,此前没有门禁覆盖拥有大量长 Session 的用户。由均匀小 Session 组成的语料会低估工作量,因为真实语料呈重尾分布。

决定

session-corpus 基准在合成的当前代 Zstandard 语料上测量三个 Web Host 端点。产品实现不做任何改动。

工作负载

2026-09-28,一名开发者的本地 DSH home 在 88 个项目目录中保存了 1,650 个 Session。统计只从每个 Session 的最新代中提取聚合计数;内容、身份和路径都没有离开那台机器。corpus-shape.ts 为三个维度记录 22 个上分位锚点:逻辑事件数(展开紧凑 delta 行)、解压字节数和轮次数。

分位事件数逻辑字节数轮次数
p505386,1951
p907091,849,5033
p997,2298,479,55418
max84,46748,139,8772,181

n 个 Session 的语料中,排名 r 取不低于分位 (r + 1) / n 的第一个锚点,因此合成语料的每个分位都不低于实测分位。所有维度共享同一排名:长 Session 在每个维度上都长。在 1,000 个 Session 时,这种上阶梯采样比实测分位函数多出 16% 的事件和 14% 的字节;更粗的锚点最多多出 40%。

其他实测聚合值也用于确定夹具:每个项目目录 19 个 Session,每六个 Session 中一个 subagent 子会话(实测为 1,650 个中的 288 个),20,000 字符的 system prompt(实测最大事件的中位数为 21.9 KB),每个流式 delta 3.9 个字符,每个 Zstandard 帧 1.6 个事件。轮次中持续加入工具步骤(assistant 消息、工具调用、工具结果)直到达到事件目标;随后对负载字符数做割线搜索,从下方达到字节目标。文本来自 Zipf 分布的合成词表。其存储压缩比为 2.8,低于实测的 3.4,因此夹具存储的压缩字节多于源语料。

语料构建

两个准备进程(对应标准托管运行器的每个 CPU)把锚点分成字节均衡的两组。每个进程为每个锚点生成一份事件主体,只压缩一次,并与该锚点的投影记录一起保存。随后播种进程把每个 Session 写成独立的 header 帧加上其锚点的主体。帧压缩和文件写入每次并发八个:每个待完成的压缩都持有一个原生 Zstandard 上下文,不加限制的压缩峰值 RSS 达到 7.7 GB,超过标准托管运行器的内存。共享锚点的 Session 只在 header 身份、创建时间、项目目录和父链接上不同。创建时间是排名的固定置换。列表顺序跟随各主体的最后提示时间,共享锚点的 Session 也共享该时间;这不影响列表成本。事件携带 Session.append 记录的字段,紧凑流来自生产 AssistantStreamAccumulator,行通过持久化包的 eventLines 编码。保存之前,每个准备进程按 sessionQuery.readSession 的方式,通过 Session.create 回放其生成的主体。

投影缓存属于列表端点:每个冷行都携带缓存的投影值。准备过程通过公开的 sessionProjectionCache.coldSnapshot 回写,为每个锚点折叠一次。之后播种进程用 serializeRecord 为每个 Session 写入逐记录文档,并把身份重新绑定到各自的 header。通过 domain API 写入时每条记录一次 fsync,10,000 个 Session 耗时 89 s。列表 worker 要求返回的每一行都携带投影,因此存储布局变化会使基准失败,而不是静默去掉这部分工作。

测量端点

worker 是编译后的纯 Node 进程,每次都针对已播种根目录全新启动。它们组合生产 Session store、投影注册表、JSONL 持久化、使用 openAt: first-search 和内存索引的 SQLite Session query、JSON 存储和投影缓存、Web 投影集合、AgentLoop 以及真实的 SessionController。传输查找、上传注册、默认模型选择和 Workspace 注册表使用惰性提供者,因为没有被测路径依赖它们。

用例语料样本数终点
列表3,0003Host 启动;首次 session.list;同一进程中重复的 session.list
内容搜索1,0001首次 session.search,包括索引构建;使用不同词项的第二次查询
Fork1,000,独立根目录3,然后 1十个等长度分层的中点排名和排名 988 的 session.fork 返回;然后排名 999 一次

此文件(包括播种)必须在标准托管 CI 上五分钟内完成,其最后一个用例断言该上限。1,000 个 Session 的搜索和最长 Session 的一次 fork 约占其中 170 s,因此两者各取一个样本。列表语料是剩余时间内能容纳的最大规模。预算约束列表启动、首次和重复的中位数,以及 fork 分层中位数和排名 990 的中位数;搜索和最长 fork 约束各自的单个样本。搜索和 fork 使用各自独立的 1,000 个 Session 根目录,因此 fork 子会话不会进入搜索索引。排名 988 是实测 p99(7,229 个事件)上最高的顶层排名;fork subagent 子会话会增加一次谱系查找,而排名 990 已落入 99.3% 锚点。列表、搜索和 fork 进程的峰值 RSS 设有预算,使用共享的 1.25 余量,不做时间缩放。每个子进程的截止时间等于文件上限,每个测试的截止时间长于其顺序子进程之和,因此失败的用例会在删除临时根目录之前回收所有子进程。

校准证据

预算使用标准双 CPU 托管 CI 的期望值(Linux x64、Node 24.21、PR #5403),取高于 AMD EPYC 7763 与 EPYC 9V74 运行器中较高记录中位数的整数,再乘以共享的 1.25 余量。同一次运行内的样本差异最多 15%;两种运行器型号在单个端点上最多相差 25%。托管与参考机器的比值为 2.4 到 3.4,高于共享的参考缩放系数 2,因此按参考机器缩放会拒绝正常的托管运行。

端点(7763 / 9V74)托管中位数期望值预算
列表 3,000:启动2,317.8 / 2,204.5 ms2,400 ms3,000 ms
列表 3,000:首次3,228.7 / 2,633.6 ms3,300 ms4,125 ms
列表 3,000:重复2,636.4 / 2,235.1 ms2,700 ms3,375 ms
搜索 1,000:首次(一个样本)144,359 / 135,065 ms145,000 ms181,250 ms
搜索 1,000:重复(一个样本)3,426.3 / 2,704 ms3,500 ms4,375 ms
Fork:分层中位数95.5 / 73.5 ms100 ms125 ms
Fork:p99,排名 988(仅 7763)1,066.2 ms1,100 ms1,375 ms
Fork:最长(一个样本)19,162.1 / 20,326.7 ms20,400 ms25,500 ms
峰值 RSS:列表 / 搜索 / fork742.7 / 1,197.8 / 745.1 MB(较高型号)750 / 1,200 / 750 MB938 / 1,500 / 938 MB

单进程播种时,整个文件在 EPYC 7763 运行器上耗时 294.5 s(播种 85 s,列表 26 s,搜索 149 s,fork 34 s),在 9V74 上耗时 266.6 s。并行准备后,它在 7763 上耗时 276.8 s:两个准备进程分别耗时 59 s 和 65 s,因为它们与 Zstandard 和垃圾回收线程共享两个 CPU;写入语料耗时 4.6 s。其他用例在同一运行器型号的多次运行之间差异小于 3%。列表语料每增加 1,000 个 Session 约多花 9 s,因此更大的列表语料会让留给运行器差异的时间不足上限的十分之一。

在 Apple M5 Pro 和 Node 26.5 上的隔离运行中,1,000 个 Session 的首次列表为 311–358 ms,5,000 个 Session 的首次列表为 1,364–1,603 ms,首次搜索为 57 s,fork 最长 Session 为 6.0–6.4 s。10,000 个 Session 的首次列表耗时 3.2–5.4 s,峰值 RSS 为 1.9 GB。

考虑过的替代方案

  • 脱敏复制真实 Session。 已否决:基准从不使用录制的用户材料。
  • 通过 Session.append 生成每个 Session。 已否决:逐次追加的验证和冻结在每个 Session 上重复,使播种耗时数分钟。
  • 省略投影缓存。 已否决:行会缺少 Web 列表显示的值,从而低估列表工作量。
  • 单独的 CI 任务。 已否决:基准共享一台空闲的标准运行器。此文件限制在五分钟内,任务超时从 15 分钟提高到 20 分钟。
  • 以五个样本列出 5,000 个 Session,并测两种列表规模。 已否决:加上其他用例后,此文件在标准托管 CI 上耗时 6.6 分钟。

后果

该门禁覆盖单 Session 门禁看不到的语料规模列表、搜索和 fork 成本。在标准托管 CI 上,它已经显示:内容搜索在首次使用时花 139 s 构建索引,校准时 5,000 个 Session 的首次列表耗时 5.1 s,fork 最长 Session 耗时 17.5 到 20.3 s。播种写入约 1.5 GB。

排除项:语料只包含当前代,而大多数真实 Session 以旧代存储,这会增加列表的修订哈希和读取时迁移。只有 22 份不同主体,因此搜索索引的是重复文本。Fork 不挂载 Agent preset,也不附加 Workspace。调用在进程内完成,不经过 Typert 传输或浏览器。不驱逐文件系统缓存,因此“冷”指全新进程,而不是冷存储。