mod classification;
mod histogram;
mod moments;
pub use classification::{DistributionShape, TailSeverity};
pub use histogram::{HistogramBucket, LatencyHistogram};
use crate::statistics::percentile;
use moments::{calculate_jitter, calculate_moments};
#[derive(Debug, Clone)]
pub struct LatencyDistribution {
pub p50: f64,
pub p90: f64,
pub p99: f64,
pub p999: f64,
pub jitter: f64,
pub tail_ratio: f64,
pub bimodality_coefficient: f64,
pub histogram: LatencyHistogram,
pub sample_count: usize,
pub min: f64,
pub max: f64,
pub mean: f64,
pub std_dev: f64,
pub skewness: f64,
pub kurtosis: f64,
pub outlier_ratio: f64,
}
impl LatencyDistribution {
pub fn analyze(samples: &[f64]) -> Option<Self> {
if samples.is_empty() {
return None;
}
let n = samples.len();
let mean = samples.iter().sum::<f64>() / n as f64;
let min = samples.iter().cloned().fold(f64::INFINITY, f64::min);
let max = samples.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let variance = if n > 1 {
samples.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (n - 1) as f64
} else {
0.0
};
let std_dev = variance.sqrt();
let p50 = percentile(samples, 0.50);
let p90 = percentile(samples, 0.90);
let p99 = percentile(samples, 0.99);
let p999 = percentile(samples, 0.999);
let jitter = calculate_jitter(samples);
let tail_ratio = if p50 > 0.0 { p99 / p50 } else { 1.0 };
let (skewness, kurtosis) = calculate_moments(samples, mean, std_dev);
let bimodality_coefficient = if kurtosis > 0.0 {
(skewness.powi(2) + 1.0) / kurtosis
} else {
0.0
};
let outlier_count = samples
.iter()
.filter(|&&x| (x - mean).abs() > 3.0 * std_dev)
.count();
let outlier_ratio = outlier_count as f64 / n as f64 * 100.0;
let histogram = LatencyHistogram::build(samples, 20);
Some(Self {
p50,
p90,
p99,
p999,
jitter,
tail_ratio,
bimodality_coefficient,
histogram,
sample_count: n,
min,
max,
mean,
std_dev,
skewness,
kurtosis,
outlier_ratio,
})
}
pub fn tail_severity(&self) -> TailSeverity {
TailSeverity::from_ratio(self.tail_ratio)
}
pub fn distribution_shape(&self) -> DistributionShape {
DistributionShape::classify(self.bimodality_coefficient, self.histogram.entropy)
}
pub fn has_tail_problem(&self) -> bool {
self.tail_ratio > 3.0
}
pub fn is_bimodal(&self) -> bool {
self.bimodality_coefficient > 0.555
}
pub fn summary(&self) -> String {
format!(
"n={} p50={:.2}µs p99={:.2}µs tail_ratio={:.2} jitter={:.2}µs shape={}",
self.sample_count,
self.p50,
self.p99,
self.tail_ratio,
self.jitter,
self.distribution_shape().name()
)
}
}
#[cfg(test)]
mod tests;