dbx_core/engine/
benchmark.rs1use crate::error::{DbxError, DbxResult};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::fs;
12use std::path::PathBuf;
13use std::sync::{Arc, RwLock};
14use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct BenchmarkResult {
19 pub name: String,
21
22 pub avg_time_ms: f64,
24
25 pub min_time_ms: f64,
27
28 pub max_time_ms: f64,
30
31 pub std_dev_ms: f64,
33
34 pub sample_count: usize,
36
37 pub timestamp: i64,
39}
40
41impl BenchmarkResult {
42 pub fn new(name: String, samples: &[Duration]) -> Self {
44 let sample_count = samples.len();
45
46 let times_ms: Vec<f64> = samples.iter().map(|d| d.as_secs_f64() * 1000.0).collect();
48
49 let avg_time_ms = times_ms.iter().sum::<f64>() / sample_count as f64;
51 let min_time_ms = times_ms.iter().cloned().fold(f64::INFINITY, f64::min);
52 let max_time_ms = times_ms.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
53
54 let variance = times_ms
56 .iter()
57 .map(|t| {
58 let diff = t - avg_time_ms;
59 diff * diff
60 })
61 .sum::<f64>()
62 / sample_count as f64;
63 let std_dev_ms = variance.sqrt();
64
65 Self {
66 name,
67 avg_time_ms,
68 min_time_ms,
69 max_time_ms,
70 std_dev_ms,
71 sample_count,
72 timestamp: SystemTime::now()
73 .duration_since(UNIX_EPOCH)
74 .unwrap()
75 .as_secs() as i64,
76 }
77 }
78}
79
80pub struct BenchmarkRunner {
82 baseline: Arc<RwLock<HashMap<String, BenchmarkResult>>>,
84
85 threshold: f64,
87
88 baseline_path: PathBuf,
90
91 sample_count: usize,
93}
94
95impl BenchmarkRunner {
96 pub fn new() -> Self {
98 Self {
99 baseline: Arc::new(RwLock::new(HashMap::new())),
100 threshold: 1.1, baseline_path: PathBuf::from("target/benchmark_baseline.json"),
102 sample_count: 100,
103 }
104 }
105
106 pub fn with_threshold(mut self, threshold: f64) -> Self {
108 self.threshold = threshold;
109 self
110 }
111
112 pub fn with_baseline_path(mut self, path: PathBuf) -> Self {
114 self.baseline_path = path;
115 self
116 }
117
118 pub fn with_sample_count(mut self, count: usize) -> Self {
120 self.sample_count = count;
121 self
122 }
123
124 pub fn run<F>(&self, name: &str, mut f: F) -> DbxResult<BenchmarkResult>
126 where
127 F: FnMut(),
128 {
129 let mut samples = Vec::with_capacity(self.sample_count);
130
131 for _ in 0..5 {
133 f();
134 }
135
136 for _ in 0..self.sample_count {
138 let start = Instant::now();
139 f();
140 let duration = start.elapsed();
141 samples.push(duration);
142 }
143
144 Ok(BenchmarkResult::new(name.to_string(), &samples))
145 }
146
147 pub fn save_baseline(&self) -> DbxResult<()> {
149 let baseline = self.baseline.read().unwrap();
150 let json = serde_json::to_string_pretty(&*baseline)?;
151
152 if let Some(parent) = self.baseline_path.parent() {
154 fs::create_dir_all(parent)?;
155 }
156
157 fs::write(&self.baseline_path, json)?;
158 Ok(())
159 }
160
161 pub fn load_baseline(&self) -> DbxResult<()> {
163 if !self.baseline_path.exists() {
164 return Ok(()); }
166
167 let json = fs::read_to_string(&self.baseline_path)?;
168 let loaded: HashMap<String, BenchmarkResult> = serde_json::from_str(&json)?;
169
170 let mut baseline = self.baseline.write().unwrap();
171 *baseline = loaded;
172
173 Ok(())
174 }
175
176 pub fn update_baseline(&self, name: &str, result: &BenchmarkResult) {
178 self.baseline
179 .write()
180 .unwrap()
181 .insert(name.to_string(), result.clone());
182 }
183
184 pub fn check_regression(&self, name: &str, result: &BenchmarkResult) -> DbxResult<()> {
186 let baseline = self.baseline.read().unwrap();
187
188 if let Some(baseline_result) = baseline.get(name) {
189 let ratio = result.avg_time_ms / baseline_result.avg_time_ms;
190
191 if ratio > self.threshold {
192 return Err(DbxError::PerformanceRegression {
193 name: name.to_string(),
194 baseline: baseline_result.avg_time_ms,
195 current: result.avg_time_ms,
196 ratio,
197 });
198 }
199 }
200
201 Ok(())
202 }
203
204 pub fn run_and_check<F>(&self, name: &str, f: F) -> DbxResult<BenchmarkResult>
206 where
207 F: FnMut(),
208 {
209 let result = self.run(name, f)?;
210 self.check_regression(name, &result)?;
211 Ok(result)
212 }
213}
214
215impl Default for BenchmarkRunner {
216 fn default() -> Self {
217 Self::new()
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use std::thread;
225
226 #[test]
229 fn test_benchmark_runner_basic() {
230 let runner = BenchmarkRunner::new();
231
232 let result = runner
233 .run("test_sleep", || {
234 thread::sleep(Duration::from_millis(1));
235 })
236 .unwrap();
237
238 assert_eq!(result.name, "test_sleep");
239 assert!(result.avg_time_ms >= 1.0);
240 assert!(result.sample_count > 0);
241 }
242
243 #[test]
244 fn test_baseline_save_load() {
245 let temp_path = PathBuf::from("target/test_baseline.json");
246 let runner = BenchmarkRunner::new().with_baseline_path(temp_path.clone());
247
248 let result = runner
250 .run("test_op", || {
251 let _ = 1 + 1;
252 })
253 .unwrap();
254
255 runner.update_baseline("test_op", &result);
257 runner.save_baseline().unwrap();
258
259 let runner2 = BenchmarkRunner::new().with_baseline_path(temp_path.clone());
261 runner2.load_baseline().unwrap();
262
263 let baseline = runner2.baseline.read().unwrap();
265 assert!(baseline.contains_key("test_op"));
266
267 let _ = fs::remove_file(temp_path);
269 }
270
271 #[test]
272 fn test_regression_detection() {
273 let temp_path = PathBuf::from("target/test_regression.json");
274 let runner = BenchmarkRunner::new()
275 .with_baseline_path(temp_path.clone())
276 .with_threshold(1.5); let baseline_result = runner
280 .run("fast_op", || {
281 let _ = 1 + 1;
282 })
283 .unwrap();
284
285 runner.update_baseline("fast_op", &baseline_result);
286
287 let slow_result = BenchmarkResult {
289 name: "fast_op".to_string(),
290 avg_time_ms: baseline_result.avg_time_ms * 2.0, min_time_ms: 0.0,
292 max_time_ms: 0.0,
293 std_dev_ms: 0.0,
294 sample_count: 100,
295 timestamp: SystemTime::now()
296 .duration_since(UNIX_EPOCH)
297 .unwrap()
298 .as_secs() as i64,
299 };
300
301 let result = runner.check_regression("fast_op", &slow_result);
303 assert!(result.is_err());
304
305 let _ = fs::remove_file(temp_path);
307 }
308
309 #[test]
310 fn test_threshold_configuration() {
311 let runner = BenchmarkRunner::new().with_threshold(2.0); assert_eq!(runner.threshold, 2.0);
314 }
315
316 #[test]
317 fn test_benchmark_comparison() {
318 let runner = BenchmarkRunner::new();
319
320 let fast = runner
322 .run("fast", || {
323 let _ = 1 + 1;
324 })
325 .unwrap();
326
327 let slow = runner
329 .run("slow", || {
330 thread::sleep(Duration::from_micros(10));
331 })
332 .unwrap();
333
334 assert!(slow.avg_time_ms > fast.avg_time_ms);
336 }
337}