Skip to content

Commit 132c650

Browse files
committed
Add --repeat to examples
1 parent bce5c61 commit 132c650

File tree

24 files changed

+1055
-352
lines changed

24 files changed

+1055
-352
lines changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
#!/usr/bin/env python3
2+
"""Generate benchmark report for GitHub Actions summary from JSON summaries."""
3+
4+
import argparse
5+
import json
6+
import urllib.parse
7+
from pathlib import Path
8+
from typing import Dict, List, Any
9+
10+
11+
def find_latest_all_examples_file(search_dir: Path = None) -> Path:
12+
"""Find the latest all-examples JSON file."""
13+
if search_dir is None:
14+
search_dir = Path("benchmark_summaries")
15+
16+
if not search_dir.exists():
17+
raise FileNotFoundError(f"Directory not found: {search_dir}")
18+
19+
all_summaries = list(search_dir.glob("all-examples-*.json"))
20+
if not all_summaries:
21+
raise FileNotFoundError(f"No all-examples-*.json files found in {search_dir}")
22+
23+
# Sort by modification time, newest first
24+
return max(all_summaries, key=lambda p: p.stat().st_mtime)
25+
26+
27+
def generate_report_from_json(
28+
json_file: Path,
29+
branch_path: str = "",
30+
perfetto_host: str = "https://perfetto.irreducible.com"
31+
) -> str:
32+
"""Generate complete benchmark report from a JSON summary file."""
33+
output = []
34+
35+
if not json_file.exists():
36+
output.append(f"JSON file not found: {json_file}")
37+
return "\n".join(output)
38+
39+
with open(json_file) as f:
40+
data = json.load(f)
41+
42+
if not data:
43+
output.append("No data in summary file.")
44+
return "\n".join(output)
45+
46+
# Auto-detect machine name from the data
47+
machine = data[0].get("machine", "") if data else ""
48+
if machine:
49+
output.append(f"# Benchmark Report for {machine}")
50+
output.append("")
51+
52+
# Generate metrics table
53+
output.append("## 📈 Benchmark Metrics")
54+
output.append("")
55+
56+
# Group by circuit
57+
circuits: Dict[str, List[Dict[str, Any]]] = {}
58+
for entry in data:
59+
circuit = entry.get("circuit", "unknown")
60+
if circuit not in circuits:
61+
circuits[circuit] = []
62+
circuits[circuit].append(entry)
63+
64+
for circuit in sorted(circuits.keys()):
65+
output.append(f"### {circuit}")
66+
67+
# Show parameters if available
68+
if params := circuits[circuit][0].get("parameters"):
69+
output.append(f"**Parameters:** {params}")
70+
71+
output.append("")
72+
output.append("| Config | Witness (ms) | Prove (ms) | Verify (ms) | Proof Size (bytes) |")
73+
output.append("|--------|--------------|------------|-------------|-------------------|")
74+
75+
# Sort for consistent display
76+
entries = sorted(circuits[circuit], key=lambda x: (
77+
x.get('threading', 'single'),
78+
not x.get('fusion', False)
79+
))
80+
81+
for entry in entries:
82+
config = entry.get('threading', 'single')
83+
if entry.get('fusion'):
84+
config += "-fusion"
85+
86+
witness = entry.get('avg_witness_ms', 0)
87+
prove = entry.get('avg_prove_ms', 0)
88+
verify = entry.get('avg_verify_ms', 0)
89+
proof_size = entry.get('avg_proof_size_bytes', 0)
90+
91+
output.append(f"| {config} | {witness:.2f} | {prove:.2f} | {verify:.2f} | {proof_size:.0f} |")
92+
93+
output.append("")
94+
95+
# Generate Perfetto trace links
96+
output.append("## 📊 Perfetto Traces")
97+
output.append("")
98+
99+
# Group by circuit again for trace links
100+
for circuit in sorted(circuits.keys()):
101+
output.append(f"### {circuit}")
102+
103+
if params := circuits[circuit][0].get("parameters"):
104+
output.append(f"**Parameters:** {params}")
105+
106+
output.append("")
107+
108+
for entry in sorted(circuits[circuit], key=lambda x: (
109+
x.get('threading', 'single'),
110+
not x.get('fusion', False)
111+
)):
112+
config = entry.get('threading', 'single')
113+
if entry.get('fusion'):
114+
config += "-fusion"
115+
116+
trace_files = entry.get('trace_files', [])
117+
if trace_files:
118+
links = []
119+
for i, trace_path in enumerate(trace_files, 1):
120+
# Build S3 URL from the trace path
121+
# trace_path already contains timestamp: sha256/20250903-092341-80a3f42/filename.perfetto-trace
122+
if branch_path:
123+
# For CI with S3 links
124+
s3_key = f"traces/monbijou/{branch_path}/{trace_path}"
125+
trace_url = f"{perfetto_host}/{s3_key}"
126+
encoded_url = urllib.parse.quote_plus(trace_url)
127+
perfetto_ui_url = f"{perfetto_host}/#!/?url={encoded_url}"
128+
links.append(f"[{i}]({perfetto_ui_url})")
129+
else:
130+
# For local testing, just show trace file names
131+
filename = Path(trace_path).name
132+
links.append(f"[{i}]({filename})")
133+
134+
if links:
135+
output.append(f"- **{config}**: {' '.join(links)}")
136+
137+
output.append("")
138+
139+
return "\n".join(output)
140+
141+
142+
def main():
143+
parser = argparse.ArgumentParser(description="Generate benchmark report from JSON summary file")
144+
parser.add_argument("--json-file", type=Path, default=None,
145+
help="Path to all-examples JSON file (auto-detects latest if not provided)")
146+
parser.add_argument("--summaries-dir", type=Path, default=Path("benchmark_summaries"),
147+
help="Directory to search for all-examples JSON files (default: benchmark_summaries)")
148+
parser.add_argument("--branch-path", type=str, default="",
149+
help="S3 branch path for Perfetto links (e.g., 'main' or 'branch-feature')")
150+
parser.add_argument("--perfetto-host", type=str, default="https://perfetto.irreducible.com",
151+
help="Perfetto host URL")
152+
parser.add_argument("--output", type=Path, default=None,
153+
help="Output file (default: stdout)")
154+
155+
args = parser.parse_args()
156+
157+
# Determine which JSON file to use
158+
if args.json_file:
159+
json_file = args.json_file
160+
print(f"DEBUG: Using specified JSON file: {json_file}")
161+
else:
162+
try:
163+
json_file = find_latest_all_examples_file(args.summaries_dir)
164+
print(f"DEBUG: Auto-detected JSON file: {json_file}")
165+
except FileNotFoundError as e:
166+
print(f"DEBUG: Error finding JSON file: {e}")
167+
return 1
168+
169+
# Generate the report
170+
try:
171+
report = generate_report_from_json(
172+
json_file,
173+
args.branch_path,
174+
args.perfetto_host
175+
)
176+
except Exception as e:
177+
print(f"Error generating report: {e}")
178+
return 1
179+
180+
# Output the report
181+
if args.output:
182+
args.output.write_text(report)
183+
print(f"Report written to {args.output}")
184+
else:
185+
print(report)
186+
187+
return 0
188+
189+
190+
if __name__ == "__main__":
191+
main()

