Skip to content

Commit 8f7c257

Browse files
committed
ci: add engine benchmark workflow with schelk snapshot management
1 parent 13ae215 commit 8f7c257

2 files changed

Lines changed: 436 additions & 0 deletions

File tree

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
#!/usr/bin/env python3
2+
"""Parse reth-bench CSV output and generate a summary JSON + markdown comparison.
3+
4+
Usage:
5+
bench-engine-summary.py <combined_csv> <gas_csv> \
6+
--output-summary <summary.json> \
7+
--output-markdown <comment.md> \
8+
[--baseline <baseline.json>]
9+
10+
The baseline file defaults to /reth-bench/baseline.json if it exists.
11+
"""
12+
13+
import argparse
14+
import csv
15+
import json
16+
import os
17+
import sys
18+
from pathlib import Path
19+
20+
GIGAGAS = 1_000_000_000
21+
BASELINE_PATH = Path("/reth-bench/baseline.json")
22+
REGRESSION_THRESHOLD = 0.05 # 5%
23+
24+
25+
def parse_combined_csv(path: str) -> list[dict]:
26+
"""Parse combined_latency.csv into a list of per-block dicts."""
27+
rows = []
28+
with open(path) as f:
29+
reader = csv.DictReader(f)
30+
for row in reader:
31+
rows.append({
32+
"block_number": int(row["block_number"]),
33+
"gas_used": int(row["gas_used"]),
34+
"gas_limit": int(row["gas_limit"]),
35+
"transaction_count": int(row["transaction_count"]),
36+
"new_payload_latency_us": int(row["new_payload_latency"]),
37+
"fcu_latency_us": int(row["fcu_latency"]),
38+
"total_latency_us": int(row["total_latency"]),
39+
})
40+
return rows
41+
42+
43+
def parse_gas_csv(path: str) -> list[dict]:
44+
"""Parse total_gas.csv into a list of per-block dicts."""
45+
rows = []
46+
with open(path) as f:
47+
reader = csv.DictReader(f)
48+
for row in reader:
49+
rows.append({
50+
"block_number": int(row["block_number"]),
51+
"gas_used": int(row["gas_used"]),
52+
"time_us": int(row["time"]),
53+
})
54+
return rows
55+
56+
57+
def compute_summary(combined: list[dict], gas: list[dict]) -> dict:
58+
"""Compute aggregate metrics from parsed CSV data."""
59+
total_gas = sum(r["gas_used"] for r in combined)
60+
blocks = len(combined)
61+
62+
# Execution-only duration: sum of per-block total_latency
63+
exec_duration_us = sum(r["total_latency_us"] for r in combined)
64+
exec_duration_s = exec_duration_us / 1_000_000
65+
66+
# Wall-clock duration from gas CSV (last timestamp)
67+
wall_duration_us = gas[-1]["time_us"] if gas else exec_duration_us
68+
wall_duration_s = wall_duration_us / 1_000_000
69+
70+
# Per-block Ggas/s
71+
per_block_ggas = []
72+
for r in combined:
73+
lat_s = r["total_latency_us"] / 1_000_000
74+
if lat_s > 0:
75+
per_block_ggas.append(r["gas_used"] / lat_s / GIGAGAS)
76+
77+
avg_new_payload_ms = (
78+
sum(r["new_payload_latency_us"] for r in combined) / blocks / 1_000
79+
if blocks else 0
80+
)
81+
avg_fcu_ms = (
82+
sum(r["fcu_latency_us"] for r in combined) / blocks / 1_000
83+
if blocks else 0
84+
)
85+
86+
return {
87+
"blocks": blocks,
88+
"total_gas": total_gas,
89+
"wall_clock_s": round(wall_duration_s, 3),
90+
"execution_s": round(exec_duration_s, 3),
91+
"wall_clock_ggas_s": round(total_gas / wall_duration_s / GIGAGAS, 4) if wall_duration_s > 0 else 0,
92+
"execution_ggas_s": round(total_gas / exec_duration_s / GIGAGAS, 4) if exec_duration_s > 0 else 0,
93+
"avg_new_payload_ms": round(avg_new_payload_ms, 2),
94+
"avg_fcu_ms": round(avg_fcu_ms, 2),
95+
"median_block_ggas_s": round(sorted(per_block_ggas)[len(per_block_ggas) // 2], 4) if per_block_ggas else 0,
96+
"p10_block_ggas_s": round(sorted(per_block_ggas)[len(per_block_ggas) // 10], 4) if len(per_block_ggas) >= 10 else 0,
97+
}
98+
99+
100+
def format_change(current: float, baseline: float) -> str:
101+
"""Format a % change with arrow indicator."""
102+
if baseline == 0:
103+
return "N/A"
104+
pct = (current - baseline) / baseline * 100
105+
if abs(pct) < 0.5:
106+
return f"~0%"
107+
arrow = "🔺" if pct > 0 else "🔻"
108+
return f"{arrow} {pct:+.1f}%"
109+
110+
111+
def is_regression(current: float, baseline: float) -> bool:
112+
"""Check if a metric regressed beyond threshold (lower is worse for Ggas/s)."""
113+
if baseline == 0:
114+
return False
115+
return (baseline - current) / baseline > REGRESSION_THRESHOLD
116+
117+
118+
def generate_markdown(summary: dict, baseline: dict | None) -> str:
119+
"""Generate a markdown comment body comparing current vs baseline."""
120+
lines = ["## ⚡ Engine Benchmark Results", ""]
121+
122+
if baseline:
123+
has_regression = (
124+
is_regression(summary["execution_ggas_s"], baseline["execution_ggas_s"])
125+
or is_regression(summary["median_block_ggas_s"], baseline["median_block_ggas_s"])
126+
)
127+
128+
if has_regression:
129+
lines.append("> [!CAUTION]")
130+
lines.append(f"> Performance regression detected (>{REGRESSION_THRESHOLD*100:.0f}% drop in Ggas/s)")
131+
lines.append("")
132+
133+
lines.append("| Metric | This PR | main | Change |")
134+
lines.append("|--------|---------|------|--------|")
135+
136+
metrics = [
137+
("Execution Ggas/s", "execution_ggas_s", True),
138+
("Wall-clock Ggas/s", "wall_clock_ggas_s", True),
139+
("Median block Ggas/s", "median_block_ggas_s", True),
140+
("P10 block Ggas/s", "p10_block_ggas_s", True),
141+
("Avg newPayload (ms)", "avg_new_payload_ms", False),
142+
("Avg FCU (ms)", "avg_fcu_ms", False),
143+
]
144+
145+
for label, key, higher_is_better in metrics:
146+
cur = summary[key]
147+
base = baseline[key]
148+
if higher_is_better:
149+
change = format_change(cur, base)
150+
else:
151+
change = format_change(base, cur) if base != 0 else "N/A"
152+
lines.append(f"| {label} | {cur} | {base} | {change} |")
153+
154+
lines.append("")
155+
lines.append(f"Blocks: {summary['blocks']} | "
156+
f"Total gas: {summary['total_gas']:,} | "
157+
f"Execution time: {summary['execution_s']}s")
158+
else:
159+
lines.append("| Metric | Value |")
160+
lines.append("|--------|-------|")
161+
lines.append(f"| Execution Ggas/s | {summary['execution_ggas_s']} |")
162+
lines.append(f"| Wall-clock Ggas/s | {summary['wall_clock_ggas_s']} |")
163+
lines.append(f"| Median block Ggas/s | {summary['median_block_ggas_s']} |")
164+
lines.append(f"| P10 block Ggas/s | {summary['p10_block_ggas_s']} |")
165+
lines.append(f"| Avg newPayload (ms) | {summary['avg_new_payload_ms']} |")
166+
lines.append(f"| Avg FCU (ms) | {summary['avg_fcu_ms']} |")
167+
lines.append("")
168+
lines.append(f"Blocks: {summary['blocks']} | "
169+
f"Total gas: {summary['total_gas']:,} | "
170+
f"Execution time: {summary['execution_s']}s")
171+
lines.append("")
172+
lines.append("*No baseline found — first run on main will establish it.*")
173+
174+
return "\n".join(lines)
175+
176+
177+
def main():
178+
parser = argparse.ArgumentParser(description="Parse reth-bench results")
179+
parser.add_argument("combined_csv", help="Path to combined_latency.csv")
180+
parser.add_argument("gas_csv", help="Path to total_gas.csv")
181+
parser.add_argument("--output-summary", required=True, help="Output JSON summary path")
182+
parser.add_argument("--output-markdown", required=True, help="Output markdown path")
183+
parser.add_argument("--baseline", default=None, help="Baseline JSON path")
184+
args = parser.parse_args()
185+
186+
combined = parse_combined_csv(args.combined_csv)
187+
gas = parse_gas_csv(args.gas_csv)
188+
189+
if not combined:
190+
print("No results found in combined CSV", file=sys.stderr)
191+
sys.exit(1)
192+
193+
summary = compute_summary(combined, gas)
194+
195+
with open(args.output_summary, "w") as f:
196+
json.dump(summary, f, indent=2)
197+
print(f"Summary written to {args.output_summary}")
198+
199+
# Load baseline
200+
baseline = None
201+
baseline_path = Path(args.baseline) if args.baseline else BASELINE_PATH
202+
if baseline_path.exists():
203+
with open(baseline_path) as f:
204+
baseline = json.load(f)
205+
print(f"Loaded baseline from {baseline_path}")
206+
else:
207+
print(f"No baseline found at {baseline_path}")
208+
209+
markdown = generate_markdown(summary, baseline)
210+
211+
with open(args.output_markdown, "w") as f:
212+
f.write(markdown)
213+
print(f"Markdown written to {args.output_markdown}")
214+
215+
216+
if __name__ == "__main__":
217+
main()

0 commit comments

Comments
 (0)