From cbbef79fa3374386e9f3639ca3d4dd947cef111a Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Wed, 22 Jul 2026 19:00:55 +0800 Subject: [PATCH] ci: retune consolidated jobs for stacked workload --- ...ence-based-larger-hosted-runners.i18n.yaml | 4 +- ...22-evidence-based-larger-hosted-runners.md | 8 +- ...evidence-based-larger-hosted-runners.zh.md | 8 +- .github/workflows/ci.yml | 77 ++++++++++++------- 4 files changed, 61 insertions(+), 36 deletions(-) diff --git a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml index d2114f1bd0..d151088a0a 100644 --- a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-22-evidence-based-larger-hosted-runners.md: bb10a53058a7ae1a46d493933868887bb2f55265 -2026-07-22-evidence-based-larger-hosted-runners.zh.md: ffaf958560dae6e044da58f910c0f53e21789344 +2026-07-22-evidence-based-larger-hosted-runners.md: 6170aca3c2675069a74d951b11273ed5d2731de8 +2026-07-22-evidence-based-larger-hosted-runners.zh.md: bd2095b17bdb954382b455bd160405a10be56ddb diff --git a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md index bb10a53058..6170aca3c2 100644 --- a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md +++ b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md @@ -16,9 +16,9 @@ The organization keeps twelve x64 larger-runner pools in the repo-restricted `ds Production CI uses five larger-runner executions and one standard-runner aggregator. The primary Node inventory is not sharded: -- `node 24 / complete` uses one 96-core Linux runner. One checkout, setup, cache restore, and install feeds all 42 primary gates. `run-gates` starts up to 32 independent gates; ESLint uses 32 workers, coverage uses at most 12, and snapshot replay uses at most 16. Build starts immediately beside typecheck, coverage, lint, and documentation work, while snapshot replay and publication consumers retain explicit dependencies on emitted `lib/` output. +- `node 24 / complete` uses one 96-core Linux runner. One checkout, setup, cache restore, and install feeds all 42 primary gates. `run-gates` starts up to 16 independent gates; ESLint uses 16 workers, coverage uses at most 12, and snapshot replay uses at most 8. Build starts as soon as the first short gates release scheduler slots, while snapshot replay and publication consumers retain explicit dependencies on emitted `lib/` output. Pull requests restore pnpm and ESLint caches without saving them, so cache compression and upload do not extend the required job; the master serial references refresh those caches outside the pull-request critical path. - Node 22.19 and Node 26 use the 4- and 32-core Linux pools for their runtime compatibility smokes. Python 3.10 uses the 8-core Linux pool for the complete keyless SDK suite. These are environment contracts, not slices of the primary Node gate inventory. -- `windows node 24 / complete` uses one 32-core Windows runner. One setup feeds the required package build, required production site build, and complete observational portability inventory. Required failures fail the job; observational failures are reported as non-blocking. ESLint stays single-threaded because 16 ESLint workers took 174.54 seconds, while the outer scheduler retains 32 slots. +- `windows node 24 / complete` uses one 32-core Windows runner. One setup feeds the required package build, required production site build, and complete observational portability inventory. Required failures fail the job; observational failures are reported as non-blocking. ESLint stays single-threaded because 16 ESLint workers took 174.54 seconds, coverage uses at most 12 workers, and the outer scheduler retains 16 slots. The job restores only the small master-refreshed ESLint cache and performs a clean pnpm install instead of restoring or saving the many-file package store. The former gate-level and coarse primary shard jobs are absent from the workflow. Their static, lint, coverage, snapshot, and scenario shard selectors are also absent from the repository, so an unused diagnostic path cannot preserve a second CI architecture. @@ -38,11 +38,13 @@ The same benchmark measured the required Windows build surfaces across every pro Repository work gains little above 16 Windows cores, but the 32-core pool can start the complete outer inventory together. A [retargeted production validation](https://github.com/deepseek-harness/deepseek-harness/actions/runs/29907581119/attempts/2) completed the full one-box Windows inventory in 173 seconds, including coverage and snapshot replay, so Windows remains consolidated. +The larger client package graph makes cache mechanics and scheduler pressure part of the measured workload. In [one exact-head production run](https://github.com/deepseek-harness/deepseek-harness/actions/runs/29912577681), Linux spent 39 seconds in repository gates but 69 seconds in the complete job, while Windows spent 117 seconds in repository gates and 228 seconds in the complete job. The Windows pnpm cache downloaded its 154 MB archive in about two seconds but spent 27 seconds extracting it, followed by a 23-second install and a 14-second post-job save. A [cacheless all-size trace](https://github.com/deepseek-harness/deepseek-harness/actions/runs/29913033155) completed the same 32-core Windows install in 27 seconds. Production therefore avoids the Windows package-store cache, uses restore-only caches on latency-critical pull-request jobs, and bounds outer concurrency so typecheck, lint, coverage, and build do not oversubscribe one host. + Two host effects remain part of the decision. A standard Node 26 job once spent 36 of its 67 seconds in `Set up job`, which is why environment contracts use distinct larger-runner pools instead of standard capacity. A Linux candidate spent 18 seconds registering a 50 KB Bubblewrap package because the hosted image scanned 202,507 package-database files. [`scripts/prepare-ci-bubblewrap.sh`](../../../../scripts/prepare-ci-bubblewrap.sh) instead verifies and extracts the pinned payload into the ephemeral runner directory, runs a functional confinement probe, and overlaps that preparation with dependency installation. Coverage remains capped at 12 forks. Thirty-two forks crashed Node 24's CJS lexer twice, and a later 16-fork run reproduced the worker loss and invalid coverage result. Twelve forks preserve process headroom without becoming the single-host critical path. -The workflow retains three manual measurement suites. `suite=larger-runner-benchmark` compares isolated critical lanes across every size, `suite=consolidated-runner-benchmark` compares whole aggregates, and `suite=serial-reference` remains the unsharded cross-platform completeness oracle. `suite=optimized-larger-runners` dispatches the exact production topology against a branch ref when a pull request cannot form a merge commit. +The workflow retains two manual measurement suites. `suite=larger-runner-benchmark` compares isolated critical lanes across every size, and `suite=consolidated-runner-benchmark` compares whole aggregates. Complete serial Linux, macOS, and Windows references run only when `master` moves; pull requests run only the optimized jobs. ## Alternatives considered diff --git a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md index ffaf958560..bd2095b17b 100644 --- a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md +++ b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md @@ -16,9 +16,9 @@ Status: implemented 生产 CI 包含 5 次大型运行器执行和 1 个标准运行器聚合作业。主 Node 门禁清单不再分片: -- `node 24 / complete` 使用一台 96 核 Linux 运行器。只需执行一次代码检出、设置、缓存恢复和安装,即可供全部 42 项主门禁使用。`run-gates` 最多同时启动 32 项相互独立的门禁;ESLint 使用 32 个工作线程,覆盖率最多使用 12 个,快照回放最多使用 16 个。构建会立即与类型检查、覆盖率、lint 和文档工作同时启动,而快照回放和发布消费方仍显式依赖生成的 `lib/` 输出。 +- `node 24 / complete` 使用一台 96 核 Linux 运行器。只需执行一次代码检出、设置、缓存恢复和安装,即可供全部 42 项主门禁使用。`run-gates` 最多同时启动 16 项相互独立的门禁;ESLint 使用 16 个工作线程,覆盖率最多使用 12 个,快照回放最多使用 8 个。第一批短门禁释放调度器槽位后,构建会立即启动,而快照回放和发布消费方仍显式依赖生成的 `lib/` 输出。拉取请求会恢复 pnpm 和 ESLint 缓存但不保存,因此缓存压缩和上传不会延长必需作业;master 上的串行参考会在拉取请求关键路径之外刷新这些缓存。 - Node 22.19 和 Node 26 分别使用 4 核和 32 核 Linux 池运行各自的运行时兼容性冒烟测试。Python 3.10 使用 8 核 Linux 池运行完整的无密钥 SDK 套件。这些作业属于环境契约,并非主 Node 门禁清单的分片。 -- `windows node 24 / complete` 使用一台 32 核 Windows 运行器。一次设置供必需的包构建、必需的生产网站构建以及完整的观测性可移植性清单共用。任何必需项失败都会使作业失败;观测项失败则报告为非阻塞。ESLint 保持单线程,因为 16 个 ESLint 工作线程耗时 174.54 秒,外层调度器则保留 32 个槽位。 +- `windows node 24 / complete` 使用一台 32 核 Windows 运行器。一次设置供必需的包构建、必需的生产网站构建以及完整的观测性可移植性清单共用。任何必需项失败都会使作业失败;观测项失败则报告为非阻塞。ESLint 保持单线程,因为 16 个 ESLint 工作线程耗时 174.54 秒;覆盖率最多使用 12 个工作线程,外层调度器则保留 16 个槽位。该作业仅恢复由 master 刷新的较小 ESLint 缓存,并在干净环境中执行 pnpm 安装,而不恢复或保存包含大量文件的包存储。 原有的门禁级和粗粒度主流程分片作业已从工作流中移除。相应的静态、lint、覆盖率、快照和场景分片选择器也已从仓库中移除,因此未使用的诊断路径无法继续维系第二套 CI 架构。 @@ -38,11 +38,13 @@ Status: implemented Windows 仓库工作在超过 16 核后收益很小,但 32 核池可以让完整的外层清单同时启动。一次[重新定向的生产验证](https://github.com/deepseek-harness/deepseek-harness/actions/runs/29907581119/attempts/2)在 173 秒内完成了单机 Windows 完整清单,其中包括覆盖率和快照回放,因此 Windows 继续采用合并执行方式。 +客户端包依赖图增大后,缓存机制和调度器压力也成为实测工作负载的一部分。在[一次分支头精确的生产运行](https://github.com/deepseek-harness/deepseek-harness/actions/runs/29912577681)中,Linux 的仓库门禁耗时 39 秒,完整作业耗时 69 秒;Windows 的仓库门禁耗时 117 秒,完整作业耗时 228 秒。Windows pnpm 缓存的 154 MB 归档下载耗时约 2 秒,但解压耗时 27 秒,随后安装耗时 23 秒,作业结束后的保存又耗时 14 秒。一次[无缓存的全规格运行轨迹](https://github.com/deepseek-harness/deepseek-harness/actions/runs/29913033155)在 27 秒内完成了同一台 32 核 Windows 运行器上的安装。因此,生产环境不使用 Windows 包存储缓存,在对延迟敏感的拉取请求作业中使用只恢复不保存的缓存,并限制外层并发度,以免类型检查、lint、覆盖率和构建在同一台主机上过度争用资源。 + 两项主机效应仍构成这项决策的依据。一个标准 Node 26 作业曾在总共 67 秒的耗时中,把 36 秒用在 `Set up job` 上,因此各项环境契约使用不同的大型运行器池,而非标准容量。一个 Linux 候选作业在注册 50 KB 的 Bubblewrap 包时耗时 18 秒,因为托管映像扫描了 202,507 个包数据库文件。[`scripts/prepare-ci-bubblewrap.sh`](../../../../scripts/prepare-ci-bubblewrap.sh) 改为验证固定包内容并将其解压到临时运行器目录,执行功能性隔离探针,并让这项准备工作与依赖安装重叠执行。 覆盖率仍限制为 12 个 fork。32 个 fork 曾两次导致 Node 24 的 CJS 词法分析器崩溃,后来一次使用 16 个 fork 的运行又复现了工作进程丢失和无效的覆盖率结果。12 个 fork 能保留进程余量,同时不会成为单机关键路径。 -工作流保留 3 项手动测量套件。`suite=larger-runner-benchmark` 比较所有规格下相互独立的关键通道,`suite=consolidated-runner-benchmark` 比较完整聚合流程,`suite=serial-reference` 则继续作为未分片的跨平台完整性判定基准。当拉取请求无法生成合并提交时,`suite=optimized-larger-runners` 会直接针对分支引用触发与生产环境完全相同的拓扑。 +工作流保留 2 项手动测量套件。`suite=larger-runner-benchmark` 比较所有规格下相互独立的关键通道,`suite=consolidated-runner-benchmark` 比较完整聚合流程。只有在 `master` 移动时,才运行完整的 Linux、macOS 和 Windows 串行参考;拉取请求只运行优化后的作业。 ## 曾考虑的替代方案 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7ea422056..deba685f81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,10 +37,10 @@ jobs: env: DSH_COVERAGE_MAX_WORKERS: '12' DSH_ESLINT_CACHE: '1' - DSH_ESLINT_CONCURRENCY: '32' - DSH_GATE_CONCURRENCY: '32' - DSH_PUBLINT_CONCURRENCY: '32' - DSH_SNAPSHOT_MAX_CONCURRENCY: '16' + DSH_ESLINT_CONCURRENCY: '16' + DSH_GATE_CONCURRENCY: '16' + DSH_PUBLINT_CONCURRENCY: '16' + DSH_SNAPSHOT_MAX_CONCURRENCY: '8' steps: - uses: actions/checkout@v6 @@ -54,14 +54,16 @@ jobs: corepack enable echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" - - uses: actions/cache@v4 + # Pull requests consume the default-branch cache but do not put cache + # compression and upload on the paid latency-critical path. + - uses: actions/cache/restore@v4 with: path: ${{ steps.pnpm-store.outputs.path }} key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm- - - uses: actions/cache@v4 + - uses: actions/cache/restore@v4 with: path: .cache/eslint key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} @@ -160,10 +162,10 @@ jobs: env: # Keep ESLint itself single-threaded: 16 ESLint workers took 174 seconds on # this image. The outer scheduler still overlaps lint with the other gates. - DSH_COVERAGE_MAX_WORKERS: '4' + DSH_COVERAGE_MAX_WORKERS: '12' DSH_ESLINT_CACHE: '1' - DSH_GATE_CONCURRENCY: '32' - DSH_PUBLINT_CONCURRENCY: '32' + DSH_GATE_CONCURRENCY: '16' + DSH_PUBLINT_CONCURRENCY: '16' steps: - uses: actions/checkout@v6 @@ -177,30 +179,20 @@ jobs: with: node-version: ${{ env.PRIMARY_NODE_VERSION }} - - name: Enable corepack and resolve pnpm store path - id: pnpm-store - shell: pwsh - run: | - corepack enable - "path=$(pnpm store path --silent)" >> $env:GITHUB_OUTPUT - - - uses: actions/cache@v4 - with: - path: ${{ steps.pnpm-store.outputs.path }} - key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm- - - - uses: actions/cache@v4 + - uses: actions/cache/restore@v4 with: path: .cache/eslint key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} restore-keys: | ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full- - - name: Install (immutable) + # Extracting the many-file pnpm store cache is slower on this image than + # a clean parallel install, and saving it adds more latency after gates. + - name: Enable corepack and install (immutable) shell: pwsh - run: pnpm install --frozen-lockfile + run: | + corepack enable + pnpm install --frozen-lockfile - name: Run blocking and observational Windows gates concurrently shell: pwsh @@ -221,8 +213,26 @@ jobs: with: node-version: ${{ env.PRIMARY_NODE_VERSION }} - - name: Enable corepack (pnpm) - run: corepack enable + - name: Enable corepack and resolve pnpm store path + id: pnpm-store + run: | + corepack enable + echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" + + # Master refreshes the caches that pull requests restore without saving. + - uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-store.outputs.path }} + key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm- + + - uses: actions/cache@v4 + with: + path: .cache/eslint + key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} + restore-keys: | + ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full- - name: Install (immutable) run: pnpm install --frozen-lockfile @@ -234,6 +244,7 @@ jobs: env: DSH_COVERAGE_MAX_WORKERS: '1' DSH_E2E_MAX_WORKERS: '1' + DSH_ESLINT_CACHE: '1' DSH_GATE_CONCURRENCY: '1' DSH_PUBLINT_CONCURRENCY: '1' DSH_SNAPSHOT_MAX_CONCURRENCY: '1' @@ -286,6 +297,15 @@ jobs: shell: pwsh run: corepack enable + # Master refreshes the small cache that pull requests restore without + # putting package-store extraction back on the Windows critical path. + - uses: actions/cache@v4 + with: + path: .cache/eslint + key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} + restore-keys: | + ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full- + - name: Install (immutable) shell: pwsh run: pnpm install --frozen-lockfile @@ -295,6 +315,7 @@ jobs: env: DSH_COVERAGE_MAX_WORKERS: '1' DSH_E2E_MAX_WORKERS: '1' + DSH_ESLINT_CACHE: '1' DSH_GATE_CONCURRENCY: '1' DSH_PUBLINT_CONCURRENCY: '1' DSH_SNAPSHOT_MAX_CONCURRENCY: '1'