Skip to content

Split the CI Test job into shards and cut its runtime - #2269

Merged
MaxGhenis merged 3 commits into
mainfrom
ci-test-runtime-2099
Oct 12, 2026
Merged

MaxGhenis merged 3 commits into
mainfrom
ci-test-runtime-2099

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2099

What the hour was spent on

Measured from the log of run 38075384588 (10 October, Test passed in 59.6 minutes):

  • YAML tests: 2.6 minutes (2,946 tests, serial).
  • pytest: 55.8 minutes on four pytest-xdist workers, each busy for about 55 minutes: 220 worker-minutes for 3,532 tests.
  • 51 *_properties.py files take 74% of the worker time. No test dominates: the slowest takes 3.5%.

Change

  • Split the Test job into a YAML job, three shards of the Python tests (--shard K/N, defined in policyengine_uk/tests/conftest.py), and a final Test job. The final job fails unless both test jobs passed and .github/check_test_shards.py finds that every collected test ran in exactly one shard and none failed.
  • Run the final Test job on ubuntu-24.04-arm, the shards' runners. On 10 October it waited 32 minutes for an ubuntu-latest runner after the shards had finished. On this PR's second run it started 4 seconds after the last shard.
  • Measure coverage with sys.monitoring (COVERAGE_CORE=sysmon). On one property file this reported the same 12,262 of 18,401 lines, at 6% CPU overhead instead of 33%.

Measured result

Run YAML Python shards Final Test Wall time
Before: main, run 38075384588 (one job) 2.6 min 55.8 min (one job) — 59.6 min
This PR, first run 38085920460 (ubuntu-latest final job) 4.2 min 14.8 / 19.9 / 15.2 min 5 s after a 31.8 min queue 56.3 min
This PR, rebased run 38149483980 (ARM final job) 4.3 min 18.8 / 23.8 / 15.5 min 5 s, started 4 s after the shards 24.0 min

The rebased run's shard 2 is red because of a defect already on main, not this change. test_dataframe_dataset.py::test_an_exported_frame_loads_back_and_exports_itself fails on main too (run 38143054188 at 7343f94), and Hypothesis spent over 5 minutes shrinking it, which is why that shard took 23.8 minutes. Its fix is being handled separately. Once it lands I'll rebase this PR and re-run.

Invariants (tested in policyengine_uk/tests/code_health/test_ci_test_shards.py)

  1. shard_of(nodeid, N) is in 1..N and depends only on its arguments (SHA-256, not Python's per-process hash), so every runner and xdist worker agrees.
  2. For any collection and any N, the N shards are disjoint and their union is the whole collection, so a test added later lands in exactly one shard.
  3. parse_shard accepts exactly K/N with 1 <= K <= N.
  4. junit_key gives a node id the name pytest gives it in JUnit XML (differential test against pytest's own mangle_test_address).
  5. The check passes when every collected test ran in exactly one shard and none failed, and names any test that is missing, repeated, not collected or failed (property).
  6. With real pytest runs, with and without xdist, the listing and the shards' JUnit files pass the check, and fail it when a shard's file is left out or repeated.

Review

GPT-6.1 Sol reviewed head 841d2a8 and approved it with no blocking findings (~/reviews/budget-ci-w2/review-2269-841d2a889.md). Its two minor findings are handled in the follow-up from workstream W2 of the Autumn Budget CI plan, which moves this check off JUnit:

  • There is no direct test that a <failure> or <error> element counts as failed.
  • JUnit naming can map two distinct node ids to one name, for example a/b/test_c.py::t and a/b.test_c.py::t.

That follow-up also stops the check from accepting skips and expected failures that no reviewer has accepted. This PR is unchanged in what it accepts.

Left for later

  • A gc.freeze() plus GC-threshold change cut CPU on one property file from 77 to 61 seconds. It isn't in this PR, so the shard split can land first.
  • Codecov uploads fail on PR and main runs ("Token required because branch is protected"), so the coverage data is computed and discarded.

axiom: n/a: CI infrastructure

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits October 11, 2026 10:59
The Test job took 59 minutes (run 38075384588): 2.6 minutes of YAML tests,
then 55.8 minutes of pytest on four balanced xdist workers, 220
worker-minutes in all. No test dominates: 51 property-test files take 74%
of it and the slowest test takes 3.5%. So the job is split rather than one
test fixed.

- `--shard K/N` (tests/conftest.py) keeps the tests whose node id hashes to
  shard K of N. The shard is a function of the node id alone, so every
  collected test is in exactly one shard, including tests added later.
- The workflows run the YAML tests in one job and the Python tests as a
  matrix of three shards, N taken from the size of the matrix.
- A final `Test` job runs .github/check_test_shards.py over the shards'
  JUnit files and an unsharded `pytest --collect-only -q` listing. It fails
  unless every collected test ran in exactly one shard and none failed.
- Coverage is measured with sys.monitoring (COVERAGE_CORE=sysmon) instead
  of the C trace function. On one property-test file the two report the
  same 12,262 of 18,401 lines, and sys.monitoring adds 6% to the CPU time
  where the trace function adds 33%.

Refs #2099

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…first

On 10 October the 5-second final Test job waited 32 minutes for an
ubuntu-latest runner after the shards finished (run 38085920460). It now
runs on ubuntu-24.04-arm like the shards. The job-result check runs
first, and the shard check still runs after a failure so its report
names the failed tests. Record the measured job times by the timeouts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis force-pushed the ci-test-runtime-2099 branch from 777bf64 to 841d2a8 Compare October 11, 2026 15:00
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 11, 2026 15:27
@MaxGhenis

MaxGhenis commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator Author

Hand-off to the UK hub from W2 of the Autumn Budget CI plan (session local_6c1ab389). I finished this PR for its original session, which the usage limit cut off.

@MaxGhenis
MaxGhenis merged commit ff51368 into main Oct 12, 2026
11 checks passed
@MaxGhenis
MaxGhenis deleted the ci-test-runtime-2099 branch October 12, 2026 03:07
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.

CI Test job takes 4-6 hours and has hit the 6-hour limit

1 participant