From a41c149ce1a2be212581298eea497bd5fbc64779 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 19 Aug 2026 09:08:14 +0900 Subject: [PATCH 1/3] ci: run the CPython suite as trailing steps of cargo test (ubuntu-24.04) The `cpython-tests` job repeated the checkout, toolchain install, Cargo cache restore and Charon/LLBC download that `cargo-test-linux` already does on the same runner image, and needs the same prepared artifacts. Its two steps move into that job's shared step anchor, guarded on `runner.os == 'Linux'` so the macOS and Windows copies skip them, and the job is deleted. Both steps also carry `!cancelled()`, so a failing cargo pass above them no longer suppresses the suite the way plain step ordering would; as separate jobs neither could hide the other. `cargo-test-linux` takes the deleted job's `timeout-minutes`, raised to 90 to cover the work now in it. `pyre/cpython_tests/README.md` and the `--cpython-suite` docstring in `pyre/check.py` are repointed. Assisted-by: Claude --- .github/workflows/pyre-ci.yml | 117 +++++++++++----------------------- pyre/check.py | 2 +- pyre/cpython_tests/README.md | 6 +- 3 files changed, 42 insertions(+), 83 deletions(-) diff --git a/.github/workflows/pyre-ci.yml b/.github/workflows/pyre-ci.yml index fdce7711133..964ca06d431 100644 --- a/.github/workflows/pyre-ci.yml +++ b/.github/workflows/pyre-ci.yml @@ -261,6 +261,10 @@ jobs: runs-on: ubuntu-24.04 needs: prepare-charon-llbc-linux if: ${{ !cancelled() && needs.prepare-charon-llbc-linux.result == 'success' }} + # Two cargo passes plus the release build and run of the CPython suite. + # The cap carries over from the suite's former job; the passes above it + # measured ~34 min together, so it bounds a hang without bounding the work. + timeout-minutes: 90 env: # See prepare-charon-llbc: downstream jobs download the prepared Charon # artifact into this workspace path. @@ -272,6 +276,15 @@ jobs: with: persist-credentials: false - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + - name: Set up CPython + # Only the Linux copy of this job runs the CPython suite, which needs an + # interpreter to drive its runner. The other two copies share these steps + # through the anchor, so the condition keeps the install off them. + id: cpython + if: runner.os == 'Linux' + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.14" - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: cache-bin: false @@ -379,6 +392,31 @@ jobs: -p tlr -p tl -p tla -p tiny2 -p tiny3 -p tinyframe \ -p braininterp -p dualtape -p tlc -p calc -p i64env -p spcount + # The CPython suite rides this job's Linux copy rather than a job of its + # own: it needs the same checkout, toolchain, Cargo cache and prepared + # Charon/LLBC set, and those cost a job's worth of setup to assemble a + # second time. `!cancelled()` keeps the suite reporting even when a cargo + # pass above it fails — as separate jobs neither could hide the other, and + # a plain step order would. + - name: Build pyre-dynasm + if: ${{ !cancelled() && runner.os == 'Linux' }} + shell: bash + run: cargo build --release -p pyrex --bin pyre-dynasm --no-default-features --features dynasm + - name: Run CPython suite (gate regressions, JIT on) + # This is the only place CI runs the suite — `pyre/check.py` keeps the + # stage behind `--cpython-suite`, which no job passes, so its wall time is + # paid once. The runner gates against the shared baseline plus its own + # `baseline.linux-x86_64.json` overlay; `PLATFORM_GATED` only handles + # modules CPython skips wholesale on this host. + # + # The runner has 3 cores, so `--jobs 4` oversubscribes them and stretches + # the wall time of whichever module is running when the extra job lands. + # `--timeout` is per module: `test.test_asyncio` alone takes ~117s of CPU + # and ~2m47s of wall time, which does not fit the former 120s limit. + if: ${{ !cancelled() && runner.os == 'Linux' }} + shell: bash + run: ${{ steps.cpython.outputs.python-path }} pyre/cpython_tests/run.py --backend dynasm --baseline pyre/cpython_tests/baseline.json --jobs 3 --timeout 300 + cargo-test-macos: name: cargo test (macos-latest) runs-on: macos-latest @@ -639,85 +677,6 @@ jobs: CHARON_VERSION: nightly-2026.05.29 steps: *pyre-check-steps - cpython-tests: - name: CPython suite (gate) - runs-on: ubuntu-24.04 - # Artifacts are named for this Linux runner, so wait for the Linux prepare - # leg; waiting for macOS races the Linux upload. This job is the only place - # CI runs the suite — `pyre/check.py` keeps the stage behind - # `--cpython-suite`, which no job passes, so its wall time is paid once. - # Separately, this runner gates against the shared baseline plus its own - # `baseline.linux-x86_64.json` overlay; `PLATFORM_GATED` only handles - # modules CPython skips wholesale on this host. - needs: prepare-charon-llbc-linux - if: ${{ !cancelled() && needs.prepare-charon-llbc-linux.result == 'success' }} - timeout-minutes: 30 - env: - # See prepare-charon-llbc: downstream jobs download the prepared Charon - # artifact into this workspace path. - PYRE_SHARED_BUILD: ${{ github.workspace }}/.pyre-build - CHARON_VERSION: nightly-2026.05.29 - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable - - name: Set up CPython - id: cpython - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: "3.14" - - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - with: - cache-bin: false - shared-key: ${{ runner.arch }} - # Restore-only on PRs (they pull main's cache via restore-keys); save - # only on main so per-PR target caches don't multiply across refs past - # GitHub's 10 GB cache budget and trigger LRU eviction. - save-if: ${{ github.ref == 'refs/heads/main' }} - - name: Download Charon artifact - # Run-scoped handoff from prepare-charon-llbc. Cross-run reuse still - # comes from the prepare job's cache; consumers avoid cache eviction - # races by downloading the prepared install tree directly. - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: charon-${{ runner.os }}-${{ runner.arch }} - path: .pyre-build/charon - - name: Restore Charon executable permissions (Unix) - if: runner.os != 'Windows' - shell: bash - run: chmod +x .pyre-build/charon/*/charon .pyre-build/charon/*/charon-driver - - name: Download LLBC artifact - # Run-scoped handoff from prepare-charon-llbc. Unlike actions/cache, - # an artifact is exempt from the repo-wide 10 GB cache budget and its - # LRU eviction, so a small ullbc set cannot vanish between prepare and - # a late-scheduled consumer (the macOS legs start well after the - # Linux/Windows legs — long enough for a cached entry to be evicted). - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: llbc-${{ runner.os }}-${{ runner.arch }} - path: build/llbc - - name: Verify prepared Charon/LLBC - shell: bash - run: | - test -d .pyre-build/charon - for crate in majit-rlib pyre-object pyre-interpreter pyre-jit; do - test -s "build/llbc/${crate}.ullbc" - # The sidecar carries the artefact's own file table, which is what - # `source=` hashes. Without it the fingerprint falls back to the whole - # cargo closure and every consumer reports LLBC STALE, so fail here - # where the cause is still visible. - test -s "build/llbc/${crate}.ullbc.readfiles" - done - - name: Build pyre-dynasm - run: cargo build --release -p pyrex --bin pyre-dynasm --no-default-features --features dynasm - - name: Run CPython suite (gate regressions, JIT on) - # The runner has 3 cores, so `--jobs 4` oversubscribes them and stretches - # the wall time of whichever module is running when the extra job lands. - # `--timeout` is per module: `test.test_asyncio` alone takes ~117s of CPU - # and ~2m47s of wall time, which does not fit the former 120s limit. - run: ${{ steps.cpython.outputs.python-path }} pyre/cpython_tests/run.py --backend dynasm --baseline pyre/cpython_tests/baseline.json --jobs 3 --timeout 300 - sandbox-build: name: sandbox build + e2e (ubuntu-24.04) runs-on: ubuntu-24.04 diff --git a/pyre/check.py b/pyre/check.py index 8a27f65cda4..77f21307423 100644 --- a/pyre/check.py +++ b/pyre/check.py @@ -3095,7 +3095,7 @@ def run_cpython_suite(self): Off by default and reached only through `--cpython-suite`: the suite costs more wall time than every other stage here put together, and the - `cpython-tests` CI job already runs it on its own schedule. Pass the + `cargo-test-linux` CI job already runs it on its own schedule. Pass the flag locally when a change could move a verdict the synthetic corpus does not cover. """ diff --git a/pyre/cpython_tests/README.md b/pyre/cpython_tests/README.md index ee532e75310..3ad533ff933 100644 --- a/pyre/cpython_tests/README.md +++ b/pyre/cpython_tests/README.md @@ -72,9 +72,9 @@ before any test, a denied resource, or a suite whose every case was skipped). ## CI -- `.github/workflows/pyre-ci.yml` job `cpython-tests` — gates PRs on the - baseline-`PASS` subset, dynasm with **JIT on** (`MAJIT_STRICT=1`), on - `ubuntu-24.04` (x86_64). The baseline is recorded on linux-x86_64 and the JIT +- `.github/workflows/pyre-ci.yml` job `cargo-test-linux`, in its trailing + `Run CPython suite` step — gates PRs on the baseline-`PASS` subset, dynasm + with **JIT on** (`MAJIT_STRICT=1`), on `ubuntu-24.04` (x86_64). The baseline is recorded on linux-x86_64 and the JIT codegen is architecture-specific, so local baseline comparisons must use the same host. - `.github/workflows/pyre-cpython-nightly.yml` — non-gating nightly `--full` From 01a40db1a39649ad2a8960041c56089f8cf69b28 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 19 Aug 2026 09:46:03 +0900 Subject: [PATCH 2/3] ci: move the pyre-wasm web-binding build off the check.py leg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The step sat ahead of `Run pyre/check.py` on the premise that building `--features web` first would leave check.py's later `--features wasm-host` build sharing its compiled dependencies. Measured on a cold target dir: the web build compiles 328 crates in 6m00 and leaves the wasm-host build compiling 166 instead of 323 — 1m42 against a 2m32 cold control. Half the graph still recompiles because check.py sets `RUSTFLAGS='-C link-arg=--export-table -C link-arg=--growable-table --cfg getrandom_backend="custom"'` and the web step sets none. On the ubuntu runner the step measures 11.7 min and the saving it buys is worth about 1.7. It moves to `sandbox-build`, which already has the checkout, toolchain, Cargo cache and prepared Charon/LLBC set it needs, is on the same runner image, and at 15.4 min sits far off the run's critical path. It goes first in that job so the compile signal still arrives early. Nothing executes the artefact either way; check.py writes its own pyre_wasm.wasm. That job is renamed to `sandbox e2e + wasm web build (ubuntu-24.04)`. Assisted-by: Claude --- .github/workflows/pyre-ci.yml | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/.github/workflows/pyre-ci.yml b/.github/workflows/pyre-ci.yml index 964ca06d431..049fb63288d 100644 --- a/.github/workflows/pyre-ci.yml +++ b/.github/workflows/pyre-ci.yml @@ -535,18 +535,6 @@ jobs: # native backends. if: runner.os == 'Linux' run: rustup target add wasm32-unknown-unknown - - name: Build pyre-wasm web binding (Linux only) - # check.py builds and exercises the wasm-host binding below. Build the - # browser binding in the same release target first so the two flavours - # share their compiled dependencies without spending two additional - # runner jobs on checkout, cache restore, and LLBC download. check.py's - # later wasm-host build intentionally replaces pyre_wasm.wasm before it - # snapshots and runs that module. - if: runner.os == 'Linux' - run: | - cargo build --release -p pyre-wasm \ - --target wasm32-unknown-unknown \ - --no-default-features --features web - name: Run pyre/check.py env: PYRE_CHECK_PYTHON3: ${{ steps.cpython.outputs.python-path }} @@ -678,7 +666,7 @@ jobs: steps: *pyre-check-steps sandbox-build: - name: sandbox build + e2e (ubuntu-24.04) + name: sandbox e2e + wasm web build (ubuntu-24.04) runs-on: ubuntu-24.04 needs: prepare-charon-llbc-linux if: ${{ !cancelled() && needs.prepare-charon-llbc-linux.result == 'success' }} @@ -727,6 +715,26 @@ jobs: # where the cause is still visible. test -s "build/llbc/${crate}.ullbc.readfiles" done + - name: Add wasm32 target + run: rustup target add wasm32-unknown-unknown + - name: Build pyre-wasm web binding + # Compile check only: nothing here runs the browser binding, and the + # `pyre/check.py` job overwrites pyre_wasm.wasm with its own wasm-host + # build before it snapshots and runs that module. It runs here, and + # first, because this job already carries the checkout, toolchain, Cargo + # cache and prepared Charon/LLBC set the build needs, and finishes well + # inside the wall time of the check.py leg it used to sit in. + # + # It was placed ahead of check.py on the premise that the two wasm + # flavours would share compiled dependencies. Measured: the web build + # takes 6m and leaves the later wasm-host build 50s cheaper (2m32 -> + # 1m42), because check.py sets RUSTFLAGS the web build does not and half + # the graph recompiles under the difference. So the sharing is real and + # costs about seven times what it returns. + run: | + cargo build --release -p pyre-wasm \ + --target wasm32-unknown-unknown \ + --no-default-features --features web - name: Build pyre --features sandbox # A green sandbox build is the fails-closed proof: every mediated module # names libc through host_seam::sys, so any direct syscall left outside From 5ee4f5643ddf5d70fe591da69f3bada73e679369 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 19 Aug 2026 10:44:35 +0900 Subject: [PATCH 3/3] ci: give the per-crate LLBC caches a restore-keys fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four entries carried an exact key and nothing else, so a miss restored nothing. The key is the wide digest — `Compute LLBC fingerprints` runs before any `.readfiles` exists, so `source=` is the whole cargo closure there and one edit anywhere misses all four. Every run therefore re-extracted the whole set: 10.6 min on ubuntu, 19.5 on windows, 22.3 on macOS. With a `restore-keys` prefix a miss falls back to the crate's most recent entry, and `extract-llbc.py` adjudicates: it recomputes each crate's stamp against the current tree, narrowing `source=` through the restored `.readfiles`, and compares it to the restored stamp as exact text. Verified locally — a comment appended to `pyre/pyre-jit/src/call_jit.rs`, which is in pyre-jit's readfiles and no other crate's, re-extracts pyre-jit alone and skips the other three; with nothing changed all four skip in 2s. The key therefore only chooses what is offered. A stale restore costs a download and is re-extracted, and cannot reach the artefact. Measured shares of one extraction: pyre-interpreter 376s, pyre-jit 106s, pyre-object 53s, majit-rlib 40s. Over the last 60 commits on main the share of commits touching no file in a crate's readfiles is 48%, 33%, 73% and 92% respectively, and 8% of commits touch all four. Weighting the first by the second puts the expected saving near half of each Extract step. Assisted-by: Claude --- .github/workflows/pyre-ci.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/pyre-ci.yml b/.github/workflows/pyre-ci.yml index 049fb63288d..fa91cf7ff64 100644 --- a/.github/workflows/pyre-ci.yml +++ b/.github/workflows/pyre-ci.yml @@ -178,6 +178,19 @@ jobs: # current. The `llbc2-` prefix retires the keys whose payload predates it — # the key is the wide digest, which no edit to the extraction driver moves, # so without a new namespace the shapeless entries would restore forever. + # + # Each entry also takes a `restore-keys` prefix, so a miss falls back to + # this crate's most recent entry instead of restoring nothing. The exact + # key is the WIDE digest — `Compute LLBC fingerprints` runs before any + # `.readfiles` exists, so `source=` is the whole cargo closure there and + # any edit anywhere misses every crate. The fallback is what lets the + # narrow digest decide instead: `extract-llbc.py` recomputes each crate's + # stamp against the current tree, using the restored `.readfiles` to + # narrow `source=`, and compares it to the restored stamp as exact text. + # A crate whose own closure did not move skips; every other crate + # re-extracts. So the key only chooses what is OFFERED — a stale + # restore costs a download and is then re-extracted, it cannot survive + # into the artefact. - name: Cache LLBC (majit-rlib) id: llbc-cache-rlib uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 @@ -188,6 +201,8 @@ jobs: build/llbc/majit-rlib.ullbc.readfiles build/llbc/majit-rlib.*.layouts.ullbc key: llbc2-majit-rlib-${{ runner.os }}-${{ runner.arch }}-${{ env.CHARON_VERSION }}-${{ steps.llbc-fingerprint.outputs.majit_rlib }} + restore-keys: | + llbc2-majit-rlib-${{ runner.os }}-${{ runner.arch }}-${{ env.CHARON_VERSION }}- - name: Cache LLBC (pyre-object) id: llbc-cache-object uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 @@ -198,6 +213,8 @@ jobs: build/llbc/pyre-object.ullbc.readfiles build/llbc/pyre-object.*.layouts.ullbc key: llbc2-pyre-object-${{ runner.os }}-${{ runner.arch }}-${{ env.CHARON_VERSION }}-${{ steps.llbc-fingerprint.outputs.pyre_object }} + restore-keys: | + llbc2-pyre-object-${{ runner.os }}-${{ runner.arch }}-${{ env.CHARON_VERSION }}- - name: Cache LLBC (pyre-interpreter) id: llbc-cache-interpreter uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 @@ -208,6 +225,8 @@ jobs: build/llbc/pyre-interpreter.ullbc.readfiles build/llbc/pyre-interpreter.*.layouts.ullbc key: llbc2-pyre-interpreter-${{ runner.os }}-${{ runner.arch }}-${{ env.CHARON_VERSION }}-${{ steps.llbc-fingerprint.outputs.pyre_interpreter }} + restore-keys: | + llbc2-pyre-interpreter-${{ runner.os }}-${{ runner.arch }}-${{ env.CHARON_VERSION }}- - name: Cache LLBC (pyre-jit) id: llbc-cache-jit uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 @@ -217,6 +236,8 @@ jobs: build/llbc/pyre-jit.ullbc.fingerprint build/llbc/pyre-jit.ullbc.readfiles key: llbc2-pyre-jit-${{ runner.os }}-${{ runner.arch }}-${{ env.CHARON_VERSION }}-${{ steps.llbc-fingerprint.outputs.pyre_jit }} + restore-keys: | + llbc2-pyre-jit-${{ runner.os }}-${{ runner.arch }}-${{ env.CHARON_VERSION }}- - name: Extract LLBC # Runs only when some crate's cache missed. extract-llbc.py self-skips # the crates whose .ullbc + matching .fingerprint stamp were restored