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 ()
0 commit comments