Files
foxhunt/services/ml_training_service/tests/baselines/regression_detection.rs
jgrusewski 7458f1be01 feat(wave12): E2E validation complete - 225-feature pipeline ready
 Validation Results:
- PPO training: 24.2s (1 epoch, 950 samples, dim=225)
- Feature extraction: 105μs/bar (9.5x faster than target)
- Model checkpoint: 293KB (147KB actor + 146KB critic)
- GPU memory: 145MB used (96.4% headroom)
- Zero dimension mismatches

📊 Success Criteria (5/5):
 Feature dimension = 225 (Wave C 201 + Wave D 24)
 Model state_dim = 225
 Training completed without errors
 Checkpoint saved successfully
 No dimension mismatch errors

📁 Training Data Ready:
- ES.FUT: 2.9MB, 180 days
- NQ.FUT: 4.4MB, 180 days
- 6E.FUT: 2.8MB, 180 days
- ZN.FUT: 65KB, 90 days (clean)

🚀 Next: Full production model retraining (4 models, ~10min GPU time)

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 22:48:04 +02:00

300 lines
11 KiB
Rust

//! 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<RegressionViolation>,
}
#[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<Self> {
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());
}
}