1use anyhow::{Context, Result};
6use std::process::Command;
7
8pub fn run_bench(
10 bench_name: &str,
11 counters: Option<&str>,
12 check_regression: bool,
13 threshold: f64,
14 _roofline: bool,
15) -> Result<()> {
16 println!("\n=== CGP Bench: {bench_name} ===\n");
17 print_bench_header(bench_name, counters, check_regression, threshold);
18
19 let output = run_cargo_bench(bench_name)?;
20 let stdout = String::from_utf8_lossy(&output.stdout);
21 let stderr = String::from_utf8_lossy(&output.stderr);
22
23 if !output.status.success() {
24 handle_bench_failure(bench_name, &stderr);
25 return Ok(());
26 }
27
28 print_bench_results(&stdout);
29 run_perf_overlay_if_requested(bench_name, counters);
30 if check_regression {
31 print_regression_check(&stdout, threshold);
32 }
33
34 println!();
35 Ok(())
36}
37
38fn print_bench_header(
40 bench_name: &str,
41 counters: Option<&str>,
42 check_regression: bool,
43 threshold: f64,
44) {
45 if counters.is_some() && which::which("perf").is_ok() {
46 println!(" Hardware counter overlay: enabled");
47 }
48 println!(" Running: cargo bench --bench {bench_name}");
49 if let Some(c) = counters {
50 println!(" Hardware counters: {c}");
51 }
52 if check_regression {
53 println!(" Regression check: threshold={threshold}%");
54 }
55}
56
57fn run_cargo_bench(bench_name: &str) -> Result<std::process::Output> {
59 let mut cmd = Command::new("cargo");
60 cmd.arg("bench");
61 if let Some((bench, filter)) = bench_name.split_once('/') {
62 cmd.arg("--bench").arg(bench).arg("--").arg(filter);
63 } else {
64 cmd.arg("--bench").arg(bench_name);
65 }
66 cmd.arg("--no-fail-fast");
67 cmd.output()
68 .with_context(|| format!("Failed to run cargo bench --bench {bench_name}"))
69}
70
71fn handle_bench_failure(bench_name: &str, stderr: &str) {
73 if stderr.contains("no bench target") || stderr.contains("can't find") {
74 println!(" Benchmark '{bench_name}' not found.");
75 list_available_benches();
76 return;
77 }
78 eprintln!(" cargo bench failed:\n{stderr}");
79}
80
81fn list_available_benches() {
83 println!(" Available benchmarks:");
84 let Ok(lo) = Command::new("cargo")
85 .args(["bench", "--bench", "nonexistent_xyz_123", "--", "--list"])
86 .output()
87 else {
88 return;
89 };
90 let lo_stderr = String::from_utf8_lossy(&lo.stderr);
91 for line in lo_stderr.lines() {
92 if line.contains("bench target") || line.contains("available") {
93 println!(" {line}");
94 }
95 }
96}
97
98fn print_bench_results(stdout: &str) {
100 let results = parse_bench_results(stdout);
101 if results.is_empty() {
102 println!(" Criterion output:");
103 for line in stdout.lines().take(30) {
104 if !line.trim().is_empty() {
105 println!(" {line}");
106 }
107 }
108 return;
109 }
110 println!(" Results:");
111 for r in &results {
112 let change_str = match &r.change {
113 Some(c) => format!(" ({c})"),
114 None => String::new(),
115 };
116 println!(" {:40} {}{}", r.name, r.timing, change_str);
117 }
118}
119
120fn parse_bench_results(stdout: &str) -> Vec<BenchResult> {
122 let mut results: Vec<BenchResult> = Vec::new();
123 for line in stdout.lines() {
124 if !line.contains("time:") {
125 continue;
126 }
127 let parts: Vec<&str> = line.splitn(2, "time:").collect();
128 if parts.len() != 2 {
129 continue;
130 }
131 let name = parts[0].trim().to_string();
132 let timing = parts[1].trim().to_string();
133 let change = extract_change(line);
134 results.push(BenchResult {
135 name,
136 timing,
137 change,
138 });
139 }
140 results
141}
142
143fn run_perf_overlay_if_requested(bench_name: &str, counters: Option<&str>) {
145 let Some(counter_list) = counters else {
146 return;
147 };
148 if which::which("perf").is_ok() {
149 println!("\n --- perf stat overlay ---");
150 run_perf_overlay(bench_name, counter_list);
151 } else {
152 println!("\n perf not available — skipping hardware counter overlay.");
153 println!(" Install linux-tools-common for hardware counter support.");
154 }
155}
156
157fn print_regression_check(stdout: &str, threshold: f64) {
159 println!("\n Regression check (threshold={threshold}%):");
160 let mut regressions = 0;
161 for line in stdout.lines() {
162 if line.contains("regressed") || line.contains("Performance has regressed") {
163 println!(" \x1b[31mREGRESSION\x1b[0m: {line}");
164 regressions += 1;
165 } else if line.contains("improved") {
166 println!(" \x1b[32mIMPROVED\x1b[0m: {line}");
167 }
168 }
169 if regressions > 0 {
170 println!("\n \x1b[31m{regressions} regression(s) detected!\x1b[0m");
171 } else {
172 println!(" No regressions detected.");
173 }
174}
175
176struct BenchResult {
178 name: String,
179 timing: String,
180 change: Option<String>,
181}
182
183fn extract_change(line: &str) -> Option<String> {
185 if line.contains("change:") {
186 line.split("change:").nth(1).map(|s| s.trim().to_string())
187 } else {
188 None
189 }
190}
191
192fn run_perf_overlay(bench_name: &str, counters: &str) {
194 let events = counters.replace(' ', "");
195
196 let mut cmd = Command::new("perf");
198 cmd.arg("stat")
199 .arg("-e")
200 .arg(&events)
201 .arg("-x")
202 .arg(",")
203 .arg("cargo")
204 .arg("bench")
205 .arg("--bench")
206 .arg(bench_name)
207 .arg("--")
208 .arg("--quick"); match cmd.output() {
211 Ok(output) => {
212 let stderr = String::from_utf8_lossy(&output.stderr);
213
214 for line in stderr.lines() {
216 let line = line.trim();
217 if line.is_empty() || line.starts_with('#') || line.starts_with("Performance") {
218 continue;
219 }
220 if line.contains("seconds time elapsed") {
221 println!(" Wall time: {}", line.trim());
222 continue;
223 }
224
225 let fields: Vec<&str> = line.split(',').collect();
226 if fields.len() >= 3 {
227 let value = fields[0].trim();
228 let event = fields[2].trim();
229 if !value.is_empty() && !event.is_empty() {
230 println!(" {event:40} {value:>14}");
231 }
232 }
233 }
234 }
235 Err(e) => {
236 println!(" perf stat failed: {e}");
237 println!(" Try: sudo sysctl kernel.perf_event_paranoid=2");
238 }
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn test_extract_change_none() {
248 assert!(extract_change("some time: [1.0 ns 2.0 ns]").is_none());
249 }
250
251 #[test]
252 fn test_extract_change_present() {
253 let change = extract_change("some time: [1.0 ns] change: +5.2%");
254 assert!(change.is_some());
255 assert!(change.unwrap().contains("+5.2%"));
256 }
257
258 #[test]
259 fn test_bench_result_struct() {
260 let r = BenchResult {
261 name: "test".to_string(),
262 timing: "1.0 ns".to_string(),
263 change: None,
264 };
265 assert_eq!(r.name, "test");
266 }
267}