fix(codegen): coerce declared-only addition results #2134
Workflow file for this run
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
| name: GC Ratchet | |
| # Regression gate for GC retention, evacuation accounting, and memory against | |
| # the pinned baseline in benchmarks/gc_ratchet/baseline/gc-ratchet-v1.json. | |
| # | |
| # THIS JOB IS DESIGNED TO BE ABLE TO FAIL. There is no `continue-on-error`, no | |
| # `|| true`, and no pipe between the checker and the shell's exit status. That | |
| # is deliberate and load-bearing: `gc-stress` in test.yml is | |
| # `continue-on-error: true`, which is why a regression sat behind it through | |
| # three merges. A gate that cannot fail is not a gate. | |
| # | |
| # NOT the public benchmark baseline. benchmarks/run_public_baseline.sh produces | |
| # benchmarks/results/public-node-bun-v1.json (published Node/Bun evidence, gates | |
| # `lint`). This is the internal Perry-vs-Perry GC ratchet. Do not conflate them. | |
| on: | |
| pull_request: | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| # ONE GROUP PER MAIN COMMIT, and cancel ONLY pull-request runs. | |
| # | |
| # A shared group with unconditional cancel-in-progress looked right and was | |
| # wrong: `main` is busy and the macOS runner pool is deep enough that a run | |
| # can sit queued for 40+ minutes, so every new merge cancelled the previous | |
| # main run before it ever got a runner. Observed on the first day this | |
| # workflow existed — three consecutive main runs cancelled, zero executed. | |
| # A gate that is always cancelled never fails, which is the same hole as | |
| # `continue-on-error: true` wearing a different hat. | |
| # | |
| # ***SCOPING `cancel-in-progress` TO PULL REQUESTS DID NOT FIX IT (#7205).*** | |
| # It protects a run that has already STARTED. GitHub additionally allows at | |
| # most one PENDING run per group: when a new run enters a group, "any | |
| # previously pending job or workflow in the concurrency group will be | |
| # cancelled" — regardless of `cancel-in-progress`. Every `main` run here is | |
| # pending for tens of minutes, so a merge cadence faster than the queue depth | |
| # kept cancelling all of them. Re-measured after the first fix: runs | |
| # 30707891646 (#7192), 30708370282 (#7198) and 30707189162 (#7195) all ended | |
| # `cancelled` with `jobs: []` — never reached a runner, zero executions in | |
| # three merges. | |
| # | |
| # Keying the group on the COMMIT for push events is what actually fixes it: | |
| # each merged commit gets a group of its own, so no two `main` runs can ever | |
| # contend and none can supersede another. They queue independently and each | |
| # eventually gets its own answer. PR runs keep sharing a per-ref group and | |
| # keep superseding themselves, which is still correct — only the head | |
| # commit's result gates the merge. | |
| group: gc-ratchet-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| env: | |
| CARGO_TERM_COLOR: always | |
| MACOSX_DEPLOYMENT_TARGET: "13.0" | |
| jobs: | |
| gc-ratchet: | |
| # macos-14 is arm64, which matches the `darwin-arm64` platform key the | |
| # baseline was captured under. The checker refuses a platform mismatch | |
| # rather than comparing numbers that are not comparable, so moving this to | |
| # a linux runner would turn the gate red, not silently weaken it. | |
| runs-on: macos-14 | |
| timeout-minutes: 90 | |
| permissions: | |
| contents: read | |
| # Read-only listing of the PR's changed files, to decide whether this | |
| # change can touch the collector. Same approach `changeset-gate` uses, and | |
| # it avoids a fetch-depth: 0 clone of a large history. | |
| pull-requests: read | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| # Fast structural failure first: a broken harness or a tampered artifact | |
| # should not cost a 20-minute compiler build to discover. | |
| # | |
| # `--scope structural` is load-bearing and is the #7554 repair. This step | |
| # runs BEFORE the measurement step, so anything it fails on costs the | |
| # entire run's coverage — fourteen probes that never execute. That price | |
| # is correct for "this artifact is unreadable or tampered with" and badly | |
| # wrong for "one of its 156 cells is not bit-identical", which is what | |
| # actually happened: one cell's 6,768-byte spread failed this step and the | |
| # ratchet measured NOTHING on any branch for three days, while two GC | |
| # pacing changes (#7594, #7596) merged with hand-run A/Bs in its place. | |
| # | |
| # Under `structural` a per-cell defect is printed loudly and deferred; the | |
| # `check` step below re-derives the same defect list and fails on it, after | |
| # the probes have run and with the full table attached. So this cannot | |
| # wave a defect through to a green job — `check` is where the verdict is, | |
| # and tests/test_gc_ratchet.py's | |
| # `test_structural_preflight_defers_every_defect_it_waves_through` asserts | |
| # that coupling one planted defect shape at a time. | |
| - name: Harness unit tests and artifact validation | |
| run: | | |
| python3 -m unittest discover -s tests -p 'test_gc_ratchet.py' -v | |
| python3 benchmarks/gc_ratchet/gc_ratchet.py validate --scope structural | |
| - name: Decide whether this change can affect the collector | |
| id: relevance | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| REPOSITORY: ${{ github.repository }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "$EVENT_NAME" != "pull_request" ]]; then | |
| echo "run=true" >> "$GITHUB_OUTPUT" | |
| echo "Not a pull request; measuring." | |
| exit 0 | |
| fi | |
| gh api "repos/$REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename' > changed.txt | |
| # Anything under crates/ can move the collector: a codegen change | |
| # alters what is allocated, a transform change alters what escapes, | |
| # and a dependency bump can change allocator behaviour. The filter is | |
| # deliberately broad; it exists only to spare docs-only PRs a build. | |
| # If the listing is empty or the API failed, `set -e` already aborted | |
| # — the job does not silently fall through to "not relevant". | |
| if grep -qE '^(crates/|benchmarks/gc_ratchet/|Cargo\.(toml|lock)$|\.github/workflows/gc-ratchet\.yml$|tests/test_gc_ratchet\.py$)' changed.txt; then | |
| echo "run=true" >> "$GITHUB_OUTPUT" | |
| echo "Change touches collector-relevant paths; measuring." | |
| else | |
| echo "run=false" >> "$GITHUB_OUTPUT" | |
| echo "No collector-relevant paths changed; the pinned baseline still holds." | |
| fi | |
| - name: Install Rust toolchain | |
| if: steps.relevance.outputs.run == 'true' | |
| uses: dtolnay/rust-toolchain@stable | |
| - uses: ./.github/actions/setup-llvm22 | |
| - name: Cache cargo | |
| if: steps.relevance.outputs.run == 'true' | |
| uses: actions/cache@v6 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: ${{ runner.os }}-cargo-bench-${{ hashFiles('**/Cargo.lock') }} | |
| restore-keys: ${{ runner.os }}-cargo- | |
| - name: Setup Node oracle | |
| if: steps.relevance.outputs.run == 'true' | |
| uses: actions/setup-node@v7 | |
| with: | |
| # Single source of truth: .node-version at the repo root. Node is a | |
| # correctness input here, not a peer benchmark: every probe's stdout | |
| # is diffed against it, because a probe that silently stops | |
| # allocating exits 0 and reports a beautifully small retained heap. | |
| node-version-file: .node-version | |
| - name: Build perry and the runtime archives | |
| if: steps.relevance.outputs.run == 'true' | |
| run: | | |
| set -euo pipefail | |
| # perry-runtime and perry-stdlib are rlib-only. libperry_runtime.a and | |
| # libperry_stdlib.a come from the -static wrapper crates; building | |
| # without them links a stale archive and makes the whole measurement | |
| # vacuous. The package set is fixed here so cargo feature unification | |
| # is identical to the set used to capture the baseline. | |
| cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static | |
| for artifact in perry libperry_runtime.a libperry_stdlib.a; do | |
| test -s "target/release/$artifact" \ | |
| || { echo "::error::target/release/$artifact was not produced"; exit 1; } | |
| done | |
| - name: Verify the Node oracle version | |
| if: steps.relevance.outputs.run == 'true' | |
| run: | | |
| set -euo pipefail | |
| expected="v$(tr -d 'v \n' < .node-version)" | |
| actual="$(node --version)" | |
| test "$expected" = "$actual" \ | |
| || { echo "::error::expected Node $expected, found $actual"; exit 1; } | |
| - name: Measure | |
| if: steps.relevance.outputs.run == 'true' | |
| env: | |
| PERRY_RUNTIME_DIR: ${{ github.workspace }}/target/release | |
| # Deterministic link: use the prebuilt full stdlib rather than letting | |
| # the auto-optimizer rebuild a workload-specific archive mid-run. | |
| PERRY_NO_AUTO_OPTIMIZE: "1" | |
| run: | | |
| set -euo pipefail | |
| mkdir -p .bench-results | |
| # Load is recorded inside the measurement JSON too; printing it here | |
| # keeps it in the log next to the numbers. | |
| uptime | |
| # Pass the oracle explicitly rather than letting the harness search. | |
| # A run that cannot reach the oracle now fails the gate — "we did not | |
| # verify" must not look like "verified" — so resolution must not be | |
| # left to chance. | |
| python3 benchmarks/gc_ratchet/gc_ratchet.py measure \ | |
| --perry target/release/perry \ | |
| --repeats 7 \ | |
| --node "$(command -v node)" \ | |
| --output .bench-results/gc-ratchet-current.json | |
| # No pipe, no `|| true`, no continue-on-error: this step's exit status is | |
| # the gate. The `shared_ci` profile gates retention and the evacuation | |
| # counters, which are semantic and transfer across machine classes, and | |
| # explicitly does not gate RSS or wall time, which do not. See | |
| # benchmarks/gc_ratchet/tolerances.json for every band and its reasoning. | |
| - name: Check against the pinned baseline | |
| if: steps.relevance.outputs.run == 'true' | |
| run: | | |
| python3 benchmarks/gc_ratchet/gc_ratchet.py check \ | |
| --current .bench-results/gc-ratchet-current.json \ | |
| --profile shared_ci | |
| - name: Upload measurement | |
| if: always() && steps.relevance.outputs.run == 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: gc-ratchet-${{ github.sha }} | |
| path: | | |
| .bench-results/gc-ratchet-current.json | |
| benchmarks/gc_ratchet/baseline/gc-ratchet-v1.json | |
| retention-days: 90 |