Uh oh!
There was an error while loading. Please reload this page.
fix(devx): one path convention for the shard package list, and the measurement ranking the Test Core imbalance hypotheses - #10151
Merged
Conversation
…ted resolution base (#10056) `turbo ls --output=json` emits every `packages.items[].path` repo-relative (measured on turbo 2.10.10: 0 of 77 items absolute). `--union-into` appended absolute ones, so a single document could carry both conventions, and the obvious way to read it -- `join(REPO_ROOT, it.path)`, correct for every entry turbo wrote -- produces a garbage path for exactly the appended entries, which are the cross-package scans that step exists to keep running. Two halves, because the document and its reader each owned a piece: - the union now appends repo-relative paths, so the document turbo wrote and the document we hand on carry one rule; - `partition-test-shards.mjs` resolves `it.path` against the repo root explicitly instead of inheriting `process.cwd()`. `path.resolve` leaves an already-absolute entry alone, so a document written by the old union step still resolves to the directory it always did. The cwd half is the one that fails silently: `countTestFiles()` returns 0 for a path it cannot read, and the LPT partitioner absorbs a zero weight without complaint -- not a red step, a shard matrix that quietly stops balancing. Measured before this was pinned, same document and tree, cwd `/`: `shard 1/1: 1/1 packages, weight 0`. Placement is unchanged: against the full `turbo ls` payload this tree produces, all three shards keep byte-identical package lists and the bins stay 783/783/782. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0185KTgmREZA4sY5pnRbubXj
…ts, measured in queue builds
Comment only -- no behaviour change, and deliberately no change to the weight
function, the partitioner or the shard count. The header asserted the proxy as
settled ("file count tracks duration far better than package count") and
nothing recorded how well it holds, so the same measurement has now been
re-derived twice from scratch.
Measured 2026-08-20 from `merge_group` CI, per-package durations read out of
the turbo task groups:
- the proxy holds within roughly +-20% across the big packages, and is off by
~2.8x on one of them (@objectstack/cli, 135 files, 548.6s);
- the binning is not at fault: on the full package list a queue build hands
this script the bins come out 783/783/782, a one-file spread across 2348;
- the hard limit: sharding is by package, so a shard cannot finish faster
than its heaviest package, and @objectstack/spec (496s) and
@objectstack/cli (548s) each already exceed #4859's ~7min threshold alone.
In run 32352993803 Test Core shard 1 took 8m17.77s and spec's own suite was
8m16.4s of it -- the shard IS that package;
- run-to-run variance is separate and equally large: the cache key is
namespaced per shard and only main pushes write it, so legs of the same
shard index ranged from 79/79 cached (866ms, FULL TURBO) to 0/85 cached.
Part of #10149. The weight input is not changed here: the measurement says
re-weighting alone cannot reach that card's acceptance threshold, so which way
to go is a decision for the maintainer rather than something to pick silently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0185KTgmREZA4sY5pnRbubXj
This was referenced Aug 20, 2026
os-elon
marked this pull request as ready for review
August 20, 2026 11:53
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 20, 2026
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#10056
Part of #10149 — the measurement half only. The weight function, the partitioner and the shard count are unchanged, on purpose: the measurement says re-weighting alone cannot reach that card's acceptance threshold, and picking a direction anyway would be guessing. The open question is spelled out at the bottom for the maintainer.
Gates below were run at
d80ca88, the head of this branch.1. The measurement (this was the first deliverable)
Three competing explanations were on the table. Ranked against real
merge_groupdata, not the two-run sample the card was filed on.Test Core (2/3) = 3 secondsis not an empty bin and not a weight-0 cascadeThis was the datapoint asked to discriminate the hypotheses. From the job log of that leg (run
32335265453):A fully populated shard — 24 packages, 14,551 tests — replaying from a warm Turbo cache. It also cannot be a sibling-failure artifact: the matrix is
fail-fast: false, and that job published its attestation.H1 (the heterogeneous-path mechanism) cannot fire in a queue build
ci.yml's "Compute this shard's package set" computesSCM_BASEonly whengithub.event_name == 'pull_request'. Onmerge_groupit is empty, so theelsebranch runspnpm exec turbo ls --output=jsonand--union-intois never invoked. No absolute path is appended in a queue build, andturbo lson this tree emits 0 absolute paths of 77. The mechanism is real — it is just real onpull_requestruns, which is not where the symptom was measured.The partitioner and LPT are exonerated
Reproducing the exact input a queue build hands the script (full
turbo ls, run from the repo root):A one-file spread across 2348 — 0.13%. The card compared a 3.4× duration spread against LPT's ≤4/3 bound, but that bound is over weights, and on weights it is met with enormous margin. There is no bucketing defect.
H2 (proxy decay) is supported, and localised
Per-package durations read out of the turbo task groups in queue-build logs, against each package's weight in the same tree:
@objectstack/cli@objectstack/spec@objectstack/service-automation@objectstack/driver-turso@objectstack/client@objectstack/example-showcaseThe proxy holds to roughly ±20% across the big packages and is off by ~2.8× on
@objectstack/cli, which LPT places in bin 3 — the shard that was the long pole in every cold run sampled.H3 (cache/runner variance) is real, large, and separate
Run this shard's tests, with the Turbo cache state of each leg:322333014073233526545332352993803The apparent "the bottleneck moves between runs" signature is a warm-vs-cold comparison, not a placement change: in run
32335265453shards 1 and 2 were at 98–100% cache hits. In the two runs where shards ran cold, shard 3 was the long pole both times. Mechanism, reported and not touched here: this job's Turbo cache key is namespaced per shard — it interpolatesmatrix.shardinto the key alongsidegithub.job— and only mainpushruns write it, so each shard's cache ages independently.Ranking: H2 > H3 ≫ H1. H2 explains which shard is the long pole; H3 explains why a single build's spread cannot be read as placement; H1 is falsified for the environment the symptom lives in.
2. What this PR changes (#10056)
turbo lsemits everypackages.items[].pathrepo-relative;--union-intoappended absolute ones, so one array could carry two conventions — and the obvious way to read such a document,join(REPO_ROOT, it.path), is correct for every entry turbo wrote and garbage for exactly the appended ones.Both halves of the chain, since the document and its reader each owned a piece:
partition-test-shards.mjsresolvesit.pathagainst the repo root explicitly instead of inheritingprocess.cwd().path.resolveleaves an already-absolute entry alone, so a document written by the old union step still resolves where it always did.The card left the choice open between three normalisations; this takes the third (consumer resolves explicitly) and the first (writer matches turbo), because the third alone leaves the document heterogeneous for the future consumer the card warned about, and the first alone leaves the cwd fragility it measured.
Verification
Placement is unchanged — the entry-gate consequence the card warned about does not fire. Against the full
turbo lspayload this tree produces, all three shards keep byte-identical package lists and the bins stay 783/783/782.Reverse-verified rather than asserted. With the resolution base ablated back to
process.cwd()and the ablation confirmed on disk, run from/:Every package in bin 1, shards 2 and 3 empty, exit code 0 — the silent failure mode, worse at full scale than the single-package measurement on the card showed. Restored, from the same cwd:
shard 1/3: 22/76 packages, weight 783 (all bins: 783/783/782). The new self-test case fails on the ablated build (path: resolution moved with the cwd) and passes on the restored one.Self-tests extended on both sides of the cross-script invariant: the reader pins repo-relative resolution, absolute passthrough, and cwd-independence of the resulting weight; the writer pins the appended convention end-to-end through the real
unionInto()on the fixture that first measured the divergence.3. Gates
Re-derived from the actual changed files with
node scripts/pm/dispatch-gates.mjs(no paths passed), which narrowed the dispatch's lead list —ci.ymlis untouched, so the four workflow-shaped families it named are no longer implicated. All run atd80ca88:node scripts/check-cross-package-test-inputs.mjsOK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.check-cross-package-test-inputs --self-testAll 60 self-test cases passed.partition-test-shards --self-testpartition-test-shards: self-test OKcheck-nul-bytescheck-nul-bytes: OK (scanned 6066 text file(s) ... no raw ASCII control bytes)check-shard-attestation✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).check-required-contexts✓ check-required-contexts: 6 required context name(s) pinned across 2 workflow(s)skip-changeset: rootscripts/only, nothing published changes.4. The open question, for the maintainer
Sharding is by package, so a shard can never finish faster than its single heaviest package. Two packages already exceed #4859's
Test Core 最慢分片 ≤ ~7minon their own:@objectstack/specat 496s (8m16s) and@objectstack/cliat 548s (9m09s). Measured consequence — in run32352993803, Test Core shard 1 took 8m17.77s wall and spec's own suite was 8m16.4s of it. The shard is that one package.So the route this dispatch flagged as most likely — keep LPT, replace the weight input with measured per-package duration — would improve balance and still not reach the threshold. Its best case is a makespan of ~9m09s, the cost of
@objectstack/clialone, against a ≤7min target.Three directions, none taken here:
--shardwithinspecandclionly, package-level everywhere else). Reaches the threshold; the partitioner header records why blanket file-level sharding was rejected, and a two-package exception is a new mechanism with its own failure modes.@objectstack/cli's suite — at 3.5–4.1 s/file against a ~1.4 s/file norm it is the outlier, not just the biggest.Which of these is wanted is a call about the acceptance target itself, so it is left to the maintainer.
Generated by Claude Code