Skip to content

ci: cut test wall time without losing coverage#3267

Merged
messense merged 2 commits into
PyO3:mainfrom
messense:ci-cut-test-times
Jul 19, 2026
Merged

ci: cut test wall time without losing coverage#3267
messense merged 2 commits into
PyO3:mainfrom
messense:ci-cut-test-times

Conversation

@messense

Copy link
Copy Markdown
Member

Summary

Addresses part of #2189. Measured where the time goes in a recent Test run (job/step timings + the full nextest log of the slowest cell): the slowest matrix cells (windows-latest, macos-15-intel) take 19–22 min, of which ~12 min is nextest — and the last ~5 minutes of that run nearly serially. All changes below are scheduling/config-level; no test is removed.

nextest scheduling (.config/nextest.toml)

  • Schedule the slowest tests first (pgo, conda, uniffi, uv multi-python) with a priority = 100 override (supported since nextest 0.9.91). Previously the pgo tests (77s, 115s) and integration_pyo3_mixed_conda (152s) started last and drained the run into a single-test tail.
  • Relax the uniffi override from threads-required = "num-cpus" to 2. Claiming the whole machine made all 9 uniffi tests run one at a time — ~4 min of serialized wall time per job on Windows.

Test changes

  • test_integration_conda builds against 2 conda envs (3.10 + 3.14) instead of 5 (3.10–3.14). Conda env creation dominates this test (152s on Windows); oldest + newest keeps the multi-env interpreter discovery coverage.

Workflow changes

  • Enable the existing faster-tests feature in the nextest runs (matrix + Alpine): test wheels are stored uncompressed and stripped, which is exactly what the feature was added for. The deflate path stays covered by the compression unit tests and by the jobs that use a release maturin without the feature (auditwheel, docker, cross-compile).
  • Disable debuginfo on all platforms, not just Windows — smaller test artifacts mean faster links, wheel packing and pip installs.
  • Run the zig cross-compile step once per host OS instead of for both 3.9 and pypy. It mainly exercises host-OS-specific plumbing; pypy adds nothing zig-specific. macos-latest keeps its pypy run since it has no 3.9 cell, so every OS retains zig coverage. This shaves ~2–3 min off two of the three slowest jobs (windows/pypy, macos-intel/pypy).
  • Skip setup-miniconda on the -dev cells. The conda tests create their own envs independent of the matrix python, so once per OS (the 3.9/pypy cells) is enough, and the -dev cells drop the ~2 min miniconda setup.

Expected effect

The slowest cells should go from ~20 min to roughly 12–14 min. Follow-up (separate PR): sharing cargo target dirs per fixture would cut the ~3100 rustc invocations per job (55% uncacheable by sccache: cdylibs, proc-macros, build scripts), which is the next big lever.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NNNLg7XZnxM8Aiqrzo6c5q

messense and others added 2 commits July 19, 2026 10:06
The slowest matrix cells (windows-latest, macos-15-intel) spend ~12 of
their ~20 minutes in nextest, and the log timings show most of the tail
is serialized:

- The slowest tests (pgo, conda, uniffi, uv multi-python) were scheduled
  last, so the run drains into a single-test tail. Schedule them first
  via nextest priority overrides (nextest >= 0.9.91).
- threads-required = num-cpus made every uniffi test run exclusively,
  ~4 min of serialized wall time per job. Relax it to 2 threads.
- test_integration_conda created five conda envs (3.10-3.14, 152s on
  windows); oldest + newest keeps the coverage for half the cost.
- Enable the existing faster-tests feature in CI so test wheels are
  stored uncompressed and stripped; the deflate path stays covered by
  unit tests and the release-binary jobs (auditwheel, docker, cross).
- Disable debuginfo on all platforms instead of only Windows: smaller
  artifacts mean faster links, wheel packing and pip installs.
- Run the zig cross-compile step once per host OS instead of twice
  (it was running for both 3.9 and pypy); zig mostly exercises
  host-OS-specific plumbing. macos-latest keeps its pypy run since it
  has no 3.9 cell.
- Skip miniconda setup on the -dev cells; the conda tests create their
  own envs independent of the matrix python, so once per OS is enough
  and the -dev cells skip the ~2 min setup.

See PyO3#2189

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNNLg7XZnxM8Aiqrzo6c5q
The faster-tests feature was coupled to strip=true in the test harness,
and enabling it in CI broke uniffi proc-macro tests: bindings are
generated by reading uniffi metadata from the compiled cdylib, which
-C strip=symbols removes, producing an empty Python module. pyo3 stub
generation introspects artifacts the same way (nextest's fail-fast just
cancelled the run before those could fail).

Decouple strip from faster-tests: tests always build with strip
disabled, matching what CI did before faster-tests was enabled, and
faster-tests keeps only its uncompressed-wheel behavior which is where
the speedup comes from.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNNLg7XZnxM8Aiqrzo6c5q
@messense
messense merged commit 23d766b into PyO3:main Jul 19, 2026
85 checks passed
@messense
messense deleted the ci-cut-test-times branch July 19, 2026 04:04
messense added a commit that referenced this pull request Jul 19, 2026
## Summary

Follow-up to #3267 (see #2189): the remaining big lever for test time.
Isolated per-case target dirs meant every test case recompiled its whole
dependency tree — ~3100 rustc invocations per CI job, 55% of which
sccache cannot cache (cdylib links, proc-macros, build scripts).

### Design

- Cases with compatible build configurations share a per-family target
dir (`shared-pyo3` / `shared-cffi` / `shared-uniffi` / `shared-misc`),
so dependency trees compile once per family. Cargo's own build lock
makes concurrent builds of *different* crates in one dir safe, and
artifacts for different feature sets coexist keyed by cargo's
fingerprint.
- Concurrent maturin builds of the **same** crate are *not* safe in a
shared dir: maturin stages built artifacts through `target/maturin/`
(renaming them out of cargo's output path and back), so two same-crate
builds race on the staged files — reproduced locally with the develop +
integration uniffi-proc-macro cases running concurrently. Tests
therefore hold a per-fixture `fs4` file lock for the duration of the
build (the same pattern the harness already uses for the cffi-provider
venv). Different fixtures still build in parallel.
- Cases keep an isolated dir when they change compile flags (pgo, zig,
explicit `--target`), build against unusual interpreters (conda, uv
multi-python), or assert on target dir contents (pep517 profile checks,
stable-ABI selection which deletes the dir).
- One small `src/` hardening: the pyo3 config file maturin writes into
`target/maturin/` is now written via temp file + rename so concurrent
builds sharing a target dir never observe a partial config.
- The develop test timeouts go from 120s to 600s since a test can now
legitimately wait on the fixture lock; the timeouts still catch hangs.

### Local measurements (full suite, cold state, no sccache, Apple
Silicon)

| | main | this branch |
|---|---|---|
| Wall time | 50.7s | 47.5–63.8s |
| User CPU | **511s** | **287s** |
| Sys CPU | 85s | 52s |
| Involuntary context switches | 899k | 467k |
| `test-crates/targets` size after run | 3.6 GB | 2.1 GB |
| Result | 345 passed | 345 passed (3 runs) + warm rerun 32.8s |

Total CPU work drops ~44% with no wall-time regression and no lock
starvation (the slowest single test got *faster*, 38.5s → 27.2s). A
many-core dev machine hides main's redundant compiles behind
parallelism; CI's 4-core runners are CPU-bound, so the CPU cut is what
should translate to CI wall time there. CI won't see the full 44% since
sccache already absorbs ~60% of compiles — this targets exactly the
uncacheable remainder.
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