//! Performance Tracking for E2E Tests //! //! Provides comprehensive performance monitoring and metrics collection //! for E2E tests including latency tracking, throughput measurement, //! and performance regression detection. use anyhow::{Context, Result}; use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use tracing::{debug, info, warn}; /// Performance metric data point #[derive(Debug, Clone)] pub struct MetricPoint { pub timestamp: chrono::DateTime, pub value: f64, pub tags: HashMap, } /// Performance statistics for a metric #[derive(Debug, Clone)] pub struct MetricStats { pub count: u64, pub sum: f64, pub min: f64, pub max: f64, pub mean: f64, pub std_dev: f64, pub p50: f64, pub p95: f64, pub p99: f64, } /// Latency measurement helper #[derive(Debug)] pub struct LatencyTracker { start_time: Instant, operation_name: String, } impl LatencyTracker { /// Start tracking latency for an operation pub fn start(operation_name: &str) -> Self { Self { start_time: Instant::now(), operation_name: operation_name.to_string(), } } /// Stop tracking and return the duration pub fn stop(self) -> (String, Duration) { let duration = self.start_time.elapsed(); (self.operation_name, duration) } } /// Performance tracker for E2E tests #[derive(Debug)] pub struct PerformanceTracker { session_id: String, metrics: Arc>>>, thresholds: HashMap, start_time: Instant, } impl PerformanceTracker { /// Create a new performance tracker pub fn new(session_id: &str) -> Result { info!( "📊 Initializing performance tracker for session: {}", session_id ); let thresholds = Self::create_default_thresholds(); Ok(Self { session_id: session_id.to_string(), metrics: Arc::new(Mutex::new(HashMap::new())), thresholds, start_time: Instant::now(), }) } /// Record a metric value pub fn record_metric(&self, name: &str, value: f64) -> Result<()> { self.record_metric_with_tags(name, value, HashMap::new()) } /// Record a metric value with tags pub fn record_metric_with_tags( &self, name: &str, value: f64, tags: HashMap, ) -> Result<()> { let mut metrics = self.metrics.lock().unwrap(); let metric_point = MetricPoint { timestamp: chrono::Utc::now(), value, tags, }; metrics .entry(name.to_string()) .or_default() .push(metric_point); debug!("📈 Recorded metric '{}' = {}", name, value); // Check threshold if configured if let Some(&threshold) = self.thresholds.get(name) { if value > threshold { warn!( "⚠️ Metric '{}' ({}) exceeds threshold ({})", name, value, threshold ); } } Ok(()) } /// Record latency metric from duration pub fn record_latency(&self, operation: &str, duration: Duration) -> Result<()> { let latency_ms = duration.as_millis() as f64; let mut tags = HashMap::new(); tags.insert("operation".to_string(), operation.to_string()); self.record_metric_with_tags("latency_ms", latency_ms, tags) } /// Start latency tracking pub fn start_latency_tracking(&self, operation: &str) -> LatencyTracker { LatencyTracker::start(operation) } /// Record throughput metric (operations per second) pub fn record_throughput( &self, operation: &str, operations: u64, duration: Duration, ) -> Result<()> { let throughput = operations as f64 / duration.as_secs_f64(); let mut tags = HashMap::new(); tags.insert("operation".to_string(), operation.to_string()); self.record_metric_with_tags("throughput_ops_per_sec", throughput, tags) } /// Record error rate metric pub fn record_error_rate(&self, operation: &str, errors: u64, total: u64) -> Result<()> { let error_rate = if total > 0 { errors as f64 / total as f64 } else { 0.0 }; let mut tags = HashMap::new(); tags.insert("operation".to_string(), operation.to_string()); self.record_metric_with_tags("error_rate", error_rate, tags) } /// Get statistics for a metric pub fn get_metric_stats(&self, name: &str) -> Result> { let metrics = self.metrics.lock().unwrap(); if let Some(points) = metrics.get(name) { if points.is_empty() { return Ok(None); } let values: Vec = points.iter().map(|p| p.value).collect(); let stats = Self::calculate_stats(&values); Ok(Some(stats)) } else { Ok(None) } } /// Get all metric names pub fn get_metric_names(&self) -> Vec { let metrics = self.metrics.lock().unwrap(); metrics.keys().cloned().collect() } /// Get metric values for a specific metric pub fn get_metric_values(&self, name: &str) -> Result> { let metrics = self.metrics.lock().unwrap(); Ok(metrics .get(name) .map(|points| points.iter().map(|p| p.value).collect()) .unwrap_or_default()) } /// Generate performance report pub fn generate_report(&self) -> Result { let metrics = self.metrics.lock().unwrap(); let session_duration = self.start_time.elapsed(); let mut metric_summaries = HashMap::new(); let mut violations = Vec::new(); for (name, points) in metrics.iter() { if !points.is_empty() { let values: Vec = points.iter().map(|p| p.value).collect(); let stats = Self::calculate_stats(&values); // Check for threshold violations if let Some(&threshold) = self.thresholds.get(name) { if stats.max > threshold { violations.push(ThresholdViolation { metric_name: name.clone(), threshold, actual_value: stats.max, violation_type: "max_exceeded".to_string(), }); } } metric_summaries.insert(name.clone(), stats); } } Ok(PerformanceReport { session_id: self.session_id.clone(), session_duration, metric_summaries, threshold_violations: violations, total_metrics_collected: metrics.len(), report_generated_at: chrono::Utc::now(), }) } /// Export metrics to JSON pub fn export_metrics_json(&self) -> Result { let metrics = self.metrics.lock().unwrap(); let export_data = ExportData { session_id: self.session_id.clone(), session_duration_ms: self.start_time.elapsed().as_millis() as u64, metrics: metrics.clone(), exported_at: chrono::Utc::now(), }; serde_json::to_string_pretty(&export_data).context("Failed to serialize metrics to JSON") } /// Save metrics to file pub fn save_metrics_to_file(&self, file_path: &str) -> Result<()> { let json_data = self.export_metrics_json()?; std::fs::write(file_path, json_data) .with_context(|| format!("Failed to write metrics to file: {}", file_path))?; info!("📁 Saved performance metrics to: {}", file_path); Ok(()) } /// Set threshold for a metric pub fn set_threshold(&mut self, metric_name: &str, threshold: f64) { self.thresholds.insert(metric_name.to_string(), threshold); debug!("🎯 Set threshold for '{}': {}", metric_name, threshold); } /// Calculate statistics for values fn calculate_stats(values: &[f64]) -> MetricStats { if values.is_empty() { return MetricStats { count: 0, sum: 0.0, min: 0.0, max: 0.0, mean: 0.0, std_dev: 0.0, p50: 0.0, p95: 0.0, p99: 0.0, }; } let mut sorted_values = values.to_vec(); sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap()); let count = values.len() as u64; let sum: f64 = values.iter().sum(); let mean = sum / values.len() as f64; let min = sorted_values[0]; let max = sorted_values[sorted_values.len() - 1]; // Calculate standard deviation let variance: f64 = values.iter().map(|x| (x - mean).powi(2)).sum::() / values.len() as f64; let std_dev = variance.sqrt(); // Calculate percentiles let p50 = Self::percentile(&sorted_values, 0.5); let p95 = Self::percentile(&sorted_values, 0.95); let p99 = Self::percentile(&sorted_values, 0.99); MetricStats { count, sum, min, max, mean, std_dev, p50, p95, p99, } } /// Calculate percentile from sorted values fn percentile(sorted_values: &[f64], percentile: f64) -> f64 { if sorted_values.is_empty() { return 0.0; } let index = (percentile * (sorted_values.len() - 1) as f64).round() as usize; sorted_values[index.min(sorted_values.len() - 1)] } /// Create default thresholds for common metrics fn create_default_thresholds() -> HashMap { let mut thresholds = HashMap::new(); // Latency thresholds (milliseconds) thresholds.insert("latency_ms".to_string(), 1000.0); // 1 second thresholds.insert("order_submission_latency_ms".to_string(), 100.0); thresholds.insert("ml_inference_latency_ms".to_string(), 200.0); thresholds.insert("config_update_latency_ms".to_string(), 500.0); // Error rate thresholds thresholds.insert("error_rate".to_string(), 0.05); // 5% // Throughput thresholds (minimum ops/sec) thresholds.insert("throughput_ops_per_sec".to_string(), 1.0); thresholds } } /// Performance report structure #[derive(Debug)] pub struct PerformanceReport { pub session_id: String, pub session_duration: Duration, pub metric_summaries: HashMap, pub threshold_violations: Vec, pub total_metrics_collected: usize, pub report_generated_at: chrono::DateTime, } impl PerformanceReport { /// Print a summary of the performance report pub fn print_summary(&self) { info!("📊 Performance Report Summary"); info!("Session ID: {}", self.session_id); info!("Session Duration: {:?}", self.session_duration); info!("Total Metrics: {}", self.total_metrics_collected); info!("Threshold Violations: {}", self.threshold_violations.len()); if !self.threshold_violations.is_empty() { warn!("⚠️ Threshold Violations:"); for violation in &self.threshold_violations { warn!( " {} exceeded threshold {} with value {}", violation.metric_name, violation.threshold, violation.actual_value ); } } info!("📈 Key Metrics:"); for (name, stats) in &self.metric_summaries { if name.contains("latency") { info!( " {}: mean={:.2}ms, p95={:.2}ms, p99={:.2}ms", name, stats.mean, stats.p95, stats.p99 ); } else if name.contains("throughput") { info!( " {}: mean={:.2} ops/sec, max={:.2} ops/sec", name, stats.mean, stats.max ); } else { info!( " {}: mean={:.2}, min={:.2}, max={:.2}", name, stats.mean, stats.min, stats.max ); } } } } /// Threshold violation information #[derive(Debug)] pub struct ThresholdViolation { pub metric_name: String, pub threshold: f64, pub actual_value: f64, pub violation_type: String, } /// Export data structure for JSON serialization #[derive(Debug, serde::Serialize)] struct ExportData { session_id: String, session_duration_ms: u64, metrics: HashMap>, exported_at: chrono::DateTime, } impl serde::Serialize for MetricPoint { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { use serde::ser::SerializeStruct; let mut state = serializer.serialize_struct("MetricPoint", 3)?; state.serialize_field("timestamp", &self.timestamp.to_rfc3339())?; state.serialize_field("value", &self.value)?; state.serialize_field("tags", &self.tags)?; state.end() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_performance_tracker_creation() { let tracker = PerformanceTracker::new("test_session"); assert!(tracker.is_ok()); let tracker = tracker.unwrap(); assert_eq!(tracker.session_id, "test_session"); } #[test] fn test_metric_recording() { let tracker = PerformanceTracker::new("test").unwrap(); let result = tracker.record_metric("test_metric", 42.0); assert!(result.is_ok()); let values = tracker.get_metric_values("test_metric").unwrap(); assert_eq!(values.len(), 1); assert_eq!(values[0], 42.0); } #[test] fn test_stats_calculation() { let values = vec![1.0, 2.0, 3.0, 4.0, 5.0]; let stats = PerformanceTracker::calculate_stats(&values); assert_eq!(stats.count, 5); assert_eq!(stats.min, 1.0); assert_eq!(stats.max, 5.0); assert_eq!(stats.mean, 3.0); assert_eq!(stats.p50, 3.0); } #[test] fn test_latency_tracker() { let tracker = LatencyTracker::start("test_operation"); std::thread::sleep(Duration::from_millis(1)); let (operation, duration) = tracker.stop(); assert_eq!(operation, "test_operation"); assert!(duration.as_millis() >= 1); } #[test] fn test_percentile_calculation() { let values = vec![1.0, 2.0, 3.0, 4.0, 5.0]; assert_eq!(PerformanceTracker::percentile(&values, 0.0), 1.0); assert_eq!(PerformanceTracker::percentile(&values, 0.5), 3.0); assert_eq!(PerformanceTracker::percentile(&values, 1.0), 5.0); } }