use std::time::Duration;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PerformanceMetrics {
pub avg_tick_latency_ms: f64,
pub tick_latency_p95_ms: f64,
pub tick_latency_p99_ms: f64,
pub tick_miss_rate: f64,
pub tick_jitter: f64,
pub makespan_ratio: f64,
pub raw_throughput_per_sec: f64,
pub effective_throughput_per_sec: f64,
pub total_duration_ms: f64,
pub llm_invocations: u64,
pub llm_invoke_errors: u64,
pub llm_error_rate: f64,
}
impl PerformanceMetrics {
pub fn from_latencies(
latencies: &[Duration],
target_tick_duration: Option<Duration>,
total_actions: u64,
max_ticks: u64,
) -> Self {
if latencies.is_empty() {
return Self::default();
}
let mut latencies_ms: Vec<f64> =
latencies.iter().map(|d| d.as_secs_f64() * 1000.0).collect();
latencies_ms.sort_by(|a, b| a.partial_cmp(b).unwrap());
let n = latencies_ms.len();
let sum: f64 = latencies_ms.iter().sum();
let mean = sum / n as f64;
let variance: f64 = latencies_ms.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n as f64;
let std_dev = variance.sqrt();
let jitter = if mean > 0.0 { std_dev / mean } else { 0.0 };
let p95_idx = ((n as f64) * 0.95).ceil() as usize - 1;
let p99_idx = ((n as f64) * 0.99).ceil() as usize - 1;
let p95 = latencies_ms[p95_idx.min(n - 1)];
let p99 = latencies_ms[p99_idx.min(n - 1)];
let miss_rate = if let Some(target) = target_tick_duration {
let target_ms = target.as_secs_f64() * 1000.0;
let misses = latencies_ms.iter().filter(|&&l| l > target_ms).count();
misses as f64 / n as f64
} else {
0.0
};
let total_duration_ms = sum;
let throughput = if total_duration_ms > 0.0 {
(total_actions as f64) / (total_duration_ms / 1000.0)
} else {
0.0
};
let makespan_ratio = if max_ticks > 0 {
n as f64 / max_ticks as f64
} else {
0.0
};
Self {
avg_tick_latency_ms: mean,
tick_latency_p95_ms: p95,
tick_latency_p99_ms: p99,
tick_miss_rate: miss_rate,
tick_jitter: jitter,
makespan_ratio,
raw_throughput_per_sec: throughput,
effective_throughput_per_sec: 0.0, total_duration_ms,
llm_invocations: 0, llm_invoke_errors: 0, llm_error_rate: 0.0, }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_latencies() {
let metrics = PerformanceMetrics::from_latencies(&[], None, 0, 0);
assert_eq!(metrics.avg_tick_latency_ms, 0.0);
}
#[test]
fn test_latency_calculations() {
let latencies: Vec<Duration> = (1..=100).map(Duration::from_millis).collect();
let metrics = PerformanceMetrics::from_latencies(
&latencies,
Some(Duration::from_millis(80)),
1000,
100,
);
assert!((metrics.avg_tick_latency_ms - 50.5).abs() < 1.0);
assert!((metrics.tick_latency_p95_ms - 95.0).abs() < 1.0);
assert!((metrics.tick_latency_p99_ms - 99.0).abs() < 1.0);
assert!((metrics.tick_miss_rate - 0.2).abs() < 0.01);
assert!((metrics.makespan_ratio - 1.0).abs() < 0.01);
}
}