Skip to content

Commit 136373b

Browse files
authored
Merge pull request #27 from arnoox/performance-metrics
Performance metrics
2 parents 222cfc4 + 56f38ec commit 136373b

8 files changed

Lines changed: 753 additions & 1 deletion

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@ docs/.ub_cache/
3030

3131
# Temporary files
3232
*.tmp
33-
*.bak
33+
*.bak
34+
*.pyc

crates/herkos-tests/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ herkos-core = { version = "0.2.0", path = "../herkos-core" }
1818
[dev-dependencies]
1919
criterion = "0.8.2"
2020

21+
[features]
22+
baseline_benches = []
23+
2124
[[bench]]
2225
name = "herkos_runtime_benchmark"
2326
harness = false

crates/herkos-tests/benches/herkos_runtime_benchmark.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ fn fibo_5_wasm_bench(c: &mut Criterion) {
99
});
1010
}
1111

12+
#[cfg(feature = "baseline_benches")]
1213
fn fibo_5_orig_bench(c: &mut Criterion) {
1314
c.bench_function("fib 5 plain rust", |b| b.iter(|| fibo_orig(black_box(5))));
1415
}
@@ -20,6 +21,7 @@ fn fibo_20_wasm_bench(c: &mut Criterion) {
2021
});
2122
}
2223

24+
#[cfg(feature = "baseline_benches")]
2325
fn fibo_20_orig_bench(c: &mut Criterion) {
2426
c.bench_function("fib 20 plain rust", |b| b.iter(|| fibo_orig(black_box(20))));
2527
}
@@ -33,6 +35,7 @@ fn memsort_100_wasm_bench(c: &mut Criterion) {
3335
});
3436
}
3537