.github/scripts/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Requirements for run_benchmarks.py
2+
perfetto>=0.14.0

.github/scripts/run_benchmarks

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/bin/bash
2+
# Wrapper script for run_benchmarks.py with automatic venv management
3+
4+
set -e # Exit on error
5+
6+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7+
VENV_DIR="$SCRIPT_DIR/.venv"
8+
REQUIREMENTS_FILE="$SCRIPT_DIR/requirements.txt"
9+
REQUIREMENTS_HASH_FILE="$VENV_DIR/.requirements_hash"
10+
PYTHON_SCRIPT="$SCRIPT_DIR/run_benchmarks.py"
11+
12+
# Colors for output
13+
RED='\033[0;31m'
14+
GREEN='\033[0;32m'
15+
YELLOW='\033[1;33m'
16+
NC='\033[0m' # No Color
17+
18+
# Function to calculate hash of requirements.txt
19+
get_requirements_hash() {
20+
if [[ -f "$REQUIREMENTS_FILE" ]]; then
21+
if command -v md5sum >/dev/null 2>&1; then
22+
md5sum "$REQUIREMENTS_FILE" | cut -d' ' -f1
23+
elif command -v md5 >/dev/null 2>&1; then
24+
md5 -q "$REQUIREMENTS_FILE"
25+
else
26+
# Fallback to modification time if no hash command available
27+
stat -f "%m" "$REQUIREMENTS_FILE" 2>/dev/null || stat -c "%Y" "$REQUIREMENTS_FILE" 2>/dev/null || echo "0"
28+
fi
29+
else
30+
echo "no_requirements"
31+
fi
32+
}
33+
34+
# Function to check if requirements have changed
35+
requirements_changed() {
36+
if [[ ! -f "$REQUIREMENTS_HASH_FILE" ]]; then
37+
return 0 # No hash file means requirements changed (or first run)
38+
fi
39+
40+
current_hash=$(get_requirements_hash)
41+
stored_hash=$(cat "$REQUIREMENTS_HASH_FILE" 2>/dev/null || echo "")
42+
43+
[[ "$current_hash" != "$stored_hash" ]]
44+
}
45+
46+
# Create venv if it doesn't exist
47+
if [[ ! -d "$VENV_DIR" ]]; then
48+
echo -e "${YELLOW}Creating Python virtual environment...${NC}"
49+
python3 -m venv "$VENV_DIR"
50+
echo -e "${GREEN}Virtual environment created.${NC}"
51+
fi
52+
53+
# Activate venv
54+
source "$VENV_DIR/bin/activate"
55+
56+
# Check if requirements have changed and install/update if needed
57+
if requirements_changed; then
58+
echo -e "${YELLOW}Requirements have changed. Installing/updating dependencies...${NC}"
59+
60+
# Upgrade pip first
61+
pip install --upgrade pip >/dev/null 2>&1
62+
63+
if [[ -f "$REQUIREMENTS_FILE" ]]; then
64+
pip install -r "$REQUIREMENTS_FILE"
65+
66+
# Store the new hash
67+
get_requirements_hash > "$REQUIREMENTS_HASH_FILE"
68+
echo -e "${GREEN}Dependencies installed successfully.${NC}"
69+
else
70+
echo -e "${YELLOW}No requirements.txt found. Skipping dependency installation.${NC}"
71+
fi
72+
else
73+
echo -e "${GREEN}Dependencies are up to date.${NC}"
74+
fi
75+
76+
# Run the Python script with all arguments passed to this wrapper
77+
echo -e "${GREEN}Running benchmark script...${NC}"
78+
echo "----------------------------------------"
79+
python "$PYTHON_SCRIPT" "$@"

0 commit comments

Comments
 (0)