From 4e5306f7ff20cc0d516b1d8b6ca2503cbd36f68d Mon Sep 17 00:00:00 2001 From: Mark Henderson Date: Tue, 25 Aug 2026 09:44:46 -0400 Subject: [PATCH 1/2] ci: run zebra-consensus benches on zebra-touching commits Path-gated informational job (not a required check): runs the script-cache and UTXO-lookup criterion benches on PRs and main pushes that touch the benched crates, and posts the timings to the step summary. Bench steps are guarded on their bench files existing, so this lands before #45/#46 and activates as they merge. rust-cache persists target/criterion, so criterion also reports a change estimate against the previous cached run. Co-Authored-By: Claude Fable 5 --- .github/workflows/zebra-bench.yml | 141 ++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 .github/workflows/zebra-bench.yml diff --git a/.github/workflows/zebra-bench.yml b/.github/workflows/zebra-bench.yml new file mode 100644 index 00000000..8c97cafe --- /dev/null +++ b/.github/workflows/zebra-bench.yml @@ -0,0 +1,141 @@ +name: Zebra consensus benchmarks + +# Runs the zebra-consensus criterion benches on every zebra-touching commit: +# the transparent script cache bench (benches/script.rs) and the block-path +# UTXO lookup bench (benches/utxo_lookup.rs). Informational, not a required +# check: absolute times on shared runners are noisy, but the ratios the +# benches exist to watch (cache miss vs hit, serial vs overlapped lookups) +# are measured within one run on one box, and an order-of-magnitude +# regression is visible through the noise. +# +# Each bench step is guarded on its bench file existing, so this workflow can +# land before the PRs that add the benches and activates as they merge. + +on: + # No `paths:` filter, for the reasons written up at the top of z3-smoke.yml; + # the `changes` job below is the equivalent gate. + pull_request: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +jobs: + changes: + name: Path gate + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + relevant: ${{ steps.gate.outputs.relevant }} + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + # Depth 2 diffs the PR merge commit (or the pushed merge) against + # what it landed on. + fetch-depth: 2 + + - name: Decide whether this change can move the benchmarks + id: gate + env: + EVENT_NAME: ${{ github.event_name }} + run: | + set -euo pipefail + + # A manual dispatch is an explicit request; only `push` and + # `pull_request` go through the diff gate. + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + echo "relevant=true" >> "$GITHUB_OUTPUT" + echo "Manual dispatch: benching unconditionally." >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + specs=( + ':(glob)zebra/zebra-consensus/**' + ':(glob)zebra/zebra-chain/**' + ':(glob)zebra/zebra-script/**' + ':(glob)zebra/Cargo.toml' + ':(glob)zebra/Cargo.lock' + ':(glob).github/workflows/zebra-bench.yml' + ) + + # Fail open: "we could not tell" must never silently mean "no CI". + if ! git diff --name-only HEAD^1 HEAD > "$RUNNER_TEMP/changed" 2>/dev/null; then + echo "relevant=true" >> "$GITHUB_OUTPUT" + echo "::warning::could not diff against the parent; benching unconditionally" + exit 0 + fi + git diff --name-only HEAD^1 HEAD -- "${specs[@]}" > "$RUNNER_TEMP/matched" + + total=$(( $(wc -l < "$RUNNER_TEMP/changed") )) + hits=$(( $(wc -l < "$RUNNER_TEMP/matched") )) + if [ "$hits" -gt 0 ]; then relevant=true; else relevant=false; fi + echo "relevant=$relevant" >> "$GITHUB_OUTPUT" + + { + echo "### zebra bench path gate" + echo + echo "\`$total\` file(s) changed, \`$hits\` of them reaching the benched crates." + } >> "$GITHUB_STEP_SUMMARY" + + bench: + name: zebra-consensus benches + needs: changes + if: needs.changes.outputs.relevant == 'true' + runs-on: ubuntu-latest + # First uncached run builds the zebra workspace's bench profile (~40 min); + # cached runs are dominated by the benches themselves (~10 min). + timeout-minutes: 75 + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 1 + + # rustup resolves zebra's rust-toolchain.toml on first use. This cache + # namespace stays separate from z3-regtest's (different artifact sets), + # and only main saves it: per-PR saves of a multi-GB target tree would + # push the repo past the Actions cache quota and evict the smoke and + # regtest caches. PR runs restore main's entry. + - name: Rust build cache + uses: Swatinem/rust-cache@v2 + with: + prefix-key: v1-bench + save-if: ${{ github.ref == 'refs/heads/main' }} + workspaces: | + zebra -> target + + - name: Script cache bench + working-directory: zebra + run: | + set -euo pipefail + if [ ! -f zebra-consensus/benches/script.rs ]; then + echo "benches/script.rs not on this branch yet; skipping" >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + cargo bench -p zebra-consensus --bench script 2>&1 | tee "$RUNNER_TEMP/script.out" + { + echo "### script bench" + echo '```' + grep -B1 -A2 "time:" "$RUNNER_TEMP/script.out" || echo "no timings parsed" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + - name: UTXO lookup bench + working-directory: zebra + run: | + set -euo pipefail + if [ ! -f zebra-consensus/benches/utxo_lookup.rs ]; then + echo "benches/utxo_lookup.rs not on this branch yet; skipping" >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + cargo bench -p zebra-consensus --bench utxo_lookup 2>&1 | tee "$RUNNER_TEMP/utxo.out" + { + echo "### utxo_lookup bench" + echo '```' + grep -B1 -A2 "time:" "$RUNNER_TEMP/utxo.out" || echo "no timings parsed" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" From 784071fb4fb4c253b83dfb33a26a0f00c3b73796 Mon Sep 17 00:00:00 2001 From: Mark Henderson Date: Wed, 26 Aug 2026 09:33:31 -0400 Subject: [PATCH 2/2] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/zebra-bench.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/zebra-bench.yml b/.github/workflows/zebra-bench.yml index 8c97cafe..a7b39d7d 100644 --- a/.github/workflows/zebra-bench.yml +++ b/.github/workflows/zebra-bench.yml @@ -63,12 +63,21 @@ jobs: ) # Fail open: "we could not tell" must never silently mean "no CI". - if ! git diff --name-only HEAD^1 HEAD > "$RUNNER_TEMP/changed" 2>/dev/null; then + base="HEAD^1" + if [ "$EVENT_NAME" = "push" ]; then + before=$(jq -r '.before // empty' "$GITHUB_EVENT_PATH" 2>/dev/null || true) + if [ -n "$before" ] && [ "$before" != "0000000000000000000000000000000000000000" ]; then + base="$before" + fi + fi + + if ! git diff --name-only "$base" HEAD > "$RUNNER_TEMP/changed" 2>/dev/null; then echo "relevant=true" >> "$GITHUB_OUTPUT" - echo "::warning::could not diff against the parent; benching unconditionally" + echo "::warning::could not diff against the base; benching unconditionally" + echo "Could not diff against the base; benching unconditionally." >> "$GITHUB_STEP_SUMMARY" exit 0 fi - git diff --name-only HEAD^1 HEAD -- "${specs[@]}" > "$RUNNER_TEMP/matched" + git diff --name-only "$base" HEAD -- "${specs[@]}" > "$RUNNER_TEMP/matched" total=$(( $(wc -l < "$RUNNER_TEMP/changed") )) hits=$(( $(wc -l < "$RUNNER_TEMP/matched") ))