Uh oh!
There was an error while loading. Please reload this page.
ci: shard Test Core six ways to break the runner contention - #10376
Merged
Conversation
Test Core's per-package durations are contended wall clock, not package cost: `--concurrency=4` on a 4-vCPU runner puts up to four suites on four cores. Measured like-for-like against the same suites run alone on an idle 4-core box (#10152 / PR #10258), neither long pole is inherently over #4859's 420s line -- cli 548.6s contended vs 337.13s alone, spec 496.4s vs 325.31s. What is over the line is the co-tenancy. Six is the smallest shard count that puts `@objectstack/spec` -- 415 of the workspace's ~2360 test files, and indivisible because sharding is BY PACKAGE -- alone in its bin (415/389/389/389/389/388). Past six its shard cannot improve, only the others can. `--concurrency=4` is deliberately unchanged: narrower shards may want a lower value, but moving both at once makes the next measurement unreadable. The two gate self-tests that anchor on ci.yml's literal text move with it. The `grow the matrix` anchor gains the full matrix including its closing bracket -- the dogfood job also spells `shard: [1, 2, 3]` and `String.replace` takes the first match, so the short anchor would have silently started mutating dogfood's matrix instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C5fGRTUHXFevsEdMEFDadM
Not for merge. Reverted in this PR before it leaves draft. Touching a turbo globalDependencies entry is what forces this PR's Test Core run to (a) schedule the FULL package list instead of the empty affected set a `.github/**`-only diff produces, and (b) miss every turbo cache entry, so the six shards report real cold-cache wall clock rather than ">>> FULL TURBO". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C5fGRTUHXFevsEdMEFDadM
This was referenced Aug 20, 2026
…stcore-shard-contention
The measurement it existed for is done: run 32396909164 scheduled the full 77-package list across six shards with `Cached: 0 cached, 68 total` on cli's shard, and the numbers are in the PR body. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C5fGRTUHXFevsEdMEFDadM
qq9340100
marked this pull request as ready for review
August 20, 2026 17:53
qq9340100
enabled auto-merge
August 20, 2026 17:53
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#10227
Implements the maintainer's option C ruling of 2026-08-20 (recorded on the card): attack the runner contention by raising the Test Core shard count, with a like-for-like measurement on real GitHub 4-vCPU runners as a mandatory pre-step. Option B (rebasing the threshold) is rejected on that ruling;
scripts/partition-test-shards.mjssemantics are untouched and there are no manual per-package exemption lists.Result: the measurement clears the line.
@objectstack/clidrops from 464.37s to 302.21s, and@objectstack/spec— alone on its own runner — finishes its whole shard in 283s, cold Turbo cache included. Numbers and method below.The change
testjob matrix[1, 2, 3]→[1, 2, 3, 4, 5, 6], plus the counts mechanically coupled to it and checked statically bycheck:shard-attestation: the jobname:, the--shard N/Margument, the attestation--total, the artifact name, and the gate's declared leg.--concurrency=4is unchanged. Narrower shards may well want a lower value, but moving both knobs in one run makes the resulting measurement unreadable. That is a follow-up, not this PR.Why six, not the four or five the card's arithmetic suggested. Sharding is BY PACKAGE, so no shard can finish faster than its single heaviest package, and the weight function is test-file count.
@objectstack/speccarries 415 of the workspace's ~2360 test files. At five shards or fewer the partitioner must co-schedule it; at six it is alone in its bin. Bins frompartition-test-shards.mjsagainst the full 76-package list, unchanged script:@objectstack/spec@objectstack/cliSix is the smallest count that isolates the heaviest indivisible suite. Past six, spec's shard cannot improve, only the others can.
A second effect that only shows up on the affected-set path: on a small affected set — the common PR — six shards give
clia runner to itself. Verified locally against a three-package affected set: shard 1 spec, shard 2 cli, shard 3 client, shards 4–6 empty and short-circuiting on the existing[ ! -s ]guard.Pre-step: the measurement
The instrument named in the dispatch does not work as stated, and that is finding #1. A
.github/**-only PR has an affected set of exactly 0 packages — measured locally against this branch withturbo ls --affectedfrommerge-base(origin/main, HEAD):So an ordinary run of this PR schedules six shards, each of which prints "No packages on this shard — nothing to test", attests, and measures nothing. (
.github/workflows/ci.ymlis in thecorepaths-filter, so the job does start; it simply has no work.)This PR therefore carried a temporary measurement scaffold — one comment appended to
tsup.config.ts, a turboglobalDependenciesentry — which made the affected set the full 77 packages and invalidated every task hash. The scaffold has been reverted; the diff under review is three files. The scaffold's effect is confirmed in the run itself:Cached: 0 cached, 68 totalon cli's shard.Baseline — 3 shards, real runners
push-to-mainrun 32392712980, full package list,ubuntu-latest4-vCPU:Run this shard's testsMeasured — 6 shards, real runners
Run 32396909164, full 77-package list, cold Turbo cache, all six shards green:
Run this shard's tests@objectstack/specalone — 1 of 1 pkg, 11059 tests@objectstack/cli+ 13 — 14 of 14 pkgs, 7129 testsThe number that decides it
The step totals above include a cold dependency build, because the scaffold invalidates every hash — on shard 4/6, 312s elapsed before cli's suite even started. So the step column is an upper bound, not the steady-state shard time. The metric that is immune to that, and that isolates exactly the variable this card is about, is the per-package vitest
Duration:Identical work both legs — 137 files, 1500 tests — and wall clock falls 35%, from 464.37s to 302.21s, now 118s under the 420s line. The CPU total falls with it, ~1410 CPU-s to ~909 CPU-s. That second number is the point: the same tests do not become cheaper, they stop paying for cache-thrash and context switching against three co-tenant suites. It is a direct confirmation that the durations this job's logs print are contention, not cost — the card's central claim, now measured on the runners rather than on a dev box.
spec's side is settled even more simply: alone on shard 1/6 it finished the entire step, cold build included, in 283s against 496.4s contended.This closes both of the card's confidence gaps. Gap 1 (dev box vs real runner): cli costs ~1410 CPU-s on a runner against 1291 CPU-s on the dev box in #10152, so runner cores are only ~9% slower — the margin survives the environment change. Gap 2 (per-shard fixed overhead): at the median it is ~50-60s, far too small to eat the margin.
⚠ But gap 2 has a tail, and it is a real cost of this change
Fixed overhead is contended, not constant.
Checkout repositoryacross the six shards of one run: 155s, 14s, 21s, 13s, 13s, 309s. Same workflow, same commit, same runner label — a 24x spread, because six shards plus three dogfood legs plus Build Core, Build Docs, Console Pin Gate and Temporal Conformance all pullfetch-depth: 0simultaneously. Two shards paid 194s and 346s of overhead, straight off the budget. The 3-shard baseline was tight by comparison (57s / 65s / 61s).That is filed separately as #10383 with the numbers — it is out of this card's region and wants its own measurement across several runs before anyone changes the checkout.
Coupled changes
scripts/check-shard-attestation.mjsandscripts/check-required-contexts.mjseach carry self-test fixtures that mutate ci.yml by string replacement and assert the mutation matched rather than silently judging the pristine file. They did their job: both went red on this change and named the dead anchor. Three anchors moved with the matrix.One is worth naming, because it would have rotted quietly. The
grow the matrixfixture anchored onshard: [1, 2, 3]— which the dogfood job also spells. With the test matrix now six wide,String.replacewould have taken dogfood's match first: still red, still for a plausible-looking reason, and no longer the assertion written there. The anchor now carries the full matrix including its closing bracket, with a comment saying why.Verification
successcounting the sixshard-attest-test-N-of-6credentials — so the coupled--total/--leg/ artifact-name edits are proven on a real run, not only by the static drift guard.3ce316f8withnode scripts/pm/dispatch-gates.mjs(no args, three-dot semantics against merge base0bfbeb70c) and run there. Their own verdict lines:check-node-version: OK (29 setup-node step(s) across 26 workflow(s), all on Node 22).✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).check-workflow-status-functions: OK (scanned 26 workflow file(s), 45 job(s) …)OK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.check-nul-bytes: OK (scanned 6115 text file(s) … no raw ASCII control bytes).check:required-contextsgreen (full registry printed).pnpm lintgreen repo-wide..github/**andscripts/**only, no published package changes, soskip-changesetis applied.Notes for the neighbours
testjob's shard matrix, the coupled attestation counts, and the--concurrency=4comment. It does not touch theconcurrency:group block or any Turbo cache restore/save step. The chosen shard count is 6, which multiplies that card's per-shard cache-key namespaces sixfold; stated on that issue when this PR opened. Note that shards 4–6 have no main-seeded namespace yet and will fall back through the shard-agnosticLinux-turbo-test-restore key until a main push seeds them.@objectstack/example-showcase#testfails on mass sys_*Find operation failed+Acquire connection error: operation timed out— hit a merge-queue build and a PR run within 2h on unrelated prose-only diffs #10293 (pool-timeout flake) — shard membership is re-derived by the partitioner, so the flaky suite's co-tenants change. Six shards mean fewer concurrent suites per runner, which reduces the resource pressure that timeout is most likely reacting to; but this PR neither fixes nor tests that, and the flake should be re-observed against the new placement.Cost, stated plainly
Per-shard fixed overhead is paid once per shard and the merge queue multiplies it by the speculative build count. Six shards trade roughly 1.7x the billed runner-minutes for a shorter critical path. This buys latency, not cheaper CI, and that trade is written into the ci.yml comment so the next reader does not have to rediscover it.
Generated by Claude Code