Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
50 changes: 50 additions & 0 deletions .github/workflows/cargo-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,53 @@ jobs:
uses: ./.github/actions/codecov
with:
token: ${{ secrets.CODECOV_TOKEN }}

benchmark:
name: Run benchmarks
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write # needed to push the updated baseline back to the repo
pull-requests: write # needed to post a comment when an alert is triggered
steps:
- uses: actions/checkout@v7
- uses: actions-rust-lang/setup-rust-toolchain@v1

- name: Run cargo bench
shell: bash
run: cargo bench --features bench --no-fail-fast

- name: Convert Criterion results to JSON
run: python3 benchmark/convert_criterion_output.py

- name: Store benchmark result
uses: benchmark-action/github-action-benchmark@v1
with:
tool: "customSmallerIsBetter"
output-file-path: benchmark-results.json
external-data-json-path: ./benchmark/benchmark-data.json
# Fail the workflow if any benchmark regresses by more than 30% relative
# to the stored baseline. CI runners have variable load, so a threshold
# that is too tight will cause spurious failures; 130% is a reasonable
# starting point and can be tightened once baseline stability is known.
alert-threshold: "130%"
fail-on-alert: true
# GitHub API token to make a commit comment
github-token: ${{ secrets.GITHUB_TOKEN }}
# Post a comment on the commit when an alert is triggered, listing which
# benchmarks regressed and by how much.
comment-on-alert: true
# Always write a job summary table, not only when a regression is found.
summary-always: true

- name: Commit updated baseline
# Only update the stored baseline when code is merged to main. On pull
# requests the job reads the baseline for comparison but must not overwrite
# it, since the PR branch has not yet been reviewed and merged.
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: |
git config user.name "GitHub Actions"
git config user.email "github-actions@github.com"
git add benchmark/benchmark-data.json
git diff --staged --quiet || git commit -m "chore: update benchmark baseline [skip ci]"
git push
39 changes: 39 additions & 0 deletions benchmark/convert_criterion_output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Convert Criterion benchmark results to the JSON format expected by
github-action-benchmark (customSmallerIsBetter).

Criterion writes one estimates.json file per benchmark under:
target/criterion/<group>/<bench_id>/new/estimates.json

where any '/' in a group name is sanitised to '_' by Criterion when creating
directory names. The benchmark name used for tracking is derived by stripping
the leading target/criterion/ prefix and the trailing /new/estimates.json
suffix, then joining the remaining path components with '/'.

The output is written to benchmark-results.json in the repository root, in the
format:
[{"name": "<name>", "value": <median_ns>, "unit": "ns"}, ...]

All paths are resolved relative to the repository root (the parent directory of
the directory containing this script), so the script can be invoked from any
working directory.
"""

import json
from pathlib import Path

repo_root = Path(__file__).parent.parent
criterion_dir = repo_root / "target" / "criterion"
output_file = repo_root / "benchmark-results.json"

results = []
for estimates_file in sorted(criterion_dir.rglob("new/estimates.json")):
parts = estimates_file.relative_to(criterion_dir).parts[:-2]
name = "/".join(parts)
data = json.loads(estimates_file.read_text())
value = data["median"]["point_estimate"]
results.append({"name": name, "value": value, "unit": "ns"})

output_file.write_text(json.dumps(results, indent=2))
print(f"Converted {len(results)} Criterion benchmark results to {output_file}")
for r in results:
print(f" {r['name']}: {r['value']:.0f} ns")
Loading