sklears_utils/
performance.rs

1//! Performance measurement and profiling utilities
2//!
3//! This module provides utilities for measuring performance, memory usage,
4//! and timing operations in machine learning workflows.
5
6use crate::{UtilsError, UtilsResult};
7use std::collections::HashMap;
8use std::time::{Duration, Instant};
9
10/// Timer for measuring execution time
11#[derive(Debug, Clone)]
12pub struct Timer {
13    start: Option<Instant>,
14    measurements: HashMap<String, Duration>,
15}
16
17impl Timer {
18    /// Create a new timer
19    pub fn new() -> Self {
20        Self {
21            start: None,
22            measurements: HashMap::new(),
23        }
24    }
25
26    /// Start timing
27    pub fn start(&mut self) {
28        self.start = Some(Instant::now());
29    }
30
31    /// Stop timing and return elapsed duration
32    pub fn stop(&mut self) -> UtilsResult<Duration> {
33        let start = self
34            .start
35            .take()
36            .ok_or_else(|| UtilsError::InvalidParameter("Timer not started".to_string()))?;
37        Ok(start.elapsed())
38    }
39
40    /// Stop timing and store result with a label
41    pub fn stop_and_store(&mut self, label: String) -> UtilsResult<Duration> {
42        let duration = self.stop()?;
43        self.measurements.insert(label, duration);
44        Ok(duration)
45    }
46
47    /// Time a closure and return the result and duration
48    pub fn time<F, R>(&mut self, f: F) -> (R, Duration)
49    where
50        F: FnOnce() -> R,
51    {
52        let start = Instant::now();
53        let result = f();
54        let duration = start.elapsed();
55        (result, duration)
56    }
57
58    /// Time a closure, store the result with a label, and return the result
59    pub fn time_and_store<F, R>(&mut self, label: String, f: F) -> R
60    where
61        F: FnOnce() -> R,
62    {
63        let (result, duration) = self.time(f);
64        self.measurements.insert(label, duration);
65        result
66    }
67
68    /// Get all measurements
69    pub fn measurements(&self) -> &HashMap<String, Duration> {
70        &self.measurements
71    }
72
73    /// Get a specific measurement
74    pub fn get_measurement(&self, label: &str) -> Option<Duration> {
75        self.measurements.get(label).copied()
76    }
77
78    /// Clear all measurements
79    pub fn clear(&mut self) {
80        self.measurements.clear();
81    }
82
83    /// Get summary statistics
84    pub fn summary(&self) -> TimerSummary {
85        let mut total = Duration::from_secs(0);
86        let mut min = Duration::from_secs(u64::MAX);
87        let mut max = Duration::from_secs(0);
88
89        for &duration in self.measurements.values() {
90            total += duration;
91            min = min.min(duration);
92            max = max.max(duration);
93        }
94
95        let count = self.measurements.len();
96        let average = if count > 0 {
97            total / count as u32
98        } else {
99            Duration::from_secs(0)
100        };
101
102        TimerSummary {
103            count,
104            total,
105            average,
106            min: if min == Duration::from_secs(u64::MAX) {
107                Duration::from_secs(0)
108            } else {
109                min
110            },
111            max,
112        }
113    }
114}
115
116impl Default for Timer {
117    fn default() -> Self {
118        Self::new()
119    }
120}
121
122/// Summary statistics for timer measurements
123#[derive(Debug, Clone, PartialEq)]
124pub struct TimerSummary {
125    pub count: usize,
126    pub total: Duration,
127    pub average: Duration,
128    pub min: Duration,
129    pub max: Duration,
130}
131
132/// Memory usage tracker
133#[derive(Debug, Clone)]
134pub struct MemoryTracker {
135    baseline: Option<usize>,
136    measurements: HashMap<String, usize>,
137}
138
139impl MemoryTracker {
140    /// Create a new memory tracker
141    pub fn new() -> Self {
142        Self {
143            baseline: None,
144            measurements: HashMap::new(),
145        }
146    }
147
148    /// Set baseline memory usage
149    pub fn set_baseline(&mut self) {
150        if let Ok(usage) = get_memory_usage() {
151            self.baseline = Some(usage);
152        }
153    }
154
155    /// Record current memory usage with a label
156    pub fn record(&mut self, label: String) -> UtilsResult<usize> {
157        let usage = get_memory_usage()?;
158        self.measurements.insert(label, usage);
159        Ok(usage)
160    }
161
162    /// Get memory usage relative to baseline
163    pub fn relative_usage(&self) -> UtilsResult<Option<isize>> {
164        if let Some(baseline) = self.baseline {
165            let current = get_memory_usage()?;
166            Ok(Some(current as isize - baseline as isize))
167        } else {
168            Ok(None)
169        }
170    }
171
172    /// Get all measurements
173    pub fn measurements(&self) -> &HashMap<String, usize> {
174        &self.measurements
175    }
176
177    /// Get peak memory usage
178    pub fn peak_usage(&self) -> Option<usize> {
179        self.measurements.values().max().copied()
180    }
181
182    /// Clear all measurements
183    pub fn clear(&mut self) {
184        self.measurements.clear();
185        self.baseline = None;
186    }
187}
188
189impl Default for MemoryTracker {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195/// Performance profiler combining timing and memory tracking
196#[derive(Debug, Clone)]
197pub struct Profiler {
198    timer: Timer,
199    memory: MemoryTracker,
200    active_sessions: HashMap<String, (Instant, usize)>,
201}
202
203impl Profiler {
204    /// Create a new profiler
205    pub fn new() -> Self {
206        Self {
207            timer: Timer::new(),
208            memory: MemoryTracker::new(),
209            active_sessions: HashMap::new(),
210        }
211    }
212
213    /// Start a profiling session
214    pub fn start_session(&mut self, name: String) -> UtilsResult<()> {
215        let start_time = Instant::now();
216        let start_memory = get_memory_usage()?;
217        self.active_sessions
218            .insert(name, (start_time, start_memory));
219        Ok(())
220    }
221
222    /// End a profiling session and record results
223    pub fn end_session(&mut self, name: &str) -> UtilsResult<ProfileResult> {
224        let (start_time, start_memory) = self
225            .active_sessions
226            .remove(name)
227            .ok_or_else(|| UtilsError::InvalidParameter(format!("Session '{name}' not found")))?;
228
229        let duration = start_time.elapsed();
230        let end_memory = get_memory_usage()?;
231        let memory_delta = end_memory as isize - start_memory as isize;
232
233        self.timer.measurements.insert(name.to_string(), duration);
234        self.memory
235            .measurements
236            .insert(name.to_string(), end_memory);
237
238        Ok(ProfileResult {
239            name: name.to_string(),
240            duration,
241            memory_delta,
242            start_memory,
243            end_memory,
244        })
245    }
246
247    /// Profile a closure
248    pub fn profile<F, R>(&mut self, name: String, f: F) -> UtilsResult<(R, ProfileResult)>
249    where
250        F: FnOnce() -> R,
251    {
252        self.start_session(name.clone())?;
253        let result = f();
254        let profile_result = self.end_session(&name)?;
255        Ok((result, profile_result))
256    }
257
258    /// Get timer reference
259    pub fn timer(&self) -> &Timer {
260        &self.timer
261    }
262
263    /// Get memory tracker reference
264    pub fn memory(&self) -> &MemoryTracker {
265        &self.memory
266    }
267
268    /// Clear all data
269    pub fn clear(&mut self) {
270        self.timer.clear();
271        self.memory.clear();
272        self.active_sessions.clear();
273    }
274
275    /// Generate a performance report
276    pub fn report(&self) -> ProfileReport {
277        ProfileReport {
278            timer_summary: self.timer.summary(),
279            peak_memory: self.memory.peak_usage(),
280            total_sessions: self.timer.measurements.len(),
281            active_sessions: self.active_sessions.len(),
282        }
283    }
284}
285
286impl Default for Profiler {
287    fn default() -> Self {
288        Self::new()
289    }
290}
291
292/// Result from a profiling session
293#[derive(Debug, Clone)]
294pub struct ProfileResult {
295    pub name: String,
296    pub duration: Duration,
297    pub memory_delta: isize,
298    pub start_memory: usize,
299    pub end_memory: usize,
300}
301
302/// Overall performance report
303#[derive(Debug, Clone)]
304pub struct ProfileReport {
305    pub timer_summary: TimerSummary,
306    pub peak_memory: Option<usize>,
307    pub total_sessions: usize,
308    pub active_sessions: usize,
309}
310
311/// Benchmark runner for repeated measurements
312#[derive(Debug)]
313pub struct Benchmark {
314    name: String,
315    iterations: usize,
316    warmup_iterations: usize,
317    measurements: Vec<Duration>,
318}
319
320impl Benchmark {
321    /// Create a new benchmark
322    pub fn new(name: String, iterations: usize) -> Self {
323        Self {
324            name,
325            iterations,
326            warmup_iterations: 10,
327            measurements: Vec::with_capacity(iterations),
328        }
329    }
330
331    /// Set number of warmup iterations
332    pub fn with_warmup(mut self, warmup_iterations: usize) -> Self {
333        self.warmup_iterations = warmup_iterations;
334        self
335    }
336
337    /// Run the benchmark
338    pub fn run<F>(&mut self, mut f: F) -> BenchmarkResult
339    where
340        F: FnMut(),
341    {
342        // Warmup phase
343        for _ in 0..self.warmup_iterations {
344            f();
345        }
346
347        // Measurement phase
348        self.measurements.clear();
349        for _ in 0..self.iterations {
350            let start = Instant::now();
351            f();
352            let duration = start.elapsed();
353            self.measurements.push(duration);
354        }
355
356        self.analyze()
357    }
358
359    fn analyze(&self) -> BenchmarkResult {
360        let mut measurements = self.measurements.clone();
361        measurements.sort();
362
363        let len = measurements.len();
364        let total: Duration = measurements.iter().sum();
365        let average = total / len as u32;
366
367        let median = if len % 2 == 0 {
368            (measurements[len / 2 - 1] + measurements[len / 2]) / 2
369        } else {
370            measurements[len / 2]
371        };
372
373        let min = measurements[0];
374        let max = measurements[len - 1];
375
376        // Calculate percentiles
377        let p95_index = ((len as f64) * 0.95) as usize;
378        let p99_index = ((len as f64) * 0.99) as usize;
379        let p95 = measurements[p95_index.min(len - 1)];
380        let p99 = measurements[p99_index.min(len - 1)];
381
382        // Calculate standard deviation
383        let variance_sum: f64 = measurements
384            .iter()
385            .map(|&d| {
386                let diff = d.as_secs_f64() - average.as_secs_f64();
387                diff * diff
388            })
389            .sum();
390        let variance = variance_sum / len as f64;
391        let std_dev = Duration::from_secs_f64(variance.sqrt());
392
393        BenchmarkResult {
394            name: self.name.clone(),
395            iterations: len,
396            total,
397            average,
398            median,
399            min,
400            max,
401            std_dev,
402            p95,
403            p99,
404        }
405    }
406}
407
408/// Benchmark analysis results
409#[derive(Debug, Clone)]
410pub struct BenchmarkResult {
411    pub name: String,
412    pub iterations: usize,
413    pub total: Duration,
414    pub average: Duration,
415    pub median: Duration,
416    pub min: Duration,
417    pub max: Duration,
418    pub std_dev: Duration,
419    pub p95: Duration,
420    pub p99: Duration,
421}
422
423impl BenchmarkResult {
424    /// Format results as a human-readable string
425    pub fn format(&self) -> String {
426        format!(
427            "Benchmark: {}\n\
428             Iterations: {}\n\
429             Average: {:?}\n\
430             Median: {:?}\n\
431             Min: {:?}\n\
432             Max: {:?}\n\
433             Std Dev: {:?}\n\
434             95th percentile: {:?}\n\
435             99th percentile: {:?}",
436            self.name,
437            self.iterations,
438            self.average,
439            self.median,
440            self.min,
441            self.max,
442            self.std_dev,
443            self.p95,
444            self.p99
445        )
446    }
447}
448
449/// Get current memory usage in bytes
450fn get_memory_usage() -> UtilsResult<usize> {
451    #[cfg(target_os = "linux")]
452    {
453        use std::fs;
454        let status = fs::read_to_string("/proc/self/status").map_err(|e| {
455            UtilsError::InvalidParameter(format!("Failed to read memory info: {e}"))
456        })?;
457
458        for line in status.lines() {
459            if line.starts_with("VmRSS:") {
460                let parts: Vec<&str> = line.split_whitespace().collect();
461                if parts.len() >= 2 {
462                    let kb: usize = parts[1].parse().map_err(|e| {
463                        UtilsError::InvalidParameter(format!("Failed to parse memory: {e}"))
464                    })?;
465                    return Ok(kb * 1024); // Convert KB to bytes
466                }
467            }
468        }
469        Err(UtilsError::InvalidParameter(
470            "Memory info not found".to_string(),
471        ))
472    }
473
474    #[cfg(target_os = "macos")]
475    {
476        // On macOS, we'll use a simpler approach with task_info
477        // For now, return a placeholder value
478        use std::process::Command;
479        let output = Command::new("ps")
480            .args(["-o", "rss=", "-p"])
481            .arg(std::process::id().to_string())
482            .output()
483            .map_err(|e| {
484                UtilsError::InvalidParameter(format!("Failed to get memory usage: {e}"))
485            })?;
486
487        let output_str = String::from_utf8_lossy(&output.stdout);
488        let kb: usize = output_str
489            .trim()
490            .parse()
491            .map_err(|e| UtilsError::InvalidParameter(format!("Failed to parse memory: {e}")))?;
492        Ok(kb * 1024) // Convert KB to bytes
493    }
494
495    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
496    {
497        // For other platforms, return a default value
498        Ok(0)
499    }
500}
501
502/// Performance regression detection system
503#[derive(Debug, Clone)]
504pub struct RegressionDetector {
505    baselines: HashMap<String, BaselineMetrics>,
506    threshold_factor: f64,
507    min_samples: usize,
508}
509
510/// Baseline performance metrics for regression detection
511#[derive(Debug, Clone)]
512pub struct BaselineMetrics {
513    pub average_duration: Duration,
514    pub std_dev: Duration,
515    pub sample_count: usize,
516    pub last_updated: std::time::SystemTime,
517    pub historical_durations: Vec<Duration>,
518}
519
520/// Regression detection result
521#[derive(Debug, Clone)]
522pub struct RegressionResult {
523    pub test_name: String,
524    pub current_duration: Duration,
525    pub baseline_average: Duration,
526    pub deviation_factor: f64,
527    pub is_regression: bool,
528    pub confidence_level: f64,
529}
530
531impl RegressionDetector {
532    /// Create a new regression detector
533    pub fn new() -> Self {
534        Self {
535            baselines: HashMap::new(),
536            threshold_factor: 1.5, // 50% slower is considered a regression
537            min_samples: 5,
538        }
539    }
540
541    /// Set the regression threshold factor (e.g., 1.5 = 50% slower)
542    pub fn with_threshold(mut self, threshold_factor: f64) -> Self {
543        self.threshold_factor = threshold_factor;
544        self
545    }
546
547    /// Set minimum samples required for regression detection
548    pub fn with_min_samples(mut self, min_samples: usize) -> Self {
549        self.min_samples = min_samples;
550        self
551    }
552
553    /// Record a baseline measurement
554    pub fn record_baseline(&mut self, test_name: String, duration: Duration) {
555        let test_name_clone = test_name.clone();
556        let entry = self
557            .baselines
558            .entry(test_name)
559            .or_insert_with(|| BaselineMetrics {
560                average_duration: Duration::from_secs(0),
561                std_dev: Duration::from_secs(0),
562                sample_count: 0,
563                last_updated: std::time::SystemTime::now(),
564                historical_durations: Vec::new(),
565            });
566
567        entry.historical_durations.push(duration);
568        entry.sample_count += 1;
569        entry.last_updated = std::time::SystemTime::now();
570
571        // Keep only the last 100 measurements to avoid unbounded growth
572        if entry.historical_durations.len() > 100 {
573            entry.historical_durations.remove(0);
574        }
575
576        // Recalculate statistics - work with a cloned entry to avoid borrowing issues
577        let mut entry_clone = entry.clone();
578        let _ = entry; // Release the mutable reference to self.baselines
579        self.update_statistics(&mut entry_clone, &test_name_clone);
580
581        // Update the entry back in the map
582        if let Some(stored_entry) = self.baselines.get_mut(&test_name_clone) {
583            *stored_entry = entry_clone;
584        }
585    }
586
587    /// Check for performance regression
588    pub fn check_regression(
589        &self,
590        test_name: &str,
591        current_duration: Duration,
592    ) -> Option<RegressionResult> {
593        let baseline = self.baselines.get(test_name)?;
594
595        if baseline.sample_count < self.min_samples {
596            return None;
597        }
598
599        let current_secs = current_duration.as_secs_f64();
600        let baseline_secs = baseline.average_duration.as_secs_f64();
601        let deviation_factor = current_secs / baseline_secs;
602
603        let is_regression = deviation_factor > self.threshold_factor;
604
605        // Calculate confidence level using t-test-like approach
606        let std_dev_secs = baseline.std_dev.as_secs_f64();
607        let z_score =
608            (current_secs - baseline_secs) / (std_dev_secs / (baseline.sample_count as f64).sqrt());
609        let confidence_level = if z_score > 0.0 {
610            1.0 - (-z_score * z_score / 2.0).exp() // Approximation of normal CDF
611        } else {
612            0.0
613        };
614
615        Some(RegressionResult {
616            test_name: test_name.to_string(),
617            current_duration,
618            baseline_average: baseline.average_duration,
619            deviation_factor,
620            is_regression,
621            confidence_level,
622        })
623    }
624
625    /// Get baseline metrics for a test
626    pub fn get_baseline(&self, test_name: &str) -> Option<&BaselineMetrics> {
627        self.baselines.get(test_name)
628    }
629
630    /// List all tracked tests
631    pub fn tracked_tests(&self) -> Vec<&String> {
632        self.baselines.keys().collect()
633    }
634
635    /// Clear all baselines
636    pub fn clear_baselines(&mut self) {
637        self.baselines.clear();
638    }
639
640    /// Update statistics for a baseline
641    fn update_statistics(&mut self, baseline: &mut BaselineMetrics, test_name: &str) {
642        if baseline.historical_durations.is_empty() {
643            return;
644        }
645
646        // Calculate average
647        let total: Duration = baseline.historical_durations.iter().sum();
648        baseline.average_duration = total / baseline.historical_durations.len() as u32;
649
650        // Calculate standard deviation
651        let mean_secs = baseline.average_duration.as_secs_f64();
652        let variance_sum: f64 = baseline
653            .historical_durations
654            .iter()
655            .map(|d| {
656                let diff = d.as_secs_f64() - mean_secs;
657                diff * diff
658            })
659            .sum();
660
661        let variance = variance_sum / baseline.historical_durations.len() as f64;
662        baseline.std_dev = Duration::from_secs_f64(variance.sqrt());
663
664        // Update the entry in the map
665        self.baselines
666            .insert(test_name.to_string(), baseline.clone());
667    }
668}
669
670impl Default for RegressionDetector {
671    fn default() -> Self {
672        Self::new()
673    }
674}
675
676impl RegressionResult {
677    /// Format the regression result as a human-readable string
678    pub fn format(&self) -> String {
679        let status = if self.is_regression {
680            "REGRESSION DETECTED"
681        } else {
682            "OK"
683        };
684        format!(
685            "Test: {} [{}]\n\
686             Current: {:?}\n\
687             Baseline: {:?}\n\
688             Deviation: {:.2}x\n\
689             Confidence: {:.1}%",
690            self.test_name,
691            status,
692            self.current_duration,
693            self.baseline_average,
694            self.deviation_factor,
695            self.confidence_level * 100.0
696        )
697    }
698}
699
700#[allow(non_snake_case)]
701#[cfg(test)]
702mod tests {
703    use super::*;
704    use std::thread;
705
706    #[test]
707    fn test_timer_basic() {
708        let mut timer = Timer::new();
709
710        timer.start();
711        thread::sleep(Duration::from_millis(10));
712        let duration = timer.stop().unwrap();
713
714        assert!(duration >= Duration::from_millis(10));
715        assert!(duration < Duration::from_millis(50)); // Allow some tolerance
716    }
717
718    #[test]
719    fn test_timer_closure() {
720        let mut timer = Timer::new();
721
722        let (result, duration) = timer.time(|| {
723            thread::sleep(Duration::from_millis(10));
724            42
725        });
726
727        assert_eq!(result, 42);
728        assert!(duration >= Duration::from_millis(10));
729    }
730
731    #[test]
732    fn test_timer_store() {
733        let mut timer = Timer::new();
734
735        timer.start();
736        thread::sleep(Duration::from_millis(10));
737        timer.stop_and_store("test".to_string()).unwrap();
738
739        assert!(timer.get_measurement("test").is_some());
740        assert!(timer.get_measurement("test").unwrap() >= Duration::from_millis(10));
741    }
742
743    #[test]
744    fn test_timer_summary() {
745        let mut timer = Timer::new();
746
747        timer
748            .measurements
749            .insert("test1".to_string(), Duration::from_millis(100));
750        timer
751            .measurements
752            .insert("test2".to_string(), Duration::from_millis(200));
753        timer
754            .measurements
755            .insert("test3".to_string(), Duration::from_millis(300));
756
757        let summary = timer.summary();
758        assert_eq!(summary.count, 3);
759        assert_eq!(summary.min, Duration::from_millis(100));
760        assert_eq!(summary.max, Duration::from_millis(300));
761        assert_eq!(summary.average, Duration::from_millis(200));
762    }
763
764    #[test]
765    fn test_memory_tracker() {
766        let mut tracker = MemoryTracker::new();
767
768        tracker.set_baseline();
769        tracker.record("test".to_string()).unwrap();
770
771        assert!(tracker.measurements().contains_key("test"));
772        assert!(tracker.peak_usage().is_some());
773    }
774
775    #[test]
776    fn test_profiler() {
777        let mut profiler = Profiler::new();
778
779        let (result, profile_result) = profiler
780            .profile("test".to_string(), || {
781                thread::sleep(Duration::from_millis(10));
782                42
783            })
784            .unwrap();
785
786        assert_eq!(result, 42);
787        assert_eq!(profile_result.name, "test");
788        assert!(profile_result.duration >= Duration::from_millis(10));
789    }
790
791    #[test]
792    fn test_benchmark() {
793        let mut benchmark = Benchmark::new("test_benchmark".to_string(), 10).with_warmup(2);
794
795        let result = benchmark.run(|| {
796            thread::sleep(Duration::from_millis(1));
797        });
798
799        assert_eq!(result.name, "test_benchmark");
800        assert_eq!(result.iterations, 10);
801        assert!(result.average >= Duration::from_millis(1));
802        assert!(result.min <= result.median);
803        assert!(result.median <= result.max);
804    }
805
806    #[test]
807    fn test_benchmark_result_format() {
808        let result = BenchmarkResult {
809            name: "test".to_string(),
810            iterations: 100,
811            total: Duration::from_millis(1000),
812            average: Duration::from_millis(10),
813            median: Duration::from_millis(9),
814            min: Duration::from_millis(5),
815            max: Duration::from_millis(20),
816            std_dev: Duration::from_millis(2),
817            p95: Duration::from_millis(15),
818            p99: Duration::from_millis(18),
819        };
820
821        let formatted = result.format();
822        assert!(formatted.contains("test"));
823        assert!(formatted.contains("100"));
824        assert!(formatted.contains("10ms"));
825    }
826
827    #[test]
828    fn test_profiler_sessions() {
829        let mut profiler = Profiler::new();
830
831        profiler.start_session("session1".to_string()).unwrap();
832        thread::sleep(Duration::from_millis(10));
833        let result = profiler.end_session("session1").unwrap();
834
835        assert_eq!(result.name, "session1");
836        assert!(result.duration >= Duration::from_millis(10));
837    }
838
839    #[test]
840    fn test_profiler_report() {
841        let mut profiler = Profiler::new();
842
843        profiler.profile("test1".to_string(), || {}).unwrap();
844        profiler.profile("test2".to_string(), || {}).unwrap();
845
846        let report = profiler.report();
847        assert_eq!(report.total_sessions, 2);
848        assert_eq!(report.active_sessions, 0);
849    }
850
851    #[test]
852    fn test_regression_detector_baseline() {
853        let mut detector = RegressionDetector::new();
854
855        // Record several baseline measurements
856        for i in 0..10 {
857            detector.record_baseline("test_func".to_string(), Duration::from_millis(100 + i));
858        }
859
860        let baseline = detector.get_baseline("test_func").unwrap();
861        assert_eq!(baseline.sample_count, 10);
862        assert!(baseline.average_duration >= Duration::from_millis(100));
863        assert!(baseline.average_duration <= Duration::from_millis(110));
864    }
865
866    #[test]
867    fn test_regression_detector_no_regression() {
868        let mut detector = RegressionDetector::new().with_min_samples(3);
869
870        // Record baseline measurements around 100ms
871        for _ in 0..5 {
872            detector.record_baseline("test_func".to_string(), Duration::from_millis(100));
873        }
874
875        // Test with similar performance (no regression)
876        let result = detector.check_regression("test_func", Duration::from_millis(105));
877        assert!(result.is_some());
878        let result = result.unwrap();
879        assert!(!result.is_regression);
880        assert_eq!(result.test_name, "test_func");
881    }
882
883    #[test]
884    fn test_regression_detector_with_regression() {
885        let mut detector = RegressionDetector::new()
886            .with_min_samples(3)
887            .with_threshold(1.5);
888
889        // Record baseline measurements around 100ms
890        for _ in 0..5 {
891            detector.record_baseline("test_func".to_string(), Duration::from_millis(100));
892        }
893
894        // Test with 2x slower performance (regression)
895        let result = detector.check_regression("test_func", Duration::from_millis(200));
896        assert!(result.is_some());
897        let result = result.unwrap();
898        assert!(result.is_regression);
899        assert!(result.deviation_factor > 1.5);
900    }
901
902    #[test]
903    fn test_regression_detector_insufficient_samples() {
904        let mut detector = RegressionDetector::new().with_min_samples(5);
905
906        // Record only 2 baseline measurements (less than min_samples)
907        for _ in 0..2 {
908            detector.record_baseline("test_func".to_string(), Duration::from_millis(100));
909        }
910
911        // Should return None due to insufficient samples
912        let result = detector.check_regression("test_func", Duration::from_millis(200));
913        assert!(result.is_none());
914    }
915
916    #[test]
917    fn test_regression_detector_tracked_tests() {
918        let mut detector = RegressionDetector::new();
919
920        detector.record_baseline("test1".to_string(), Duration::from_millis(100));
921        detector.record_baseline("test2".to_string(), Duration::from_millis(200));
922
923        let tracked = detector.tracked_tests();
924        assert_eq!(tracked.len(), 2);
925        assert!(tracked.contains(&&"test1".to_string()));
926        assert!(tracked.contains(&&"test2".to_string()));
927    }
928
929    #[test]
930    fn test_regression_result_format() {
931        let result = RegressionResult {
932            test_name: "test_function".to_string(),
933            current_duration: Duration::from_millis(200),
934            baseline_average: Duration::from_millis(100),
935            deviation_factor: 2.0,
936            is_regression: true,
937            confidence_level: 0.95,
938        };
939
940        let formatted = result.format();
941        assert!(formatted.contains("test_function"));
942        assert!(formatted.contains("REGRESSION DETECTED"));
943        assert!(formatted.contains("2.00x"));
944        assert!(formatted.contains("95.0%"));
945    }
946
947    #[test]
948    fn test_regression_detector_clear() {
949        let mut detector = RegressionDetector::new();
950
951        detector.record_baseline("test1".to_string(), Duration::from_millis(100));
952        detector.record_baseline("test2".to_string(), Duration::from_millis(200));
953
954        assert_eq!(detector.tracked_tests().len(), 2);
955
956        detector.clear_baselines();
957        assert_eq!(detector.tracked_tests().len(), 0);
958    }
959}