Files
foxhunt/testing/e2e/src/performance.rs
jgrusewski 9c3d741a08 refactor: restructure repo — crates/, bin/, testing/ layout
Move 17 library crates into crates/, CLI binary into bin/fxt,
consolidate 10 test crates into testing/, split config crate
from deployment config files.

Root directory reduced from 38+ to ~17 directories.
All Cargo.toml paths and build.rs proto refs updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 11:56:00 +01:00

484 lines
15 KiB
Rust

//! 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<chrono::Utc>,
pub value: f64,
pub tags: HashMap<String, String>,
}
/// 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<Mutex<HashMap<String, Vec<MetricPoint>>>>,
thresholds: HashMap<String, f64>,
start_time: Instant,
}
impl PerformanceTracker {
/// Create a new performance tracker
pub fn new(session_id: &str) -> Result<Self> {
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<String, String>,
) -> 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<Option<MetricStats>> {
let metrics = self.metrics.lock().unwrap();
if let Some(points) = metrics.get(name) {
if points.is_empty() {
return Ok(None);
}
let values: Vec<f64> = 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<String> {
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<Vec<f64>> {
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<PerformanceReport> {
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<f64> = 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<String> {
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::<f64>() / 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<String, f64> {
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<String, MetricStats>,
pub threshold_violations: Vec<ThresholdViolation>,
pub total_metrics_collected: usize,
pub report_generated_at: chrono::DateTime<chrono::Utc>,
}
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<String, Vec<MetricPoint>>,
exported_at: chrono::DateTime<chrono::Utc>,
}
impl serde::Serialize for MetricPoint {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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);
}
}