cbtop/latency_distribution/
mod.rs1mod classification;
16mod histogram;
17mod moments;
18
19pub use classification::{DistributionShape, TailSeverity};
20pub use histogram::{HistogramBucket, LatencyHistogram};
21
22use crate::statistics::percentile;
23use moments::{calculate_jitter, calculate_moments};
24
25#[derive(Debug, Clone)]
27pub struct LatencyDistribution {
28 pub p50: f64,
30 pub p90: f64,
32 pub p99: f64,
34 pub p999: f64,
36 pub jitter: f64,
38 pub tail_ratio: f64,
40 pub bimodality_coefficient: f64,
42 pub histogram: LatencyHistogram,
44 pub sample_count: usize,
46 pub min: f64,
48 pub max: f64,
50 pub mean: f64,
52 pub std_dev: f64,
54 pub skewness: f64,
56 pub kurtosis: f64,
58 pub outlier_ratio: f64,
60}
61
62impl LatencyDistribution {
63 pub fn analyze(samples: &[f64]) -> Option<Self> {
65 if samples.is_empty() {
66 return None;
67 }
68
69 let n = samples.len();
70
71 let mean = samples.iter().sum::<f64>() / n as f64;
73 let min = samples.iter().cloned().fold(f64::INFINITY, f64::min);
74 let max = samples.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
75
76 let variance = if n > 1 {
78 samples.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (n - 1) as f64
79 } else {
80 0.0
81 };
82 let std_dev = variance.sqrt();
83
84 let p50 = percentile(samples, 0.50);
86 let p90 = percentile(samples, 0.90);
87 let p99 = percentile(samples, 0.99);
88 let p999 = percentile(samples, 0.999);
89
90 let jitter = calculate_jitter(samples);
92
93 let tail_ratio = if p50 > 0.0 { p99 / p50 } else { 1.0 };
95
96 let (skewness, kurtosis) = calculate_moments(samples, mean, std_dev);
98
99 let bimodality_coefficient = if kurtosis > 0.0 {
101 (skewness.powi(2) + 1.0) / kurtosis
102 } else {
103 0.0
104 };
105
106 let outlier_count = samples
108 .iter()
109 .filter(|&&x| (x - mean).abs() > 3.0 * std_dev)
110 .count();
111 let outlier_ratio = outlier_count as f64 / n as f64 * 100.0;
112
113 let histogram = LatencyHistogram::build(samples, 20);
115
116 Some(Self {
117 p50,
118 p90,
119 p99,
120 p999,
121 jitter,
122 tail_ratio,
123 bimodality_coefficient,
124 histogram,
125 sample_count: n,
126 min,
127 max,
128 mean,
129 std_dev,
130 skewness,
131 kurtosis,
132 outlier_ratio,
133 })
134 }
135
136 pub fn tail_severity(&self) -> TailSeverity {
138 TailSeverity::from_ratio(self.tail_ratio)
139 }
140
141 pub fn distribution_shape(&self) -> DistributionShape {
143 DistributionShape::classify(self.bimodality_coefficient, self.histogram.entropy)
144 }
145
146 pub fn has_tail_problem(&self) -> bool {
148 self.tail_ratio > 3.0
149 }
150
151 pub fn is_bimodal(&self) -> bool {
153 self.bimodality_coefficient > 0.555
154 }
155
156 pub fn summary(&self) -> String {
158 format!(
159 "n={} p50={:.2}µs p99={:.2}µs tail_ratio={:.2} jitter={:.2}µs shape={}",
160 self.sample_count,
161 self.p50,
162 self.p99,
163 self.tail_ratio,
164 self.jitter,
165 self.distribution_shape().name()
166 )
167 }
168}
169
170#[cfg(test)]
171mod tests;