Skip to content

perf(codegen): specialize undefined loop filters #2501

perf(codegen): specialize undefined loop filters

perf(codegen): specialize undefined loop filters #2501

name: Auto-Optimize App Patterns
# Runs the `benchmarks/app-patterns` kernels through the AUTO-OPTIMIZE link —
# the default path, and the one no other gate in this repo covers.
#
# WHY THIS EXISTS (#7475)
#
# `perry file.ts -o out` rebuilds perry-runtime + perry-stdlib with a per-app
# Cargo feature set into `target/perry-auto-<hash>/` and links those archives
# OVER whatever `PERRY_RUNTIME_DIR` points at. Almost every other gate sets
# `PERRY_NO_AUTO_OPTIMIZE=1` for a deterministic link — gc-ratchet says so
# inline, and so do a dozen `crates/perry/tests` cases — so the binary users
# actually get was, until this job, tested by nothing.
#
# #7475 is what that cost. `object_deep_clone` threw `TypeError: next is not a
# function` under auto-optimize and printed the correct checksum under
# `PERRY_NO_AUTO_OPTIMIZE=1`. The bug was in neither the kernel nor the feature
# set: the iterator drain held live values in bare Rust locals across an
# allocating `.next()`. BOTH links had the defect —
# `PERRY_GC_PROTECT_FROMSPACE=1` faults on both — but only the feature-stripped
# one allocated in the order that made the stale read observable. That is the
# general shape: a latent stale-root read is invisible until something perturbs
# allocation timing, and the auto-optimize link perturbs it per app.
#
# CHECKED AGAINST CLAUDE.md's FOUR WAYS A GATE CAN BE UNABLE TO FAIL
#
# 1. no `continue-on-error`, no `|| true`, no pipe swallowing the script's
# exit status;
# 2. NOT in branch protection's required contexts yet, deliberately — a new
# gate has never been green, so promoting it on day one blocks every open
# PR. Promote after the first green run on `main`; leaving that undone is
# itself hazard 2 (see `gc-root-dominance`);
# 3. `concurrency` cancels pull-request runs only; push runs are keyed on the
# commit so queued `main` runs cannot cancel each other (#7205);
# 4. the subject is ASSERTED live. `scripts/auto_opt_app_patterns.sh` reads
# the linker command line out of `perry -v` and requires it to name a
# `perry-auto-*/…/libperry_runtime.a` that exists on disk. A run in which
# the auto-optimizer quietly fell back to the prebuilt archives — which it
# does, by design, whenever the cargo rebuild fails — would otherwise pass
# every output comparison while testing the exact configuration this job
# does not care about.
#
# The skip list inside the script is EMPTY as of #7497 — every kernel in
# `benchmarks/app-patterns/kernels` is gated. A skip entry that matches no kernel
# FAILS, so an exemption cannot outlive its fix; that rule is what took
# `promise_all_chains` out of the list in the same PR that fixed it.
on:
pull_request:
# PR arm is OPT-IN via the `run-extended-tests` label (see the header of
# test.yml and docs/src/testing/ci-tiers.md): an unlabelled PR still gets
# a run, but every job in it is skipped, which costs no runner slot. The
# main-line arm (schedule / tags) is unchanged. `labeled` re-fires the run
# when the label lands.
types: [opened, synchronize, reopened, labeled]
# POST-MERGE ARM: staggered six-hourly sweep of `main`, NOT one run per merge.
# `push: branches: [main]` starved this gate and nine others for two days
# (#7856): ~29 jobs enqueued per merge at 58 merges/day against a ~9-job
# concurrency ceiling. The pull-request arm is unchanged -- every PR is still
# measured. ***DO NOT RESTORE `push: branches: [main]`.***
# Rationale, measurement and cost: docs/src/testing/ci-gate-scheduling.md
schedule:
- cron: "22 */6 * * *"
push:
tags: ["v*"]
workflow_dispatch:
permissions:
contents: read
concurrency:
# ***#7966: KEY EVERY MAIN-LINE RUN ON `github.run_id`, NOT `github.sha`.***
# The previous expression read `github.event_name == 'push' && github.sha ||
# github.ref`. That was #7205's fix and it keyed on the event being `push` --
# correct while the main-line arm WAS `push: branches: [main]`. #7856 moved the
# main-line arm to `schedule:`, which falls through to `github.ref` (constant
# `refs/heads/main`), so every scheduled run shared one group again and #7205
# came straight back. Measured 2026-08-12 on all ten scheduled gates, the same
# shape every time: oldest run `queued` holding the group, the two after it
# `cancelled` with `jobs: 0`, newest `pending`. `github.run_id` is unique per
# run, so schedule / tag-push / workflow_dispatch each get a group of their own
# and none can supersede another. PR runs keep the shared per-ref group and
# keep superseding themselves, which is still what we want.
group: auto-opt-app-patterns-${{ github.event_name }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CARGO_TERM_COLOR: always
jobs:
auto-opt-app-patterns:
# PR arm is opt-in (label `run-extended-tests`); see the `on:` block.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-extended-tests')
runs-on: ubuntu-latest
# The auto-optimize rebuild is a second full release build of
# perry-runtime + perry-stdlib on top of the workspace build, so this is a
# long job even with a warm cargo cache.
timeout-minutes: 90
permissions:
contents: read
pull-requests: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Decide whether this change can affect a compiled app
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; running the kernels."
exit 0
fi
gh api "repos/$REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename' > changed.txt
# Deliberately broad: anything under crates/ changes the compiled
# binary, and the auto-optimize feature selection reads the manifests.
# The filter only spares docs-only PRs a compiler build; `set -e`
# already aborted if the listing failed, so this cannot silently fall
# through to "not relevant".
# `.github/actions/setup-llvm22/` is in the list because it configures
# the LLVM the gate's compiler is built against — a change there can
# move the generated code without touching a single line under crates/.
if grep -qE '^(crates/|benchmarks/app-patterns/|scripts/auto_opt_app_patterns\.sh$|Cargo\.(toml|lock)$|\.node-version$|\.github/actions/setup-llvm22/|\.github/workflows/auto-opt-app-patterns\.yml$)' changed.txt; then
echo "run=true" >> "$GITHUB_OUTPUT"
echo "Change can affect a compiled app; running the kernels."
else
echo "run=false" >> "$GITHUB_OUTPUT"
echo "No app-affecting paths changed."
fi
- name: Install Rust toolchain
if: steps.relevance.outputs.run == 'true'
run: rustup toolchain install nightly-2026-08-20 --profile minimal
- uses: ./.github/actions/setup-llvm22
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
if: steps.relevance.outputs.run == 'true'
with:
shared-key: "${{ runner.os }}-perry"
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install clang
if: steps.relevance.outputs.run == 'true'
run: |
sudo apt-get update
sudo apt-get install -y clang
- name: Setup Node oracle
if: steps.relevance.outputs.run == 'true'
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
# Single source of truth: .node-version. Every kernel's stdout is
# diffed against this node, so the pin is a correctness input.
node-version-file: .node-version
- name: Build perry and the prebuilt runtime archives
if: steps.relevance.outputs.run == 'true'
env:
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS: "-C linker-features=-lld"
run: |
set -euo pipefail
# The auto-optimize path builds its OWN archives, but the driver still
# needs the prebuilt ones on disk for its fallback probe — and they are
# what the failure mode under test silently substitutes, so a run
# without them could not distinguish the two. perry-runtime and
# perry-stdlib are rlib-only; the `.a`s come from the -static wrappers.
cargo build --release \
-p perry -p perry-runtime -p perry-stdlib \
-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
# GATING, and the reason hazard 4 is actually closed rather than asserted.
# The liveness check is a text matcher over the linker command line; a
# matcher that stops matching reports "no archive" (loud), but one that
# matches too much reports a PASS for a fallback run (silent). `--self-test`
# feeds it a canned log of exactly that shape — the `auto-optimize: built …`
# message followed by a link line naming the PREBUILT archive — and fails
# if it is accepted. It caught a real over-match while this gate was being
# written.
- name: Prove the liveness matcher can still fail
if: steps.relevance.outputs.run == 'true'
run: ./scripts/auto_opt_app_patterns.sh --self-test
# GATING. No pipe, no `|| true`: this step's exit status IS the gate. The
# liveness assertion (the link line must name a perry-auto archive) lives
# inside the script so a local run gets it too.
- name: Run the app-pattern kernels through the auto-optimize link
if: steps.relevance.outputs.run == 'true'
env:
PERRY_RUNTIME_DIR: ${{ github.workspace }}/target/release
run: ./scripts/auto_opt_app_patterns.sh