This repository was archived by the owner on Aug 7, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 99
ci(bench): WASM + native perf-regression tracking with sticky PR comments #1002
Open
WiktorStarczewski
wants to merge
11
commits into
next
Choose a base branch
from
wiktor/bench-tracking
base: next
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
73b54cd
ci(bench): add WASM + native perf-regression tracking
WiktorStarczewski 5c7c32f
ci(bench): add packed-permutation throughput + end-to-end synthetic p…
WiktorStarczewski f742e76
ci(bench): tighten regression threshold to 10% + drive variance below it
WiktorStarczewski 5fc04e0
ci(bench): satisfy crypto repo's CI gates
WiktorStarczewski 120c50b
fix(bench): driver hung after writing JSON; explicit process.exit
WiktorStarczewski 214b0ac
ci(bench): address review — split privileges, pin actions, fix no-std…
WiktorStarczewski ae75f95
ci(bench): satisfy actionlint shellcheck + zizmor artipacked audit
WiktorStarczewski 4718b59
ci(bench): fork-PR gate, pipefail, serialize publishers (2nd review p…
WiktorStarczewski da13e80
ci(bench): pull_request_target for fork PRs (matches web-sdk pattern)
WiktorStarczewski 6c223b7
Merge branch 'next' into wiktor/bench-tracking
WiktorStarczewski 08ad7c6
ci(bench): workflow_run split — close GHAS cache-poisoning alerts
WiktorStarczewski File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,215 @@ | ||
| # Performance regression tracking for miden-crypto — UNTRUSTED stage. | ||
| # | ||
| # Two jobs run on every PR + every push to `next`: | ||
| # | ||
| # 1. bench-wasm: headless-Chromium + wasm-pack harness in miden-bench-wasm/ | ||
| # tracks RPO/RPX/Poseidon2/Blake3/Keccak hash perf as | ||
| # compiled to wasm32 + run in V8 — the runtime real users | ||
| # ship in. | ||
| # | ||
| # 2. bench-native: cargo bench --bench {hash,word,transpose} on the GHA | ||
| # runner. Tracks the same primitives at native speed for | ||
| # comparison + regression detection. | ||
| # | ||
| # Both jobs run PR-controlled code (Rust, npm install, Node) and so MUST | ||
| # NOT hold any privileged token. This workflow runs on `pull_request` | ||
| # (not `pull_request_target`), declares `permissions: contents: read` at | ||
| # both workflow and job scope, and strips the persisted token from | ||
| # `actions/checkout`. The results are uploaded as artifacts and consumed | ||
| # by the separate `bench-publish.yml` workflow, which fires via | ||
| # `workflow_run` from a trusted base-repo context. | ||
| # | ||
| # Why two workflows: GitHub Advanced Security's | ||
| # `actions/cache-poisoning/poisonable-step` rule flags any step under | ||
| # `pull_request_target` that executes PR-supplied code, because the | ||
| # implicit `ACTIONS_RUNTIME_TOKEN` always present in such jobs grants | ||
| # write access to the Actions cache. The split — untrusted bench in | ||
| # this workflow, trusted publish in `bench-publish.yml` triggered by | ||
| # `workflow_run` — is the GitHub Security Lab's documented pattern | ||
| # for "bench on PR, comment on PR, can't trust the PR's code." See | ||
| # https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ | ||
|
|
||
| name: bench | ||
|
|
||
| on: | ||
| pull_request: | ||
| push: | ||
| branches: [next] | ||
| workflow_dispatch: | ||
|
|
||
| # Read-only at workflow scope; per-job permissions narrow this further. | ||
| # `bench-publish.yml` is where the write scopes live. | ||
| permissions: | ||
| contents: read | ||
|
|
||
| # Skip duplicate runs on PR sync events. | ||
| concurrency: | ||
| group: bench-${{ github.workflow }}-${{ github.head_ref || github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| bench-wasm: | ||
| name: WASM perf (Chromium / V8) | ||
| runs-on: ubuntu-latest | ||
| # Local Apple M5 wall-clock for the full bench is ~40s; GHA ubuntu- | ||
| # latest x86_64 is ~2.7× slower across these benches (measured), | ||
| # putting the bench step at ~2 min. Plus wasm-pack build (~3 min) | ||
| # + Chromium install (~30s) + setup overhead = ~6-7 min typical. | ||
| # 20 min cap leaves comfortable headroom for the worst-case run. | ||
| timeout-minutes: 20 | ||
| permissions: | ||
| contents: read | ||
| steps: | ||
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 | ||
| with: | ||
| # The default checkout writes the GHA token to `.git/config`, | ||
| # which PR-controlled build scripts (build.rs, npm postinstall, | ||
| # etc.) would then have access to. Strip it — this job has no | ||
| # business holding any credential. | ||
| persist-credentials: false | ||
|
|
||
| - name: Install Rust + wasm32 target | ||
| run: | | ||
| rustup update --no-self-update | ||
| rustup target add wasm32-unknown-unknown | ||
|
|
||
| - name: Install wasm-pack | ||
| # Pinned to v0.4.0 SHA. The action installs wasm-pack v0.13.1. | ||
| uses: jetli/wasm-pack-action@0d096b08b4e5a7de8c28de67e11e945404e9eefa # pin@v0.4.0 | ||
| with: | ||
| version: "v0.13.1" | ||
|
|
||
| - name: Build miden-bench-wasm | ||
| # `--target web` produces an ESM JS file that imports/instantiates | ||
| # the .wasm via `fetch()` — what the static page expects. The | ||
| # `--release` profile is required: without it the per-op cost | ||
| # is dominated by debug-mode overflow checks and the numbers are | ||
| # meaningless as a perf-tracking signal. | ||
| run: | | ||
| wasm-pack build --release --target web \ | ||
| --out-dir static/pkg miden-bench-wasm | ||
|
|
||
| - name: Install Node deps | ||
| working-directory: miden-bench-wasm | ||
| run: npm install --no-audit --no-fund | ||
|
|
||
| - name: Install Chromium | ||
| working-directory: miden-bench-wasm | ||
| # Chromium-only — the action's matrix support is via npx, but we | ||
| # only target one browser for now (V8 fidelity to the wallet). | ||
| run: npx playwright install --with-deps chromium | ||
|
|
||
| - name: Run benches | ||
| id: run | ||
| working-directory: miden-bench-wasm | ||
| run: | | ||
| # `pipefail` is load-bearing: the driver streams to a file via | ||
| # a shell redirect, so a panicking driver would still leave a | ||
| # partial `results.json` on disk; `set -e` ensures the step | ||
| # fails fast and the trusted publish workflow never sees a bad | ||
| # baseline. | ||
| set -euo pipefail | ||
| node driver.mjs > results.json | ||
| echo "results=$(wc -l < results.json) entries written" | ||
| # Surface the per-bench medians in the workflow summary so a | ||
| # human can eyeball them without clicking into the artifact. | ||
| { | ||
| echo '## WASM bench results' | ||
| echo '' | ||
| echo '| bench | median (ns/iter) |' | ||
| echo '|---|---|' | ||
| jq -r '.[] | "| \(.name) | \(.value | tostring) |"' results.json | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
|
|
||
| - name: Write metadata for publish workflow | ||
| working-directory: miden-bench-wasm | ||
| # `workflow_run.pull_requests` is empty for fork PRs, so the | ||
| # trusted publish workflow can't get the PR number from the | ||
| # event payload. Stash the PR number + event + head SHA here so | ||
| # the publish workflow has somewhere to look. Note: this file | ||
| # is PR-author-controlled by being on the runner where PR code | ||
| # executed — the publish workflow MUST validate / sanity-check | ||
| # it rather than trust it blindly. | ||
| run: | | ||
| set -euo pipefail | ||
| jq -n \ | ||
| --arg event "${{ github.event_name }}" \ | ||
| --arg pr_number "${{ github.event.pull_request.number || '' }}" \ | ||
| --arg pr_head_sha "${{ github.event.pull_request.head.sha || '' }}" \ | ||
| '{event: $event, pr_number: $pr_number, pr_head_sha: $pr_head_sha}' \ | ||
| > metadata.json | ||
|
|
||
| - name: Upload raw results as artifact | ||
| # Picked up by `bench-publish.yml` via `workflow_run`. The | ||
| # publish workflow runs in the base repo's trusted context with | ||
| # write-scope token; this artifact is the only thing crossing | ||
| # the trust boundary. | ||
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # pin@v4 | ||
| with: | ||
| name: bench-wasm-results | ||
| path: | | ||
| miden-bench-wasm/results.json | ||
| miden-bench-wasm/metadata.json | ||
| retention-days: 30 | ||
|
|
||
| bench-native: | ||
| name: Native perf (Linux x86_64) | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 20 | ||
| permissions: | ||
| contents: read | ||
| steps: | ||
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # pin@v4 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: Install Rust | ||
| run: rustup update --no-self-update | ||
|
|
||
| - name: Run benches | ||
| id: run | ||
| # Initial bench triage: | ||
| # - hash: RPO/RPX/Poseidon2/Blake3/Keccak merge + sequential. | ||
| # The headline. Fast (sub-second per group). | ||
| # - word: Felt arithmetic primitives. Foundational, fast. | ||
| # - transpose: Matrix transpose. Foundational, fast. | ||
| # | ||
| # Deliberately excluded for now (revisit after first PR lands): | ||
| # - smt / merkle / partial_mt / store: moderate runtime, not the | ||
| # headline; defer until we know the wasm + tracked-native | ||
| # setup is stable. | ||
| # - large_smt / large_smt_forest / sparse_path: multi-minute | ||
| # runtimes. Wrong fit for per-PR alerts; ideal for a separate | ||
| # nightly bench job. | ||
| # - encryption / dsa: defer pending review of which sub-benches | ||
| # are stable. | ||
| run: | | ||
| # `pipefail` ensures a failed `cargo bench` (compile or run) | ||
| # propagates through the `tee` and fails the step. Without | ||
| # it, only tee's exit status is observed and a broken bench | ||
| # would silently produce an empty/partial `bench-native.txt` | ||
| # for the trusted publish workflow to ingest as a baseline. | ||
| set -euo pipefail | ||
| cargo bench --bench hash --bench word --bench transpose \ | ||
|
Collaborator
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. Could we add |
||
| -p miden-crypto -- --output-format bencher 2>&1 \ | ||
| | tee bench-native.txt | ||
|
|
||
| - name: Write metadata for publish workflow | ||
| # See `bench-wasm` for why we emit this. | ||
| run: | | ||
| set -euo pipefail | ||
| jq -n \ | ||
| --arg event "${{ github.event_name }}" \ | ||
| --arg pr_number "${{ github.event.pull_request.number || '' }}" \ | ||
| --arg pr_head_sha "${{ github.event.pull_request.head.sha || '' }}" \ | ||
| '{event: $event, pr_number: $pr_number, pr_head_sha: $pr_head_sha}' \ | ||
| > metadata.json | ||
|
|
||
| - name: Upload raw results as artifact | ||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # pin@v4 | ||
| with: | ||
| name: bench-native-results | ||
| path: | | ||
| bench-native.txt | ||
| metadata.json | ||
| retention-days: 30 | ||
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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| [package] | ||
| description = "WASM benchmarks for miden-crypto hash primitives, run in headless Chromium for production-fidelity perf tracking." | ||
| edition = "2024" | ||
| homepage = "https://github.com/0xMiden/crypto" | ||
| license = "MIT OR Apache-2.0" | ||
| name = "miden-bench-wasm" | ||
| publish = false | ||
| readme = "README.md" | ||
| repository = "https://github.com/0xMiden/crypto" | ||
| rust-version.workspace = true | ||
| version.workspace = true | ||
|
|
||
| [lib] | ||
| crate-type = ["cdylib", "rlib"] | ||
| # No tests / doctests in this crate — it's a wasm-bindgen export surface, | ||
| # exercised end-to-end via the Playwright driver in `driver.mjs`. Setting | ||
| # both to false silences the `cargo build --tests` warning emitted on | ||
| # `cdylib` targets that have no `#[test]` items. | ||
| doctest = false | ||
| test = false | ||
|
|
||
| # Inherit workspace-level lint config (clippy denies, etc.). Required by | ||
| # `make workspace-check`. | ||
| [lints] | ||
| workspace = true | ||
|
|
||
| # `getrandom` is declared as a direct dep purely to enable the `wasm_js` | ||
| # Cargo feature on the transitive 0.4 version pulled in by `rand 0.10` | ||
| # (see comment on the dep itself for why this is necessary). No symbol | ||
| # from `getrandom` is named in our source, so cargo-shear flags it as | ||
| # unused — but removing it would silently re-break the wasm32 link. | ||
| [package.metadata.cargo-shear] | ||
| ignored = ["getrandom"] | ||
|
|
||
| [dependencies] | ||
| # Internal — track perf of the public hash primitives. We pull `miden-crypto` | ||
| # (not just `miden-field`) because the benches measure RPO/RPX/Poseidon2/ | ||
| # Blake3/Keccak end-to-end, which live in miden-crypto's `hash` module. | ||
| # `default-features = false` strips `concurrent` (rayon — won't build for | ||
| # wasm32-unknown-unknown without wasm-bindgen-rayon scaffolding); we keep | ||
| # `std` so `once_cell` uses real `std::sync` locks rather than falling | ||
| # back to `critical-section`. The latter would require a host-provided | ||
| # `_critical_section_*` impl that the browser doesn't supply, producing | ||
| # unresolved `import "env"` references at module load. | ||
| miden-crypto = { default-features = false, features = ["std"], path = "../miden-crypto" } | ||
|
|
||
| # Lifted STARK prover for the end-to-end synthetic-prove bench. `testing` | ||
| # pulls the LiftedBlake3Air fixture + the goldilocks_blake3 config helpers | ||
| # we use to assemble a small representative prove. We do NOT enable | ||
| # `parallel` — `p3-maybe-rayon` falls back to a serial impl on wasm32, and | ||
| # wasm-bindgen-rayon scaffolding is out of scope for the bench. | ||
| miden-lifted-stark = { features = ["testing"], workspace = true } | ||
|
|
||
| # Plonky3 building blocks: `Radix2DitParallel` is the DFT impl | ||
| # `miden-lifted-stark` expects (with `parallel` off it runs serially); | ||
| # `RowMajorMatrix` is the trace container; `Field` is needed to name the | ||
| # `<Felt as Field>::Packing` associated type used by the packed-perm | ||
| # benches; `Blake3Air::generate_trace_rows` produces the synthetic | ||
| # Blake3 AIR trace used by the end-to-end prove bench. | ||
| p3-blake3-air = { default-features = false, workspace = true } | ||
| p3-dft = { default-features = false, workspace = true } | ||
| p3-field = { default-features = false, workspace = true } | ||
| p3-matrix = { default-features = false, workspace = true } | ||
| p3-symmetric = { default-features = false, workspace = true } | ||
|
|
||
| # wasm-bindgen lets us export Rust fns to JS so the Playwright driver can | ||
| # invoke them from the bench page. `web-sys` exposes `performance.now()` | ||
| # for sub-µs timing. | ||
| wasm-bindgen = "0.2" | ||
| web-sys = { features = ["Performance", "Window"], version = "0.3" } | ||
|
|
||
| # `getrandom` 0.4 (pulled transitively via `rand 0.10 → getrandom`) needs | ||
| # the `wasm_js` feature on wasm32-unknown-unknown, otherwise its build | ||
| # fails with `cannot find function fill_inner in module backends`. Cargo | ||
| # unifies features within a major, but NOT across majors, so even though | ||
| # the transitive dep is 0.4 we have to declare 0.4 explicitly here to | ||
| # turn the feature on. (0.3 has the same feature, but it isn't in our | ||
| # tree and adding it wouldn't unify with the 0.4 transitive.) | ||
| getrandom = { features = ["wasm_js"], version = "0.4" } | ||
|
|
||
| # Random input data for the benches. Stable seeded RNG so PR-time runs | ||
| # and baseline runs use byte-identical inputs. | ||
| rand.workspace = true | ||
| rand_chacha.workspace = true |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.