Skip to content

ci: balance test shards by measured time per file - #905

Closed
pengfei-threemoonslab wants to merge 1 commit into
mainfrom
claude/ci-shard-time-balance
Closed

pengfei-threemoonslab wants to merge 1 commit into
mainfrom
claude/ci-shard-time-balance

Conversation

@pengfei-threemoonslab

Copy link
Copy Markdown
Contributor

Summary

CI's three test shards were balanced by test count, not time. This PR balances them by measured seconds per test file. It fixes the suite (3) cancellation on #904 and restores main's margin.

The problem

A file of forty git-fixture tests costs more than a file of four hundred pure ones, so item count is a poor proxy for time.

What changed

  • ci_sharding.shard_assignment takes optional seconds per file. A file without a measurement (new or renamed) costs its item count at the measured seconds per item. The rate is computed identically in every shard (fsum over sorted paths), so the partition stays deterministic.
  • tests/shard_seconds.json holds one full-suite measurement: 363 files and 8,966 test-seconds. It was taken on Derive the application comparison scope from the change when --scope is omitted (#875) #904's branch, so it also covers that PR's new file. An entry for a file not in the collection is ignored.
  • scripts/measure_shard_seconds.py regenerates the file from a --junitxml run.
  • conftest.py loads the measurement. If it is missing or unreadable, balancing falls back to item counts.
  • ci.yml's comment says to re-measure before adding a shard.

What does not change:

  • the three required check names (suite (1)–suite (3));
  • the union-of-shards property, determinism, and the fail-loud empty-shard rule.

A stale measurement only unbalances; it never drops a file.

Evidence

Share of measured test time per shard:

shard 1 shard 2 shard 3
main, by count (today) 25.3% 30.0% 44.7%
#904, by count (today) 41.5% 14.2% 44.3%
main, by time (this PR) 33.3% 33.3% 33.3%
#904, by time (this PR) 33.3% 33.3% 33.3%

The by-count rows reproduce CI's actual shape. Main's shards take 6.8/6.8/11.6 test-minutes (27/27/46%). #904's shard 2 is the small one, with 1 and 3 at the cap.

Tests

tests/test_shard_partition.py, all passing locally, including the real-collection union check:

  • a slow file of few tests gets a shard of its own when measured, and shares one when only counted;
  • an unmeasured file costs its items at the measured rate;
  • the timed assignment is deterministic regardless of input order;
  • a missing or malformed measurement is ignored;
  • the committed measurement names only test files, with non-negative times.

🤖 Generated with Claude Code

The suite's three CI shards were balanced by collected item count. Item count
is a poor proxy for time: a file of forty git-fixture tests costs more than a
file of four hundred pure ones.

- On `main`, shard 3 took 13 of its 15 minutes while shards 1 and 2 took 8.
- Any new test file reshuffled most files. #904's one new file moved 298 of
  363, which put shard 1 at 14 minutes and cancelled shard 3 at the cap.

Shards are now balanced by measured seconds per file, in
`tests/shard_seconds.json`. `scripts/measure_shard_seconds.py` writes that file
from a `--junitxml` run. A file without a measurement costs its item count at
the measured seconds per item. A stale measurement only unbalances, never drops
a file. The union property, determinism and fail-loud rules are unchanged.

On one full-suite measurement:
- balancing by count gives main 25/30/45% of the work, matching CI's
  6.8/6.8/11.6-minute shards;
- balancing by time gives 33/33/33%, for main and for #904.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant