1use std::collections::HashMap;
13use std::time::{Duration, SystemTime};
14
15use anyhow::Result;
16use serde::{Deserialize, Serialize};
17use uuid::Uuid;
18
19use crate::ring_buffer::TimestampedRingBuffer;
20
21use super::analysis;
22use super::KernelProfileData;
23
24#[derive(Debug)]
26pub struct PerformanceRegressionDetector {
27 baseline_profiles: HashMap<String, BaselineProfile>,
28 regression_alerts: Vec<RegressionAlert>,
29 statistical_analyzer: StatisticalAnalyzer,
30 alert_thresholds: RegressionThresholds,
31 execution_history: HashMap<String, TimestampedRingBuffer<f64>>,
37}
38
39const EXECUTION_HISTORY_CAPACITY: usize = 500;
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct BaselineProfile {
45 pub kernel_name: String,
46 pub baseline_performance: Duration,
47 pub performance_distribution: PerformanceDistribution,
48 pub established_date: SystemTime,
49 pub confidence_interval: (Duration, Duration),
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct PerformanceDistribution {
64 pub mean_secs: f64,
66 pub std_dev_secs: f64,
68 pub percentiles: HashMap<u8, f64>,
70 pub outlier_threshold_secs: f64,
72 pub sample_count: usize,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct RegressionAlert {
81 pub alert_id: Uuid,
82 pub kernel_name: String,
83 pub alert_type: RegressionType,
84 pub severity: RegressionSeverity,
85 pub current_performance: Duration,
86 pub baseline_performance: Duration,
87 pub regression_magnitude: f64,
88 pub detection_timestamp: SystemTime,
89 pub potential_causes: Vec<String>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub enum RegressionType {
94 PerformanceDegradation,
95 MemoryUsageIncrease,
96 OccupancyDecrease,
97 BandwidthUtilizationDrop,
98 EnergyEfficiencyLoss,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub enum RegressionSeverity {
103 Minor, Moderate, Major, Critical, }
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct RegressionThresholds {
111 pub minor_threshold: f64,
117 pub moderate_threshold: f64,
118 pub major_threshold: f64,
119 pub critical_threshold: f64,
129 pub detection_window: Duration,
130 pub confidence_level: f64,
131}
132
133#[derive(Debug)]
134pub struct StatisticalAnalyzer {
135 sample_size_requirements: HashMap<String, usize>,
136 statistical_tests: Vec<StatisticalTest>,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct StatisticalTest {
141 pub test_name: String,
142 pub test_type: TestType,
143 pub significance_level: f64,
145 pub observed_power: Option<f64>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub enum TestType {
156 TTest,
157 MannWhitneyU,
158 KolmogorovSmirnov,
159 ChangePointDetection,
160 AnomalyDetection,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct RegressionStatus {
165 pub has_regression: bool,
170 pub regression_alerts: Vec<RegressionAlert>,
173 pub performance_trend: PerformanceTrend,
174 pub baseline_comparison: BaselineComparison,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub enum PerformanceTrend {
179 Improving,
180 Stable,
181 Degrading,
182 Volatile,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct BaselineComparison {
187 pub current_vs_baseline: f64,
191 pub statistical_significance: f64,
195 pub confidence_interval: (f64, f64),
198}
199
200impl PerformanceRegressionDetector {
201 pub fn new() -> Result<Self> {
202 Ok(Self {
203 baseline_profiles: HashMap::new(),
204 regression_alerts: vec![],
205 statistical_analyzer: StatisticalAnalyzer::new()?,
206 alert_thresholds: RegressionThresholds {
207 minor_threshold: 0.05,
208 moderate_threshold: 0.15,
209 major_threshold: 0.30,
210 critical_threshold: 0.50,
211 detection_window: Duration::from_secs(3600),
212 confidence_level: 0.95,
213 },
214 execution_history: HashMap::new(),
215 })
216 }
217
218 pub fn new_empty() -> Self {
219 Self {
220 baseline_profiles: HashMap::new(),
221 regression_alerts: vec![],
222 statistical_analyzer: StatisticalAnalyzer::new_empty(),
223 alert_thresholds: RegressionThresholds {
224 minor_threshold: 0.05,
225 moderate_threshold: 0.15,
226 major_threshold: 0.30,
227 critical_threshold: 0.50,
228 detection_window: Duration::from_secs(3600),
229 confidence_level: 0.95,
230 },
231 execution_history: HashMap::new(),
232 }
233 }
234
235 fn recent_comparison(
247 &self,
248 kernel_name: &str,
249 now_ns: u64,
250 ) -> Option<analysis::BaselineTestOutcome> {
251 let baseline = self.baseline_profiles.get(kernel_name)?;
252 let history = self.execution_history.get(kernel_name)?;
253
254 let established_ns = analysis::system_time_to_ns(baseline.established_date);
255 let window_start_ns = established_ns
256 .max(now_ns.saturating_sub(self.alert_thresholds.detection_window.as_nanos() as u64));
257 let recent = history.values_in_range(window_start_ns, now_ns);
258 if recent.len() < analysis::MIN_COMPARISON_SAMPLES {
259 return None;
260 }
261
262 analysis::compare_to_baseline(
263 baseline.performance_distribution.mean_secs,
264 baseline.performance_distribution.std_dev_secs,
265 baseline.performance_distribution.sample_count,
266 &recent,
267 )
268 }
269
270 pub fn check_regression(
277 &mut self,
278 kernel_name: &str,
279 profile_data: &KernelProfileData,
280 ) -> Result<()> {
281 let now_ns = analysis::system_time_to_ns(SystemTime::now());
282 let sample_secs = profile_data.execution_time.as_secs_f64();
283
284 self.execution_history
285 .entry(kernel_name.to_string())
286 .or_insert_with(|| TimestampedRingBuffer::new(EXECUTION_HISTORY_CAPACITY))
287 .push_now(sample_secs, now_ns);
288
289 if !self.baseline_profiles.contains_key(kernel_name) {
290 if let Some(samples) = self.execution_history.get(kernel_name).and_then(|h| {
294 (h.len() >= analysis::MIN_BASELINE_SAMPLES)
295 .then(|| h.iter_ordered().map(|v| v.value).collect::<Vec<f64>>())
296 }) {
297 let baseline = analysis::establish_baseline(kernel_name, &samples);
298 self.statistical_analyzer
299 .sample_size_requirements
300 .insert(kernel_name.to_string(), analysis::MIN_BASELINE_SAMPLES);
301 self.baseline_profiles.insert(kernel_name.to_string(), baseline);
302 }
303 return Ok(());
304 }
305
306 let Some(test_outcome) = self.recent_comparison(kernel_name, now_ns) else {
307 return Ok(());
308 };
309
310 let alpha = 1.0 - self.alert_thresholds.confidence_level;
311 self.statistical_analyzer.statistical_tests.push(StatisticalTest {
312 test_name: format!("Welch's t-test ({kernel_name})"),
313 test_type: TestType::TTest,
314 significance_level: alpha,
315 observed_power: analysis::observed_power(
316 test_outcome.t_statistic,
317 test_outcome.degrees_of_freedom,
318 alpha,
319 ),
320 });
321
322 let baseline = self
323 .baseline_profiles
324 .get(kernel_name)
325 .ok_or_else(|| anyhow::anyhow!("baseline for '{}' vanished mid-check", kernel_name))?;
326 let check = analysis::detect_regression(
327 kernel_name,
328 baseline,
329 test_outcome,
330 &self.alert_thresholds,
331 );
332 if let Some(alert) = check.new_alert {
333 self.regression_alerts.push(alert);
334 }
335
336 Ok(())
337 }
338
339 pub fn get_status(&self, kernel_name: &str) -> Result<Option<RegressionStatus>> {
348 if !self.baseline_profiles.contains_key(kernel_name) {
349 return Ok(None);
350 }
351 let now_ns = analysis::system_time_to_ns(SystemTime::now());
352 let Some(test_outcome) = self.recent_comparison(kernel_name, now_ns) else {
353 return Ok(None);
354 };
355 let baseline = self
356 .baseline_profiles
357 .get(kernel_name)
358 .ok_or_else(|| anyhow::anyhow!("baseline for '{}' vanished mid-check", kernel_name))?;
359 let check = analysis::detect_regression(
360 kernel_name,
361 baseline,
362 test_outcome,
363 &self.alert_thresholds,
364 );
365
366 let regression_alerts: Vec<RegressionAlert> = self
367 .regression_alerts
368 .iter()
369 .filter(|alert| alert.kernel_name == kernel_name)
370 .cloned()
371 .collect();
372
373 Ok(Some(RegressionStatus {
374 has_regression: check.new_alert.is_some(),
375 regression_alerts,
376 performance_trend: check.performance_trend,
377 baseline_comparison: check.baseline_comparison,
378 }))
379 }
380}
381
382impl StatisticalAnalyzer {
383 fn new() -> Result<Self> {
384 Ok(Self {
385 sample_size_requirements: HashMap::new(),
386 statistical_tests: vec![],
387 })
388 }
389
390 fn new_empty() -> Self {
391 Self {
392 sample_size_requirements: HashMap::new(),
393 statistical_tests: vec![],
394 }
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401 use std::time::Duration as StdDuration;
402
403 fn thresholds() -> RegressionThresholds {
404 RegressionThresholds {
405 minor_threshold: 0.05,
406 moderate_threshold: 0.15,
407 major_threshold: 0.30,
408 critical_threshold: 0.50,
409 detection_window: StdDuration::from_secs(3600),
410 confidence_level: 0.95,
411 }
412 }
413
414 fn profile(exec_secs: f64) -> KernelProfileData {
415 KernelProfileData {
416 execution_time: StdDuration::from_secs_f64(exec_secs),
417 grid_size: (128, 1, 1),
418 block_size: (256, 1, 1),
419 shared_memory_bytes: 4096,
420 registers_per_thread: 32,
421 occupancy: 0.5,
422 compute_utilization: 0.5,
423 memory_bandwidth_utilization: 0.5,
424 warp_efficiency: 0.9,
425 memory_efficiency: 0.8,
426 }
427 }
428
429 #[test]
430 fn test_new_has_no_baseline_and_no_status() {
431 let detector = PerformanceRegressionDetector::new().expect("new ok");
432 assert!(
433 detector.get_status("nope").expect("get_status ok").is_none(),
434 "an unknown kernel has no baseline, so status must be an honest None"
435 );
436 }
437
438 #[test]
439 fn test_status_stays_none_below_baseline_sample_count() {
440 let mut detector = PerformanceRegressionDetector::new().expect("new ok");
441 for _ in 0..(analysis::MIN_BASELINE_SAMPLES - 1) {
442 detector.check_regression("k", &profile(0.001)).expect("check ok");
443 }
444 assert!(
445 detector.get_status("k").expect("get_status ok").is_none(),
446 "fewer than MIN_BASELINE_SAMPLES real measurements must not fabricate a baseline"
447 );
448 }
449
450 #[test]
451 fn test_stable_kernel_reports_no_regression_with_real_stats() {
452 let mut detector = PerformanceRegressionDetector::new().expect("new ok");
453 for _ in 0..(analysis::MIN_BASELINE_SAMPLES + analysis::MIN_COMPARISON_SAMPLES) {
457 detector.check_regression("stable_kernel", &profile(0.001)).expect("check ok");
458 }
459 let status = detector
460 .get_status("stable_kernel")
461 .expect("get_status ok")
462 .expect("baseline should be established by now");
463 assert!(
464 !status.has_regression,
465 "identical samples must not be flagged as a regression"
466 );
467 assert!(
468 status.baseline_comparison.current_vs_baseline.abs() < 1e-6,
469 "recent mean equals baseline mean -> ~0% difference, got {}",
470 status.baseline_comparison.current_vs_baseline
471 );
472 }
473
474 #[test]
475 fn test_significant_slowdown_produces_real_alert() {
476 let mut detector = PerformanceRegressionDetector::new().expect("new ok");
477 for _ in 0..20 {
479 detector.check_regression("slow_kernel", &profile(0.0010)).expect("check ok");
480 }
481 for _ in 0..10 {
483 detector.check_regression("slow_kernel", &profile(0.0020)).expect("check ok");
484 }
485 let status = detector
486 .get_status("slow_kernel")
487 .expect("get_status ok")
488 .expect("baseline established");
489 assert!(
490 status.has_regression,
491 "a real, sustained 2x slowdown must be detected, got {:?}",
492 status.baseline_comparison
493 );
494 assert!(
495 status.baseline_comparison.current_vs_baseline > 50.0,
496 "current_vs_baseline should reflect the real ~100% slowdown, got {}",
497 status.baseline_comparison.current_vs_baseline
498 );
499 assert!(
500 !status.regression_alerts.is_empty(),
501 "check_regression must have logged a real RegressionAlert"
502 );
503 assert_eq!(status.regression_alerts[0].kernel_name, "slow_kernel");
504 }
505
506 #[test]
507 fn test_speedup_is_improving_not_a_regression() {
508 let mut detector = PerformanceRegressionDetector::new().expect("new ok");
509 for _ in 0..20 {
510 detector.check_regression("fast_kernel", &profile(0.0020)).expect("check ok");
511 }
512 for _ in 0..10 {
513 detector.check_regression("fast_kernel", &profile(0.0005)).expect("check ok");
514 }
515 let status = detector
516 .get_status("fast_kernel")
517 .expect("get_status ok")
518 .expect("baseline established");
519 assert!(
520 !status.has_regression,
521 "getting faster must never be reported as a regression"
522 );
523 assert!(
524 status.baseline_comparison.current_vs_baseline < 0.0,
525 "a real speedup must show a negative current_vs_baseline, got {}",
526 status.baseline_comparison.current_vs_baseline
527 );
528 }
529
530 #[test]
531 fn test_classify_severity_uses_configured_thresholds() {
532 let t = thresholds();
533 assert!(matches!(
534 analysis::classify_severity(0.04, &t),
535 RegressionSeverity::Minor
536 ));
537 assert!(matches!(
538 analysis::classify_severity(0.10, &t),
539 RegressionSeverity::Moderate
540 ));
541 assert!(matches!(
542 analysis::classify_severity(0.20, &t),
543 RegressionSeverity::Major
544 ));
545 assert!(matches!(
546 analysis::classify_severity(0.60, &t),
547 RegressionSeverity::Critical
548 ));
549 }
550
551 #[test]
552 fn test_shrinking_variance_is_not_mislabeled_volatile() {
553 let t = thresholds();
561 let baseline_samples = [
562 0.0008, 0.0012, 0.0008, 0.0012, 0.0008, 0.0012, 0.0008, 0.0012,
563 ];
564 let baseline = analysis::establish_baseline("k", &baseline_samples);
565 let outcome = analysis::BaselineTestOutcome {
566 relative_change: 0.0, relative_change_ci: (0.0, 0.0),
568 t_statistic: 0.0,
569 p_value: 1.0, degrees_of_freedom: 10.0,
571 variance_ratio: 0.1, };
573 let check = analysis::detect_regression("k", &baseline, outcome, &t);
574 assert!(
575 matches!(check.performance_trend, PerformanceTrend::Stable),
576 "a recent window that became MORE consistent (variance_ratio well below 1.0) with \
577 no significant mean shift must be Stable, not mislabeled Volatile, got {:?}",
578 check.performance_trend
579 );
580 }
581
582 #[test]
583 fn test_growing_variance_is_labeled_volatile() {
584 let t = thresholds();
588 let baseline_samples = [
589 0.0008, 0.0012, 0.0008, 0.0012, 0.0008, 0.0012, 0.0008, 0.0012,
590 ];
591 let baseline = analysis::establish_baseline("k", &baseline_samples);
592 let outcome = analysis::BaselineTestOutcome {
593 relative_change: 0.0,
594 relative_change_ci: (0.0, 0.0),
595 t_statistic: 0.0,
596 p_value: 1.0,
597 degrees_of_freedom: 10.0,
598 variance_ratio: 5.0,
599 };
600 let check = analysis::detect_regression("k", &baseline, outcome, &t);
601 assert!(
602 matches!(check.performance_trend, PerformanceTrend::Volatile),
603 "a recent variance >= 3x baseline must still be Volatile, got {:?}",
604 check.performance_trend
605 );
606 }
607
608 #[test]
617 fn test_welch_statistics_are_invariant_to_the_time_unit() {
618 const BASELINE: [f64; 10] = [
621 1.031, 0.987, 1.004, 1.019, 0.973, 1.011, 0.996, 1.027, 0.981, 1.008,
622 ];
623 const RECENT: [f64; 8] = [1.137, 1.152, 1.129, 1.161, 1.143, 1.156, 1.134, 1.148];
624
625 let p_at_scale = |scale: f64| -> f64 {
626 let baseline_samples: Vec<f64> = BASELINE.iter().map(|v| v * scale).collect();
627 let recent_samples: Vec<f64> = RECENT.iter().map(|v| v * scale).collect();
628 let baseline = analysis::establish_baseline("k", &baseline_samples);
629 let outcome = analysis::compare_to_baseline(
630 baseline.performance_distribution.mean_secs,
631 baseline.performance_distribution.std_dev_secs,
632 baseline.performance_distribution.sample_count,
633 &recent_samples,
634 )
635 .expect("both windows have enough real samples");
636 outcome.p_value
637 };
638
639 let seconds = p_at_scale(1.0);
640 let millis = p_at_scale(1e-3);
641 let micros = p_at_scale(1e-6);
642
643 assert!(seconds > 0.0 && seconds < 1.0, "sanity: got {seconds}");
644 assert!(
645 (millis - seconds).abs() < 1e-9,
646 "millisecond scale must give the same p-value: {millis} vs {seconds}"
647 );
648 assert!(
649 (micros - seconds).abs() < 1e-9,
650 "microsecond scale must give the same p-value: {micros} vs {seconds}"
651 );
652 assert!(
653 micros > 0.0,
654 "a microsecond-scale std_dev must not collapse to zero"
655 );
656 }
657
658 #[test]
661 fn test_far_tail_p_value_does_not_underflow_to_zero() {
662 let baseline_samples: Vec<f64> =
663 (0..40).map(|i| 1.0 + if i % 2 == 0 { 0.001 } else { -0.001 }).collect();
664 let recent_samples: Vec<f64> =
665 (0..40).map(|i| 1.5 + if i % 2 == 0 { 0.001 } else { -0.001 }).collect();
666 let baseline = analysis::establish_baseline("k", &baseline_samples);
667 let outcome = analysis::compare_to_baseline(
668 baseline.performance_distribution.mean_secs,
669 baseline.performance_distribution.std_dev_secs,
670 baseline.performance_distribution.sample_count,
671 &recent_samples,
672 )
673 .expect("both windows have enough real samples");
674
675 assert!(
676 outcome.t_statistic.abs() > 100.0,
677 "sanity: got t={}",
678 outcome.t_statistic
679 );
680 assert!(
681 outcome.p_value > 0.0,
682 "an enormous but finite t-statistic has a tiny, non-zero p-value; got exactly 0.0"
683 );
684 assert!(
685 outcome.p_value < 1e-30,
686 "and it must still be tiny: {}",
687 outcome.p_value
688 );
689 }
690
691 #[test]
694 fn test_recorded_statistical_test_reports_observed_power_not_one_minus_p() {
695 let mut detector = PerformanceRegressionDetector::new().expect("new ok");
696 for _ in 0..analysis::MIN_BASELINE_SAMPLES {
697 detector.check_regression("k", &profile(0.001)).expect("check ok");
698 }
699 for i in 0..(analysis::MIN_COMPARISON_SAMPLES + 4) {
702 let jitter = if i % 2 == 0 { 1.0e-6 } else { -1.0e-6 };
703 detector.check_regression("k", &profile(0.0015 + jitter)).expect("check ok");
704 }
705
706 let test = detector
707 .statistical_analyzer
708 .statistical_tests
709 .last()
710 .expect("a comparison must have been recorded");
711 let power = test.observed_power.expect("a finite t-statistic yields a power");
712 assert!(
713 (0.0..=1.0).contains(&power),
714 "power must be a probability, got {power}"
715 );
716 assert!(
717 (test.significance_level - 0.05).abs() < 1e-9,
718 "alpha comes from the configured confidence level"
719 );
720
721 }
725
726 #[test]
729 fn test_observed_power_is_not_one_minus_p() {
730 let t = 2.085_963_447_265_837;
732 let df = 20.0;
733 let alpha = 0.05;
734
735 let power = analysis::observed_power(t, df, alpha).expect("computable");
736 assert!(
737 (power - 0.5).abs() < 1e-3,
738 "at the critical value the noncentral-t power is ~0.5, got {power}"
739 );
740
741 let p =
742 trustformers_core::statistics::student_t_two_sided_p_value(t, df).expect("computable");
743 assert!(
744 (p - alpha).abs() < 1e-9,
745 "sanity: t is the alpha critical value, p={p}"
746 );
747 assert!(
748 ((1.0 - p) - power).abs() > 0.4,
749 "1 - p = {} is a different quantity from power = {power}",
750 1.0 - p
751 );
752
753 let strong = analysis::observed_power(8.0, df, alpha).expect("computable");
755 assert!(strong > 0.99, "got {strong}");
756 assert_eq!(analysis::observed_power(f64::NAN, df, alpha), None);
758 assert_eq!(analysis::observed_power(t, 0.0, alpha), None);
759 assert_eq!(analysis::observed_power(t, df, 0.0), None);
760 }
761
762 #[test]
763 fn test_statistical_analyzer_new() {
764 let analyzer = StatisticalAnalyzer::new().expect("new ok");
765 assert!(analyzer.sample_size_requirements.is_empty());
766 assert!(analyzer.statistical_tests.is_empty());
767 }
768
769 #[test]
770 fn test_statistical_analyzer_new_empty() {
771 let analyzer = StatisticalAnalyzer::new_empty();
772 assert!(analyzer.sample_size_requirements.is_empty());
773 }
774}