Skip to main content

trustformers_debug/
regression_detector.rs

1//! AI-powered Performance Regression Detection System
2//!
3//! This module provides advanced statistical analysis and machine learning-based
4//! detection of performance regressions in model training and inference, enabling
5//! early detection of performance degradation with high accuracy.
6// reason: debug/profiling scaffolding — structs are constructed and their fields/methods
7// are retained for the data model, serialization completeness, and future consumers that
8// do not yet read every member. Consolidated from many item-level #[allow(dead_code)].
9#![allow(dead_code)]
10
11use anyhow::Result;
12use serde::{Deserialize, Serialize};
13use std::collections::{HashMap, VecDeque};
14use std::time::SystemTime;
15use tracing::info;
16use uuid::Uuid;
17
18/// Configuration for regression detection
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct RegressionDetectionConfig {
21    /// Enable regression detection
22    pub enable_detection: bool,
23    /// Minimum number of data points for analysis
24    pub min_data_points: usize,
25    /// Statistical significance threshold (p-value)
26    pub significance_threshold: f64,
27    /// Minimum performance degradation percentage to trigger alert
28    pub min_degradation_threshold: f64,
29    /// Maximum historical data window in hours
30    pub max_history_hours: u64,
31    /// Smoothing factor for exponential moving averages
32    pub ema_smoothing_factor: f64,
33    /// Enable advanced ML-based detection
34    pub enable_ml_detection: bool,
35    /// Confidence threshold for ML predictions
36    pub ml_confidence_threshold: f64,
37    /// Enable seasonal adjustment
38    pub enable_seasonal_adjustment: bool,
39    /// Enable outlier detection before regression analysis
40    pub enable_outlier_filtering: bool,
41}
42
43impl Default for RegressionDetectionConfig {
44    fn default() -> Self {
45        Self {
46            enable_detection: true,
47            min_data_points: 10,
48            significance_threshold: 0.05,
49            min_degradation_threshold: 5.0, // 5% degradation
50            max_history_hours: 24,
51            ema_smoothing_factor: 0.3,
52            enable_ml_detection: true,
53            ml_confidence_threshold: 0.8,
54            enable_seasonal_adjustment: true,
55            enable_outlier_filtering: true,
56        }
57    }
58}
59
60/// Types of metrics to monitor for regressions
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
62pub enum MetricType {
63    /// Training/inference latency
64    Latency,
65    /// Memory usage
66    MemoryUsage,
67    /// CPU utilization
68    CpuUtilization,
69    /// GPU utilization
70    GpuUtilization,
71    /// Throughput (operations per second)
72    Throughput,
73    /// Model accuracy/loss
74    ModelAccuracy,
75    /// Custom metric
76    Custom(String),
77}
78
79/// Performance metric data point
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct MetricDataPoint {
82    pub metric_type: MetricType,
83    pub value: f64,
84    pub timestamp: SystemTime,
85    pub session_id: Uuid,
86    pub metadata: HashMap<String, String>,
87}
88
89/// Historical metric series for analysis
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct MetricSeries {
92    pub metric_type: MetricType,
93    pub data_points: VecDeque<MetricDataPoint>,
94    pub baseline_statistics: BaselineStatistics,
95    pub last_updated: SystemTime,
96}
97
98/// Baseline statistics for comparison
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct BaselineStatistics {
101    pub mean: f64,
102    pub std_dev: f64,
103    pub median: f64,
104    pub percentile_95: f64,
105    pub percentile_99: f64,
106    pub trend_slope: f64,
107    pub seasonal_pattern: Option<Vec<f64>>,
108    pub sample_count: usize,
109    pub last_computed: SystemTime,
110}
111
112/// Regression detection result
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct RegressionDetection {
115    pub detection_id: Uuid,
116    pub metric_type: MetricType,
117    pub regression_type: RegressionType,
118    pub severity: RegressionSeverity,
119    pub confidence: f64,
120    pub degradation_percentage: f64,
121    pub statistical_significance: f64,
122    pub affected_period: (SystemTime, SystemTime),
123    pub root_cause_analysis: RootCauseAnalysis,
124    pub recommendations: Vec<String>,
125    pub detected_at: SystemTime,
126}
127
128/// Types of performance regressions
129#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
130pub enum RegressionType {
131    /// Sudden step change in performance
132    StepChange,
133    /// Gradual degradation over time
134    GradualDegradation,
135    /// Increased variance/instability
136    VarianceIncrease,
137    /// Periodic performance drops
138    PeriodicRegression,
139    /// Outlier-driven regression
140    OutlierRegression,
141    /// Complex multi-factorial regression
142    ComplexRegression,
143}
144
145/// Severity levels for regressions
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
147pub enum RegressionSeverity {
148    Low,
149    Medium,
150    High,
151    Critical,
152}
153
154/// Root cause analysis results
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct RootCauseAnalysis {
157    pub likely_causes: Vec<PotentialCause>,
158    pub correlated_metrics: Vec<String>,
159    pub environmental_factors: Vec<String>,
160    pub change_points: Vec<SystemTime>,
161    pub anomaly_score: f64,
162}
163
164/// Potential cause for performance regression
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct PotentialCause {
167    pub cause_type: CauseType,
168    pub description: String,
169    pub confidence: f64,
170    pub supporting_evidence: Vec<String>,
171}
172
173/// Types of potential causes
174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
175pub enum CauseType {
176    CodeChange,
177    DataChange,
178    ResourceContention,
179    HardwareIssue,
180    ConfigurationChange,
181    EnvironmentalFactor,
182    ModelDrift,
183    Unknown,
184}
185
186/// Advanced regression detector with ML capabilities
187pub struct RegressionDetector {
188    config: RegressionDetectionConfig,
189    metric_series: HashMap<MetricType, MetricSeries>,
190    anomaly_detector: AnomalyDetector,
191    trend_analyzer: TrendAnalyzer,
192    change_point_detector: ChangePointDetector,
193    seasonal_decomposer: SeasonalDecomposer,
194    ml_predictor: Option<MLPredictor>,
195    detection_history: VecDeque<RegressionDetection>,
196}
197
198/// Statistical anomaly detector
199#[derive(Debug)]
200struct AnomalyDetector {
201    z_score_threshold: f64,
202    iqr_multiplier: f64,
203    isolation_forest_threshold: f64,
204}
205
206impl AnomalyDetector {
207    fn new() -> Self {
208        Self {
209            z_score_threshold: 3.0,
210            iqr_multiplier: 1.5,
211            isolation_forest_threshold: 0.1,
212        }
213    }
214
215    /// Detect outliers using multiple methods
216    fn detect_outliers(&self, values: &[f64]) -> Vec<bool> {
217        if values.is_empty() {
218            return vec![];
219        }
220
221        let z_score_outliers = self.detect_z_score_outliers(values);
222        let iqr_outliers = self.detect_iqr_outliers(values);
223
224        // Combine methods using majority voting
225        z_score_outliers
226            .iter()
227            .zip(iqr_outliers.iter())
228            .map(|(&z_outlier, &iqr_outlier)| z_outlier || iqr_outlier)
229            .collect()
230    }
231
232    fn detect_z_score_outliers(&self, values: &[f64]) -> Vec<bool> {
233        let mean = values.iter().sum::<f64>() / values.len() as f64;
234        let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
235        let std_dev = variance.sqrt();
236
237        values
238            .iter()
239            .map(|&value| {
240                if std_dev > 0.0 {
241                    ((value - mean) / std_dev).abs() > self.z_score_threshold
242                } else {
243                    false
244                }
245            })
246            .collect()
247    }
248
249    fn detect_iqr_outliers(&self, values: &[f64]) -> Vec<bool> {
250        let mut sorted_values = values.to_vec();
251        sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
252
253        let q1 = Self::percentile(&sorted_values, 25.0);
254        let q3 = Self::percentile(&sorted_values, 75.0);
255        let iqr = q3 - q1;
256
257        let lower_bound = q1 - self.iqr_multiplier * iqr;
258        let upper_bound = q3 + self.iqr_multiplier * iqr;
259
260        values.iter().map(|&value| value < lower_bound || value > upper_bound).collect()
261    }
262
263    fn percentile(sorted_values: &[f64], percentile: f64) -> f64 {
264        if sorted_values.is_empty() {
265            return 0.0;
266        }
267
268        let index = (percentile / 100.0) * (sorted_values.len() - 1) as f64;
269        let lower = index.floor() as usize;
270        let upper = index.ceil() as usize;
271
272        if lower == upper {
273            sorted_values[lower]
274        } else {
275            let weight = index - lower as f64;
276            sorted_values[lower] * (1.0 - weight) + sorted_values[upper] * weight
277        }
278    }
279}
280
281/// Trend analysis for regression detection
282#[derive(Debug)]
283struct TrendAnalyzer {
284    window_size: usize,
285    significance_threshold: f64,
286}
287
288impl TrendAnalyzer {
289    fn new(window_size: usize, significance_threshold: f64) -> Self {
290        Self {
291            window_size,
292            significance_threshold,
293        }
294    }
295
296    /// Detect trend changes using linear regression
297    fn detect_trend_change(&self, values: &[f64]) -> Option<TrendChangeResult> {
298        if values.len() < self.window_size {
299            return None;
300        }
301
302        let recent_values = &values[values.len() - self.window_size..];
303        let baseline_values = if values.len() >= 2 * self.window_size {
304            &values[values.len() - 2 * self.window_size..values.len() - self.window_size]
305        } else {
306            &values[0..values.len() - self.window_size]
307        };
308
309        let recent_slope = self.calculate_slope(recent_values);
310        let baseline_slope = self.calculate_slope(baseline_values);
311
312        let slope_change = recent_slope - baseline_slope;
313        let significance = self.calculate_trend_significance(recent_values, recent_slope);
314
315        if significance < self.significance_threshold {
316            Some(TrendChangeResult {
317                slope_change,
318                recent_slope,
319                baseline_slope,
320                significance,
321                is_regression: slope_change > 0.0, // Positive slope = performance degradation
322            })
323        } else {
324            None
325        }
326    }
327
328    fn calculate_slope(&self, values: &[f64]) -> f64 {
329        if values.len() < 2 {
330            return 0.0;
331        }
332
333        let n = values.len() as f64;
334        let sum_x = (0..values.len()).sum::<usize>() as f64;
335        let sum_y = values.iter().sum::<f64>();
336        let sum_xy = values.iter().enumerate().map(|(i, &y)| i as f64 * y).sum::<f64>();
337        let sum_x_squared = (0..values.len()).map(|i| (i as f64).powi(2)).sum::<f64>();
338
339        let denominator = n * sum_x_squared - sum_x.powi(2);
340        if denominator.abs() < 1e-10 {
341            0.0
342        } else {
343            (n * sum_xy - sum_x * sum_y) / denominator
344        }
345    }
346
347    fn calculate_trend_significance(&self, values: &[f64], slope: f64) -> f64 {
348        // Simplified t-test for trend significance
349        if values.len() < 3 {
350            return 1.0;
351        }
352
353        let n = values.len() as f64;
354        let mean_x = (values.len() - 1) as f64 / 2.0;
355        let ss_x = (0..values.len()).map(|i| (i as f64 - mean_x).powi(2)).sum::<f64>();
356
357        // Calculate residuals with proper intercept
358        let mean_y = values.iter().sum::<f64>() / n;
359        let intercept = mean_y - slope * mean_x;
360        let predicted: Vec<f64> = (0..values.len()).map(|i| intercept + slope * i as f64).collect();
361
362        let residuals: Vec<f64> = values
363            .iter()
364            .zip(predicted.iter())
365            .map(|(&actual, &pred)| actual - pred)
366            .collect();
367
368        let mse = residuals.iter().map(|&r| r.powi(2)).sum::<f64>() / (n - 2.0);
369        let se_slope = (mse / ss_x).sqrt();
370
371        if se_slope > 0.0 {
372            let t_stat = slope / se_slope;
373            // Simplified p-value approximation
374            let df = n - 2.0;
375            if df > 0.0 {
376                2.0 * (1.0 - Self::t_distribution_cdf(t_stat.abs(), df))
377            } else {
378                1.0
379            }
380        } else {
381            1.0
382        }
383    }
384
385    fn t_distribution_cdf(t: f64, df: f64) -> f64 {
386        // Simplified approximation of t-distribution CDF
387        // In practice, would use a proper statistical library
388        let x = t / (df + t.powi(2)).sqrt();
389        0.5 + 0.5 * x.atan() * (2.0 / std::f64::consts::PI)
390    }
391}
392
393#[derive(Debug)]
394struct TrendChangeResult {
395    slope_change: f64,
396    recent_slope: f64,
397    baseline_slope: f64,
398    significance: f64,
399    is_regression: bool,
400}
401
402/// Change point detection using statistical methods
403#[derive(Debug)]
404struct ChangePointDetector {
405    min_segment_length: usize,
406    penalty_factor: f64,
407}
408
409impl ChangePointDetector {
410    fn new(min_segment_length: usize, penalty_factor: f64) -> Self {
411        Self {
412            min_segment_length,
413            penalty_factor,
414        }
415    }
416
417    /// Detect change points using CUSUM algorithm
418    fn detect_change_points(&self, values: &[f64]) -> Vec<usize> {
419        if values.len() < 2 * self.min_segment_length {
420            return vec![];
421        }
422
423        let mut change_points = vec![];
424        let mut current_start = 0;
425
426        while current_start + 2 * self.min_segment_length <= values.len() {
427            if let Some(change_point) = self.find_next_change_point(&values[current_start..]) {
428                let absolute_change_point = current_start + change_point;
429                change_points.push(absolute_change_point);
430                current_start = absolute_change_point + self.min_segment_length;
431            } else {
432                break;
433            }
434        }
435
436        change_points
437    }
438
439    fn find_next_change_point(&self, values: &[f64]) -> Option<usize> {
440        let n = values.len();
441        if n < 2 * self.min_segment_length {
442            return None;
443        }
444
445        let mut max_statistic = 0.0;
446        let mut best_change_point = None;
447
448        for t in self.min_segment_length..n - self.min_segment_length {
449            let statistic = self.cusum_statistic(values, t);
450            if statistic > max_statistic {
451                max_statistic = statistic;
452                best_change_point = Some(t);
453            }
454        }
455
456        // Apply penalty for multiple change points
457        let threshold = self.penalty_factor * (n as f64).ln();
458        if max_statistic > threshold {
459            best_change_point
460        } else {
461            None
462        }
463    }
464
465    fn cusum_statistic(&self, values: &[f64], change_point: usize) -> f64 {
466        let segment1 = &values[0..change_point];
467        let segment2 = &values[change_point..];
468
469        let mean1 = segment1.iter().sum::<f64>() / segment1.len() as f64;
470        let mean2 = segment2.iter().sum::<f64>() / segment2.len() as f64;
471        let overall_mean = values.iter().sum::<f64>() / values.len() as f64;
472
473        let n1 = segment1.len() as f64;
474        let n2 = segment2.len() as f64;
475        let n = values.len() as f64;
476
477        // Calculate variance
478        let variance = values.iter().map(|&x| (x - overall_mean).powi(2)).sum::<f64>() / (n - 1.0);
479
480        if variance > 0.0 {
481            (n1 * (mean1 - overall_mean).powi(2) + n2 * (mean2 - overall_mean).powi(2)) / variance
482        } else {
483            0.0
484        }
485    }
486}
487
488/// Seasonal decomposition for time series analysis
489#[derive(Debug)]
490struct SeasonalDecomposer {
491    period: usize,
492    enable_decomposition: bool,
493}
494
495impl SeasonalDecomposer {
496    fn new(period: usize) -> Self {
497        Self {
498            period,
499            enable_decomposition: true,
500        }
501    }
502
503    /// Decompose time series into trend, seasonal, and residual components
504    fn decompose(&self, values: &[f64]) -> Option<SeasonalComponents> {
505        if !self.enable_decomposition || values.len() < 2 * self.period {
506            return None;
507        }
508
509        let trend = self.extract_trend(values);
510        let detrended = self.subtract_series(values, &trend);
511        let seasonal = self.extract_seasonal(&detrended);
512        let residual = self.subtract_series(&detrended, &seasonal);
513
514        Some(SeasonalComponents {
515            trend,
516            seasonal,
517            residual,
518        })
519    }
520
521    fn extract_trend(&self, values: &[f64]) -> Vec<f64> {
522        // Moving average for trend extraction
523        let window_size = self.period;
524        let mut trend = vec![0.0; values.len()];
525
526        for i in 0..values.len() {
527            let start = i.saturating_sub(window_size / 2);
528            let end = std::cmp::min(i + window_size / 2 + 1, values.len());
529
530            let sum: f64 = values[start..end].iter().sum();
531            trend[i] = sum / (end - start) as f64;
532        }
533
534        trend
535    }
536
537    fn extract_seasonal(&self, detrended: &[f64]) -> Vec<f64> {
538        let mut seasonal = vec![0.0; detrended.len()];
539        let mut seasonal_pattern = vec![0.0; self.period];
540        let mut pattern_counts = vec![0usize; self.period];
541
542        // Calculate average seasonal pattern
543        for (i, &value) in detrended.iter().enumerate() {
544            let season_index = i % self.period;
545            seasonal_pattern[season_index] += value;
546            pattern_counts[season_index] += 1;
547        }
548
549        // Normalize by counts
550        for i in 0..self.period {
551            if pattern_counts[i] > 0 {
552                seasonal_pattern[i] /= pattern_counts[i] as f64;
553            }
554        }
555
556        // Apply seasonal pattern
557        for (i, seasonal_value) in seasonal.iter_mut().enumerate() {
558            *seasonal_value = seasonal_pattern[i % self.period];
559        }
560
561        seasonal
562    }
563
564    fn subtract_series(&self, series1: &[f64], series2: &[f64]) -> Vec<f64> {
565        series1.iter().zip(series2.iter()).map(|(&a, &b)| a - b).collect()
566    }
567}
568
569#[derive(Debug, Clone, Serialize, Deserialize)]
570struct SeasonalComponents {
571    trend: Vec<f64>,
572    seasonal: Vec<f64>,
573    residual: Vec<f64>,
574}
575
576/// ML-based predictor for advanced regression detection
577#[derive(Debug)]
578struct MLPredictor {
579    model_type: MLModelType,
580    feature_extractor: FeatureExtractor,
581    prediction_threshold: f64,
582}
583
584#[derive(Debug)]
585enum MLModelType {
586    IsolationForest,
587    LSTM,
588    AutoEncoder,
589}
590
591#[derive(Debug)]
592struct FeatureExtractor {
593    window_size: usize,
594    statistical_features: bool,
595    frequency_features: bool,
596}
597
598impl MLPredictor {
599    fn new(model_type: MLModelType, prediction_threshold: f64) -> Self {
600        Self {
601            model_type,
602            feature_extractor: FeatureExtractor {
603                window_size: 50,
604                statistical_features: true,
605                frequency_features: true,
606            },
607            prediction_threshold,
608        }
609    }
610
611    /// Predict if current pattern indicates regression
612    fn predict_regression(&self, values: &[f64]) -> Option<MLPrediction> {
613        if values.len() < self.feature_extractor.window_size {
614            return None;
615        }
616
617        let features = self.feature_extractor.extract_features(values);
618
619        // Simplified ML prediction (in practice would use trained models)
620        let anomaly_score = self.calculate_anomaly_score(&features);
621        let confidence = self.calculate_confidence(&features);
622
623        if anomaly_score > self.prediction_threshold {
624            Some(MLPrediction {
625                anomaly_score,
626                confidence,
627                feature_importance: self.calculate_feature_importance(&features),
628                predicted_severity: self.predict_severity(anomaly_score),
629            })
630        } else {
631            None
632        }
633    }
634
635    fn calculate_anomaly_score(&self, features: &[f64]) -> f64 {
636        // Simplified anomaly scoring based on feature deviation
637        let mean = features.iter().sum::<f64>() / features.len() as f64;
638        let variance =
639            features.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / features.len() as f64;
640
641        variance.sqrt() / (mean.abs() + 1e-6)
642    }
643
644    fn calculate_confidence(&self, features: &[f64]) -> f64 {
645        // Simplified confidence calculation
646        let feature_consistency = 1.0
647            - (features.iter().map(|&x| (x - features[0]).abs()).sum::<f64>()
648                / (features.len() as f64 * features[0].abs() + 1e-6));
649
650        feature_consistency.max(0.0).min(1.0)
651    }
652
653    fn calculate_feature_importance(&self, features: &[f64]) -> Vec<f64> {
654        // Simplified feature importance based on magnitude
655        let max_magnitude = features.iter().map(|x| x.abs()).fold(0.0, f64::max);
656
657        if max_magnitude > 0.0 {
658            features.iter().map(|&x| x.abs() / max_magnitude).collect()
659        } else {
660            vec![0.0; features.len()]
661        }
662    }
663
664    fn predict_severity(&self, anomaly_score: f64) -> RegressionSeverity {
665        if anomaly_score > 0.8 {
666            RegressionSeverity::Critical
667        } else if anomaly_score > 0.6 {
668            RegressionSeverity::High
669        } else if anomaly_score > 0.4 {
670            RegressionSeverity::Medium
671        } else {
672            RegressionSeverity::Low
673        }
674    }
675}
676
677impl FeatureExtractor {
678    fn extract_features(&self, values: &[f64]) -> Vec<f64> {
679        let mut features = Vec::new();
680
681        if self.statistical_features {
682            features.extend(self.extract_statistical_features(values));
683        }
684
685        if self.frequency_features {
686            features.extend(self.extract_frequency_features(values));
687        }
688
689        features
690    }
691
692    fn extract_statistical_features(&self, values: &[f64]) -> Vec<f64> {
693        let mean = values.iter().sum::<f64>() / values.len() as f64;
694        let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
695        let std_dev = variance.sqrt();
696
697        let min = values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
698        let max = values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
699        let range = max - min;
700
701        // Skewness
702        let skewness = if std_dev > 0.0 {
703            values.iter().map(|x| ((x - mean) / std_dev).powi(3)).sum::<f64>() / values.len() as f64
704        } else {
705            0.0
706        };
707
708        // Kurtosis
709        let kurtosis = if std_dev > 0.0 {
710            values.iter().map(|x| ((x - mean) / std_dev).powi(4)).sum::<f64>() / values.len() as f64
711                - 3.0
712        } else {
713            0.0
714        };
715
716        vec![mean, std_dev, min, max, range, skewness, kurtosis]
717    }
718
719    fn extract_frequency_features(&self, values: &[f64]) -> Vec<f64> {
720        // Simplified frequency domain features
721        let mut features = Vec::new();
722
723        // Calculate differences to get "frequencies"
724        let differences: Vec<f64> = values.windows(2).map(|w| (w[1] - w[0]).abs()).collect();
725
726        if !differences.is_empty() {
727            let mean_diff = differences.iter().sum::<f64>() / differences.len() as f64;
728            let max_diff = differences.iter().fold(0.0f64, |a, &b| a.max(b));
729            features.extend([mean_diff, max_diff]);
730        }
731
732        features
733    }
734}
735
736#[derive(Debug, Clone, Serialize, Deserialize)]
737struct MLPrediction {
738    anomaly_score: f64,
739    confidence: f64,
740    feature_importance: Vec<f64>,
741    predicted_severity: RegressionSeverity,
742}
743
744impl RegressionDetector {
745    /// Create a new regression detector
746    pub fn new(config: RegressionDetectionConfig) -> Self {
747        let ml_predictor = if config.enable_ml_detection {
748            Some(MLPredictor::new(
749                MLModelType::IsolationForest,
750                config.ml_confidence_threshold,
751            ))
752        } else {
753            None
754        };
755
756        let trend_analyzer =
757            TrendAnalyzer::new(config.min_data_points, config.significance_threshold);
758
759        Self {
760            config,
761            metric_series: HashMap::new(),
762            anomaly_detector: AnomalyDetector::new(),
763            trend_analyzer,
764            change_point_detector: ChangePointDetector::new(5, 2.0),
765            seasonal_decomposer: SeasonalDecomposer::new(24), // Hourly patterns
766            ml_predictor,
767            detection_history: VecDeque::new(),
768        }
769    }
770
771    /// Add a new metric data point
772    pub fn add_metric_data_point(&mut self, data_point: MetricDataPoint) -> Result<()> {
773        let metric_type = data_point.metric_type.clone();
774        let max_data_points = (self.config.max_history_hours * 60) as usize; // Assume 1 point per minute
775        let min_data_points = self.config.min_data_points;
776
777        // Update series data
778        let data_points_len = {
779            let series =
780                self.metric_series.entry(metric_type.clone()).or_insert_with(|| MetricSeries {
781                    metric_type: metric_type.clone(),
782                    data_points: VecDeque::new(),
783                    baseline_statistics: BaselineStatistics::default(),
784                    last_updated: SystemTime::now(),
785                });
786
787            // Add data point
788            series.data_points.push_back(data_point);
789            series.last_updated = SystemTime::now();
790
791            // Maintain window size
792            while series.data_points.len() > max_data_points {
793                series.data_points.pop_front();
794            }
795
796            series.data_points.len()
797        };
798
799        // Update baseline statistics
800        self.update_baseline_statistics(&metric_type)?;
801
802        // Check for regressions
803        if data_points_len >= min_data_points {
804            if let Some(detection) = self.detect_regression(&metric_type)? {
805                self.detection_history.push_back(detection);
806
807                // Maintain detection history size
808                while self.detection_history.len() > 1000 {
809                    self.detection_history.pop_front();
810                }
811            }
812        }
813
814        Ok(())
815    }
816
817    /// Detect regressions for a specific metric
818    pub fn detect_regression(
819        &mut self,
820        metric_type: &MetricType,
821    ) -> Result<Option<RegressionDetection>> {
822        let series = match self.metric_series.get(metric_type) {
823            Some(series) => series,
824            None => return Ok(None),
825        };
826
827        if series.data_points.len() < self.config.min_data_points {
828            return Ok(None);
829        }
830
831        let values: Vec<f64> = series.data_points.iter().map(|dp| dp.value).collect();
832
833        // Filter outliers if enabled
834        let filtered_values = if self.config.enable_outlier_filtering {
835            self.filter_outliers(&values)
836        } else {
837            values.clone()
838        };
839
840        // Multiple detection methods
841        let mut detections = Vec::new();
842
843        // 1. Statistical trend analysis
844        if let Some(trend_result) = self.trend_analyzer.detect_trend_change(&filtered_values) {
845            if trend_result.is_regression {
846                let severity = self.calculate_severity(trend_result.slope_change);
847                detections.push(RegressionDetection {
848                    detection_id: Uuid::new_v4(),
849                    metric_type: metric_type.clone(),
850                    regression_type: RegressionType::GradualDegradation,
851                    severity,
852                    confidence: 1.0 - trend_result.significance,
853                    degradation_percentage: trend_result.slope_change * 100.0,
854                    statistical_significance: trend_result.significance,
855                    affected_period: self.calculate_affected_period(series),
856                    root_cause_analysis: self.analyze_root_causes(series, &filtered_values),
857                    recommendations: self.generate_recommendations(
858                        &RegressionType::GradualDegradation,
859                        trend_result.slope_change,
860                    ),
861                    detected_at: SystemTime::now(),
862                });
863            }
864        }
865
866        // 2. Change point detection
867        let change_points = self.change_point_detector.detect_change_points(&filtered_values);
868        if let Some(latest_change_point) = change_points.last() {
869            let before = &filtered_values[0..*latest_change_point];
870            let after = &filtered_values[*latest_change_point..];
871
872            if !before.is_empty() && !after.is_empty() {
873                let before_mean = before.iter().sum::<f64>() / before.len() as f64;
874                let after_mean = after.iter().sum::<f64>() / after.len() as f64;
875                let degradation = ((after_mean - before_mean) / before_mean) * 100.0;
876
877                if degradation > self.config.min_degradation_threshold {
878                    detections.push(RegressionDetection {
879                        detection_id: Uuid::new_v4(),
880                        metric_type: metric_type.clone(),
881                        regression_type: RegressionType::StepChange,
882                        severity: self.calculate_severity(degradation / 100.0),
883                        confidence: 0.8,
884                        degradation_percentage: degradation,
885                        statistical_significance: 0.01, // High confidence for step changes
886                        affected_period: self.calculate_affected_period(series),
887                        root_cause_analysis: self.analyze_root_causes(series, &filtered_values),
888                        recommendations: self.generate_recommendations(
889                            &RegressionType::StepChange,
890                            degradation / 100.0,
891                        ),
892                        detected_at: SystemTime::now(),
893                    });
894                }
895            }
896        }
897
898        // 3. ML-based detection
899        if let Some(ref ml_predictor) = self.ml_predictor {
900            if let Some(ml_prediction) = ml_predictor.predict_regression(&filtered_values) {
901                detections.push(RegressionDetection {
902                    detection_id: Uuid::new_v4(),
903                    metric_type: metric_type.clone(),
904                    regression_type: RegressionType::ComplexRegression,
905                    severity: ml_prediction.predicted_severity,
906                    confidence: ml_prediction.confidence,
907                    degradation_percentage: ml_prediction.anomaly_score * 100.0,
908                    statistical_significance: 1.0 - ml_prediction.confidence,
909                    affected_period: self.calculate_affected_period(series),
910                    root_cause_analysis: self.analyze_root_causes(series, &filtered_values),
911                    recommendations: self.generate_recommendations(
912                        &RegressionType::ComplexRegression,
913                        ml_prediction.anomaly_score,
914                    ),
915                    detected_at: SystemTime::now(),
916                });
917            }
918        }
919
920        // Return the most severe detection
921        if let Some(detection) = detections.into_iter().max_by_key(|d| d.severity.clone()) {
922            info!(
923                "Regression detected for {:?}: {:.2}% degradation",
924                metric_type, detection.degradation_percentage
925            );
926            Ok(Some(detection))
927        } else {
928            Ok(None)
929        }
930    }
931
932    /// Get recent regression detections
933    pub fn get_recent_detections(&self, limit: usize) -> Vec<RegressionDetection> {
934        self.detection_history.iter().rev().take(limit).cloned().collect()
935    }
936
937    /// Get regression detections for a specific metric
938    pub fn get_detections_for_metric(&self, metric_type: &MetricType) -> Vec<RegressionDetection> {
939        self.detection_history
940            .iter()
941            .filter(|d| &d.metric_type == metric_type)
942            .cloned()
943            .collect()
944    }
945
946    /// Update baseline statistics for a metric
947    fn update_baseline_statistics(&mut self, metric_type: &MetricType) -> Result<()> {
948        let series = self.metric_series.get_mut(metric_type).ok_or_else(|| {
949            anyhow::anyhow!("Metric type {:?} not found in metric_series", metric_type)
950        })?;
951        let values: Vec<f64> = series.data_points.iter().map(|dp| dp.value).collect();
952
953        if values.is_empty() {
954            return Ok(());
955        }
956
957        let mean = values.iter().sum::<f64>() / values.len() as f64;
958        let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
959        let std_dev = variance.sqrt();
960
961        let mut sorted_values = values.clone();
962        sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
963
964        let median = AnomalyDetector::percentile(&sorted_values, 50.0);
965        let percentile_95 = AnomalyDetector::percentile(&sorted_values, 95.0);
966        let percentile_99 = AnomalyDetector::percentile(&sorted_values, 99.0);
967
968        let trend_slope = self.trend_analyzer.calculate_slope(&values);
969
970        let seasonal_pattern = if self.config.enable_seasonal_adjustment {
971            self.seasonal_decomposer
972                .decompose(&values)
973                .map(|components| components.seasonal)
974        } else {
975            None
976        };
977
978        series.baseline_statistics = BaselineStatistics {
979            mean,
980            std_dev,
981            median,
982            percentile_95,
983            percentile_99,
984            trend_slope,
985            seasonal_pattern,
986            sample_count: values.len(),
987            last_computed: SystemTime::now(),
988        };
989
990        Ok(())
991    }
992
993    fn filter_outliers(&self, values: &[f64]) -> Vec<f64> {
994        let outlier_mask = self.anomaly_detector.detect_outliers(values);
995        values
996            .iter()
997            .zip(outlier_mask.iter())
998            .filter(|(_, &is_outlier)| !is_outlier)
999            .map(|(&value, _)| value)
1000            .collect()
1001    }
1002
1003    fn calculate_severity(&self, degradation_ratio: f64) -> RegressionSeverity {
1004        let degradation_percentage = degradation_ratio.abs() * 100.0;
1005
1006        if degradation_percentage > 50.0 {
1007            RegressionSeverity::Critical
1008        } else if degradation_percentage > 25.0 {
1009            RegressionSeverity::High
1010        } else if degradation_percentage > 10.0 {
1011            RegressionSeverity::Medium
1012        } else {
1013            RegressionSeverity::Low
1014        }
1015    }
1016
1017    fn calculate_affected_period(&self, series: &MetricSeries) -> (SystemTime, SystemTime) {
1018        let start = series.data_points.front().map(|dp| dp.timestamp).unwrap_or(SystemTime::now());
1019        let end = series.data_points.back().map(|dp| dp.timestamp).unwrap_or(SystemTime::now());
1020        (start, end)
1021    }
1022
1023    fn analyze_root_causes(&self, series: &MetricSeries, values: &[f64]) -> RootCauseAnalysis {
1024        let mut likely_causes = Vec::new();
1025        let correlated_metrics = Vec::new();
1026        let environmental_factors = Vec::new();
1027
1028        // Analyze patterns to identify potential causes
1029        let change_points = self.change_point_detector.detect_change_points(values);
1030        let change_point_timestamps: Vec<SystemTime> = change_points
1031            .iter()
1032            .filter_map(|&idx| series.data_points.get(idx).map(|dp| dp.timestamp))
1033            .collect();
1034
1035        // Check for sudden changes (potential code/config changes)
1036        if !change_points.is_empty() {
1037            likely_causes.push(PotentialCause {
1038                cause_type: CauseType::CodeChange,
1039                description: "Sudden performance change detected, possibly due to code deployment"
1040                    .to_string(),
1041                confidence: 0.7,
1042                supporting_evidence: vec![format!(
1043                    "Change point detected at {} locations",
1044                    change_points.len()
1045                )],
1046            });
1047        }
1048
1049        // Check for gradual degradation (potential resource issues)
1050        let trend_slope = self.trend_analyzer.calculate_slope(values);
1051        if trend_slope > 0.01 {
1052            likely_causes.push(PotentialCause {
1053                cause_type: CauseType::ResourceContention,
1054                description:
1055                    "Gradual performance degradation suggests resource contention or memory leaks"
1056                        .to_string(),
1057                confidence: 0.6,
1058                supporting_evidence: vec![format!("Positive trend slope: {:.4}", trend_slope)],
1059            });
1060        }
1061
1062        // Calculate anomaly score
1063        let mean = values.iter().sum::<f64>() / values.len() as f64;
1064        let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
1065        let anomaly_score = variance.sqrt() / (mean + 1e-6);
1066
1067        RootCauseAnalysis {
1068            likely_causes,
1069            correlated_metrics,
1070            environmental_factors,
1071            change_points: change_point_timestamps,
1072            anomaly_score,
1073        }
1074    }
1075
1076    fn generate_recommendations(
1077        &self,
1078        regression_type: &RegressionType,
1079        degradation: f64,
1080    ) -> Vec<String> {
1081        let mut recommendations = Vec::new();
1082
1083        match regression_type {
1084            RegressionType::StepChange => {
1085                recommendations
1086                    .push("Investigate recent deployments or configuration changes".to_string());
1087                recommendations
1088                    .push("Review system logs around the time of performance change".to_string());
1089                recommendations
1090                    .push("Consider rolling back recent changes if possible".to_string());
1091            },
1092            RegressionType::GradualDegradation => {
1093                recommendations
1094                    .push("Monitor resource utilization (CPU, memory, disk)".to_string());
1095                recommendations.push("Check for memory leaks or resource exhaustion".to_string());
1096                recommendations
1097                    .push("Review long-running processes and background tasks".to_string());
1098            },
1099            RegressionType::VarianceIncrease => {
1100                recommendations
1101                    .push("Investigate system stability and hardware issues".to_string());
1102                recommendations.push("Check for intermittent network or I/O problems".to_string());
1103            },
1104            RegressionType::ComplexRegression => {
1105                recommendations.push("Perform detailed profiling and analysis".to_string());
1106                recommendations
1107                    .push("Investigate multiple potential causes simultaneously".to_string());
1108            },
1109            _ => {
1110                recommendations.push("Perform comprehensive system analysis".to_string());
1111            },
1112        }
1113
1114        if degradation > 0.5 {
1115            recommendations.push("URGENT: Consider immediate mitigation actions".to_string());
1116            recommendations.push("Alert on-call team for immediate investigation".to_string());
1117        } else if degradation > 0.25 {
1118            recommendations.push("Schedule investigation within 24 hours".to_string());
1119        }
1120
1121        recommendations
1122    }
1123}
1124
1125impl Default for BaselineStatistics {
1126    fn default() -> Self {
1127        Self {
1128            mean: 0.0,
1129            std_dev: 0.0,
1130            median: 0.0,
1131            percentile_95: 0.0,
1132            percentile_99: 0.0,
1133            trend_slope: 0.0,
1134            seasonal_pattern: None,
1135            sample_count: 0,
1136            last_computed: SystemTime::now(),
1137        }
1138    }
1139}
1140
1141/// Integration with main debug session
1142impl crate::DebugSession {
1143    /// Enable regression detection for this debug session
1144    pub async fn enable_regression_detection(
1145        &mut self,
1146        config: RegressionDetectionConfig,
1147    ) -> Result<RegressionDetector> {
1148        let detector = RegressionDetector::new(config);
1149        info!(
1150            "Enabled regression detection for debug session {}",
1151            self.id()
1152        );
1153        Ok(detector)
1154    }
1155}
1156
1157#[cfg(test)]
1158mod tests {
1159    use super::*;
1160
1161    #[tokio::test]
1162    async fn test_regression_detector_creation() {
1163        let config = RegressionDetectionConfig::default();
1164        let detector = RegressionDetector::new(config);
1165
1166        assert!(detector.metric_series.is_empty());
1167        assert!(detector.detection_history.is_empty());
1168    }
1169
1170    #[tokio::test]
1171    async fn test_add_metric_data_point() {
1172        let config = RegressionDetectionConfig::default();
1173        let mut detector = RegressionDetector::new(config);
1174
1175        let data_point = MetricDataPoint {
1176            metric_type: MetricType::Latency,
1177            value: 100.0,
1178            timestamp: SystemTime::now(),
1179            session_id: Uuid::new_v4(),
1180            metadata: HashMap::new(),
1181        };
1182
1183        assert!(detector.add_metric_data_point(data_point).is_ok());
1184        assert_eq!(detector.metric_series.len(), 1);
1185    }
1186
1187    #[test]
1188    fn test_anomaly_detection() {
1189        let detector = AnomalyDetector::new();
1190        let values = vec![1.0, 2.0, 3.0, 2.0, 1.0, 100.0]; // 100.0 is an outlier
1191
1192        let outliers = detector.detect_outliers(&values);
1193        assert_eq!(outliers.len(), values.len());
1194        assert!(outliers[5]); // Last value should be detected as outlier
1195    }
1196
1197    #[test]
1198    fn test_trend_analysis() {
1199        let analyzer = TrendAnalyzer::new(3, 0.9);
1200        let values = [1.0, 1.1, 1.2, 10.0, 20.0, 30.0];
1201
1202        // Test that trend analyzer can calculate slopes
1203        let recent_values = &values[3..6]; // [10.0, 20.0, 30.0]
1204        let baseline_values = &values[0..3]; // [1.0, 1.1, 1.2]
1205
1206        let recent_slope = analyzer.calculate_slope(recent_values);
1207        let baseline_slope = analyzer.calculate_slope(baseline_values);
1208
1209        // Recent slope should be much higher than baseline
1210        assert!(recent_slope > baseline_slope);
1211        assert!(recent_slope > 0.0);
1212    }
1213
1214    #[test]
1215    fn test_change_point_detection() {
1216        let detector = ChangePointDetector::new(3, 2.0);
1217        let values = vec![1.0, 1.0, 1.0, 1.0, 5.0, 5.0, 5.0, 5.0]; // Change at index 4
1218
1219        let change_points = detector.detect_change_points(&values);
1220        assert!(!change_points.is_empty());
1221    }
1222
1223    #[test]
1224    fn test_seasonal_decomposition() {
1225        let decomposer = SeasonalDecomposer::new(4);
1226        let values = vec![1.0, 2.0, 3.0, 4.0, 1.0, 2.0, 3.0, 4.0, 1.0, 2.0, 3.0, 4.0];
1227
1228        let components = decomposer.decompose(&values);
1229        assert!(components.is_some());
1230
1231        let comp = components.expect("operation failed in test");
1232        assert_eq!(comp.trend.len(), values.len());
1233        assert_eq!(comp.seasonal.len(), values.len());
1234        assert_eq!(comp.residual.len(), values.len());
1235    }
1236
1237    #[test]
1238    fn test_feature_extraction() {
1239        let extractor = FeatureExtractor {
1240            window_size: 10,
1241            statistical_features: true,
1242            frequency_features: true,
1243        };
1244
1245        let values = vec![1.0, 2.0, 3.0, 4.0, 5.0, 4.0, 3.0, 2.0, 1.0, 2.0];
1246        let features = extractor.extract_features(&values);
1247
1248        assert!(!features.is_empty());
1249        assert!(features.len() >= 7); // At least statistical features
1250    }
1251}