#![allow(clippy::cast_precision_loss)]
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct MeasurementProtocol {
pub latency_samples: usize,
pub latency_percentiles: Vec<f64>,
pub throughput_duration: Duration,
pub throughput_ramp_up: Duration,
pub memory_samples: usize,
pub memory_interval: Duration,
}
impl Default for MeasurementProtocol {
fn default() -> Self {
Self {
latency_samples: 100,
latency_percentiles: vec![50.0, 90.0, 95.0, 99.0, 99.9],
throughput_duration: Duration::from_secs(60),
throughput_ramp_up: Duration::from_secs(10),
memory_samples: 10,
memory_interval: Duration::from_secs(1),
}
}
}
impl MeasurementProtocol {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_latency_samples(mut self, samples: usize) -> Self {
self.latency_samples = samples;
self
}
#[must_use]
pub fn with_percentiles(mut self, percentiles: Vec<f64>) -> Self {
self.latency_percentiles = percentiles;
self
}
#[must_use]
pub fn with_throughput_duration(mut self, duration: Duration) -> Self {
self.throughput_duration = duration;
self
}
#[must_use]
pub fn with_memory_samples(mut self, samples: usize) -> Self {
self.memory_samples = samples;
self
}
}
#[derive(Debug, Clone)]
pub struct LatencyStatistics {
pub mean: Duration,
pub std_dev: Duration,
pub min: Duration,
pub max: Duration,
pub p50: Duration,
pub p90: Duration,
pub p95: Duration,
pub p99: Duration,
pub p999: Duration,
pub samples: usize,
pub confidence_interval_95: (Duration, Duration),
}
impl LatencyStatistics {
#[must_use]
pub fn from_samples(samples: &[Duration]) -> Self {
assert!(!samples.is_empty(), "samples must not be empty");
let n = samples.len();
let n_f64 = n as f64;
let sum_nanos: u128 = samples.iter().map(Duration::as_nanos).sum();
let mean_nanos = sum_nanos / n as u128;
let mean = Duration::from_nanos(mean_nanos as u64);
let variance: f64 = samples
.iter()
.map(|s| {
let diff = s.as_nanos() as f64 - mean_nanos as f64;
diff * diff
})
.sum::<f64>()
/ (n_f64 - 1.0).max(1.0);
let std_dev_nanos = variance.sqrt();
let std_dev = Duration::from_nanos(std_dev_nanos as u64);
let mut sorted: Vec<Duration> = samples.to_vec();
sorted.sort();
let min = sorted[0];
let max = sorted[n - 1];
let percentile = |p: f64| -> Duration {
let idx = ((p / 100.0) * n_f64).ceil() as usize;
sorted[idx.saturating_sub(1).min(n - 1)]
};
let p50 = percentile(50.0);
let p90 = percentile(90.0);
let p95 = percentile(95.0);
let p99 = percentile(99.0);
let p999 = percentile(99.9);
let t_value = if n >= 30 { 1.96 } else { 2.0 + 4.0 / n_f64 };
let margin = std_dev_nanos * t_value / n_f64.sqrt();
let lower = Duration::from_nanos((mean_nanos as f64 - margin).max(0.0) as u64);
let upper = Duration::from_nanos((mean_nanos as f64 + margin) as u64);
Self {
mean,
std_dev,
min,
max,
p50,
p90,
p95,
p99,
p999,
samples: n,
confidence_interval_95: (lower, upper),
}
}
}
pub fn detect_outliers(samples: &[f64], threshold: f64) -> Vec<usize> {
if samples.len() < 3 {
return Vec::new();
}
let mut sorted = samples.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let median = if sorted.len().is_multiple_of(2) {
f64::midpoint(sorted[sorted.len() / 2 - 1], sorted[sorted.len() / 2])
} else {
sorted[sorted.len() / 2]
};
let mut deviations: Vec<f64> = samples.iter().map(|x| (x - median).abs()).collect();
deviations.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let mad = if deviations.len().is_multiple_of(2) {
f64::midpoint(
deviations[deviations.len() / 2 - 1],
deviations[deviations.len() / 2],
)
} else {
deviations[deviations.len() / 2]
};
if mad < f64::EPSILON {
return Vec::new();
}
let k = 1.4826;
samples
.iter()
.enumerate()
.filter(|(_, &x)| {
let modified_z = (x - median) / (k * mad);
modified_z.abs() > threshold
})
.map(|(i, _)| i)
.collect()
}
#[derive(Debug, Clone)]
pub struct BenchmarkMetrics {
pub name: String,
pub mean: f64,
pub std_dev: f64,
pub samples: usize,
}
#[derive(Debug, Clone)]
pub struct Regression {
pub metric: String,
pub baseline: f64,
pub current: f64,
pub change_percent: f64,
}
#[derive(Debug, Clone)]
pub struct RegressionReport {
pub regressions: Vec<Regression>,
pub warnings: Vec<Regression>,
pub improvements: Vec<Regression>,
pub passed: bool,
}
#[derive(Debug, Clone)]
pub struct RegressionDetector {
pub warning_threshold: f64,
pub failure_threshold: f64,
}
impl Default for RegressionDetector {
fn default() -> Self {
Self {
warning_threshold: 0.02, failure_threshold: 0.05, }
}
}
impl RegressionDetector {
pub fn compare(
&self,
baseline: &BenchmarkMetrics,
current: &BenchmarkMetrics,
) -> RegressionReport {
let mut regressions = Vec::new();
let mut warnings = Vec::new();
let mut improvements = Vec::new();
let change = (current.mean - baseline.mean) / baseline.mean;
let item = Regression {
metric: baseline.name.clone(),
baseline: baseline.mean,
current: current.mean,
change_percent: change * 100.0,
};
if change > self.failure_threshold {
regressions.push(item);
} else if change > self.warning_threshold {
warnings.push(item);
} else if change < -self.warning_threshold {
improvements.push(item);
}
RegressionReport {
passed: regressions.is_empty(),
regressions,
warnings,
improvements,
}
}
}
#[derive(Debug, Clone)]
pub struct WelchTTestResult {
pub t_statistic: f64,
pub degrees_of_freedom: f64,
pub p_value: f64,
pub significant: bool,
}
include!("welch_t_test.rs");
include!("statistics_measurement_protocol.rs");