38+
#[cfg(feature = "baseline_benches")]
3639
fn memsort_100_orig_bench(c: &mut Criterion) {
3740
c.bench_function("memsort 100 plain rust", |b| {
3841
b.iter(|| mem_fill_sort_sum_orig(black_box(100), black_box(42)))
@@ -48,6 +51,7 @@ fn collatz_27_wasm_bench(c: &mut Criterion) {
4851
});
4952
}
5053

54+
#[cfg(feature = "baseline_benches")]
5155
fn collatz_27_orig_bench(c: &mut Criterion) {
5256
c.bench_function("collatz 27 plain rust", |b| {
5357
b.iter(|| collatz_steps_orig(black_box(27)))
@@ -61,6 +65,7 @@ fn collatz_871_wasm_bench(c: &mut Criterion) {
6165
});
6266
}
6367

68+
#[cfg(feature = "baseline_benches")]
6469
fn collatz_871_orig_bench(c: &mut Criterion) {
6570
c.bench_function("collatz 871 plain rust", |b| {
6671
b.iter(|| collatz_steps_orig(black_box(871)))
@@ -76,6 +81,7 @@ fn isqrt_wasm_bench(c: &mut Criterion) {
7681
});
7782
}
7883

84+
#[cfg(feature = "baseline_benches")]
7985
fn isqrt_orig_bench(c: &mut Criterion) {
8086
c.bench_function("isqrt 1000000 plain rust", |b| {
8187
b.iter(|| isqrt_orig(black_box(1_000_000)))
@@ -89,6 +95,7 @@ fn gcd_wasm_bench(c: &mut Criterion) {
8995
});
9096
}
9197

98+
#[cfg(feature = "baseline_benches")]
9299
fn gcd_orig_bench(c: &mut Criterion) {
93100
c.bench_function("gcd(46368,28657) plain rust", |b| {
94101
b.iter(|| gcd_orig(black_box(46368), black_box(28657)))
@@ -104,6 +111,7 @@ fn popcount_wasm_bench(c: &mut Criterion) {
104111
});
105112
}
106113

114+
#[cfg(feature = "baseline_benches")]
107115
fn popcount_orig_bench(c: &mut Criterion) {
108116
c.bench_function("popcount 0xDEADBEEF plain rust", |b| {
109117
b.iter(|| popcount_orig(black_box(0xDEADBEEFu32 as i32)))
@@ -119,12 +127,35 @@ fn sum_recursive_100_wasm_bench(c: &mut Criterion) {
119127
});
120128
}
121129

130+
#[cfg(feature = "baseline_benches")]
122131
fn sum_recursive_100_orig_bench(c: &mut Criterion) {
123132
c.bench_function("sum_recursive 100 plain rust", |b| {
124133
b.iter(|| sum_recursive_orig(black_box(100)))
125134
});
126135
}
127136

137+
#[cfg(not(feature = "baseline_benches"))]
138+
criterion_group!(
139+
benches,
140+
// Fibonacci (pure computation)
141+
fibo_5_wasm_bench,
142+
fibo_20_wasm_bench,
143+
// Memory-intensive (bounds-checking overhead)
144+
memsort_100_wasm_bench,
145+
// Branchy control flow (division + conditionals)
146+
collatz_27_wasm_bench,
147+
collatz_871_wasm_bench,
148+
// Binary search loop (multiplication + comparison)
149+
isqrt_wasm_bench,
150+
// Euclidean algorithm (modular arithmetic loop)
151+
gcd_wasm_bench,
152+
// Bitwise tight loop
153+
popcount_wasm_bench,
154+
// Recursive function call overhead
155+
sum_recursive_100_wasm_bench,
156+
);
157+
158+
#[cfg(feature = "baseline_benches")]
128159
criterion_group!(
129160
benches,
130161
// Fibonacci (pure computation)
@@ -153,4 +184,5 @@ criterion_group!(
153184
sum_recursive_100_wasm_bench,
154185
sum_recursive_100_orig_bench,
155186
);
187+
156188
criterion_main!(benches);

scripts/bench_record.sh

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
COMMENT="${1:-}"
5+
6+
# Ensure we're in the repo root
7+
if [ ! -f Cargo.toml ]; then
8+
echo "ERROR: Cargo.toml not found. Please run this script from the repo root."
9+
exit 1
10+
fi
11+
12+
echo "=== herkos benchmark recorder ==="
13+
echo "Branch: $(git rev-parse --abbrev-ref HEAD)"
14+
echo "Commit: $(git rev-parse --short HEAD)"
15+
[ -n "$COMMENT" ] && echo "Comment: $COMMENT"
16+
echo
17+
18+
# Clean up stale temp files to avoid duplication
19+
rm -f /tmp/new_bench_rows.csv
20+
21+
# Run benchmarks
22+
echo "Running benchmarks (HERKOS_OPTIMIZE=1)..."
23+
HERKOS_OPTIMIZE=1 cargo bench -p herkos-tests
24+
25+
echo
26+
echo "Collecting results..."
27+
28+
# Capture metadata
29+
export BRANCH=$(git rev-parse --abbrev-ref HEAD)
30+
export COMMIT_SHA=$(git rev-parse HEAD)
31+
export TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
32+
33+
# Parse Criterion output to CSV
34+
python3 scripts/collect_bench.py target/criterion /tmp/new_bench_rows.csv ${COMMENT:+--comment "$COMMENT"}
35+
36+
echo
37+
echo "Pushing to metrics branch..."
38+
39+
# Configure git if not already done
40+
git config user.email "bench@local" || true
41+
git config user.name "bench" || true
42+
43+
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
44+
45+
# Fetch or create metrics branch
46+
if git fetch origin metrics 2>/dev/null; then
47+
git checkout metrics
48+
else
49+
echo "Creating orphan metrics branch..."
50+
git checkout --orphan metrics
51+
git rm -rf . --quiet 2>/dev/null || true
52+
printf '# metrics branch\n\nManual benchmark history for herkos.\n\nSee bench_history.csv for data.\n' > README.md
53+
git add README.md
54+
git commit -m "chore: init metrics branch"
55+
fi
56+
57+
# Append rows to bench_history.csv
58+
if [ -f bench_history.csv ]; then
59+
# Skip header row when appending
60+
tail -n +2 /tmp/new_bench_rows.csv >> bench_history.csv
61+
else
62+
# First time: copy entire file with header
63+
cp /tmp/new_bench_rows.csv bench_history.csv
64+
fi
65+
66+
git add bench_history.csv
67+
if ! git diff --cached --quiet; then
68+
git commit -m "bench: results for ${COMMIT_SHA:0:12}"
69+
git push origin metrics
70+
else
71+
echo "No changes to bench_history.csv (likely zero benchmarks found)"
72+
fi
73+
74+
# Return to original branch
75+
git checkout "$CURRENT_BRANCH"
76+
77+
echo "✓ Done. Pushed to metrics branch."

scripts/collect_bench.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env python3
2+
"""
3+
collect_bench.py <criterion_dir> <output_csv> [--comment "text"]
4+
5+
Walks Criterion JSON output, extracts timing statistics, and appends
6+
enriched rows to a CSV file. Creates the file with header on first run.
7+
8+
Environment variables consumed:
9+
BRANCH - git branch name
10+
COMMIT_SHA - full commit SHA
11+
TIMESTAMP - ISO-8601 timestamp
12+
"""
13+
14+
import sys
15+
import os
16+
import json
17+
import csv
18+
import pathlib
19+
import argparse
20+
21+
FIELDS = [
22+
"timestamp", "branch", "commit_sha",
23+
"benchmark_name",
24+
"mean_ns", "stddev_ns", "median_ns", "lower_ns", "upper_ns",
25+
"comments",
26+
]
27+
28+
29+
def parse_estimates(path: pathlib.Path) -> dict:
30+
"""Extract timing statistics from a Criterion estimates.json file."""
31+
with path.open() as f:
32+
data = json.load(f)
33+
return {
34+
"mean_ns": data["mean"]["point_estimate"],
35+
"stddev_ns": data["std_dev"]["point_estimate"],
36+
"median_ns": data["median"]["point_estimate"],
37+
"lower_ns": data["mean"]["confidence_interval"]["lower_bound"],
38+
"upper_ns": data["mean"]["confidence_interval"]["upper_bound"],
39+
}
40+
41+
42+
def collect_rows(criterion_dir: pathlib.Path, metadata: dict) -> list[dict]:
43+
"""Walk criterion_dir and build one row per benchmark."""
44+
rows = []
45+
for estimates_path in sorted(criterion_dir.rglob("new/estimates.json")):
46+
# Path: <criterion_dir>/<group>/<bench_name>/new/estimates.json
47+
bench_name = estimates_path.parent.parent.name
48+
try:
49+
stats = parse_estimates(estimates_path)
50+
except (KeyError, json.JSONDecodeError) as e:
51+
print(f"WARNING: skipping {estimates_path}: {e}", file=sys.stderr)
52+
continue
53+
rows.append({**metadata, "benchmark_name": bench_name, **stats})
54+
return rows
55+
56+
57+
def write_csv(output_path: pathlib.Path, rows: list[dict]) -> None:
58+
"""Append rows to CSV, writing header only if file is new."""
59+
file_exists = output_path.exists() and output_path.stat().st_size > 0
60+
with output_path.open("a", newline="") as f:
61+
writer = csv.DictWriter(f, fieldnames=FIELDS)
62+
if not file_exists:
63+
writer.writeheader()
64+
writer.writerows(rows)
65+
66+
67+
def main() -> int:
68+
parser = argparse.ArgumentParser(
69+
description="Parse Criterion benchmark results into CSV rows"
70+
)
71+
parser.add_argument("criterion_dir", help="Root Criterion output directory")
72+
parser.add_argument("output_csv", help="Output CSV file path")
73+
parser.add_argument(
74+
"--comment",
75+
default="",
76+
help="Optional comment to append to each row"
77+
)
78+
79+
args = parser.parse_args()
80+
81+
criterion_dir = pathlib.Path(args.criterion_dir)
82+
output_csv = pathlib.Path(args.output_csv)
83+
84+
if not criterion_dir.is_dir():
85+
print(f"ERROR: criterion directory not found: {criterion_dir}", file=sys.stderr)
86+
return 1
87+
88+
metadata = {
89+
"timestamp": os.environ.get("TIMESTAMP", ""),
90+
"branch": os.environ.get("BRANCH", ""),
91+
"commit_sha": os.environ.get("COMMIT_SHA", ""),
92+
"comments": args.comment,
93+
}
94+
95+
rows = collect_rows(criterion_dir, metadata)
96+
97+
if not rows:
98+
print("WARNING: no benchmark results found in criterion directory", file=sys.stderr)
99+
return 0
100+
101+
write_csv(output_csv, rows)
102+
print(f"Wrote {len(rows)} rows to {output_csv}")
103+
for r in rows:
104+
print(f" {r['benchmark_name']}: mean={r['mean_ns']:.1f}ns")
105+
return 0
106+
107+
108+
if __name__ == "__main__":
109+
sys.exit(main())

0 commit comments

Comments
 (0)