use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpanStats {
pub name: String,
pub count: usize,
pub min_ns: u64,
pub max_ns: u64,
pub mean_ns: f64,
pub median_ns: f64,
pub p95_ns: f64,
pub p99_ns: f64,
pub stddev_ns: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent: Option<String>,
}
impl SpanStats {
pub(crate) fn compute(name: String, mut samples: Vec<u64>, parent: Option<String>) -> Self {
assert!(!samples.is_empty(), "cannot compute stats for zero samples");
samples.sort_unstable();
let n = samples.len();
let min_ns = samples[0];
let max_ns = samples[n - 1];
let sum: u128 = samples.iter().map(|&x| x as u128).sum();
let mean_ns = sum as f64 / n as f64;
let median_ns = lerp_percentile(&samples, 0.50);
let p95_ns = lerp_percentile(&samples, 0.95);
let p99_ns = lerp_percentile(&samples, 0.99);
let variance = samples
.iter()
.map(|&x| {
let d = x as f64 - mean_ns;
d * d
})
.sum::<f64>()
/ n as f64;
SpanStats {
name,
count: n,
min_ns,
max_ns,
mean_ns,
median_ns,
p95_ns,
p99_ns,
stddev_ns: variance.sqrt(),
parent,
}
}
}
fn lerp_percentile(sorted: &[u64], p: f64) -> f64 {
let n = sorted.len();
if n == 1 {
return sorted[0] as f64;
}
let pos = p * (n - 1) as f64;
let lo = pos.floor() as usize;
let hi = (lo + 1).min(n - 1);
let frac = pos - lo as f64;
sorted[lo] as f64 * (1.0 - frac) + sorted[hi] as f64 * frac
}
pub fn fmt_ns(ns: f64) -> String {
if ns < 1_000.0 {
format!("{:.1} ns", ns)
} else if ns < 1_000_000.0 {
format!("{:.2} µs", ns / 1_000.0)
} else if ns < 1_000_000_000.0 {
format!("{:.2} ms", ns / 1_000_000.0)
} else {
format!("{:.3} s", ns / 1_000_000_000.0)
}
}