Repository navigation
Cut PR CI from hours to about 20 minutes: Linux arm64, 4 xdist workers, cancel superseded runs - #2115
Merged
Merged
Conversation
Neither pull request workflow had a concurrency block, so a push to a pull request left the run on the previous head going. Group runs by pull request number and cancel the one in progress when a new head arrives. Pushes to main use code_changes.yaml, which is unchanged, and manual dispatches are grouped by ref and never cancelled. The group name is a literal because pr_code_changes.yaml and code_changes.yaml share the workflow name "Code changes". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Test job moved to macos-latest in 2025 (#1092, #1207) because parameter uprating differed between Ubuntu and macOS (#1209). The parameter tree now loads in sorted order (policyengine-core sorts each directory listing), and on main (3c48247): - the full parameter tree, all 2,706 parameter objects and 43,666 dated values including the baseline copies, is byte-identical on macOS arm64, ubuntu-latest and ubuntu-24.04-arm (sha256 79abbf29...); - the 1,611 YAML tests pass on both Linux runners; - all 2,263 pytest outcomes (2,255 passed, 7 xfailed, 1 skipped) match the macOS run on 3525263 (the same code) node for node. macOS runners are the bottleneck: since 2026-10-02 the 78 completed macOS Test jobs waited a median 139 minutes for a runner and then ran a median 242 minutes; the main run on 3525263 took 4h18m in pytest. The same pytest step took 69 minutes on ubuntu-24.04-arm and 76 on ubuntu-latest. arm64 keeps the macOS runners' CPU architecture and has four physical cores to ubuntu-latest's two. Pushes to main switch too, so PR and main CI run on the same platform. The Python-Compat smoke imports stay on x86 ubuntu-latest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fixture set POLICYENGINE_UK_DEFAULT_DATASET to the unpinned dataset and never restored it, so every later Microsimulation() in the same process read the latest dataset instead of the pinned one. In the serial order no later file builds a default Microsimulation, so nothing showed. Once files are spread over worker processes, any pinned microsimulation test could land after it. The default dataset is read only when the simulation is built, so the override now lasts just that long. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policyengine-core loads every variable file of each new tax-benefit system as a module named after the system's id and leaves it in sys.modules after the system is garbage collected. Every Simulation builds its own system, so each leaves about 1,000 modules (roughly 10 MB) behind. The property tests build hundreds, and the pytest process reached 15.7 GB on a 16 GB Linux runner; on 7 GB macOS runners it swapped. An autouse fixture now drops the modules of systems that no longer exist after each test, once more than eight systems own modules. On three property-test files run locally the process stayed at about 650-730 MB instead of growing to 1.5 GB, and the files ran in 4m06s instead of 5m22s. A test checks that a dead system's modules go, a live one's stay and its simulation still computes, and that it fails if core stops leaking, so the workaround can be removed then. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Test job ran pytest in one process, using one of the runner's four cores. pytest-xdist (now a dev dependency) spreads the tests over four workers; worksteal hands queued tests to whichever worker is idle, so the long property-test files don't leave a tail. `make test` is now `test-yaml` then `test-python`, and CI passes the xdist flags through PYTEST_ARGS; without them both targets run serially, as before. Measured on ubuntu-24.04-arm, main plus this branch, in run 37120279129 (pytest step only): - 4 workers, worksteal: 14m56s, host memory peak 9.2 GB of 16 GB - 4 workers, loadfile: 17m51s, peak 11.5 GB - 1 process (round 1, before the module release): 69m24s, peak 15.8 GB Every variant reports the same outcome for every test node: the 2,263 on main (2,255 passed, 7 xfailed, 1 skipped) plus this branch's two new tests. The same loadfile run on x86 ubuntu-latest took 18m26s and peaked at 15.2 GB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A hung Test job ran until GitHub's 6-hour limit (#1992's run on 2026-10-02 did). With four xdist workers on Linux arm64 the job took 17m30s in run 37120279129 (YAML tests 2m07s, pytest 15m01s, everything else under a minute), so 60 minutes is more than three times a normal run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pyproject.toml gains a dev dependency, so the merge triggers the versioning workflow, which needs a fragment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
|
Merged at the reviewed head ffdf3e3 with a merge commit, on these gates:
Open PRs pick this up by merging |
MaxGhenis
added a commit
that referenced
this pull request
Oct 3, 2026
MaxGhenis
added a commit
that referenced
this pull request
Oct 3, 2026
Main's #2115 relocked uv.lock; take it and move only policyengine-core, from 3.32.9 to 3.32.13 (core#578: a storage holds a shared-key set only while it shares an array, removing the ~1.3 MB per simulation that 3.32.12 added). The floor stays >=3.32.12. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 3, 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 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.
The PR
Testjob was the bottleneck for every open PolicyEngine-UK PR. The figures below come from the Actions API: every "Code changes" run created since 2026-10-02T00:00Z whoseTestjob finished with success or failure. Those 78 jobs onmacos-latestwaited a median of 139 minutes for a runner and then ran for a median of 242 minutes; p90 end to end was 10.8 hours. Two runs hit GitHub's 6-hour limit: #1992's (run 37009617251) and #2088's (run 37090099823, which shows as cancelled). This PR's ownTestjob took 22m36s onubuntu-24.04-arm, and every test node gets the same outcome as on macOS.What changes, one commit each
pr_code_changes.yamlandpr_docs_changes.yamlget aconcurrencygroup keyed on the PR number, withcancel-in-progressforpull_requestevents. Pushes tomainusecode_changes.yaml, which has no concurrency group, so they are never cancelled. Manual dispatches are grouped by ref and not cancelled.Testjob moved tomacos-latestin 2025 (Some uprating fails onubuntubut notmacos#1092, Move back to correct private pension income index #1207) because uprating differed between Ubuntu and macOS (Ubuntu is returning different results from macos #1209). That is no longer the case (evidence below).ubuntu-24.04-armkeeps the macOS runners' CPU architecture and has 4 physical cores, against 2 onubuntu-latest. Themainworkflow switches too, so PR andmainCI run on the same platform. The x86Python-Compatsmoke imports are unchanged.test_latest_data_smoke.pysetPOLICYENGINE_UK_DEFAULT_DATASETto the unpinned dataset and never restored it. Any laterMicrosimulation()in the same process silently used the unpinned data. The override now lasts only while the fixture builds its simulation.sys.modulesunder the system's id and never removes them (TaxBenefitSystem construction permanently leaks ~5,600 sys.modules entries (~55-60MB) per build — unreclaimable by gc policyengine-core#520). EverySimulationbuilds its own system, so each leaves about 1,000 modules (roughly 10 MB) behind. The property tests build hundreds. Host memory peaked at 15.8 GB of the Linux runner's 16 GB during the serial pytest run. GitHub's macOS arm64 runners have 7 GB (3 M1 cores), less than half that, which fits the same suite taking anywhere from 74 minutes to over 4 hours there.--dist worksteal. pytest-xdist is a new dev dependency.make testis nowtest-yamlfollowed bytest-python. CI passesPYTEST_ARGS="-n 4 --dist worksteal"; without that argument,make teststill runs serially, as before. Hypothesis settings are untouched, and no test is removed or weakened.timeout-minutes: 60on bothTestjobs. That is more than three times the measured 17m30s.Before and after (real CI runs)
main3525263, run 37013961610hb-ctr-non-dep-exemptions, run 37017928329 (started 15:21Z on 10/2)main3c48247, serial, run 37062825983main3c48247, serial, run 37062825983main3c48247, 4 workersloadfile, before the module release, run 37062825983loadfile, run 37120279129loadfile, run 37120279129worksteal, run 37120279129mainmerged in: 2,374 tests)Peak memory is the runner's used memory from
free -m, sampled every 30 s (every 5 s in round 1). It is shown as MiB ÷ 1,000 against a total of 15,947 MiB, so "9.2 GB of 16" means 9,242 of 15,947 MiB (58%), and the serial runs used 99%.Memory peaks vary from run to run with worksteal scheduling (9.2 GB to 11.5 GB on arm across the two schedulers). If a run ever exhausts memory, the fallback is
-n 3. Each runner keeps one job slot whether pytest uses one core or four. Four workers in one job therefore cut runner time per PR about fourfold, where sharding across jobs would only have cut wall time. This matters because the org's Linux runners also queue at busy times.Evidence that Linux gives the same results
Parameterobject's full value history: 2,706 objects and 43,666 dated values, including the 1,353baselinecopies. The dump is byte-identical on macOS arm64 (local, locked environment) and in CI on ubuntu-latest and ubuntu-24.04-arm (run 37120279129 artifacts), sha25679abbf29…. Some uprating fails onubuntubut notmacos#1092 traced the original difference to chained uprating and fixed it. Ubuntu is returning different results from macos #1209, which reported a later difference between operating systems, is still open; this dump and the outcome comparison below are the evidence that no difference remains.mainrun on 3525263, the Linux serial runs on 3c48247 (the same code plus a version bump) and every xdist run report identical outcomes for all 2,263 nodes: 2,255 passed, 7 xfailed, 1 skipped. The xdist runs add only this branch's two new tests. The comparison parses each log's per-test outcome lines and reports nodes missing from either log and any outcome that differs. The only differences are the two new tests. (In the experiment logs a memory sampler sometimes printed onto the end of a result line; the parser strips that.) This PR's own run reports 2,365 passed, 8 xfailed and 1 skipped, becausemainhas gained tests since.Invariants
sys.modulesentries whose owning tax-benefit system no longer exists. It finds live systems with agcscan, and only once more than eight systems own modules, so the scan is rare. A live system's variables, and anything that introspects them, are untouched. The regression test checks both sides.make testwithoutPYTEST_ARGSruns exactly the two commands it ran before.For open PRs
Each open PR picks this up only after it merges
main(or rebases). A PR whose CI starts before that still queues for macOS. Re-running an old run does not help, because a re-run keeps the workflow file it started with.Not in this PR
sys.modulesafterexec_module. Once that ships, the regression test's precondition fails and the conftest workaround can be deleted.Simulation()re-imports all ~1,000 variable files to build a fresh system, about 1 s each. That is most of the property tests' cost and is worth a separate look.axiom: n/a: CI and test infrastructure only; no policy logic changes.
🤖 Generated with Claude Code