Skip to content

ci(test): shard the Vitest suite across 4 runners#2641

Merged
os-zhuang merged 1 commit into
mainfrom
ci/shard-vitest-suite
Jul 17, 2026
Merged

ci(test): shard the Vitest suite across 4 runners#2641
os-zhuang merged 1 commit into
mainfrom
ci/shard-vitest-suite

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

问题

Test job 就是 CI 的关键路径:一次 PR 跑 ~8.3 分钟,而整条 CI 的 wall clock 才 ~9 分钟(E2E 只要 1.2 分钟,dev-server 0.4 分钟,docs 在 PR 上直接跳过)。

慢的不是断言本身。最近一次 PR 跑的 Vitest summary:

Duration 495.33s (transform 46.84s, setup 831.17s, import 198.72s,
                  tests 177.78s, environment 155.79s)

真正执行测试只有 178s;setup 831s / import 199s / environment 156s 都是按测试文件重复支付的固定开销 —— 因为 isolate: true 会为每个文件重新执行整张模块图。559 个测试文件里约 305 个走 dom project,而 vitest.setup.dom.tsx 会 side-effect 导入 @object-ui/components / fields / plugin-dashboard / plugin-grid 并重注册 9 个 widget,平均每个文件白付 ~2.7s。

isolate 必须保持开启 —— vitest.config.mts 里已经记录过关掉它导致上千个顺序依赖失败的历史。

做法

不动 isolation,把 559 个文件切到 4 个 runner 上:

  • test:4 片矩阵,仅 PR,跑 pnpm test --shard=N/4。这里每个 job 的固定开销只有 ~30s(checkout + Node + 走热 pnpm store 的 install 只要 6s),所以 4 片是用 ~26% 的额外 runner 分钟换 ~3x 的 wall clockfail-fast: false,一片挂掉不掩盖其他片的失败。
  • test-coverage:原先 push 才跑的 coverage 路径拆成独立的不分片 job。没有任何东西阻塞它,所以保持给 Codecov 一份完整报告,不必引入 blob report 合并的复杂度和风险。

Vitest 按测试文件路径的 hash 分片,unit / dom 文件会自然交错,分片自动均衡,无需手工配平。

验证

  • vitest run --shard 确实生效且守恒:同时命中 unitdom 两个 project 的过滤器下,2 + 2 = 4 文件、9 + 14 = 23 测试。⚠️ 注意 vitest list 忽略 --shard(每片都返回全部 559 个文件),不能用它来验证。
  • 分片最大的风险是静默漏跑(测试没跑但 CI 依旧全绿),所以本 PR 自身的 CI 就是终极校验:4 片的 Test Files 求和必须 == 559,Tests 求和必须 == 6843。合并前会核对。
  • 全仓没有任何 branch protection / needs: / workflow_run 引用旧的 Test check 名(main 未开启分支保护),改名安全。

不在本 PR 范围

那 831s 的 setup 本身还可以根治 —— 把 vitest.setup.dom.tsx 拆成轻/重两档,只让真正依赖全量 ComponentRegistry 的测试付重量档的代价。那是结构性改动(需要梳理哪些测试依赖注册副作用),本地开发同样受益,单独开专项。

🤖 Generated with Claude Code

The Test job is the whole of CI's critical path: ~8.3 min of a ~9 min
wall clock. Its cost is almost entirely fixed per-file overhead rather
than the assertions — a recent PR run reported 495s of which only 178s
was `tests`, against `setup` 831s, `import` 199s and `environment` 156s
(cumulative across workers). Those are paid once per test file because
`isolate: true` re-executes the module graph for every file, and
isolation has to stay on (vitest.config.mts records the order-dependent
failures that turning it off caused).

So rather than touch isolation, split the 559 files across 4 runners:

- test: a 4-shard matrix, PR-only, running `pnpm test --shard=N/4`.
  Fixed overhead per job here is only ~30s (checkout + Node + install
  off a warm pnpm store), so 4 shards trade ~26% more runner minutes for
  ~3x less wall clock. fail-fast: false so one broken shard doesn't mask
  the others.
- test-coverage: the former push-only coverage path, split into its own
  unsharded job. Nothing blocks on it, so it keeps feeding Codecov one
  complete report instead of needing a blob-report merge.

Vitest shards by hashing each test file's path, so unit/dom files
interleave and the shards self-balance. Verified that `vitest run
--shard` really splits and conserves the file/test totals across
projects — note `vitest list` ignores --shard, so it cannot be used to
check this. Nothing (branch protection, `needs:`, workflow_run)
references the old `Test` check name, so the rename is safe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Jul 17, 2026 2:29pm

Request Review

@os-zhuang
os-zhuang merged commit 71be244 into main Jul 17, 2026
13 checks passed
@os-zhuang
os-zhuang deleted the ci/shard-vitest-suite branch July 17, 2026 14:37
os-zhuang added a commit that referenced this pull request Jul 18, 2026
…y setup (#2644)

Follow-up to #2641, which sharded the Vitest suite but only parallelized
its cost. This reduces the cost itself.

`setup` was the largest line item in a CI run — 990s cumulative vs 205s
of actual `tests` — because `vitest.setup.dom.tsx` side-effect imports
@object-ui/components, fields, plugin-dashboard and plugin-grid and
re-registers 9 widgets, and under `isolate: true` that module graph
re-executes for every DOM test file. Measured at ~3.3s of pure setup per
file. But an empirical run of the whole `dom` project under a trimmed
setup showed only ~20 of ~310 files actually render through the
ComponentRegistry; the other ~290 paid that 3.3s for nothing.

Split the DOM tests into two projects by need:

- `dom` (default): the new light `vitest.setup.dom-light.tsx` — base
  polyfills + jest-dom + RTL cleanup, none of the heavy package graphs.
- `dom-heavy`: the ~20 `heavyDomTests` that drive the registry, keeping
  the full `vitest.setup.dom.tsx` (which apps/console also still uses).

`heavyDomTests` was derived empirically, not guessed: every file in it
failed under the light setup (e.g. "page:card not registered"). The list
is self-correcting — a heavy test left off it lands in the light project
and fails loudly in CI, so coverage can't be silently lost, only turned
red with a pointer to the fix.

Behavior-preserving: full suite is 566 files / 6892 tests
(6868 passed, 24 skipped) both before and after, at the same commit.
Local cumulative `setup` drops from ~990s to ~167s.

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant