1use std::collections::HashMap;
4use std::time::Duration;
5
6#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
8pub struct BenchmarkResult {
9 pub name: String,
10 pub category: String,
11 pub input_size: usize,
12 pub iterations: usize,
13 pub mean_ns: u64,
14 pub stddev_ns: u64,
15 pub min_ns: u64,
16 pub max_ns: u64,
17 pub throughput_mbps: Option<f64>,
18}
19
20impl BenchmarkResult {
21 pub fn mean_duration(&self) -> Duration {
23 Duration::from_nanos(self.mean_ns)
24 }
25
26 pub fn ops_per_sec(&self) -> f64 {
28 if self.mean_ns == 0 {
29 return 0.0;
30 }
31 1_000_000_000.0 / self.mean_ns as f64
32 }
33}
34
35#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
37pub struct BenchmarkReport {
38 pub title: String,
39 pub timestamp: String,
40 pub results: Vec<BenchmarkResult>,
41 pub system_info: SystemInfo,
42}
43
44#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
46pub struct SystemInfo {
47 pub os: String,
48 pub arch: String,
49 pub num_cpus: usize,
50}
51
52impl Default for SystemInfo {
53 fn default() -> Self {
54 Self {
55 os: std::env::consts::OS.to_string(),
56 arch: std::env::consts::ARCH.to_string(),
57 num_cpus: std::thread::available_parallelism()
58 .map(|n| n.get())
59 .unwrap_or(1),
60 }
61 }
62}
63
64#[derive(Debug, Clone)]
66pub struct BenchmarkComparison {
67 pub name: String,
68 pub baseline_ns: u64,
69 pub current_ns: u64,
70 pub change_pct: f64,
71 pub regression: bool,
72}
73
74impl BenchmarkReport {
75 pub fn new(title: impl Into<String>) -> Self {
77 Self {
78 title: title.into(),
79 timestamp: chrono::Utc::now().to_rfc3339(),
80 results: Vec::new(),
81 system_info: SystemInfo::default(),
82 }
83 }
84
85 pub fn add(&mut self, result: BenchmarkResult) {
87 self.results.push(result);
88 }
89
90 pub fn by_category(&self) -> HashMap<&str, Vec<&BenchmarkResult>> {
92 let mut map: HashMap<&str, Vec<&BenchmarkResult>> = HashMap::new();
93 for result in &self.results {
94 map.entry(&result.category).or_default().push(result);
95 }
96 map
97 }
98
99 pub fn compare(&self, baseline: &BenchmarkReport) -> Vec<BenchmarkComparison> {
101 let baseline_map: HashMap<&str, &BenchmarkResult> = baseline
102 .results
103 .iter()
104 .map(|r| (r.name.as_str(), r))
105 .collect();
106
107 self.results
108 .iter()
109 .filter_map(|current| {
110 let base = baseline_map.get(current.name.as_str())?;
111 let change_pct = if base.mean_ns == 0 {
112 0.0
113 } else {
114 ((current.mean_ns as f64 - base.mean_ns as f64) / base.mean_ns as f64) * 100.0
115 };
116
117 Some(BenchmarkComparison {
118 name: current.name.clone(),
119 baseline_ns: base.mean_ns,
120 current_ns: current.mean_ns,
121 change_pct,
122 regression: change_pct > 10.0, })
124 })
125 .collect()
126 }
127
128 pub fn summary(&self) -> String {
130 let mut lines = vec![
131 format!("╔══ {} ══╗", self.title),
132 format!(
133 " System: {} {} ({} CPUs)",
134 self.system_info.os, self.system_info.arch, self.system_info.num_cpus
135 ),
136 format!(" Time: {}", self.timestamp),
137 format!(" Benchmarks: {}", self.results.len()),
138 String::new(),
139 ];
140
141 for (category, results) in self.by_category() {
142 lines.push(format!(" ── {} ──", category));
143 for r in results {
144 let tp = r
145 .throughput_mbps
146 .map(|t| format!(" | {:.1} MB/s", t))
147 .unwrap_or_default();
148 lines.push(format!(
149 " {:<35} {:>10.2?} mean | {:.0} ops/s{}",
150 r.name,
151 r.mean_duration(),
152 r.ops_per_sec(),
153 tp
154 ));
155 }
156 lines.push(String::new());
157 }
158
159 lines.push("╚══════════════════════════════════╝".to_string());
160 lines.join("\n")
161 }
162
163 pub fn to_json(&self) -> String {
165 serde_json::to_string_pretty(self).unwrap_or_default()
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn test_report_creation() {
175 let mut report = BenchmarkReport::new("Test Suite");
176 report.add(BenchmarkResult {
177 name: "encrypt_256b".into(),
178 category: "crypto".into(),
179 input_size: 256,
180 iterations: 1000,
181 mean_ns: 5000,
182 stddev_ns: 200,
183 min_ns: 4500,
184 max_ns: 6000,
185 throughput_mbps: Some(48.8),
186 });
187
188 assert_eq!(report.results.len(), 1);
189 assert!(report.summary().contains("encrypt_256b"));
190 }
191
192 #[test]
193 fn test_comparison() {
194 let mut baseline = BenchmarkReport::new("baseline");
195 baseline.add(BenchmarkResult {
196 name: "op1".into(),
197 category: "test".into(),
198 input_size: 0,
199 iterations: 100,
200 mean_ns: 1000,
201 stddev_ns: 50,
202 min_ns: 900,
203 max_ns: 1100,
204 throughput_mbps: None,
205 });
206
207 let mut current = BenchmarkReport::new("current");
208 current.add(BenchmarkResult {
209 name: "op1".into(),
210 category: "test".into(),
211 input_size: 0,
212 iterations: 100,
213 mean_ns: 1200, stddev_ns: 60,
215 min_ns: 1000,
216 max_ns: 1400,
217 throughput_mbps: None,
218 });
219
220 let comparisons = current.compare(&baseline);
221 assert_eq!(comparisons.len(), 1);
222 assert!(comparisons[0].regression);
223 assert!((comparisons[0].change_pct - 20.0).abs() < 0.1);
224 }
225
226 #[test]
227 fn test_by_category() {
228 let mut report = BenchmarkReport::new("test");
229 report.add(BenchmarkResult {
230 name: "a".into(),
231 category: "crypto".into(),
232 input_size: 0,
233 iterations: 1,
234 mean_ns: 100,
235 stddev_ns: 0,
236 min_ns: 100,
237 max_ns: 100,
238 throughput_mbps: None,
239 });
240 report.add(BenchmarkResult {
241 name: "b".into(),
242 category: "dht".into(),
243 input_size: 0,
244 iterations: 1,
245 mean_ns: 200,
246 stddev_ns: 0,
247 min_ns: 200,
248 max_ns: 200,
249 throughput_mbps: None,
250 });
251
252 let cats = report.by_category();
253 assert_eq!(cats.len(), 2);
254 assert!(cats.contains_key("crypto"));
255 assert!(cats.contains_key("dht"));
256 }
257
258 #[test]
259 fn test_json_roundtrip() {
260 let mut report = BenchmarkReport::new("json_test");
261 report.add(BenchmarkResult {
262 name: "op".into(),
263 category: "test".into(),
264 input_size: 64,
265 iterations: 50,
266 mean_ns: 5000,
267 stddev_ns: 100,
268 min_ns: 4800,
269 max_ns: 5500,
270 throughput_mbps: Some(12.2),
271 });
272
273 let json = report.to_json();
274 let parsed: BenchmarkReport = serde_json::from_str(&json).unwrap();
275 assert_eq!(parsed.results.len(), 1);
276 assert_eq!(parsed.results[0].name, "op");
277 }
278}