Skip to main content

dbx_core/engine/
benchmark.rs

1// Phase 0.1: 성능 벤치마크 프레임워크
2//
3// TDD 방식으로 구현:
4// 1. Red: 테스트 작성 (실패)
5// 2. Green: 최소 구현 (통과)
6// 3. Refactor: 코드 개선
7
8use 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/// 벤치마크 결과
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct BenchmarkResult {
19    /// 벤치마크 이름
20    pub name: String,
21
22    /// 평균 실행 시간 (밀리초)
23    pub avg_time_ms: f64,
24
25    /// 최소 실행 시간 (밀리초)
26    pub min_time_ms: f64,
27
28    /// 최대 실행 시간 (밀리초)
29    pub max_time_ms: f64,
30
31    /// 표준 편차
32    pub std_dev_ms: f64,
33
34    /// 샘플 수
35    pub sample_count: usize,
36
37    /// 타임스탬프
38    pub timestamp: i64,
39}
40
41impl BenchmarkResult {
42    /// 새 벤치마크 결과 생성
43    pub fn new(name: String, samples: &[Duration]) -> Self {
44        let sample_count = samples.len();
45
46        // 밀리초로 변환
47        let times_ms: Vec<f64> = samples.iter().map(|d| d.as_secs_f64() * 1000.0).collect();
48
49        // 통계 계산
50        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        // 표준 편차
55        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
80/// 성능 벤치마크 러너
81pub struct BenchmarkRunner {
82    /// 베이스라인 결과 (이름 → 결과)
83    baseline: Arc<RwLock<HashMap<String, BenchmarkResult>>>,
84
85    /// 성능 회귀 임계값 (예: 1.1 = 10% 저하 허용)
86    threshold: f64,
87
88    /// 베이스라인 파일 경로
89    baseline_path: PathBuf,
90
91    /// 샘플 수
92    sample_count: usize,
93}
94
95impl BenchmarkRunner {
96    /// 새 벤치마크 러너 생성
97    pub fn new() -> Self {
98        Self {
99            baseline: Arc::new(RwLock::new(HashMap::new())),
100            threshold: 1.1, // 10% 저하 허용
101            baseline_path: PathBuf::from("target/benchmark_baseline.json"),
102            sample_count: 100,
103        }
104    }
105
106    /// 임계값 설정
107    pub fn with_threshold(mut self, threshold: f64) -> Self {
108        self.threshold = threshold;
109        self
110    }
111
112    /// 베이스라인 경로 설정
113    pub fn with_baseline_path(mut self, path: PathBuf) -> Self {
114        self.baseline_path = path;
115        self
116    }
117
118    /// 샘플 수 설정
119    pub fn with_sample_count(mut self, count: usize) -> Self {
120        self.sample_count = count;
121        self
122    }
123
124    /// 벤치마크 실행
125    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        // Warmup (5회)
132        for _ in 0..5 {
133            f();
134        }
135
136        // 실제 측정
137        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    /// 베이스라인 저장
148    pub fn save_baseline(&self) -> DbxResult<()> {
149        let baseline = self.baseline.read().unwrap();
150        let json = serde_json::to_string_pretty(&*baseline)?;
151
152        // 디렉토리 생성
153        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    /// 베이스라인 로드
162    pub fn load_baseline(&self) -> DbxResult<()> {
163        if !self.baseline_path.exists() {
164            return Ok(()); // 베이스라인 없으면 무시
165        }
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    /// 베이스라인 업데이트
177    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    /// 성능 회귀 검사
185    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    /// 벤치마크 실행 및 회귀 검사
205    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    // TDD: Red - 테스트 작성 (실패)
227
228    #[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        // 벤치마크 실행
249        let result = runner
250            .run("test_op", || {
251                let _ = 1 + 1;
252            })
253            .unwrap();
254
255        // 베이스라인 업데이트 및 저장
256        runner.update_baseline("test_op", &result);
257        runner.save_baseline().unwrap();
258
259        // 새 러너로 로드
260        let runner2 = BenchmarkRunner::new().with_baseline_path(temp_path.clone());
261        runner2.load_baseline().unwrap();
262
263        // 베이스라인 확인
264        let baseline = runner2.baseline.read().unwrap();
265        assert!(baseline.contains_key("test_op"));
266
267        // 정리
268        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); // 50% 저하 허용
277
278        // 빠른 연산으로 베이스라인 생성
279        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        // 느린 연산 (회귀)
288        let slow_result = BenchmarkResult {
289            name: "fast_op".to_string(),
290            avg_time_ms: baseline_result.avg_time_ms * 2.0, // 2배 느림
291            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        // 회귀 검출 확인
302        let result = runner.check_regression("fast_op", &slow_result);
303        assert!(result.is_err());
304
305        // 정리
306        let _ = fs::remove_file(temp_path);
307    }
308
309    #[test]
310    fn test_threshold_configuration() {
311        let runner = BenchmarkRunner::new().with_threshold(2.0); // 100% 저하 허용
312
313        assert_eq!(runner.threshold, 2.0);
314    }
315
316    #[test]
317    fn test_benchmark_comparison() {
318        let runner = BenchmarkRunner::new();
319
320        // 빠른 연산
321        let fast = runner
322            .run("fast", || {
323                let _ = 1 + 1;
324            })
325            .unwrap();
326
327        // 느린 연산
328        let slow = runner
329            .run("slow", || {
330                thread::sleep(Duration::from_micros(10));
331            })
332            .unwrap();
333
334        // 느린 연산이 더 오래 걸림
335        assert!(slow.avg_time_ms > fast.avg_time_ms);
336    }
337}