In-job partitioned coverage
单 job 分区覆盖率
Native Windows coverage was the longest feedback path in the complete pull-request inventory. Keeping the instrumented suite in one single-worker Vitest process avoided the worker loss and Node 24 CJS lexer failures seen with larger in-process pools, but a failure could take more than fourteen minutes to appear and the gate runner withheld the child output until completion. The optimization must retain every test and
English
Problem
Native Windows coverage was the longest feedback path in the complete pull-request inventory. Keeping the instrumented suite in one single-worker Vitest process avoided the worker loss and Node 24 CJS lexer failures seen with larger in-process pools, but a failure could take more than fourteen minutes to appear and the gate runner withheld the child output until completion.
The optimization must retain every test and the merged per-file 100% thresholds. It must also stay inside the existing coverage job: splitting one suite across multiple workflow jobs would add checkout, installation, artifact transfer, and a merge job to the required topology.
Decision
The ordinary pnpm run test:coverage command remains one Vitest invocation. Linux coverage CI fixes DSH_COVERAGE_PARTITIONS=4, while native Windows fixes it at 8; no elapsed-time trigger changes either count while a run is in progress. The coverage-exempt heavy suite remains a separate uninstrumented gate beside the instrumented work.
When partitioning is enabled, scripts/run-gates.ts selects pnpm run test:coverage:partitioned for the instrumented gate. scripts/coverage-partitions.ts starts the configured Vitest children concurrently, each with one worker and one --shard=<index>/<count> option. Partition mode suppresses thresholds and coverage reporters in each child, gives every child a separate report directory, and writes one blob report per process.
The coordinator waits for every child, validates that the blob directory contains exactly the expected files, and then runs one vitest --merge-reports ... --coverage command. Only that merged command applies the repository's per-file statement, branch, function, and line thresholds, so a partition is never judged against an intentionally partial inventory.
DSH_COVERAGE_MAX_WORKERS continues to size the uninstrumented exempt gate and the ordinary non-partitioned path; it does not resize partition children. Native Windows gives the exempt gate two workers and admits four concurrent outer gates. Build, production-site validation, and instrumented coverage start immediately; exempt-heavy coverage starts only after build passes, preventing its temporary Oxlint probes from racing source compilation. The observational inventory waits only for both coverage gates to settle, so it still runs after a coverage failure; each gate's needs dependencies remain pass-required. Linux overlaps four instrumented partition processes with two exempt workers, restoring the ordinary path's former four-way instrumented concurrency while keeping every instrumented process single-worker.
Failure and output semantics
Partition children stream stdout and stderr through the coordinator. The coverage gate opts into run-gates streaming, so test progress and failures reach CI logs as they occur without buffering the complete log in the scheduler. The coordinator also retains a bounded 64 KiB combined tail per child; when a child settles unsuccessfully, it prints the spawn error, exit code, or signal and repeats that tail before validating the complete blob set, keeping the specific Vitest failure beside the final partition diagnostic.
A normal failed test still emits a blob through --coverage.reportOnFailure, allowing the merge to report the complete coverage state before the coordinator returns failure. Spawn failure, signal termination, non-zero exit, a missing or extra blob, or a failed merge all make the gate fail. The coordinator removes only its owned coverage tree and unlinks a link-shaped path instead of recursively following it.
Verification
scripts/coverage-partitions.spec.ts pins argument construction, package-script separator removal, one-worker partitions, the single merged threshold command, failed-test merging, failure diagnostics before complete-blob validation, waiting for sibling partitions after a spawn failure, and link-safe cleanup. scripts/run-gates.spec.ts pins opt-in selection, invalid-count rejection, the complete Windows inventory with its blocking split, and unbuffered streamed output.
Completed native Windows comparisons measured two partitions near 405 seconds and sixteen partitions at 112.66–122.01 seconds, but the sixteen-way schedule could put more than twenty active execution units beside build and exempt coverage on a 16-core runner. Eight partitions keep separate-process isolation while accepting a longer feedback path for a materially lower peak. Two Linux samples measured the conservative two-partition configuration at 276.68 and 282.27 seconds; that configuration was stable but halved the ordinary path's four instrumented workers. Four partitions restore that fan-out, for six total coverage execution units on the 16-core hosted runner and at most 36 across the failover VM's six runner instances. These values come from completed runs or fixed capacity bounds; an unfinished run crossing an arbitrary elapsed-time mark is not evidence for increasing concurrency.
Alternatives considered
Use workflow-level sharding. Rejected because multiple jobs repeat setup and need artifact upload, download, and a merge dependency. The selected partitioning uses multiple processes inside one job and one workspace.
Raise the Vitest worker count inside one instrumented process. Rejected because completed Windows trials at higher fan-out exposed worker exits, fixture instability, and Node 24 CJS lexer failures. Separate single-worker processes preserve isolation while still executing the selected partitions concurrently.
Use one partition count on every host. Rejected because Linux's four-process run and Windows's eight-process run have different startup costs and resource ceilings. Each fixed configuration requires its own completed end-to-end evidence.
Apply thresholds independently in each partition. Rejected because every partition intentionally sees only part of the suite and would report false uncovered files. Threshold ownership belongs to the merged report.
Consequences
Coverage pays one Vitest startup/configuration cost per partition and one report-merge cost, but it avoids another workflow topology and keeps one final threshold verdict. Partition output may interleave, while the partition start labels and Vitest file identities retain attribution.
Linux and Windows use the same coordinator with platform-specific partition counts and surrounding worker budgets. Local coverage stays simple unless a caller explicitly chooses the partitioned package script and supplies a valid count greater than one.
Future tuning starts from completed runs at one fixed configuration. Slow progress alone never raises partition count or outer concurrency, because repeated restarts would erase the only evidence needed to choose a stable setting.
中文
问题
原生 Windows 覆盖率是拉取请求完整清单中反馈最慢的路径。把插桩套件保留在单个 Vitest 进程内并只使用 1 个 worker,可以避开较大进程内 worker 池曾出现的 worker 丢失和 Node 24 CJS lexer 故障,但一次失败可能超过 14 分钟才会显现,而且门禁调度器会在子进程结束前扣住输出。
这项优化必须保留全部测试以及合并后的逐文件 100% 阈值,也必须留在既有覆盖率 job 内:若把同一套件拆到多个工作流 job,就会向必需拓扑增加 checkout、安装、产物传输和合并 job。
决策
普通的 pnpm run test:coverage 命令仍只启动一次 Vitest。Linux 覆盖率 CI 将 DSH_COVERAGE_PARTITIONS 固定为 4,原生 Windows 则固定为 8;运行期间不会由任何耗时触发器改变这两个数量。覆盖率豁免重型套件仍作为独立的无插桩门禁与插桩工作并排运行。
启用分区后,scripts/run-gates.ts 为插桩门禁选择 pnpm run test:coverage:partitioned。scripts/coverage-partitions.ts 按配置数量并发启动 Vitest 子进程,每个进程只用 1 个 worker,并各自接收一个 --shard=<index>/<count> 选项。分区模式会在各子进程中关闭阈值与覆盖率报告器,为每个子进程分配独立报告目录,并让每个进程写出 1 份 blob 报告。
协调器等待全部子进程结束,验证 blob 目录只包含预期文件,然后执行一次 vitest --merge-reports ... --coverage。只有这条合并命令应用仓库的逐文件语句、分支、函数与行阈值,因此系统不会拿有意不完整的测试清单单独判定任一分区。
DSH_COVERAGE_MAX_WORKERS 继续控制无插桩豁免门禁和普通非分区路径的规模,不会调整分区子进程。原生 Windows 为豁免门禁分配 2 个 worker,并允许 4 道外层门禁并发。构建、生产网站验证与插桩覆盖率会立即启动;豁免重型覆盖率只在构建通过后启动,避免其临时 Oxlint 探针与源码编译竞态。观测性清单只等待两道覆盖率门禁结算,因此在覆盖率失败后仍会运行;各门禁自身的 needs 依赖仍要求前置门禁通过。Linux 让 4 个插桩分区进程与 2 个豁免 worker 重叠运行,在保持每个插桩进程只有 1 个 worker 的同时,恢复普通路径原有的 4 路插桩并发。
失败与输出语义
分区子进程通过协调器流式传递 stdout 与 stderr。覆盖率门禁选择 run-gates 流式输出,因此测试进度与失败会在发生时进入 CI 日志,调度器不会缓冲完整日志。协调器还会为每个子进程保留一份有界的 64 KiB 混合输出尾部;子进程以失败状态结算时,它会打印 spawn 错误、退出码或信号,并在校验完整 blob 集合前重印这份尾部,使具体 Vitest 失败与最终分区诊断相邻。
普通测试失败仍通过 --coverage.reportOnFailure 产出 blob,使合并步骤可以先报告完整覆盖率状态,再由协调器返回失败。spawn 失败、信号终止、非零退出、blob 缺失或多余,以及合并失败都会让门禁失败。协调器只删除自己拥有的覆盖率目录树;若该路径是链接,则只 unlink,不递归跟随。
验证
scripts/coverage-partitions.spec.ts 固定了参数构造、包脚本分隔符移除、单 worker 分区、唯一一次合并阈值命令、失败测试合并、完整 blob 校验前的失败诊断、spawn 失败后等待兄弟分区,以及链接安全清理。scripts/run-gates.spec.ts 固定了显式启用、非法数量拒绝、完整 Windows 清单及其阻断性划分,以及不缓冲的流式输出。
已完成的原生 Windows 对比中,双分区耗时约 405 秒,16 分区耗时 112.66–122.01 秒,但 16 路调度与构建、豁免覆盖率并行时,会在 16 核运行器上形成超过 20 个活动执行单元。8 个分区继续保留独立进程隔离,同时接受更长的反馈路径,以显著降低峰值。两个 Linux 样本中,保守的双分区配置耗时 276.68 秒和 282.27 秒;该配置运行稳定,却把普通路径原有的 4 个插桩 worker 减半。4 个分区恢复这份并发,使 16 核托管 runner 上的覆盖率执行单元总数为 6,故障切换虚拟机的 6 个 runner 实例最多合计 36 个执行单元。这些数值来自完整运行或固定容量上限;运行尚未结束时跨过任意耗时刻度,不构成增加并发的证据。
曾考虑的替代方案
使用工作流级分片。 不予采用,因为多个 job 会重复设置工作,并需要上传、下载产物以及合并依赖。所选分区方案只在同一个 job 和工作区内使用多个进程。
提高单个插桩进程内的 Vitest worker 数。 不予采用,因为已完成的 Windows 高扇出试验暴露了 worker 退出、fixture(测试前置数据)不稳定和 Node 24 CJS lexer 故障。相互独立的单 worker 进程既保留隔离,也能让所选分区并发执行。
在每种宿主上使用相同的分区数量。 不予采用,因为 Linux 的 4 进程运行与 Windows 的 8 进程运行具有不同的启动成本与资源上限。每种固定配置都必须取得自己的端到端完整证据。
在每个分区内独立应用阈值。 不予采用,因为每个分区有意只看到套件的一部分,会误报未覆盖文件。阈值归合并报告所有。
后果
每个分区都要支付 1 次 Vitest 启动与配置开销,最后还要执行 1 次报告合并,但它不引入另一套工作流拓扑,并保留唯一的最终阈值判定。分区输出可能交错,但分区启动标签和 Vitest 文件标识仍可用于归因。
Linux 与 Windows 使用相同的协调器,并各自设置分区数量与外围 worker 预算。本地覆盖率默认保持简单;只有调用方显式选择分区包脚本并提供大于 1 的合法数量时,才启用分区。
未来调优从一个固定配置的完整运行开始。进度缓慢本身绝不会提高分区数量或外层并发,因为反复重启会抹掉选择稳定设置所需的唯一证据。