Skip to main content

shadow_benchmarks/
report.rs

1//! Benchmark report generation and comparison
2
3use std::collections::HashMap;
4use std::time::Duration;
5
6/// A benchmark result entry
7#[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    /// Mean duration
22    pub fn mean_duration(&self) -> Duration {
23        Duration::from_nanos(self.mean_ns)
24    }
25
26    /// Operations per second
27    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/// Collection of benchmark results with comparison capabilities
36#[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/// Basic system info captured with benchmarks
45#[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/// Comparison between two benchmark runs
65#[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    /// Create a new report
76    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    /// Add a benchmark result
86    pub fn add(&mut self, result: BenchmarkResult) {
87        self.results.push(result);
88    }
89
90    /// Get results by category
91    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    /// Compare against a baseline report, returns regressions
100    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, // >10% slower = regression
123                })
124            })
125            .collect()
126    }
127
128    /// Generate text summary
129    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    /// Serialize to JSON
164    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, // 20% regression
214            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}