Coverage for src/rhiza_tools/commands/analyze_benchmarks.py: 100%
47 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-30 13:37 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-30 13:37 +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 show: bool = False,
44) -> None:
45 """Analyze pytest-benchmark results and visualize them.
47 This command reads a benchmarks.json file produced by pytest-benchmark,
48 prints a reduced table with benchmark name, mean milliseconds, and operations
49 per second, and renders an interactive Plotly bar chart of mean runtimes.
51 Args:
52 benchmarks_json: Path to the benchmarks.json file. Defaults to _benchmarks/benchmarks.json.
53 output_html: Path to save the HTML visualization. Defaults to _benchmarks/benchmarks.html.
54 show: If True, open the interactive chart in a browser after saving. Defaults to False.
56 Raises:
57 SystemExit: If benchmarks.json is missing, invalid, or has no valid benchmarks.
59 Example:
60 Analyze benchmarks with default paths::
62 analyze_benchmarks_command()
64 Use custom paths::
66 analyze_benchmarks_command(
67 benchmarks_json=Path("tests/benchmarks.json"),
68 output_html=Path("reports/benchmarks.html")
69 )
71 Open the chart in a browser after saving::
73 analyze_benchmarks_command(show=True)
74 """
75 # Import pandas and plotly here to avoid requiring them as hard dependencies
76 try:
77 import pandas as pd
78 import plotly.express as px
79 except ImportError:
80 console.error(
81 "pandas and plotly are required for this command. "
82 "Install them with: uv pip install -e '.[dev]' or pip install 'rhiza-tools[dev]'"
83 )
84 sys.exit(1)
86 # Set default paths
87 if benchmarks_json is None:
88 benchmarks_json = Path("_benchmarks/benchmarks.json")
90 if output_html is None:
91 output_html = Path("_benchmarks/benchmarks.html")
93 # Check if the file exists
94 if not benchmarks_json.exists():
95 console.warning(f"benchmarks.json not found at {benchmarks_json}; skipping analysis and exiting successfully.")
96 sys.exit(0)
98 # Load pytest-benchmark JSON
99 try:
100 with benchmarks_json.open() as f:
101 data = json.load(f)
102 except json.JSONDecodeError:
103 console.warning(
104 f"benchmarks.json at {benchmarks_json} is invalid or empty; skipping analysis and exiting successfully."
105 )
106 sys.exit(0)
108 # Validate structure: require a 'benchmarks' list
109 if not isinstance(data, dict) or "benchmarks" not in data or not isinstance(data["benchmarks"], list):
110 console.warning(
111 f"benchmarks.json at {benchmarks_json} missing valid 'benchmarks' list; "
112 "skipping analysis and exiting successfully."
113 )
114 sys.exit(0)
116 # Check if benchmarks list is empty
117 if not data["benchmarks"]:
118 console.warning(
119 f"benchmarks.json at {benchmarks_json} contains no benchmarks; skipping analysis and exiting successfully."
120 )
121 sys.exit(0)
123 # Extract relevant info: Benchmark name, Mean (ms), OPS
124 benchmarks = []
125 for bench in data["benchmarks"]:
126 mean_s = bench["stats"]["mean"]
127 benchmarks.append(
128 {
129 "Benchmark": bench["name"],
130 "Mean_ms": mean_s * 1000, # convert seconds → milliseconds
131 "OPS": 1 / mean_s,
132 }
133 )
135 # Create DataFrame and sort fastest → slowest
136 df = pd.DataFrame(benchmarks)
137 df = df.sort_values("Mean_ms")
139 # Display reduced table
140 console.info("Benchmark Results:")
141 print(df[["Benchmark", "Mean_ms", "OPS"]].to_string(index=False, float_format="%.3f"))
143 # Create interactive Plotly bar chart
144 fig = px.bar(
145 df,
146 x="Benchmark",
147 y="Mean_ms",
148 color="Mean_ms",
149 color_continuous_scale="Viridis_r",
150 title="Benchmark Mean Runtime (ms) per Test",
151 text="Mean_ms",
152 )
154 fig.update_traces(texttemplate="%{text:.2f} ms", textposition="outside")
155 fig.update_layout(
156 xaxis_tickangle=-45,
157 yaxis_title="Mean Runtime (ms)",
158 coloraxis_colorbar={"title": "ms"},
159 height=600,
160 margin={"t": 100, "b": 200},
161 )
163 # Create output directory if it doesn't exist
164 output_html.parent.mkdir(parents=True, exist_ok=True)
166 # Save HTML visualization
167 fig.write_html(output_html)
168 console.success(f"Visualization saved to {output_html}")
170 # Optionally open the interactive plot in a browser
171 if show:
172 fig.show()