Coverage for src / rhiza_tools / commands / analyze_benchmarks.py: 100%
46 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-23 01:10 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-23 01:10 +0000
1"""Command to analyze pytest-benchmark results and visualize them.
3This module reads a local ``benchmarks.json`` file produced by pytest-benchmark,
4prints a reduced table with benchmark name, mean milliseconds, and operations
5per second, and renders an interactive Plotly bar chart of mean runtimes.
7Note: This command requires pandas and plotly, which are available in the dev
8dependency group. Install with: uv pip install -e ".[dev]"
10Example:
11 Analyze benchmarks with default path::
13 from rhiza_tools.commands.analyze_benchmarks import analyze_benchmarks_command
14 analyze_benchmarks_command()
16 Analyze benchmarks with custom path::
18 from pathlib import Path
19 analyze_benchmarks_command(benchmarks_json=Path("custom/benchmarks.json"))
20"""
22# /// script
23# dependencies = [
24# "pandas",
25# "plotly",
26# ]
27# ///
29import json
30import sys
31from pathlib import Path
33from rhiza_tools import console
36class BenchmarkError(Exception):
37 """Base exception for benchmark analysis errors."""
40def analyze_benchmarks_command(
41 benchmarks_json: Path | None = None,
42 output_html: Path | None = None,
43) -> None:
44 """Analyze pytest-benchmark results and visualize them.
46 This command reads a benchmarks.json file produced by pytest-benchmark,
47 prints a reduced table with benchmark name, mean milliseconds, and operations
48 per second, and renders an interactive Plotly bar chart of mean runtimes.
50 Args:
51 benchmarks_json: Path to the benchmarks.json file. Defaults to _benchmarks/benchmarks.json.
52 output_html: Path to save the HTML visualization. Defaults to _benchmarks/benchmarks.html.
54 Raises:
55 SystemExit: If benchmarks.json is missing, invalid, or has no valid benchmarks.
57 Example:
58 Analyze benchmarks with default paths::
60 analyze_benchmarks_command()
62 Use custom paths::
64 analyze_benchmarks_command(
65 benchmarks_json=Path("tests/benchmarks.json"),
66 output_html=Path("reports/benchmarks.html")
67 )
68 """
69 # Import pandas and plotly here to avoid requiring them as hard dependencies
70 try:
71 import pandas as pd
72 import plotly.express as px
73 except ImportError:
74 console.error(
75 "pandas and plotly are required for this command. "
76 "Install them with: uv pip install -e '.[dev]' or pip install 'rhiza-tools[dev]'"
77 )
78 sys.exit(1)
80 # Set default paths
81 if benchmarks_json is None:
82 benchmarks_json = Path("_benchmarks/benchmarks.json")
84 if output_html is None:
85 output_html = Path("_benchmarks/benchmarks.html")
87 # Check if the file exists
88 if not benchmarks_json.exists():
89 console.warning(f"benchmarks.json not found at {benchmarks_json}; skipping analysis and exiting successfully.")
90 sys.exit(0)
92 # Load pytest-benchmark JSON
93 try:
94 with benchmarks_json.open() as f:
95 data = json.load(f)
96 except json.JSONDecodeError:
97 console.warning(
98 f"benchmarks.json at {benchmarks_json} is invalid or empty; skipping analysis and exiting successfully."
99 )
100 sys.exit(0)
102 # Validate structure: require a 'benchmarks' list
103 if not isinstance(data, dict) or "benchmarks" not in data or not isinstance(data["benchmarks"], list):
104 console.warning(
105 f"benchmarks.json at {benchmarks_json} missing valid 'benchmarks' list; "
106 "skipping analysis and exiting successfully."
107 )
108 sys.exit(0)
110 # Check if benchmarks list is empty
111 if not data["benchmarks"]:
112 console.warning(
113 f"benchmarks.json at {benchmarks_json} contains no benchmarks; skipping analysis and exiting successfully."
114 )
115 sys.exit(0)
117 # Extract relevant info: Benchmark name, Mean (ms), OPS
118 benchmarks = []
119 for bench in data["benchmarks"]:
120 mean_s = bench["stats"]["mean"]
121 benchmarks.append(
122 {
123 "Benchmark": bench["name"],
124 "Mean_ms": mean_s * 1000, # convert seconds → milliseconds
125 "OPS": 1 / mean_s,
126 }
127 )
129 # Create DataFrame and sort fastest → slowest
130 df = pd.DataFrame(benchmarks)
131 df = df.sort_values("Mean_ms")
133 # Display reduced table
134 console.info("Benchmark Results:")
135 print(df[["Benchmark", "Mean_ms", "OPS"]].to_string(index=False, float_format="%.3f"))
137 # Create interactive Plotly bar chart
138 fig = px.bar(
139 df,
140 x="Benchmark",
141 y="Mean_ms",
142 color="Mean_ms",
143 color_continuous_scale="Viridis_r",
144 title="Benchmark Mean Runtime (ms) per Test",
145 text="Mean_ms",
146 )
148 fig.update_traces(texttemplate="%{text:.2f} ms", textposition="outside")
149 fig.update_layout(
150 xaxis_tickangle=-45,
151 yaxis_title="Mean Runtime (ms)",
152 coloraxis_colorbar={"title": "ms"},
153 height=600,
154 margin={"t": 100, "b": 200},
155 )
157 # Create output directory if it doesn't exist
158 output_html.parent.mkdir(parents=True, exist_ok=True)
160 # Save HTML visualization
161 fig.write_html(output_html)
162 console.success(f"Visualization saved to {output_html}")
164 # Show interactive plot in browser
165 fig.show()