1#![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#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct RegressionDetectionConfig {
21 pub enable_detection: bool,
23 pub min_data_points: usize,
25 pub significance_threshold: f64,
27 pub min_degradation_threshold: f64,
29 pub max_history_hours: u64,
31 pub ema_smoothing_factor: f64,
33 pub enable_ml_detection: bool,
35 pub ml_confidence_threshold: f64,
37 pub enable_seasonal_adjustment: bool,
39 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, 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
62pub enum MetricType {
63 Latency,
65 MemoryUsage,
67 CpuUtilization,
69 GpuUtilization,
71 Throughput,
73 ModelAccuracy,
75 Custom(String),
77}
78
79#[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#[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#[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#[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 degradation_percentage: f64,
120 pub p_value: Option<f64>,
132 pub affected_period: (SystemTime, SystemTime),
133 pub root_cause_analysis: RootCauseAnalysis,
134 pub recommendations: Vec<String>,
135 pub detected_at: SystemTime,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
140pub enum RegressionType {
141 StepChange,
143 GradualDegradation,
145 VarianceIncrease,
147 PeriodicRegression,
149 OutlierRegression,
151 ComplexRegression,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
157pub enum RegressionSeverity {
158 Low,
159 Medium,
160 High,
161 Critical,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct RootCauseAnalysis {
167 pub likely_causes: Vec<PotentialCause>,
168 pub correlated_metrics: Vec<String>,
169 pub environmental_factors: Vec<String>,
170 pub change_points: Vec<SystemTime>,
171 pub anomaly_score: f64,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct PotentialCause {
177 pub cause_type: CauseType,
178 pub description: String,
179 pub confidence: f64,
180 pub supporting_evidence: Vec<String>,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
185pub enum CauseType {
186 CodeChange,
187 DataChange,
188 ResourceContention,
189 HardwareIssue,
190 ConfigurationChange,
191 EnvironmentalFactor,
192 ModelDrift,
193 Unknown,
194}
195
196pub struct RegressionDetector {
198 config: RegressionDetectionConfig,
199 metric_series: HashMap<MetricType, MetricSeries>,
200 anomaly_detector: AnomalyDetector,
201 trend_analyzer: TrendAnalyzer,
202 change_point_detector: ChangePointDetector,
203 seasonal_decomposer: SeasonalDecomposer,
204 dispersion_scorer: Option<WindowDispersionScorer>,
205 detection_history: VecDeque<RegressionDetection>,
206}
207
208#[derive(Debug)]
210struct AnomalyDetector {
211 z_score_threshold: f64,
212 iqr_multiplier: f64,
213 isolation_forest_threshold: f64,
214}
215
216impl AnomalyDetector {
217 fn new() -> Self {
218 Self {
219 z_score_threshold: 3.0,
220 iqr_multiplier: 1.5,
221 isolation_forest_threshold: 0.1,
222 }
223 }
224
225 fn detect_outliers(&self, values: &[f64]) -> Vec<bool> {
227 if values.is_empty() {
228 return vec![];
229 }
230
231 let z_score_outliers = self.detect_z_score_outliers(values);
232 let iqr_outliers = self.detect_iqr_outliers(values);
233
234 z_score_outliers
236 .iter()
237 .zip(iqr_outliers.iter())
238 .map(|(&z_outlier, &iqr_outlier)| z_outlier || iqr_outlier)
239 .collect()
240 }
241
242 fn detect_z_score_outliers(&self, values: &[f64]) -> Vec<bool> {
243 let mean = values.iter().sum::<f64>() / values.len() as f64;
244 let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
245 let std_dev = variance.sqrt();
246
247 values
248 .iter()
249 .map(|&value| {
250 if std_dev > 0.0 {
251 ((value - mean) / std_dev).abs() > self.z_score_threshold
252 } else {
253 false
254 }
255 })
256 .collect()
257 }
258
259 fn detect_iqr_outliers(&self, values: &[f64]) -> Vec<bool> {
260 let mut sorted_values = values.to_vec();
261 sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
262
263 let q1 = Self::percentile(&sorted_values, 25.0);
264 let q3 = Self::percentile(&sorted_values, 75.0);
265 let iqr = q3 - q1;
266
267 let lower_bound = q1 - self.iqr_multiplier * iqr;
268 let upper_bound = q3 + self.iqr_multiplier * iqr;
269
270 values.iter().map(|&value| value < lower_bound || value > upper_bound).collect()
271 }
272
273 fn percentile(sorted_values: &[f64], percentile: f64) -> f64 {
274 if sorted_values.is_empty() {
275 return 0.0;
276 }
277
278 let index = (percentile / 100.0) * (sorted_values.len() - 1) as f64;
279 let lower = index.floor() as usize;
280 let upper = index.ceil() as usize;
281
282 if lower == upper {
283 sorted_values[lower]
284 } else {
285 let weight = index - lower as f64;
286 sorted_values[lower] * (1.0 - weight) + sorted_values[upper] * weight
287 }
288 }
289}
290
291#[derive(Debug)]
293struct TrendAnalyzer {
294 window_size: usize,
295 significance_threshold: f64,
296}
297
298impl TrendAnalyzer {
299 fn new(window_size: usize, significance_threshold: f64) -> Self {
300 Self {
301 window_size,
302 significance_threshold,
303 }
304 }
305
306 fn detect_trend_change(&self, values: &[f64]) -> Option<TrendChangeResult> {
308 if values.len() < self.window_size {
309 return None;
310 }
311
312 let recent_values = &values[values.len() - self.window_size..];
313 let baseline_values = if values.len() >= 2 * self.window_size {
314 &values[values.len() - 2 * self.window_size..values.len() - self.window_size]
315 } else {
316 &values[0..values.len() - self.window_size]
317 };
318
319 let recent_slope = self.calculate_slope(recent_values);
320 let baseline_slope = self.calculate_slope(baseline_values);
321
322 let slope_change = recent_slope - baseline_slope;
323 let significance = self.calculate_trend_significance(recent_values, recent_slope);
324
325 if significance < self.significance_threshold {
326 Some(TrendChangeResult {
327 slope_change,
328 recent_slope,
329 baseline_slope,
330 significance,
331 is_regression: slope_change > 0.0, })
333 } else {
334 None
335 }
336 }
337
338 fn calculate_slope(&self, values: &[f64]) -> f64 {
339 if values.len() < 2 {
340 return 0.0;
341 }
342
343 let n = values.len() as f64;
344 let sum_x = (0..values.len()).sum::<usize>() as f64;
345 let sum_y = values.iter().sum::<f64>();
346 let sum_xy = values.iter().enumerate().map(|(i, &y)| i as f64 * y).sum::<f64>();
347 let sum_x_squared = (0..values.len()).map(|i| (i as f64).powi(2)).sum::<f64>();
348
349 let denominator = n * sum_x_squared - sum_x.powi(2);
350 if denominator.abs() < 1e-10 {
351 0.0
352 } else {
353 (n * sum_xy - sum_x * sum_y) / denominator
354 }
355 }
356
357 fn calculate_trend_significance(&self, values: &[f64], slope: f64) -> f64 {
367 if values.len() < 3 {
368 return 1.0;
369 }
370
371 let n = values.len() as f64;
372 let mean_x = (values.len() - 1) as f64 / 2.0;
373 let ss_x = (0..values.len()).map(|i| (i as f64 - mean_x).powi(2)).sum::<f64>();
374
375 let mean_y = values.iter().sum::<f64>() / n;
377 let intercept = mean_y - slope * mean_x;
378 let predicted: Vec<f64> = (0..values.len()).map(|i| intercept + slope * i as f64).collect();
379
380 let residuals: Vec<f64> = values
381 .iter()
382 .zip(predicted.iter())
383 .map(|(&actual, &pred)| actual - pred)
384 .collect();
385
386 let mse = residuals.iter().map(|&r| r.powi(2)).sum::<f64>() / (n - 2.0);
387 let se_slope = (mse / ss_x).sqrt();
388
389 if se_slope > 0.0 {
390 let t_stat = slope / se_slope;
391 let df = n - 2.0;
392 trustformers_core::statistics::student_t_two_sided_p_value(t_stat, df).unwrap_or(1.0)
395 } else {
396 if slope.abs() > 0.0 {
399 0.0
400 } else {
401 1.0
402 }
403 }
404 }
405}
406
407#[derive(Debug)]
408struct TrendChangeResult {
409 slope_change: f64,
410 recent_slope: f64,
411 baseline_slope: f64,
412 significance: f64,
413 is_regression: bool,
414}
415
416#[derive(Debug)]
418struct ChangePointDetector {
419 min_segment_length: usize,
420 penalty_factor: f64,
421}
422
423impl ChangePointDetector {
424 fn new(min_segment_length: usize, penalty_factor: f64) -> Self {
425 Self {
426 min_segment_length,
427 penalty_factor,
428 }
429 }
430
431 fn detect_change_points(&self, values: &[f64]) -> Vec<usize> {
433 if values.len() < 2 * self.min_segment_length {
434 return vec![];
435 }
436
437 let mut change_points = vec![];
438 let mut current_start = 0;
439
440 while current_start + 2 * self.min_segment_length <= values.len() {
441 if let Some(change_point) = self.find_next_change_point(&values[current_start..]) {
442 let absolute_change_point = current_start + change_point;
443 change_points.push(absolute_change_point);
444 current_start = absolute_change_point + self.min_segment_length;
445 } else {
446 break;
447 }
448 }
449
450 change_points
451 }
452
453 fn find_next_change_point(&self, values: &[f64]) -> Option<usize> {
454 let n = values.len();
455 if n < 2 * self.min_segment_length {
456 return None;
457 }
458
459 let mut max_statistic = 0.0;
460 let mut best_change_point = None;
461
462 for t in self.min_segment_length..n - self.min_segment_length {
463 let statistic = self.cusum_statistic(values, t);
464 if statistic > max_statistic {
465 max_statistic = statistic;
466 best_change_point = Some(t);
467 }
468 }
469
470 let threshold = self.penalty_factor * (n as f64).ln();
472 if max_statistic > threshold {
473 best_change_point
474 } else {
475 None
476 }
477 }
478
479 fn cusum_statistic(&self, values: &[f64], change_point: usize) -> f64 {
480 let segment1 = &values[0..change_point];
481 let segment2 = &values[change_point..];
482
483 let mean1 = segment1.iter().sum::<f64>() / segment1.len() as f64;
484 let mean2 = segment2.iter().sum::<f64>() / segment2.len() as f64;
485 let overall_mean = values.iter().sum::<f64>() / values.len() as f64;
486
487 let n1 = segment1.len() as f64;
488 let n2 = segment2.len() as f64;
489 let n = values.len() as f64;
490
491 let variance = values.iter().map(|&x| (x - overall_mean).powi(2)).sum::<f64>() / (n - 1.0);
493
494 if variance > 0.0 {
495 (n1 * (mean1 - overall_mean).powi(2) + n2 * (mean2 - overall_mean).powi(2)) / variance
496 } else {
497 0.0
498 }
499 }
500}
501
502#[derive(Debug)]
504struct SeasonalDecomposer {
505 period: usize,
506 enable_decomposition: bool,
507}
508
509impl SeasonalDecomposer {
510 fn new(period: usize) -> Self {
511 Self {
512 period,
513 enable_decomposition: true,
514 }
515 }
516
517 fn decompose(&self, values: &[f64]) -> Option<SeasonalComponents> {
519 if !self.enable_decomposition || values.len() < 2 * self.period {
520 return None;
521 }
522
523 let trend = self.extract_trend(values);
524 let detrended = self.subtract_series(values, &trend);
525 let seasonal = self.extract_seasonal(&detrended);
526 let residual = self.subtract_series(&detrended, &seasonal);
527
528 Some(SeasonalComponents {
529 trend,
530 seasonal,
531 residual,
532 })
533 }
534
535 fn extract_trend(&self, values: &[f64]) -> Vec<f64> {
536 let window_size = self.period;
538 let mut trend = vec![0.0; values.len()];
539
540 for i in 0..values.len() {
541 let start = i.saturating_sub(window_size / 2);
542 let end = std::cmp::min(i + window_size / 2 + 1, values.len());
543
544 let sum: f64 = values[start..end].iter().sum();
545 trend[i] = sum / (end - start) as f64;
546 }
547
548 trend
549 }
550
551 fn extract_seasonal(&self, detrended: &[f64]) -> Vec<f64> {
552 let mut seasonal = vec![0.0; detrended.len()];
553 let mut seasonal_pattern = vec![0.0; self.period];
554 let mut pattern_counts = vec![0usize; self.period];
555
556 for (i, &value) in detrended.iter().enumerate() {
558 let season_index = i % self.period;
559 seasonal_pattern[season_index] += value;
560 pattern_counts[season_index] += 1;
561 }
562
563 for i in 0..self.period {
565 if pattern_counts[i] > 0 {
566 seasonal_pattern[i] /= pattern_counts[i] as f64;
567 }
568 }
569
570 for (i, seasonal_value) in seasonal.iter_mut().enumerate() {
572 *seasonal_value = seasonal_pattern[i % self.period];
573 }
574
575 seasonal
576 }
577
578 fn subtract_series(&self, series1: &[f64], series2: &[f64]) -> Vec<f64> {
579 series1.iter().zip(series2.iter()).map(|(&a, &b)| a - b).collect()
580 }
581}
582
583#[derive(Debug, Clone, Serialize, Deserialize)]
584struct SeasonalComponents {
585 trend: Vec<f64>,
586 seasonal: Vec<f64>,
587 residual: Vec<f64>,
588}
589
590#[derive(Debug)]
600struct WindowDispersionScorer {
601 feature_extractor: FeatureExtractor,
602 dispersion_threshold: f64,
604}
605
606#[derive(Debug)]
607struct FeatureExtractor {
608 window_size: usize,
609 statistical_features: bool,
610 frequency_features: bool,
611}
612
613impl WindowDispersionScorer {
614 fn new(dispersion_threshold: f64) -> Self {
615 Self {
616 feature_extractor: FeatureExtractor {
617 window_size: 50,
618 statistical_features: true,
619 frequency_features: true,
620 },
621 dispersion_threshold,
622 }
623 }
624
625 fn score_window(&self, values: &[f64]) -> Option<WindowDispersionScore> {
631 let window = self.feature_extractor.window_size;
632 if values.len() < window {
633 return None;
634 }
635
636 let features = self.feature_extractor.extract_features(values);
637 let dispersion = Self::coefficient_of_variation(&features);
638 if dispersion <= self.dispersion_threshold {
639 return None;
640 }
641
642 let recent = &values[values.len() - window..];
645 let prior_end = values.len() - window;
646 if prior_end < 2 {
647 return None;
648 }
649 let prior_start = prior_end.saturating_sub(window);
650 let prior = &values[prior_start..prior_end];
651
652 let recent_mean = trustformers_core::statistics::mean(recent)?;
653 let prior_mean = trustformers_core::statistics::mean(prior)?;
654 if prior_mean.abs() < f64::EPSILON {
655 return None;
656 }
657 let degradation_percentage = (recent_mean - prior_mean) / prior_mean.abs() * 100.0;
658
659 let test = trustformers_core::statistics::welch_t_test(recent, prior)?;
660
661 Some(WindowDispersionScore {
662 dispersion,
663 degradation_percentage,
664 p_value: test.p_value,
665 feature_magnitudes: Self::normalised_magnitudes(&features),
666 severity: Self::severity_for(dispersion),
667 })
668 }
669
670 fn coefficient_of_variation(features: &[f64]) -> f64 {
672 if features.is_empty() {
673 return 0.0;
674 }
675 let mean = features.iter().sum::<f64>() / features.len() as f64;
676 let variance =
677 features.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / features.len() as f64;
678 variance.sqrt() / (mean.abs() + 1e-6)
679 }
680
681 fn normalised_magnitudes(features: &[f64]) -> Vec<f64> {
686 let max_magnitude = features.iter().map(|x| x.abs()).fold(0.0, f64::max);
687 if max_magnitude > 0.0 {
688 features.iter().map(|&x| x.abs() / max_magnitude).collect()
689 } else {
690 vec![0.0; features.len()]
691 }
692 }
693
694 fn severity_for(dispersion: f64) -> RegressionSeverity {
695 if dispersion > 0.8 {
696 RegressionSeverity::Critical
697 } else if dispersion > 0.6 {
698 RegressionSeverity::High
699 } else if dispersion > 0.4 {
700 RegressionSeverity::Medium
701 } else {
702 RegressionSeverity::Low
703 }
704 }
705}
706
707impl FeatureExtractor {
708 fn extract_features(&self, values: &[f64]) -> Vec<f64> {
709 let mut features = Vec::new();
710
711 if self.statistical_features {
712 features.extend(self.extract_statistical_features(values));
713 }
714
715 if self.frequency_features {
716 features.extend(self.extract_frequency_features(values));
717 }
718
719 features
720 }
721
722 fn extract_statistical_features(&self, values: &[f64]) -> Vec<f64> {
723 let mean = values.iter().sum::<f64>() / values.len() as f64;
724 let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
725 let std_dev = variance.sqrt();
726
727 let min = values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
728 let max = values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
729 let range = max - min;
730
731 let skewness = if std_dev > 0.0 {
733 values.iter().map(|x| ((x - mean) / std_dev).powi(3)).sum::<f64>() / values.len() as f64
734 } else {
735 0.0
736 };
737
738 let kurtosis = if std_dev > 0.0 {
740 values.iter().map(|x| ((x - mean) / std_dev).powi(4)).sum::<f64>() / values.len() as f64
741 - 3.0
742 } else {
743 0.0
744 };
745
746 vec![mean, std_dev, min, max, range, skewness, kurtosis]
747 }
748
749 fn extract_frequency_features(&self, values: &[f64]) -> Vec<f64> {
750 let mut features = Vec::new();
752
753 let differences: Vec<f64> = values.windows(2).map(|w| (w[1] - w[0]).abs()).collect();
755
756 if !differences.is_empty() {
757 let mean_diff = differences.iter().sum::<f64>() / differences.len() as f64;
758 let max_diff = differences.iter().fold(0.0f64, |a, &b| a.max(b));
759 features.extend([mean_diff, max_diff]);
760 }
761
762 features
763 }
764}
765
766#[derive(Debug, Clone, Serialize, Deserialize)]
768struct WindowDispersionScore {
769 dispersion: f64,
771 degradation_percentage: f64,
773 p_value: f64,
775 #[allow(
777 dead_code,
778 reason = "carried for callers that surface the feature breakdown"
779 )]
780 feature_magnitudes: Vec<f64>,
781 severity: RegressionSeverity,
782}
783
784impl RegressionDetector {
785 pub fn new(config: RegressionDetectionConfig) -> Self {
787 let dispersion_scorer = if config.enable_ml_detection {
788 Some(WindowDispersionScorer::new(config.ml_confidence_threshold))
789 } else {
790 None
791 };
792
793 let trend_analyzer =
794 TrendAnalyzer::new(config.min_data_points, config.significance_threshold);
795
796 Self {
797 config,
798 metric_series: HashMap::new(),
799 anomaly_detector: AnomalyDetector::new(),
800 trend_analyzer,
801 change_point_detector: ChangePointDetector::new(5, 2.0),
802 seasonal_decomposer: SeasonalDecomposer::new(24), dispersion_scorer,
804 detection_history: VecDeque::new(),
805 }
806 }
807
808 pub fn add_metric_data_point(&mut self, data_point: MetricDataPoint) -> Result<()> {
810 let metric_type = data_point.metric_type.clone();
811 let max_data_points = (self.config.max_history_hours * 60) as usize; let min_data_points = self.config.min_data_points;
813
814 let data_points_len = {
816 let series =
817 self.metric_series.entry(metric_type.clone()).or_insert_with(|| MetricSeries {
818 metric_type: metric_type.clone(),
819 data_points: VecDeque::new(),
820 baseline_statistics: BaselineStatistics::default(),
821 last_updated: SystemTime::now(),
822 });
823
824 series.data_points.push_back(data_point);
826 series.last_updated = SystemTime::now();
827
828 while series.data_points.len() > max_data_points {
830 series.data_points.pop_front();
831 }
832
833 series.data_points.len()
834 };
835
836 self.update_baseline_statistics(&metric_type)?;
838
839 if data_points_len >= min_data_points {
841 if let Some(detection) = self.detect_regression(&metric_type)? {
842 self.detection_history.push_back(detection);
843
844 while self.detection_history.len() > 1000 {
846 self.detection_history.pop_front();
847 }
848 }
849 }
850
851 Ok(())
852 }
853
854 pub fn detect_regression(
856 &mut self,
857 metric_type: &MetricType,
858 ) -> Result<Option<RegressionDetection>> {
859 let series = match self.metric_series.get(metric_type) {
860 Some(series) => series,
861 None => return Ok(None),
862 };
863
864 if series.data_points.len() < self.config.min_data_points {
865 return Ok(None);
866 }
867
868 let values: Vec<f64> = series.data_points.iter().map(|dp| dp.value).collect();
869
870 let filtered_values = if self.config.enable_outlier_filtering {
872 self.filter_outliers(&values)
873 } else {
874 values.clone()
875 };
876
877 let mut detections = Vec::new();
879
880 if let Some(trend_result) = self.trend_analyzer.detect_trend_change(&filtered_values) {
882 if trend_result.is_regression {
883 let severity = self.calculate_severity(trend_result.slope_change);
884 detections.push(RegressionDetection {
885 detection_id: Uuid::new_v4(),
886 metric_type: metric_type.clone(),
887 regression_type: RegressionType::GradualDegradation,
888 severity,
889 degradation_percentage: trend_result.slope_change * 100.0,
890 p_value: Some(trend_result.significance),
891 affected_period: self.calculate_affected_period(series),
892 root_cause_analysis: self.analyze_root_causes(series, &filtered_values),
893 recommendations: self.generate_recommendations(
894 &RegressionType::GradualDegradation,
895 trend_result.slope_change,
896 ),
897 detected_at: SystemTime::now(),
898 });
899 }
900 }
901
902 let change_points = self.change_point_detector.detect_change_points(&filtered_values);
904 if let Some(latest_change_point) = change_points.last() {
905 let before = &filtered_values[0..*latest_change_point];
906 let after = &filtered_values[*latest_change_point..];
907
908 if !before.is_empty() && !after.is_empty() {
909 let before_mean = before.iter().sum::<f64>() / before.len() as f64;
910 let after_mean = after.iter().sum::<f64>() / after.len() as f64;
911 let degradation = ((after_mean - before_mean) / before_mean) * 100.0;
912
913 if degradation > self.config.min_degradation_threshold {
914 detections.push(RegressionDetection {
915 detection_id: Uuid::new_v4(),
916 metric_type: metric_type.clone(),
917 regression_type: RegressionType::StepChange,
918 severity: self.calculate_severity(degradation / 100.0),
919 degradation_percentage: degradation,
920 p_value: None,
926 affected_period: self.calculate_affected_period(series),
927 root_cause_analysis: self.analyze_root_causes(series, &filtered_values),
928 recommendations: self.generate_recommendations(
929 &RegressionType::StepChange,
930 degradation / 100.0,
931 ),
932 detected_at: SystemTime::now(),
933 });
934 }
935 }
936 }
937
938 if let Some(ref scorer) = self.dispersion_scorer {
944 if let Some(score) = scorer.score_window(&filtered_values) {
945 detections.push(RegressionDetection {
946 detection_id: Uuid::new_v4(),
947 metric_type: metric_type.clone(),
948 regression_type: RegressionType::ComplexRegression,
949 severity: score.severity,
950 degradation_percentage: score.degradation_percentage,
951 p_value: Some(score.p_value),
952 affected_period: self.calculate_affected_period(series),
953 root_cause_analysis: self.analyze_root_causes(series, &filtered_values),
954 recommendations: self.generate_recommendations(
955 &RegressionType::ComplexRegression,
956 score.dispersion,
957 ),
958 detected_at: SystemTime::now(),
959 });
960 }
961 }
962
963 if let Some(detection) = detections.into_iter().max_by_key(|d| d.severity.clone()) {
965 info!(
966 "Regression detected for {:?}: {:.2}% degradation",
967 metric_type, detection.degradation_percentage
968 );
969 Ok(Some(detection))
970 } else {
971 Ok(None)
972 }
973 }
974
975 pub fn get_recent_detections(&self, limit: usize) -> Vec<RegressionDetection> {
977 self.detection_history.iter().rev().take(limit).cloned().collect()
978 }
979
980 pub fn get_detections_for_metric(&self, metric_type: &MetricType) -> Vec<RegressionDetection> {
982 self.detection_history
983 .iter()
984 .filter(|d| &d.metric_type == metric_type)
985 .cloned()
986 .collect()
987 }
988
989 fn update_baseline_statistics(&mut self, metric_type: &MetricType) -> Result<()> {
991 let series = self.metric_series.get_mut(metric_type).ok_or_else(|| {
992 anyhow::anyhow!("Metric type {:?} not found in metric_series", metric_type)
993 })?;
994 let values: Vec<f64> = series.data_points.iter().map(|dp| dp.value).collect();
995
996 if values.is_empty() {
997 return Ok(());
998 }
999
1000 let mean = values.iter().sum::<f64>() / values.len() as f64;
1001 let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
1002 let std_dev = variance.sqrt();
1003
1004 let mut sorted_values = values.clone();
1005 sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1006
1007 let median = AnomalyDetector::percentile(&sorted_values, 50.0);
1008 let percentile_95 = AnomalyDetector::percentile(&sorted_values, 95.0);
1009 let percentile_99 = AnomalyDetector::percentile(&sorted_values, 99.0);
1010
1011 let trend_slope = self.trend_analyzer.calculate_slope(&values);
1012
1013 let seasonal_pattern = if self.config.enable_seasonal_adjustment {
1014 self.seasonal_decomposer
1015 .decompose(&values)
1016 .map(|components| components.seasonal)
1017 } else {
1018 None
1019 };
1020
1021 series.baseline_statistics = BaselineStatistics {
1022 mean,
1023 std_dev,
1024 median,
1025 percentile_95,
1026 percentile_99,
1027 trend_slope,
1028 seasonal_pattern,
1029 sample_count: values.len(),
1030 last_computed: SystemTime::now(),
1031 };
1032
1033 Ok(())
1034 }
1035
1036 fn filter_outliers(&self, values: &[f64]) -> Vec<f64> {
1037 let outlier_mask = self.anomaly_detector.detect_outliers(values);
1038 values
1039 .iter()
1040 .zip(outlier_mask.iter())
1041 .filter(|(_, &is_outlier)| !is_outlier)
1042 .map(|(&value, _)| value)
1043 .collect()
1044 }
1045
1046 fn calculate_severity(&self, degradation_ratio: f64) -> RegressionSeverity {
1047 let degradation_percentage = degradation_ratio.abs() * 100.0;
1048
1049 if degradation_percentage > 50.0 {
1050 RegressionSeverity::Critical
1051 } else if degradation_percentage > 25.0 {
1052 RegressionSeverity::High
1053 } else if degradation_percentage > 10.0 {
1054 RegressionSeverity::Medium
1055 } else {
1056 RegressionSeverity::Low
1057 }
1058 }
1059
1060 fn calculate_affected_period(&self, series: &MetricSeries) -> (SystemTime, SystemTime) {
1061 let start = series.data_points.front().map(|dp| dp.timestamp).unwrap_or(SystemTime::now());
1062 let end = series.data_points.back().map(|dp| dp.timestamp).unwrap_or(SystemTime::now());
1063 (start, end)
1064 }
1065
1066 fn analyze_root_causes(&self, series: &MetricSeries, values: &[f64]) -> RootCauseAnalysis {
1067 let mut likely_causes = Vec::new();
1068 let correlated_metrics = Vec::new();
1069 let environmental_factors = Vec::new();
1070
1071 let change_points = self.change_point_detector.detect_change_points(values);
1073 let change_point_timestamps: Vec<SystemTime> = change_points
1074 .iter()
1075 .filter_map(|&idx| series.data_points.get(idx).map(|dp| dp.timestamp))
1076 .collect();
1077
1078 if !change_points.is_empty() {
1080 likely_causes.push(PotentialCause {
1081 cause_type: CauseType::CodeChange,
1082 description: "Sudden performance change detected, possibly due to code deployment"
1083 .to_string(),
1084 confidence: 0.7,
1085 supporting_evidence: vec![format!(
1086 "Change point detected at {} locations",
1087 change_points.len()
1088 )],
1089 });
1090 }
1091
1092 let trend_slope = self.trend_analyzer.calculate_slope(values);
1094 if trend_slope > 0.01 {
1095 likely_causes.push(PotentialCause {
1096 cause_type: CauseType::ResourceContention,
1097 description:
1098 "Gradual performance degradation suggests resource contention or memory leaks"
1099 .to_string(),
1100 confidence: 0.6,
1101 supporting_evidence: vec![format!("Positive trend slope: {:.4}", trend_slope)],
1102 });
1103 }
1104
1105 let mean = values.iter().sum::<f64>() / values.len() as f64;
1107 let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
1108 let anomaly_score = variance.sqrt() / (mean + 1e-6);
1109
1110 RootCauseAnalysis {
1111 likely_causes,
1112 correlated_metrics,
1113 environmental_factors,
1114 change_points: change_point_timestamps,
1115 anomaly_score,
1116 }
1117 }
1118
1119 fn generate_recommendations(
1120 &self,
1121 regression_type: &RegressionType,
1122 degradation: f64,
1123 ) -> Vec<String> {
1124 let mut recommendations = Vec::new();
1125
1126 match regression_type {
1127 RegressionType::StepChange => {
1128 recommendations
1129 .push("Investigate recent deployments or configuration changes".to_string());
1130 recommendations
1131 .push("Review system logs around the time of performance change".to_string());
1132 recommendations
1133 .push("Consider rolling back recent changes if possible".to_string());
1134 },
1135 RegressionType::GradualDegradation => {
1136 recommendations
1137 .push("Monitor resource utilization (CPU, memory, disk)".to_string());
1138 recommendations.push("Check for memory leaks or resource exhaustion".to_string());
1139 recommendations
1140 .push("Review long-running processes and background tasks".to_string());
1141 },
1142 RegressionType::VarianceIncrease => {
1143 recommendations
1144 .push("Investigate system stability and hardware issues".to_string());
1145 recommendations.push("Check for intermittent network or I/O problems".to_string());
1146 },
1147 RegressionType::ComplexRegression => {
1148 recommendations.push("Perform detailed profiling and analysis".to_string());
1149 recommendations
1150 .push("Investigate multiple potential causes simultaneously".to_string());
1151 },
1152 _ => {
1153 recommendations.push("Perform comprehensive system analysis".to_string());
1154 },
1155 }
1156
1157 if degradation > 0.5 {
1158 recommendations.push("URGENT: Consider immediate mitigation actions".to_string());
1159 recommendations.push("Alert on-call team for immediate investigation".to_string());
1160 } else if degradation > 0.25 {
1161 recommendations.push("Schedule investigation within 24 hours".to_string());
1162 }
1163
1164 recommendations
1165 }
1166}
1167
1168impl Default for BaselineStatistics {
1169 fn default() -> Self {
1170 Self {
1171 mean: 0.0,
1172 std_dev: 0.0,
1173 median: 0.0,
1174 percentile_95: 0.0,
1175 percentile_99: 0.0,
1176 trend_slope: 0.0,
1177 seasonal_pattern: None,
1178 sample_count: 0,
1179 last_computed: SystemTime::now(),
1180 }
1181 }
1182}
1183
1184impl crate::DebugSession {
1186 pub async fn enable_regression_detection(
1188 &mut self,
1189 config: RegressionDetectionConfig,
1190 ) -> Result<RegressionDetector> {
1191 let detector = RegressionDetector::new(config);
1192 info!(
1193 "Enabled regression detection for debug session {}",
1194 self.id()
1195 );
1196 Ok(detector)
1197 }
1198}
1199
1200#[cfg(test)]
1201mod tests {
1202 use super::*;
1203
1204 #[test]
1207 fn trend_significance_uses_the_real_student_t_distribution() {
1208 let detector = TrendAnalyzer::new(3, 0.05);
1209 let values: Vec<f64> =
1212 (0..30).map(|i| i as f64 + if i % 2 == 0 { 0.01 } else { -0.01 }).collect();
1213 let slope = detector.calculate_slope(&values);
1214 assert!(
1215 (slope - 1.0).abs() < 0.01,
1216 "slope should be ~1, got {slope}"
1217 );
1218 let p = detector.calculate_trend_significance(&values, slope);
1219 assert!(
1220 p < 1e-6,
1221 "a near-perfect ramp must be highly significant, got p={p}"
1222 );
1223 }
1224
1225 #[test]
1226 fn trend_significance_is_high_for_pure_noise_around_a_flat_line() {
1227 let detector = TrendAnalyzer::new(3, 0.05);
1228 let values: Vec<f64> = (0..30).map(|i| if i % 2 == 0 { 1.0 } else { -1.0 }).collect();
1230 let slope = detector.calculate_slope(&values);
1231 let p = detector.calculate_trend_significance(&values, slope);
1232 assert!(p > 0.5, "a flat zig-zag must not be significant, got p={p}");
1233 }
1234
1235 #[test]
1236 fn trend_significance_matches_a_published_t_critical_value() {
1237 let p = trustformers_core::statistics::student_t_two_sided_p_value(2.228, 10.0)
1240 .expect("valid df");
1241 assert!(
1242 (p - 0.05).abs() < 1e-3,
1243 "expected p ~= 0.05 for t=2.228, df=10; got {p}"
1244 );
1245 let bogus = {
1247 let x = 2.228_f64 / (10.0 + 2.228_f64.powi(2)).sqrt();
1248 2.0 * (1.0 - (0.5 + 0.5 * x.atan() * (2.0 / std::f64::consts::PI)))
1249 };
1250 assert!(
1253 bogus > 10.0 * p,
1254 "sanity: the old approximation really was that wrong (bogus={bogus}, true={p})"
1255 );
1256 }
1257
1258 #[test]
1259 fn dispersion_scorer_reports_a_real_degradation_and_p_value() {
1260 let scorer = WindowDispersionScorer::new(0.0);
1261 let mut values: Vec<f64> = Vec::new();
1264 for i in 0..50 {
1265 values.push(1.0 + (i % 5) as f64 * 0.01);
1266 }
1267 for i in 0..50 {
1268 values.push(2.0 + (i % 5) as f64 * 0.01);
1269 }
1270 let score = scorer.score_window(&values).expect("dispersion above a zero threshold");
1271 assert!(
1272 (score.degradation_percentage - 100.0).abs() < 2.0,
1273 "expected ~+100% degradation, got {}",
1274 score.degradation_percentage
1275 );
1276 assert!(
1277 score.p_value < 1e-6,
1278 "two clearly separated windows must be highly significant, got p={}",
1279 score.p_value
1280 );
1281 }
1282
1283 #[test]
1284 fn dispersion_scorer_needs_a_previous_window_to_compare_against() {
1285 let scorer = WindowDispersionScorer::new(0.0);
1286 let values: Vec<f64> = (0..50).map(|i| i as f64).collect();
1289 assert!(scorer.score_window(&values).is_none());
1290 }
1291
1292 #[tokio::test]
1293 async fn test_regression_detector_creation() {
1294 let config = RegressionDetectionConfig::default();
1295 let detector = RegressionDetector::new(config);
1296
1297 assert!(detector.metric_series.is_empty());
1298 assert!(detector.detection_history.is_empty());
1299 }
1300
1301 #[tokio::test]
1302 async fn test_add_metric_data_point() {
1303 let config = RegressionDetectionConfig::default();
1304 let mut detector = RegressionDetector::new(config);
1305
1306 let data_point = MetricDataPoint {
1307 metric_type: MetricType::Latency,
1308 value: 100.0,
1309 timestamp: SystemTime::now(),
1310 session_id: Uuid::new_v4(),
1311 metadata: HashMap::new(),
1312 };
1313
1314 assert!(detector.add_metric_data_point(data_point).is_ok());
1315 assert_eq!(detector.metric_series.len(), 1);
1316 }
1317
1318 #[test]
1319 fn test_anomaly_detection() {
1320 let detector = AnomalyDetector::new();
1321 let values = vec![1.0, 2.0, 3.0, 2.0, 1.0, 100.0]; let outliers = detector.detect_outliers(&values);
1324 assert_eq!(outliers.len(), values.len());
1325 assert!(outliers[5]); }
1327
1328 #[test]
1329 fn test_trend_analysis() {
1330 let analyzer = TrendAnalyzer::new(3, 0.9);
1331 let values = [1.0, 1.1, 1.2, 10.0, 20.0, 30.0];
1332
1333 let recent_values = &values[3..6]; let baseline_values = &values[0..3]; let recent_slope = analyzer.calculate_slope(recent_values);
1338 let baseline_slope = analyzer.calculate_slope(baseline_values);
1339
1340 assert!(recent_slope > baseline_slope);
1342 assert!(recent_slope > 0.0);
1343 }
1344
1345 #[test]
1346 fn test_change_point_detection() {
1347 let detector = ChangePointDetector::new(3, 2.0);
1348 let values = vec![1.0, 1.0, 1.0, 1.0, 5.0, 5.0, 5.0, 5.0]; let change_points = detector.detect_change_points(&values);
1351 assert!(!change_points.is_empty());
1352 }
1353
1354 #[test]
1355 fn test_seasonal_decomposition() {
1356 let decomposer = SeasonalDecomposer::new(4);
1357 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];
1358
1359 let components = decomposer.decompose(&values);
1360 assert!(components.is_some());
1361
1362 let comp = components.expect("operation failed in test");
1363 assert_eq!(comp.trend.len(), values.len());
1364 assert_eq!(comp.seasonal.len(), values.len());
1365 assert_eq!(comp.residual.len(), values.len());
1366 }
1367
1368 #[test]
1369 fn test_feature_extraction() {
1370 let extractor = FeatureExtractor {
1371 window_size: 10,
1372 statistical_features: true,
1373 frequency_features: true,
1374 };
1375
1376 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0, 4.0, 3.0, 2.0, 1.0, 2.0];
1377 let features = extractor.extract_features(&values);
1378
1379 assert!(!features.is_empty());
1380 assert!(features.len() >= 7); }
1382}