Skip to content
This repository was archived by the owner on Aug 7, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 206 additions & 0 deletions .github/workflows/bench.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
# Performance regression tracking for miden-crypto.
#
# 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 feed `benchmark-action/github-action-benchmark`, which:
# - Stores each metric over time on the `gh-pages` branch under
# `bench/` (separate from the existing `docs/` subdir used by docs.yml,
# so the two don't collide).
# - Posts a sticky PR comment showing the diff vs the latest `next`
# baseline. Header per job, so wasm and native each get their own
# comment that updates in place.
# - Alerts (writes a comment header) on regression > 15 %. Doesn't fail
# the workflow — GHA Linux runners share CPUs and the noise floor on
# hash benches is real. False-positive alerts are cheap to ignore;
# false-negative reverts are expensive.
#
# This is deliberately one workflow with two jobs (rather than two
# separate workflow files): both produce sticky comments to the same PR,
# both use the same gh-pages branch, both share the same triage
# discipline. Putting them next to each other in one file keeps the
# regression-tracking story coherent.

name: bench

on:
pull_request:
push:
branches: [next]
workflow_dispatch:

permissions:
contents: write # github-action-benchmark needs to push to gh-pages
pull-requests: write # for sticky PR comment

# 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
steps:
- uses: actions/checkout@v4
Comment thread
WiktorStarczewski marked this conversation as resolved.
Outdated

- name: Install Rust + wasm32 target
run: |
rustup update --no-self-update
rustup target add wasm32-unknown-unknown

- name: Install wasm-pack
# Pinned major (`v0.13`) — wasm-pack pre-compiled binaries break
# at minor bumps occasionally. Bump deliberately, never floating.
uses: jetli/wasm-pack-action@v0.4.0
Comment thread
WiktorStarczewski marked this conversation as resolved.
Outdated
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
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
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: |
node driver.mjs > results.json
echo "results=$(cat results.json | wc -l) 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: Upload raw results as artifact
# Keeps the full per-batch sample distribution + driver stderr
# for post-hoc analysis when an alert fires. The
# github-action-benchmark store only retains the median.
uses: actions/upload-artifact@v4
with:
name: bench-wasm-results
path: miden-bench-wasm/results.json
retention-days: 30

- name: Track + alert on regression
uses: benchmark-action/github-action-benchmark@v1
with:
tool: customSmallerIsBetter
output-file-path: miden-bench-wasm/results.json
# Separate gh-pages subdir so this never collides with docs.yml's
# `destination_dir: docs` deploy (which lives at /docs/).
benchmark-data-dir-path: bench/wasm
gh-pages-branch: gh-pages
# Push baseline updates ONLY on push to next (not on PR runs —
# those compare against the existing baseline, they don't update it).
auto-push: ${{ github.event_name == 'push' }}
comment-on-alert: true
# Regression alert at 10 %. Run-to-run noise on GHA shared
# runners is real but accepting 15-20 % headroom would defeat
# the purpose: a 15 % regression IS a regression worth
# investigating, not the noise floor. Drive variance down
# (longer batches, more samples) to fit under this threshold
# rather than widening the threshold to fit measured noise.
# See README "Noise reduction" for the roadmap to tighten further.
alert-threshold: "110%"
fail-on-alert: false # warn via comment, don't fail the PR
summary-always: true # keep the diff comment fresh on each run
comment-always: ${{ github.event_name == 'pull_request' }}
alert-comment-cc-users: '@WiktorStarczewski'
github-token: ${{ secrets.GITHUB_TOKEN }}

bench-native:
name: Native perf (Linux x86_64)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4

- 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: |
cargo bench --bench hash --bench word --bench transpose \

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add set -o pipefail here before the pipeline? Right now a failed cargo bench compile or run can be hidden because tee exits successfully, and then the job can upload/publish a bad bench-native.txt.

-p miden-crypto -- --output-format bencher 2>&1 \
| tee bench-native.txt

- name: Upload raw results as artifact
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
uses: actions/upload-artifact@v4
with:
name: bench-native-results
path: bench-native.txt
retention-days: 30

- name: Track + alert on regression
uses: benchmark-action/github-action-benchmark@v1
with:
tool: cargo
output-file-path: bench-native.txt
benchmark-data-dir-path: bench/native
gh-pages-branch: gh-pages
auto-push: ${{ github.event_name == 'push' }}
comment-on-alert: true
# Same 10 % threshold as wasm — native benches are ns-scale and
# timer noise is proportionally larger, but criterion's own
# statistical pruning already handles much of the per-iteration
# variance. If 10 % is too tight in practice, the path forward
# is more iterations / `iai-callgrind` instruction-count
# benchmarking, NOT a wider threshold.
alert-threshold: "110%"
fail-on-alert: false
summary-always: true
comment-always: ${{ github.event_name == 'pull_request' }}
alert-comment-cc-users: '@WiktorStarczewski'
github-token: ${{ secrets.GITHUB_TOKEN }}
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ cmake-build-*

# Proptest
tests.txt

# wasm-pack build output for miden-bench-wasm
miden-bench-wasm/static/pkg/
miden-bench-wasm/node_modules/
miden-bench-wasm/results.json
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
exclude = ["miden-crypto-fuzz"]
members = [
"miden-bench",
"miden-bench-wasm",
Comment thread
WiktorStarczewski marked this conversation as resolved.
"miden-crypto",
"miden-crypto-derive",
"miden-field",
Expand Down
84 changes: 84 additions & 0 deletions miden-bench-wasm/Cargo.toml
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
Loading
Loading