diff --git a/.github/workflows/zebra-bench.yml b/.github/workflows/zebra-bench.yml new file mode 100644 index 00000000..a7b39d7d --- /dev/null +++ b/.github/workflows/zebra-bench.yml @@ -0,0 +1,150 @@ +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". + 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 base; benching unconditionally" + echo "Could not diff against the base; benching unconditionally." >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + git diff --name-only "$base" 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"