perf(ci): balance the Windows pytest shards by time, not test count (#88) - #419
Merged
Conversation
KbWen
added a commit
that referenced
this pull request
Aug 23, 2026
PR #419 CI job times: 7m18s / 8m28s / 13m7s. Worst shard 21m57s -> 13m7s = 1.67x, not the 2.2x this branch predicted from workstation-generated durations. The Known Risk caveat is confirmed: the runner weights many-small-tests relatively higher, so the 640-test shard became the slowest. Backlog row, Ship History entry and archived Work Log now state the measured numbers with the runner-regeneration follow-up recorded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
KbWen
added a commit
that referenced
this pull request
Aug 23, 2026
#171) (#420) fix(security): stop the secret scanner failing on ordinary identifiers (#171) TruffleHog's Lob detector matches a word boundary, `live` or `test`, an underscore, then exactly 35 characters from the word class -- underscores included -- so a plain snake_case name of the right length satisfies it, and its verifier returns verified. That is why `--only-verified` never bounded this class. `--exclude-detectors=lob` is now set. This was latent until it wasn't. Backlog #171 had recorded 35 same-shape identifiers already sitting in the tree, unflagged only because the scan is range-scoped. #88's committed pytest durations file -- 897 node ids in one commit -- turned that latency into an outright job failure on PR #419. The narrowing is scoped, and the scope is now machine-enforced. Exactly one detector is dropped, for a service this repo has no integration with. Every other detector still runs, and the `credential-scan` job plus the pre-commit credential floor are untouched second and third layers. A new test, test_detector_exclusions_stay_scoped_to_lob, asserts that lob is the ONLY permitted exclusion and that `--only-verified` cannot be dropped alongside it -- without it, a future "just exclude one more" edit is a one-token diff that reads as noise in review. Mutation-verified in both directions before the green was trusted: - exclusion broadened to two detectors -> test fails - `--only-verified` removed -> test fails - clean state -> 42 passed The workflow file was restored and byte-compared after the mutation run. repo-gotchas gains section 15 with the two operational facts that cost the sessions, so they stop living in a backlog row nobody re-reads: - the action walks each commit's diff across the range, so a follow-up rename does NOT clear a finding and neither does add-then-remove; only removing the introducing commit does, in practice a squash; - commit messages are in scope too, so documenting the incident can re-create the pattern inside the commit that documents it. Both were obeyed here: the shape is described in prose only, and the staged diff was self-checked against the detector's own compiled pattern -> 0 matches. This is option (a) from #171's own recorded options, not a fresh judgement made under merge pressure. Option (b) "absorb the occasional block" is what just cost a merge; option (c) "upstream a bug report" is worth doing but fixes nothing in this repo's CI. Tests: - full CI-equivalent suite, no -m filter: 897 passed, 1 skipped, exit 0 (1:16:00) -- the +1 against the prior 896 is exactly the new scope test, a verifiable delta rather than an unchanged number asserted to have changed - validate.ps1 exit 0 - pass=118 warn=3 fail=0 skip=2 (unqualified pass) - validate.sh exit 0 - pass=118 warn=4 fail=0 skip=2 (unqualified pass; the 4th WARN is this session's own stale advisory lock across a 76-minute run) - test_security_workflow.py: 41 -> 42 passed Unblocks #88 (PR #419), which needs this on main before its own scan can clear -- a rebase alone would not, per the range-walk fact above. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
) `--splits 3 --group N` ran with no committed `.test_durations`, so pytest-split fell back to an even COUNT split and clustered every subprocess-shelling deploy test onto one shard. Real job times on PR #417: 21m57s / 3m19s / 4m14s -- the worst shard carried 74% of all the work while two runners sat idle. A 897-entry durations file is now committed. Predicted shards 19.6 / 19.5 / 18.2 min against a 19.1-min ideal: worst shard drops from 74% to 34% of total work (33.3% is perfect). Scaled to the runner that is ~21m57s -> ~10.1 min, a ~2.2x CI wall-clock cut. No test, source, or workflow-behaviour change -- CI already passes --splits/--group and pytest-split picks up the default path. The row's own measurement was stale by ~3x and is corrected in place: #88 recorded "measured wall-clock 7:14"; the real numbers are above. Verified at zero cost rather than by three real CI runs: each group was collected with CI's exact invocation via --collect-only and its members' measured durations summed. `--splitting-algorithm least_duration` was also measured and is better (19.1 / 19.1 / 19.1, exactly ideal) -- and deliberately rejected, because it reorders tests across groups and 2.6% is not worth that risk. The ceiling is named rather than implied: two tests are 25.8% of the whole suite (test_validator_count_parity_on_framework at 8.0 min, test_170_underscore_meta_specs_no_status_warn at 6.8 min), and 87 of 897 tests (9.7%) carry 90% of the runtime. A perfect split can never beat the heaviest single test, so ~10 min/shard is the practical floor until those two are addressed -- a separate unit with real fidelity risk. Two paths stay closed: pytest-xdist was measured slower here, and deselecting `slow` in CI is explicitly rejected in pytest.ini (subprocess fidelity is the design). Honest provenance: the durations were generated on a Windows workstation (57.3 min total), not on windows-latest (~29.5 min total). pytest-split consumes RELATIVE weights and the dominant cost -- subprocess spawn -- is the same class on both, but absolute times differ ~1.9x. The real verification is this PR's own job times against the #417 baseline. Unknown test ids added later degrade to count-splitting, so the file goes stale gracefully; no guard detects staleness and none is claimed. Also corrects two comments that asserted the file does not exist (validate.yml:332, requirements-ci.txt:8). Tests: - validate.ps1 exit 0 - pass=118 warn=3 fail=0 skip=2 (unqualified pass) - validate.sh exit 0 - pass=118 warn=3 fail=0 skip=2 (unqualified pass; twin WARN sets identical -- the run finished inside the 60-min lock window) - full CI-equivalent suite, no -m filter: 896 passed, 1 skipped, exit 0 (47:19) - test_ci_hardening.py (reads validate.yml) -> 13 passed - scope check: .test_durations is not referenced by deploy.sh, so it is not shipped downstream This improves CI wall-clock only. A local single-process run is unaffected by shard configuration and no claim is made otherwise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR #419 CI job times: 7m18s / 8m28s / 13m7s. Worst shard 21m57s -> 13m7s = 1.67x, not the 2.2x this branch predicted from workstation-generated durations. The Known Risk caveat is confirmed: the runner weights many-small-tests relatively higher, so the 640-test shard became the slowest. Backlog row, Ship History entry and archived Work Log now state the measured numbers with the runner-regeneration follow-up recorded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
KbWen
force-pushed
the
perf/test-durations-shard-balance-88
branch
from
August 23, 2026 15:47
23df93a to
f62f6fc
Compare
The rebase onto #171 re-ran the same tree and produced 6m56s / 6m43s / 11m24s against the first run 7m18s / 8m28s / 13m7s. Two runs of the same tree differ by ~13%, so the win is recorded as a range rather than as the better number. Backlog row, Ship History entry and archived Work Log all cite both runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
KbWen
added a commit
that referenced
this pull request
Aug 23, 2026
chore(release): v1.8.23 — the downstream-report residue, finished Packages the four units this wave shipped, and the interesting part is that each one unblocked the next rather than being four independent chores: - #175 (PR #417) validate.ps1 stopped mojibaking its own output on a non-UTF-8 console. Its bootstrap reproduced #178 live. - #178 (PR #418) AGENTS.md stopped forbidding the backlog advance bootstrap.md requires. That legalised the step #88's bootstrap needed. - #88 (PR #419) Windows CI shards balance by time, not test count. Its committed durations file turned #171 from latent into a hard block. - #171 (PR #420) the secret scanner stopped failing on ordinary identifiers, which had to land before anything else could merge. Every headline number is measured, and two were revised DOWNWARD on measurement rather than published as predicted: - #88's rebalance was predicted at 2.2x from workstation-generated weights; it measured 13m7s then 11m24s against a 21m57s baseline = 1.67x-1.93x, recorded as a RANGE because two runs of the same tree differed ~13%. - #175's backlog row claimed the bug does not reproduce on pwsh 7; measured, 5.1 and 7 emit identical bytes, and the row was corrected in place. - #88's own row recorded 7:14 for an imbalance that had grown to 21m57s. Three greens were mutation-verified before being trusted: the directive-count ratchet, the new detector-scope test, and the source-only call-site guard -- whose mutation run REFUTED a review panel's headline objection. Version banners bumped on seven surfaces, enumerated by grepping the previous version on disk rather than trusting the last release's record. Each replacement asserted its own anchor; residual grep over all seven returns 0. Files #182: nothing pins version consistency across those seven surfaces. This release's own banners were bumped by hand -- same failure class as the tag step in repo-gotchas 12, which has been forgotten twice. Tests: - full CI-equivalent suite, no -m filter: 897 passed, 1 skipped, exit 0 (1:06:46) - validate.ps1 exit 0 - pass=118 warn=4 fail=0 skip=2 (unqualified pass) - validate.sh exit 0 - pass=118 warn=4 fail=0 skip=2 (unqualified pass, twins identical) - check_audit_chain.py: audit chain intact NOT done by this merge (repo-gotchas 12): git tag v1.8.23 && git push origin v1.8.23 gh release create v1.8.23 --latest Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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 free
to 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.
perf(ci): balance the Windows pytest shards by time, not test count (#88)
--splits 3 --group Nran with no committed.test_durations, so pytest-splitfell back to an even COUNT split and clustered every subprocess-shelling deploy
test onto one shard. Real job times on PR #417: 21m57s / 3m19s / 4m14s -- the
worst shard carried 74% of all the work while two runners sat idle.
A 897-entry durations file is now committed. Predicted shards 19.6 / 19.5 /
18.2 min against a 19.1-min ideal: worst shard drops from 74% to 34% of total
work (33.3% is perfect). Scaled to the runner that is ~21m57s -> ~10.1 min, a
~2.2x CI wall-clock cut. No test, source, or workflow-behaviour change -- CI
already passes --splits/--group and pytest-split picks up the default path.
The row's own measurement was stale by ~3x and is corrected in place: #88
recorded "measured wall-clock 7:14"; the real numbers are above.
Verified at zero cost rather than by three real CI runs: each group was collected
with CI's exact invocation via --collect-only and its members' measured durations
summed.
--splitting-algorithm least_durationwas also measured and is better(19.1 / 19.1 / 19.1, exactly ideal) -- and deliberately rejected, because it
reorders tests across groups and 2.6% is not worth that risk.
The ceiling is named rather than implied: two tests are 25.8% of the whole suite
(test_validator_count_parity_on_framework at 8.0 min,
test_170_underscore_meta_specs_no_status_warn at 6.8 min), and 87 of 897 tests
(9.7%) carry 90% of the runtime. A perfect split can never beat the heaviest
single test, so ~10 min/shard is the practical floor until those two are
addressed -- a separate unit with real fidelity risk. Two paths stay closed:
pytest-xdist was measured slower here, and deselecting
slowin CI is explicitlyrejected in pytest.ini (subprocess fidelity is the design).
Honest provenance: the durations were generated on a Windows workstation
(57.3 min total), not on windows-latest (~29.5 min total). pytest-split consumes
RELATIVE weights and the dominant cost -- subprocess spawn -- is the same class on
both, but absolute times differ ~1.9x. The real verification is this PR's own job
times against the #417 baseline. Unknown test ids added later degrade to
count-splitting, so the file goes stale gracefully; no guard detects staleness and
none is claimed.
Also corrects two comments that asserted the file does not exist
(validate.yml:332, requirements-ci.txt:8).
Tests:
sets identical -- the run finished inside the 60-min lock window)
shipped downstream
This improves CI wall-clock only. A local single-process run is unaffected by
shard configuration and no claim is made otherwise.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
🤖 Generated with Claude Code