DSH / Atlas
2026-08-31implementedprocessarchived 2026-09-04

serial-windows notices timeout budget and generator store-scan cost

serial-windows 的 notices 超时预算与 generator store 扫描成本

The `serial / windows (self-hosted standby)` master lane failed its `test:coverage` gate four times in a week (runs 33333033178, 33311481884, 33352293522, 33353113100), always on the same case: `scripts/gen-third-party-notices.spec.ts > THIRD_PARTY_NOTICES.md > matches what the generator produces from the current manifests`, with `Error: Test timed out in 5000ms`. Measured test wall times on the shared Windows host w

English

Problem

The serial / windows (self-hosted standby) master lane failed its test:coverage gate four times in a week (runs 33333033178, 33311481884, 33352293522, 33353113100), always on the same case: scripts/gen-third-party-notices.spec.ts > THIRD_PARTY_NOTICES.md > matches what the generator produces from the current manifests, with Error: Test timed out in 5000ms. Measured test wall times on the shared Windows host were 4149–8853 ms against Vitest's default 5000 ms per-test budget. All other 26 cases in the file finished in 0–3 ms, and the passing run two hours later (33360033028) had the same code green.

The lane runs the complete unsharded Windows gate inventory serially with DSH_COVERAGE_MAX_WORKERS=1, so render() regenerates THIRD_PARTY_NOTICES.md from the workspace manifests and the installed pnpm store on a host shared by 32 runners. The cold path is dominated by workspaceLinkedManifest, which re-ran loadWorkspaceManifests() — a glob plus reads and JSON-parses of every workspace package.json — once per cache-missing external dependency name: 130 names × 270 manifests ≈ 35k file operations, on top of a .pnpm store scan per name. Under v8 coverage instrumentation and shared-host I/O contention that crossed the 5 s default.

The lane also had no DSH_COVERAGE_TEST_TIMEOUT_MS, unlike the pull-request windows-coverage lane (ci.yml) which grants 90000 ms, so the serial reference ran the same coverage inventory at the strictest budget of any lane.

Decision

Two changes:

  1. scripts/gen-third-party-notices.ts loads the workspace manifests once in render() and threads the map through collectNpmDeps → installedMetadata → installedManifest → workspaceLinkedManifest instead of reloading it per external dependency name. The cold render() wall time on the same checkout fell from ~893 ms to ~86 ms with byte-identical output (verified by diffing the rendered documents before and after the change).

  2. ci-master.yml serial-windows step "Run complete unsharded Windows gate inventory serially" gains DSH_COVERAGE_TEST_TIMEOUT_MS: '90000', matching the pull-request windows-coverage lane budget. This extends the per-test, expect.poll, and hook budget mechanism defined by the Windows lane hook and Lefthook budget note to a second lane; that note records which lanes set the env. scripts/ci-workflow.spec.ts pins this env with a toMatchObject assertion; removing the env turns the spec red (negative control exercised).

Alternatives considered

  • Raise the lane budget only - rejected as the sole fix: it would mask the O(names × manifests) reload for every lane that runs the generator, including the pre-commit hook and the standalone --check path.
  • Module-level cache for loadWorkspaceManifests() - rejected in favor of explicit threading, which keeps the single-load contract visible at the call site and avoids a second hidden cache next to workspaceLinkedManifestCache.

Consequences

The generator resolves installed metadata from one manifest load per render() call, and clears the name-keyed linked-manifest cache at the start of each call so the cache cannot outlive the map it was resolved from. The serial-windows lane runs the coverage inventory at the same 90000 ms per-test budget as the pull-request coverage lane. THIRD_PARTY_NOTICES.md bytes are unchanged; the freshness spec still compares render() against the committed document.

中文

Problem

serial / windows (self-hosted standby) master lane 一周内四次失败在 test:coverage gate(run 33333033178、33311481884、33352293522、33353113100),失败用例每次都相同:scripts/gen-third-party-notices.spec.ts > THIRD_PARTY_NOTICES.md > matches what the generator produces from the current manifests,报 Error: Test timed out in 5000ms。共享 Windows 主机上该用例实测 4149–8853 ms,超出 Vitest 默认的 5000 ms 单测预算。文件其余 26 个用例全部 0–3 ms 通过,两小时后的 passing run(33360033028)用同一份代码全绿。

该 lane 以 DSH_COVERAGE_MAX_WORKERS=1 串行跑完整的无分片 Windows gate 清单,render() 要从 workspace manifest 和已安装的 pnpm store 全量重生成 THIRD_PARTY_NOTICES.md,而主机被 32 个 runner 共享。冷路径的代价集中在 workspaceLinkedManifest:它对每个未缓存的外部依赖名重跑一遍 loadWorkspaceManifests()——glob 并读取、解析全部 workspace package.json——即 130 名 × 270 manifest ≈ 3.5 万次文件操作,另加每个名字一次 .pnpm store 扫描。叠加 v8 覆盖率插桩与共享主机 I/O 争抢后越过 5 秒默认值。

该 lane 还没有 DSH_COVERAGE_TEST_TIMEOUT_MS,而 pull-request 的 windows-coverage lane(ci.yml)给的是 90000 ms——于是这条 serial 参考 lane 用全仓库最紧的预算跑同一份 coverage 清单。

Decision

两处改动:

  1. scripts/gen-third-party-notices.ts 在 render() 里只加载一次 workspace manifest,把 map 沿 collectNpmDeps → installedMetadata → installedManifest → workspaceLinkedManifest 显式传递,不再按外部依赖名逐个重载。同一 checkout 下冷 render() 墙钟从约 893 ms 降到约 86 ms,输出逐字节一致(改动前后渲染结果 diff 验证)。

  2. ci-master.yml serial-windows 的 "Run complete unsharded Windows gate inventory serially" 步骤增加 DSH_COVERAGE_TEST_TIMEOUT_MS: '90000',与 pull-request windows-coverage lane 对齐。这是把 Windows 覆盖率 lane 的 hook 预算与 Lefthook 套件预算 note 定义的 per-test、expect.poll 与 hook 预算机制扩展到第二个 lane;该 note 记录了哪些 lane 设置此 env。scripts/ci-workflow.spec.ts 用 toMatchObject 断言钉住该 env;删掉 env 会让 spec 变红(已做负例验证)。

Alternatives considered

  • 只放宽 lane 预算 - 否决作为唯一修复:会掩盖所有运行 generator 的 lane 上的 O(名×manifest) 重载成本,包括 pre-commit hook 与独立 --check 路径。
  • 给 loadWorkspaceManifests() 加模块级缓存 - 否决,改用显式传递:把「单次加载」契约留在调用点可见,避免在 workspaceLinkedManifestCache 之外再加一层隐藏缓存。

Consequences

generator 每次 render() 调用只加载一次 manifest 来解析已安装元数据,并在调用开头清空按名字作键的 linked-manifest 缓存,使缓存不会活过它解析自的那份 map。serial-windows lane 与 pull-request coverage lane 一样按 90000 ms 单测预算跑 coverage 清单。THIRD_PARTY_NOTICES.md 字节不变;新鲜度 spec 仍把 render() 与已提交文档逐字节比较。