From eae9233a9b9b3de369e6f2e0c47850c98b5d6596 Mon Sep 17 00:00:00 2001 From: Steve Klabnik Date: Sat, 27 Dec 2025 23:48:33 -0600 Subject: [PATCH] Add per-program breakdown charts to performance page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update bench.sh to capture per-pass timing data from compiler output - Generate individual breakdown SVGs for each benchmark program - Add program selector dropdown to performance page - Write metadata.json for dynamic chart loading 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- bench.sh | 37 ++++++++++++++-- scripts/generate-charts.py | 71 +++++++++++++++++++++++++----- website/templates/performance.html | 56 ++++++++++++++++++++++- 3 files changed, 149 insertions(+), 15 deletions(-) diff --git a/bench.sh b/bench.sh index 0c6c012d..614daa20 100755 --- a/bench.sh +++ b/bench.sh @@ -167,6 +167,7 @@ for i in "${!benchmark_names[@]}"; do # Run multiple iterations and collect timing data iteration_results=() + iteration_pass_data=() for ((iter=1; iter<=ITERATIONS; iter++)); do output_binary="$TEMP_DIR/bench_output_$$" @@ -180,6 +181,8 @@ for i in "${!benchmark_names[@]}"; do total_ms=$(echo "$timing_json" | grep -o '"total_ms":[0-9.]*' | head -1 | cut -d: -f2) if [[ -n "$total_ms" ]]; then iteration_results+=("$total_ms") + # Store full JSON for pass data extraction + iteration_pass_data+=("$timing_json") fi rm -f "$output_binary" @@ -190,7 +193,7 @@ for i in "${!benchmark_names[@]}"; do continue fi - # Calculate mean and stddev + # Calculate mean and stddev for total time sum=0 for val in "${iteration_results[@]}"; do sum=$(echo "$sum + $val" | bc -l) @@ -210,8 +213,36 @@ for i in "${!benchmark_names[@]}"; do log_info " $name: mean=${mean}ms, std=${stddev}ms (n=$count)" - # Store result for later - all_results+=("{\"name\":\"$name\",\"iterations\":$count,\"mean_ms\":$mean,\"std_ms\":$stddev}") + # Extract and aggregate per-pass timing data + # Use Python to parse JSON and compute per-pass means + pass_json=$(python3 -c " +import json +import sys + +pass_data = {} +for json_str in sys.argv[1:]: + try: + data = json.loads(json_str) + for p in data.get('passes', []): + name = p['name'] + duration = p['duration_ms'] + if name not in pass_data: + pass_data[name] = [] + pass_data[name].append(duration) + except: + pass + +# Calculate means +result = {} +for name, durations in pass_data.items(): + mean = sum(durations) / len(durations) if durations else 0 + result[name] = {'mean_ms': round(mean, 3)} + +print(json.dumps(result)) +" "${iteration_pass_data[@]}" 2>/dev/null || echo "{}") + + # Store result with pass data + all_results+=("{\"name\":\"$name\",\"iterations\":$count,\"mean_ms\":$mean,\"std_ms\":$stddev,\"passes\":$pass_json}") done # Get metadata diff --git a/scripts/generate-charts.py b/scripts/generate-charts.py index e5a8dc41..08abb77a 100755 --- a/scripts/generate-charts.py +++ b/scripts/generate-charts.py @@ -209,8 +209,34 @@ def scale_y(v: float) -> float: return "\n".join(svg_parts) -def generate_breakdown_chart(runs: list[dict]) -> str: - """Generate stacked bar chart showing time per compiler pass.""" +def get_benchmark_names(runs: list[dict]) -> list[str]: + """Get list of all benchmark names from runs.""" + names = set() + for run in runs: + for bench in run.get("benchmarks", []): + if "name" in bench: + names.add(bench["name"]) + return sorted(names) + + +def get_pass_times_for_benchmark(run: dict, benchmark_name: str) -> dict[str, float]: + """Extract pass timing for a specific benchmark from a run.""" + for bench in run.get("benchmarks", []): + if bench.get("name") == benchmark_name and "passes" in bench: + passes = bench["passes"] + return { + name: passes.get(name, {}).get("mean_ms", 0) + for name in PASS_ORDER + } + return {} + + +def generate_breakdown_chart(runs: list[dict], benchmark_name: Optional[str] = None) -> str: + """Generate stacked bar chart showing time per compiler pass. + + If benchmark_name is provided, shows data for that specific benchmark. + Otherwise, shows aggregate data across all benchmarks. + """ if not runs: return generate_empty_chart(BREAKDOWN_WIDTH, BREAKDOWN_HEIGHT, "No benchmark data available yet") @@ -218,7 +244,10 @@ def generate_breakdown_chart(runs: list[dict]) -> str: pass_times: Optional[dict[str, float]] = None commit = "" for run in reversed(runs): - pt = get_pass_times(run) + if benchmark_name: + pt = get_pass_times_for_benchmark(run, benchmark_name) + else: + pt = get_pass_times(run) if pt and any(v > 0 for v in pt.values()): pass_times = pt commit = short_commit(run.get("commit", "")) @@ -254,7 +283,7 @@ def generate_breakdown_chart(runs: list[dict]) -> str: } ''', f' ', - f' Compilation Time by Pass', + f' Compilation Time by Pass{" - " + escape_xml(benchmark_name) if benchmark_name else ""}', f' (commit: {escape_xml(commit)})', ] @@ -320,22 +349,44 @@ def main(): # Ensure output directory exists output_dir.mkdir(parents=True, exist_ok=True) - # Generate charts + # Generate timeline chart timeline_svg = generate_timeline_chart(runs) - breakdown_svg = generate_breakdown_chart(runs) - - # Write output files timeline_path = output_dir / "timeline.svg" - breakdown_path = output_dir / "breakdown.svg" - with open(timeline_path, "w") as f: f.write(timeline_svg) print(f"Generated {timeline_path}") + # Generate aggregate breakdown chart (for backwards compatibility) + breakdown_svg = generate_breakdown_chart(runs) + breakdown_path = output_dir / "breakdown.svg" with open(breakdown_path, "w") as f: f.write(breakdown_svg) print(f"Generated {breakdown_path}") + # Generate per-benchmark breakdown charts + benchmark_names = get_benchmark_names(runs) + print(f"Found {len(benchmark_names)} benchmarks: {', '.join(benchmark_names)}") + + for bench_name in benchmark_names: + bench_svg = generate_breakdown_chart(runs, bench_name) + # Use sanitized filename + safe_name = bench_name.replace(" ", "_").replace("/", "_") + bench_path = output_dir / f"breakdown_{safe_name}.svg" + with open(bench_path, "w") as f: + f.write(bench_svg) + print(f"Generated {bench_path}") + + # Write metadata JSON for the website to consume + metadata = { + "benchmarks": benchmark_names, + "run_count": len(runs), + "latest_commit": short_commit(runs[-1].get("commit", "")) if runs else None, + } + metadata_path = output_dir / "metadata.json" + with open(metadata_path, "w") as f: + json.dump(metadata, f, indent=2) + print(f"Generated {metadata_path}") + if __name__ == "__main__": main() diff --git a/website/templates/performance.html b/website/templates/performance.html index 29e94ae9..fea34deb 100644 --- a/website/templates/performance.html +++ b/website/templates/performance.html @@ -24,16 +24,68 @@

Compilation Time Trend

-

Time by Compiler Pass

+
+

Time by Compiler Pass

+
+ + +
+

Breakdown of where compilation time is spent in the most recent benchmark run.

-
+
{% set breakdown = load_data(path="static/benchmarks/breakdown.svg", format="plain") %} {{ breakdown | safe }}
+ +

Methodology