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