✅ 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>
300 lines
11 KiB
Rust
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());
|
|
}
|
|
}
|