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

1"""Command to analyze pytest-benchmark results and visualize them. 

2 

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. 

6 

7Note: This command requires pandas and plotly, which are available in the dev 

8dependency group. Install with: uv pip install -e ".[dev]" 

9 

10Example: 

11 Analyze benchmarks with default path:: 

12 

13 from rhiza_tools.commands.analyze_benchmarks import analyze_benchmarks_command 

14 analyze_benchmarks_command() 

15 

16 Analyze benchmarks with custom path:: 

17 

18 from pathlib import Path 

19 analyze_benchmarks_command(benchmarks_json=Path("custom/benchmarks.json")) 

20""" 

21 

22# /// script 

23# dependencies = [ 

24# "pandas", 

25# "plotly", 

26# ] 

27# /// 

28 

29import json 

30import sys 

31from pathlib import Path 

32 

33from rhiza_tools import console 

34 

35 

36class BenchmarkError(Exception): 

37 """Base exception for benchmark analysis errors.""" 

38 

39 

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. 

45 

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. 

49 

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. 

53 

54 Raises: 

55 SystemExit: If benchmarks.json is missing, invalid, or has no valid benchmarks. 

56 

57 Example: 

58 Analyze benchmarks with default paths:: 

59 

60 analyze_benchmarks_command() 

61 

62 Use custom paths:: 

63 

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) 

79 

80 # Set default paths 

81 if benchmarks_json is None: 

82 benchmarks_json = Path("_benchmarks/benchmarks.json") 

83 

84 if output_html is None: 

85 output_html = Path("_benchmarks/benchmarks.html") 

86 

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) 

91 

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) 

101 

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) 

109 

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) 

116 

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 ) 

128 

129 # Create DataFrame and sort fastest → slowest 

130 df = pd.DataFrame(benchmarks) 

131 df = df.sort_values("Mean_ms") 

132 

133 # Display reduced table 

134 console.info("Benchmark Results:") 

135 print(df[["Benchmark", "Mean_ms", "OPS"]].to_string(index=False, float_format="%.3f")) 

136 

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 ) 

147 

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 ) 

156 

157 # Create output directory if it doesn't exist 

158 output_html.parent.mkdir(parents=True, exist_ok=True) 

159 

160 # Save HTML visualization 

161 fig.write_html(output_html) 

162 console.success(f"Visualization saved to {output_html}") 

163 

164 # Show interactive plot in browser 

165 fig.show()