-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Add PR performance regression gate (callgrind instruction counts) #8585
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9cfad0e
fc1c47f
49610ef
6da7697
6e0e933
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| # CodSpeed benchmark tracking. | ||
| # | ||
| # Runs the criterion benchmark suites under CodSpeed's instrumentation and | ||
| # uploads the results, which gives per-PR comparison comments and a commit-by- | ||
| # commit dashboard at codspeed.io. Like the callgrind gate in perf-ci.yaml, | ||
| # CodSpeed measures with a modified Callgrind rather than wall-clock time, so | ||
| # the numbers are deterministic on shared runners. | ||
| # | ||
| # How this relates to the other two benchmark workflows: | ||
| # * perf-ci.yaml -- blocks PRs. Measures whole processes, so it is the only | ||
| # one that sees end-to-end startup and import cost. Self-contained: no | ||
| # external service, thresholds under our control. | ||
| # * cron-ci.yaml "benchmark" job -- scheduled criterion wall-clock runs | ||
| # published to the website. | ||
| # * this workflow -- in-process criterion benchmarks, reported to CodSpeed | ||
| # for trend tracking and PR comments. Informational; it does not block. | ||
| # | ||
| # The upload is tokenless: CodSpeed accepts OIDC from public repositories. | ||
|
|
||
| name: CodSpeed | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| types: [unlabeled, opened, synchronize, reopened] | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} | ||
| cancel-in-progress: true | ||
|
|
||
| env: | ||
| CARGO_INCREMENTAL: 0 | ||
| CARGO_TERM_COLOR: always | ||
| CI: true | ||
|
|
||
| jobs: | ||
| benchmarks: | ||
| if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }} | ||
| name: Run benchmarks | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 60 | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - uses: dtolnay/rust-toolchain@stable | ||
|
|
||
| # setup-python picks the version up from .python-version. The benchmark | ||
| # binaries link against libpython through pyo3 even when the CPython | ||
| # comparison benchmarks are skipped, so an interpreter has to be present. | ||
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | ||
|
|
||
| - name: Restore cargo cache | ||
| uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | ||
| with: | ||
| path: | | ||
| ~/.cargo/bin/ | ||
| ~/.cargo/registry/index/ | ||
| ~/.cargo/registry/cache/ | ||
| ~/.cargo/git/db/ | ||
| target/ | ||
| key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml') }}- | ||
| restore-keys: | | ||
| ${{ runner.os }}-stable--${{ hashFiles('**/Cargo.toml') }}- | ||
| ${{ runner.os }}-stable-- | ||
| continue-on-error: true | ||
|
|
||
| # Not available through taiki-e/install-action, so install from crates.io. | ||
| - name: Install cargo-codspeed | ||
| run: cargo install cargo-codspeed --locked | ||
|
|
||
| # --jobs 1: profile.bench uses thin LTO with codegen-units = 1, and | ||
| # CodSpeed additionally builds with full debug info for symbolisation. | ||
| # Linking several bench binaries at once under those settings exhausts | ||
| # the runner's memory and rustc is OOM-killed. | ||
| - name: Build benchmarks | ||
| run: cargo codspeed build --jobs 1 --bench execution --bench microbenchmarks --bench vm_boot | ||
|
|
||
| - name: Run benchmarks | ||
| uses: CodSpeedHQ/action@4296e51e7041e24dadb86d1d6e8b9320d223dbe8 # v5.0.3 | ||
| with: | ||
| mode: simulation | ||
| run: cargo codspeed run | ||
| env: | ||
| # Measure RustPython only; see the note on skip_cpython in | ||
| # benches/execution.rs. | ||
| RUSTPYTHON_BENCH_SKIP_CPYTHON: "1" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| # PR performance regression gate. | ||
| # | ||
| # Role split with the nightly/weekly benchmarks (cron-ci.yaml "benchmark" job): | ||
| # * cron-ci.yaml collects criterion wall-clock data on a schedule and | ||
| # publishes it to the website for long-term trend tracking. It never | ||
| # blocks a PR. | ||
| # * This workflow runs on pull requests and BLOCKS merges that regress | ||
| # interpreter performance. There is no overlap in tooling or data: | ||
| # this gate uses callgrind instruction counts, not criterion. | ||
| # | ||
| # Why instruction counts (callgrind Ir) instead of wall-clock time? | ||
| # GitHub-hosted runners are shared, throttled machines: wall-clock timings | ||
| # routinely vary by 10-40% run to run, so any wall-clock threshold is either | ||
| # too loose to catch real regressions or flaky enough to block unrelated PRs. | ||
| # Retired-instruction counts for a deterministic program are reproducible to | ||
| # well under 0.1% between runs (PYTHONHASHSEED is pinned by the runner | ||
| # script), are immune to CPU contention and frequency scaling, and stay | ||
| # comparable even when measurements run concurrently. The trade-off is a | ||
| # ~50x slowdown under callgrind, which the workload sizes in | ||
| # scripts/perf_ci.py are tuned for. See benches/perf_ci/README.md. | ||
| # | ||
| # Budget: measurement (both binaries, excluding builds) is parallelized and | ||
| # targets well under 10 minutes; builds reuse the shared cargo cache and the | ||
| # base binary is cached by commit SHA, so the base build is usually a | ||
| # cache hit. | ||
|
|
||
| name: Performance gate | ||
|
|
||
| on: | ||
| pull_request: | ||
| types: [unlabeled, opened, synchronize, reopened] | ||
| paths: | ||
| - "Cargo.toml" | ||
| - "Cargo.lock" | ||
| - "rust-toolchain.toml" | ||
| - ".cargo/config.toml" | ||
| - "crates/**" | ||
| - "src/**" | ||
| - "benches/perf_ci/**" | ||
| - "scripts/perf_ci.py" | ||
| - ".github/workflows/perf-ci.yaml" | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} | ||
| cancel-in-progress: true | ||
|
|
||
| env: | ||
| CARGO_INCREMENTAL: 0 | ||
| CARGO_PROFILE_RELEASE_DEBUG: 0 | ||
| CARGO_TERM_COLOR: always | ||
| CI: true | ||
|
|
||
| jobs: | ||
| build: | ||
| if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }} | ||
| # Named after the matrix entry rather than left to be named for it (see | ||
| # the note on generated names in ci.yaml). | ||
| name: Build rustpython (${{ matrix.rev }}) | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 90 | ||
| strategy: | ||
| matrix: | ||
| rev: [base, head] | ||
| fail-fast: true | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| fetch-depth: 0 | ||
| persist-credentials: false | ||
|
|
||
| - name: Resolve revision to build | ||
| id: resolve | ||
| # For "base", measure against the merge base with the target branch so | ||
| # the comparison only reflects this PR's diff. For "head", measure the | ||
| # checked-out merge/head commit itself. | ||
| run: | | ||
| base_sha=$(git merge-base HEAD "origin/${BASE_REF}") | ||
| if [ "${REV}" = "base" ]; then | ||
| echo "sha=${base_sha}" >> "$GITHUB_OUTPUT" | ||
| git checkout --quiet "${base_sha}" | ||
| else | ||
| echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" | ||
| fi | ||
| env: | ||
| BASE_REF: ${{ github.event.pull_request.base.ref || 'main' }} | ||
| REV: ${{ matrix.rev }} | ||
|
|
||
| # The finished binary is cached by commit SHA: every PR against the same | ||
| # main tip shares one base build, and re-runs skip both builds entirely. | ||
| - name: Restore built binary | ||
| id: bin_cache | ||
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | ||
| with: | ||
| path: target/release/rustpython | ||
| key: perf-ci-bin-v1-${{ runner.os }}-${{ steps.resolve.outputs.sha }} | ||
|
|
||
| - uses: dtolnay/rust-toolchain@stable | ||
| if: steps.bin_cache.outputs.cache-hit != 'true' | ||
|
Comment on lines
+94
to
+102
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
sed -n '92,123p' .github/workflows/perf-ci.yaml
printf '\nConfigured Rust toolchain files:\n'
find . -maxdepth 2 -name 'rust-toolchain.toml' -print -exec cat {} \;Repository: RustPython/RustPython Length of output: 1514 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' 'Workflow structure and cache/toolchain references:'
cat -n .github/workflows/perf-ci.yaml | sed -n '1,220p'
printf '\n%s\n' 'Rust toolchain and workflow references:'
rg -n --hidden --glob '!target/**' --glob '!node_modules/**' \
'rust-toolchain|dtolnay/rust-toolchain|perf-ci-bin|steps\.resolve|cache-hit|rustc -Vv|cargo build|git fetch|checkout' \
.github rust-toolchain.toml README.md 2>/dev/null || true
printf '\n%s\n' 'Relevant workflow metadata:'
rg -n --hidden --glob '!target/**' --glob '!node_modules/**' \
'name:|on:|workflow_call|pull_request|push:|permissions:|zizmor' \
.github/workflows 2>/dev/null || trueRepository: RustPython/RustPython Length of output: 50377 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
workflow = Path(".github/workflows/perf-ci.yaml").read_text()
toolchain = Path("rust-toolchain.toml").read_text()
zizmor = Path(".github/zizmor.yml").read_text()
cache = re.search(
r"- name: Restore built binary(?P<body>.*?)(?=\n - |\Z)",
workflow,
re.S,
)
rust = re.search(
r"\n - uses: dtolnay/rust-toolchain@stable(?P<body>.*?)(?=\n - |\Z)",
workflow,
re.S,
)
assert cache and rust, "Expected cache and Rust toolchain steps"
cache_body = cache.group("body")
rust_body = rust.group("body")
key = re.search(r"key:\s*(.+)", cache_body)
assert key, "Binary cache key is missing"
key_text = key.group(1)
print("binary cache key:", key_text)
print("binary cache step precedes toolchain step:", workflow.index(cache.group(0)) < workflow.index(rust.group(0)))
print("toolchain step conditional on cache miss:", "cache-hit != 'true'" in rust_body)
print("cache key contains rust compiler fingerprint:", bool(re.search(r"rustc|toolchain|compiler", key_text, re.I)))
print("configured Rust channel:", re.search(r'channel\s*=\s*"([^"]+)"', toolchain).group(1))
print("zizmor exception for dtolnay/rust-toolchain:", "dtolnay/rust-toolchain: any" in zizmor)
PY
printf '\nRelevant zizmor configuration:\n'
cat -n .github/zizmor.yml | sed -n '1,35p'Repository: RustPython/RustPython Length of output: 1777 Include the Rust compiler fingerprint in the binary cache key. The cache lookup runs before 🤖 Prompt for AI Agents |
||
|
|
||
| - name: Restore cargo cache | ||
| if: steps.bin_cache.outputs.cache-hit != 'true' | ||
| uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | ||
| with: | ||
| path: | | ||
| ~/.cargo/bin/ | ||
| ~/.cargo/registry/index/ | ||
| ~/.cargo/registry/cache/ | ||
| ~/.cargo/git/db/ | ||
| target/ | ||
| key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml') }}- | ||
| restore-keys: | | ||
| ${{ runner.os }}-stable--${{ hashFiles('**/Cargo.toml') }}- | ||
| ${{ runner.os }}-stable-- | ||
| continue-on-error: true | ||
|
|
||
| - name: Build release binary | ||
| if: steps.bin_cache.outputs.cache-hit != 'true' | ||
| run: cargo build --release --bin rustpython | ||
|
|
||
| - name: Upload binary | ||
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | ||
| with: | ||
| name: perf-ci-binary-${{ matrix.rev }} | ||
| path: target/release/rustpython | ||
| retention-days: 3 | ||
|
|
||
| measure: | ||
| name: Measure and compare instruction counts | ||
| needs: build | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| steps: | ||
| # Workloads and the runner script are always taken from the PR head, so | ||
| # both binaries execute identical guest code. | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| # No python-version here: setup-python picks up .python-version, which is | ||
| # the same CPython the rest of CI uses and matches the stdlib vendored in | ||
| # Lib/. The harness itself only needs the stdlib. | ||
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | ||
|
|
||
| - name: Install valgrind | ||
| run: sudo apt-get update && sudo apt-get install -y --no-install-recommends valgrind | ||
|
|
||
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | ||
| with: | ||
| name: perf-ci-binary-base | ||
| path: perf-bin/base | ||
|
|
||
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | ||
| with: | ||
| name: perf-ci-binary-head | ||
| path: perf-bin/head | ||
|
|
||
| # Both binaries are measured in the same checkout, so each measure run | ||
| # purges the bytecode cache and repopulates it with the binary it is | ||
| # about to measure. Without that, the second run would reuse the .pyc | ||
| # files the first one compiled and look several percent faster with an | ||
| # identical interpreter -- a bias towards head that hides regressions. | ||
| - name: Measure base | ||
| run: | | ||
| chmod +x perf-bin/base/rustpython perf-bin/head/rustpython | ||
| python3 scripts/perf_ci.py measure --binary perf-bin/base/rustpython -o perf-base.json | ||
|
|
||
| - name: Measure head | ||
| run: python3 scripts/perf_ci.py measure --binary perf-bin/head/rustpython -o perf-head.json | ||
|
|
||
| - name: Upload measurements | ||
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | ||
| with: | ||
| name: perf-ci-results | ||
| path: | | ||
| perf-base.json | ||
| perf-head.json | ||
| retention-days: 30 | ||
|
|
||
| - name: Compare against threshold | ||
| run: python3 scripts/perf_ci.py compare perf-base.json perf-head.json | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: RustPython/RustPython
Length of output: 547
🏁 Script executed:
Repository: RustPython/RustPython
Length of output: 3600
🌐 Web query:
dtolnay rust-toolchain GitHub README toolchain input stable commit pin💡 Result:
To pin the dtolnay/rust-toolchain GitHub Action to a specific commit for security purposes while ensuring the desired Rust toolchain is installed, you must provide the toolchain explicitly via an input, as the action cannot automatically infer the toolchain version from a commit SHA [1]. When pinning, you should use a commit SHA from the history of the master branch [2][3]. Follow these steps: 1. Identify a commit SHA from the master branch of the dtolnay/rust-toolchain repository [2]. 2. In your workflow YAML, use this SHA in the uses field [2][1]. 3. Explicitly define the toolchain version using the with: toolchain: input [2][1]. Example configuration: - uses: dtolnay/rust-toolchain@6190aa5 with: toolchain: stable The toolchain input accepts standard rustup specifications (e.g., stable, nightly, 1.89.0, or date-based nightlies) [2][3][4]. Note that when using a commit SHA, the action no longer relies on the "smart" behavior of moving branches (like
@stableor@nightly) to determine which toolchain to install; therefore, the explicit input is mandatory [2][1][5]. Always ensure the selected commit is within the master branch history to prevent the pin from becoming invalid due to garbage collection [2][3].Citations:
🏁 Script executed:
Repository: RustPython/RustPython
Length of output: 2888
🏁 Script executed:
Repository: RustPython/RustPython
Length of output: 1368
🏁 Script executed:
Repository: RustPython/RustPython
Length of output: 5050
Pin the action and include the compiler version in the binary cache key.
dtolnay/rust-toolchainto a full commit SHA and settoolchain: stableexplicitly.perf-ci-bin-v1-${{ runner.os }}-${{ steps.resolve.outputs.sha }}. The current key can reuse a binary built by an olderstablecompiler.🤖 Prompt for AI Agents
Source: Coding guidelines