Skip to content

fix(ci): deal unit-test shards by the work each module does - #207

Merged
Mearman merged 1 commit into
mainfrom
fix/ci-shard-balance
Nov 3, 2026
Merged

Mearman merged 1 commit into
mainfrom
fix/ci-shard-balance

Conversation

@Mearman

@Mearman Mearman commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

The unit-test job dealt its modules to the five shards by file size, sorted largest first and taken modulo the shard count. Size says little about a module's time, which is set by the ansible-playbook processes its tests start, so the shards ended up very unequal: on recent runs one shard did many times the work of another, and the heaviest hit its timeout on one of them.

scripts/ci/shard_unit_tests.py SHARD SHARDS now prints the modules for a shard, and the job reads them from it. It imports the test modules and counts each one's work from its tests, in units of one Ansible task:

  • every process a test starts (subprocess.run and its siblings) counts, through the helpers, base-class methods, super() calls, setUp, tearDown and (once per class) setUpClass it reaches; a launch site counts once however often a loop runs it, a conditional counts its dearer branch, a skipped test counts nothing;
  • a launch of a playbook adds the tasks the playbook reaches: its own, one task file for an include that names it, the average task file of a role where the name is a variable, the whole role where there is none;
  • every process start costs TASKS_PER_PROCESS_START tasks, a ratio I fitted against per-module durations in the logs of two recent successful runs (work against mean duration correlates at about 0.9 over 57 modules, file size at about 0.5). The dealing only needs a playbook start to be non-negligible beside a task, so the constant is not tuned further.

The modules are then dealt heaviest first to the shard with the least work so far (ties to the lower index, equal modules in name order). A failed dealing now fails the step instead of leaving a shard with nothing to run.

Work per shard on origin/main's 58 modules, before and after:

shard modules before work before modules after work after
0 12 45251 11 34697
1 12 37633 11 34639
2 12 63389 11 34655
3 11 9623 12 34635
4 11 17404 13 34674

Heaviest over lightest goes from 6.6 to 1.00 (heaviest over mean from 1.83 to 1.00). Playbook launch sites reached per shard ranged from 54 to 312 before and range from 104 to 278 after: a launch's cost differs with the playbook it runs, which is what the task counts account for. Replaying the dealing on the per-module durations from the same two runs, the heaviest shard was 13.5 times the lightest before and is 2.1 times after (about 1.7 and 1.3 times the mean); those durations are noisy and the after figure is a replay, not a CI result.

The timeout stays at 30 minutes: the heaviest shard's work is now 0.55 of the old heaviest's, so the guard sits further above it than before.

tests/unit/test_shard_unit_tests.py covers the counting on synthetic modules (each launcher, helpers, base classes, super(), set-up methods, skips, recursion, loops, conditionals, playbook task counts for the three include forms, a module that cannot be imported) and the dealing on a skewed table and on this tree: every module in exactly one shard for 1 to 8 shards, the same assignment whatever the listing order, the heaviest shard within Graham's bound of the mean, and every module dealt to the lightest shard so far. Dealing by position modulo the shard count, or in name order, fails the last of these (and the first also fails the heavy-module case); a work measure that stops counting launches fails the counting tests.

The shards were dealt by a module's size in bytes, which says little about its
time: a module's playbook runs decide it, so one shard held several times the
work of another and the heaviest could reach its timeout.

scripts/ci/shard_unit_tests.py counts each module's work from its tests (the
processes they start through helpers, base classes and set-up methods, with a
playbook's tasks added for a playbook run) and deals the modules heaviest first
to the shard with the least work so far. The job takes its modules from it.
@Mearman
Mearman marked this pull request as ready for review November 3, 2026 19:12
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review ⚠️ Failed 2026-11-03T19:12:08.429631Z
We couldn’t complete this review. Comment @codex security review to try again.
a26cc14 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

This security review couldn’t start because the paying account has exhausted its Codex allowance and has insufficient available credits. The payer can check their balance and add credits in the Codex usage dashboard, then try again. If you do not manage the paying account, contact this repository's admins.

@Mearman
Mearman merged commit e779aa8 into main Nov 3, 2026
16 checks passed
@Mearman
Mearman deleted the fix/ci-shard-balance branch November 3, 2026 19:12
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 2.30.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant