//! Performance Regression Detection //! //! Automatically detects performance regressions by comparing //! test results against established baselines. use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::fs; use std::path::Path; use std::time::Duration; /// Performance baseline metrics #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BaselineMetrics { pub version: String, pub description: String, pub baselines: Baselines, pub system_requirements: SystemRequirements, pub regression_thresholds: RegressionThresholds, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Baselines { pub batch_creation: BatchCreationBaseline, pub streaming: StreamingBaseline, pub state_transitions: StateTransitionBaseline, pub file_discovery: FileDiscoveryBaseline, pub memory_leak: MemoryLeakBaseline, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BatchCreationBaseline { pub description: String, pub p95_latency_ms: f64, pub throughput_ops_per_sec: f64, pub max_concurrent: u64, pub acceptable_failure_rate_percent: f64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StreamingBaseline { pub description: String, pub throughput_msg_per_sec_per_stream: f64, pub max_concurrent_streams: u64, pub backpressure_handling: bool, pub message_loss_rate_percent: f64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StateTransitionBaseline { pub description: String, pub p95_latency_ms: f64, pub throughput_updates_per_sec: f64, pub deadlock_free: bool, pub rollback_correctness: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FileDiscoveryBaseline { pub description: String, pub discovery_time_100_files_ms: f64, pub discovery_time_10k_files_ms: f64, pub concurrent_requests: u64, pub cache_invalidation: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MemoryLeakBaseline { pub description: String, pub max_rss_growth_percent_per_hour: f64, pub max_leak_after_cleanup_percent: f64, pub stream_cleanup_leak_percent: f64, pub connection_leak_percent: f64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SystemRequirements { pub min_cpu_cores: u32, pub min_ram_gb: u32, pub min_disk_gb: u32, pub postgres_connections: u32, pub recommended_environment: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RegressionThresholds { pub latency_p95_degradation_percent: f64, pub throughput_degradation_percent: f64, pub memory_growth_degradation_percent: f64, pub failure_rate_increase_percent: f64, } /// Regression detection result #[derive(Debug, Clone)] pub struct RegressionResult { pub test_name: String, pub has_regression: bool, pub violations: Vec, } #[derive(Debug, Clone)] pub struct RegressionViolation { pub metric: String, pub baseline_value: f64, pub actual_value: f64, pub degradation_percent: f64, pub threshold_percent: f64, } /// Regression detector pub struct RegressionDetector { baselines: BaselineMetrics, } impl RegressionDetector { /// Load baselines from file pub fn load_from_file(path: &Path) -> Result { let content = fs::read_to_string(path) .context("Failed to read baseline metrics file")?; let baselines: BaselineMetrics = serde_json::from_str(&content) .context("Failed to parse baseline metrics JSON")?; Ok(Self { baselines }) } /// Check batch creation performance pub fn check_batch_creation( &self, p95_latency: Duration, throughput: f64, failure_rate_percent: f64, ) -> RegressionResult { let mut violations = Vec::new(); let p95_ms = p95_latency.as_secs_f64() * 1000.0; let baseline = &self.baselines.baselines.batch_creation; // Check P95 latency if p95_ms > baseline.p95_latency_ms { let degradation = ((p95_ms - baseline.p95_latency_ms) / baseline.p95_latency_ms) * 100.0; if degradation > self.baselines.regression_thresholds.latency_p95_degradation_percent { violations.push(RegressionViolation { metric: "p95_latency_ms".to_string(), baseline_value: baseline.p95_latency_ms, actual_value: p95_ms, degradation_percent: degradation, threshold_percent: self.baselines.regression_thresholds.latency_p95_degradation_percent, }); } } // Check throughput if throughput < baseline.throughput_ops_per_sec { let degradation = ((baseline.throughput_ops_per_sec - throughput) / baseline.throughput_ops_per_sec) * 100.0; if degradation > self.baselines.regression_thresholds.throughput_degradation_percent { violations.push(RegressionViolation { metric: "throughput_ops_per_sec".to_string(), baseline_value: baseline.throughput_ops_per_sec, actual_value: throughput, degradation_percent: degradation, threshold_percent: self.baselines.regression_thresholds.throughput_degradation_percent, }); } } // Check failure rate if failure_rate_percent > baseline.acceptable_failure_rate_percent { let degradation = ((failure_rate_percent - baseline.acceptable_failure_rate_percent) / baseline.acceptable_failure_rate_percent) * 100.0; if degradation > self.baselines.regression_thresholds.failure_rate_increase_percent { violations.push(RegressionViolation { metric: "failure_rate_percent".to_string(), baseline_value: baseline.acceptable_failure_rate_percent, actual_value: failure_rate_percent, degradation_percent: degradation, threshold_percent: self.baselines.regression_thresholds.failure_rate_increase_percent, }); } } RegressionResult { test_name: "batch_creation".to_string(), has_regression: !violations.is_empty(), violations, } } /// Check memory leak performance pub fn check_memory_leak( &self, rss_growth_percent: f64, cleanup_leak_percent: f64, ) -> RegressionResult { let mut violations = Vec::new(); let baseline = &self.baselines.baselines.memory_leak; // Check RSS growth if rss_growth_percent > baseline.max_rss_growth_percent_per_hour { let degradation = ((rss_growth_percent - baseline.max_rss_growth_percent_per_hour) / baseline.max_rss_growth_percent_per_hour) * 100.0; if degradation > self.baselines.regression_thresholds.memory_growth_degradation_percent { violations.push(RegressionViolation { metric: "rss_growth_percent_per_hour".to_string(), baseline_value: baseline.max_rss_growth_percent_per_hour, actual_value: rss_growth_percent, degradation_percent: degradation, threshold_percent: self.baselines.regression_thresholds.memory_growth_degradation_percent, }); } } // Check cleanup leak if cleanup_leak_percent > baseline.max_leak_after_cleanup_percent { let degradation = ((cleanup_leak_percent - baseline.max_leak_after_cleanup_percent) / baseline.max_leak_after_cleanup_percent) * 100.0; if degradation > self.baselines.regression_thresholds.memory_growth_degradation_percent { violations.push(RegressionViolation { metric: "cleanup_leak_percent".to_string(), baseline_value: baseline.max_leak_after_cleanup_percent, actual_value: cleanup_leak_percent, degradation_percent: degradation, threshold_percent: self.baselines.regression_thresholds.memory_growth_degradation_percent, }); } } RegressionResult { test_name: "memory_leak".to_string(), has_regression: !violations.is_empty(), violations, } } /// Print regression report pub fn print_report(&self, result: &RegressionResult) { println!("\n=== Regression Detection Report: {} ===", result.test_name); if !result.has_regression { println!("✓ No performance regressions detected"); return; } println!("✗ Performance regressions detected:"); for violation in &result.violations { println!("\n Metric: {}", violation.metric); println!(" Baseline: {:.2}", violation.baseline_value); println!(" Actual: {:.2}", violation.actual_value); println!(" Degradation: {:.2}% (threshold: {:.2}%)", violation.degradation_percent, violation.threshold_percent); } } } #[cfg(test)] mod tests { use super::*; use std::time::Duration; #[test] fn test_load_baselines() { let baseline_path = Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/baselines/baseline_metrics.json"); let detector = RegressionDetector::load_from_file(&baseline_path); assert!(detector.is_ok(), "Failed to load baseline metrics"); } #[test] fn test_batch_creation_no_regression() { let baseline_path = Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/baselines/baseline_metrics.json"); let detector = RegressionDetector::load_from_file(&baseline_path).unwrap(); let result = detector.check_batch_creation( Duration::from_millis(8), // Better than baseline (10ms) 1200.0, // Better than baseline (1000 ops/sec) 0.5, // Better than baseline (1%) ); assert!(!result.has_regression); assert!(result.violations.is_empty()); } #[test] fn test_batch_creation_with_regression() { let baseline_path = Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/baselines/baseline_metrics.json"); let detector = RegressionDetector::load_from_file(&baseline_path).unwrap(); let result = detector.check_batch_creation( Duration::from_millis(15), // 50% worse than baseline (10ms) 500.0, // 50% worse than baseline (1000 ops/sec) 2.0, // 100% worse than baseline (1%) ); assert!(result.has_regression); assert!(!result.violations.is_empty()); } }