1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::Path;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct BenchmarkResult {
10 pub name: String,
11 pub mean_ns: f64,
12 pub median_ns: f64,
13 pub stddev_ns: f64,
14 pub samples: usize,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Baseline {
20 pub name: String,
21 pub created_at: chrono::DateTime<chrono::Utc>,
22 pub results: Vec<BenchmarkResult>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Comparison {
28 pub name: String,
29 pub baseline_ns: f64,
30 pub current_ns: f64,
31 pub change_pct: f64,
32 pub regression: bool,
33}
34
35const REGRESSION_THRESHOLD_PCT: f64 = 10.0;
37
38pub fn compare(current: &[BenchmarkResult], baseline: &Baseline) -> Vec<Comparison> {
40 let baseline_map: HashMap<&str, &BenchmarkResult> = baseline
41 .results
42 .iter()
43 .map(|r| (r.name.as_str(), r))
44 .collect();
45
46 current
47 .iter()
48 .filter_map(|c| {
49 let base = baseline_map.get(c.name.as_str())?;
50 let change_pct = ((c.mean_ns - base.mean_ns) / base.mean_ns) * 100.0;
51 Some(Comparison {
52 name: c.name.clone(),
53 baseline_ns: base.mean_ns,
54 current_ns: c.mean_ns,
55 change_pct,
56 regression: change_pct > REGRESSION_THRESHOLD_PCT,
57 })
58 })
59 .collect()
60}
61
62pub fn save_baseline(baseline: &Baseline, path: &Path) -> std::io::Result<()> {
64 let json = serde_json::to_string_pretty(baseline)
65 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
66 std::fs::write(path, json)
67}
68
69pub fn load_baseline(path: &Path) -> std::io::Result<Baseline> {
71 let json = std::fs::read_to_string(path)?;
72 serde_json::from_str(&json).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
73}
74
75pub fn create_baseline(name: &str, results: Vec<BenchmarkResult>) -> Baseline {
77 Baseline {
78 name: name.into(),
79 created_at: chrono::Utc::now(),
80 results,
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 fn make_result(name: &str, mean_ns: f64) -> BenchmarkResult {
89 BenchmarkResult {
90 name: name.into(),
91 mean_ns,
92 median_ns: mean_ns,
93 stddev_ns: 10.0,
94 samples: 100,
95 }
96 }
97
98 #[test]
99 fn create_baseline_works() {
100 let baseline = create_baseline("v1", vec![make_result("bench_a", 1000.0)]);
101 assert_eq!(baseline.name, "v1");
102 assert_eq!(baseline.results.len(), 1);
103 }
104
105 #[test]
106 fn compare_no_change() {
107 let baseline = create_baseline("v1", vec![make_result("bench_a", 1000.0)]);
108 let current = vec![make_result("bench_a", 1000.0)];
109 let comparisons = compare(¤t, &baseline);
110 assert_eq!(comparisons.len(), 1);
111 assert!(!comparisons[0].regression);
112 }
113
114 #[test]
115 fn compare_detects_regression() {
116 let baseline = create_baseline("v1", vec![make_result("bench_a", 1000.0)]);
117 let current = vec![make_result("bench_a", 1200.0)];
118 let comparisons = compare(¤t, &baseline);
119 assert_eq!(comparisons[0].change_pct, 20.0);
120 assert!(comparisons[0].regression);
121 }
122
123 #[test]
124 fn compare_improvement_not_regression() {
125 let baseline = create_baseline("v1", vec![make_result("bench_a", 1000.0)]);
126 let current = vec![make_result("bench_a", 800.0)];
127 let comparisons = compare(¤t, &baseline);
128 assert!(!comparisons[0].regression);
129 }
130
131 #[test]
132 fn compare_handles_missing_benchmarks() {
133 let baseline = create_baseline("v1", vec![make_result("a", 1.0)]);
134 let current = vec![make_result("b", 2.0)];
135 let comparisons = compare(¤t, &baseline);
136 assert!(comparisons.is_empty());
137 }
138
139 #[test]
140 fn save_load_round_trips() {
141 let tmp = tempfile::tempdir().unwrap();
142 let path = tmp.path().join("baseline.json");
143 let baseline = create_baseline("test", vec![make_result("x", 42.0)]);
144 save_baseline(&baseline, &path).unwrap();
145 let loaded = load_baseline(&path).unwrap();
146 assert_eq!(loaded.name, "test");
147 assert_eq!(loaded.results[0].mean_ns, 42.0);
148 }
149
150 #[test]
151 fn regression_threshold_10pct() {
152 let baseline = create_baseline("v1", vec![make_result("a", 100.0)]);
153 let current = vec![make_result("a", 109.0)]; let comparisons = compare(¤t, &baseline);
155 assert!(!comparisons[0].regression); }
157
158 #[test]
159 fn multiple_benchmarks_compared() {
160 let baseline =
161 create_baseline("v1", vec![make_result("a", 100.0), make_result("b", 200.0)]);
162 let current = vec![
163 make_result("a", 150.0), make_result("b", 190.0), ];
166 let comparisons = compare(¤t, &baseline);
167 assert_eq!(comparisons.len(), 2);
168 assert!(comparisons[0].regression);
169 assert!(!comparisons[1].regression);
170 }
171}