use std::time::Duration;
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct TimingStats {
pub samples: usize,
pub min: Duration,
pub max: Duration,
pub mean: Duration,
pub p95: Duration,
}
impl TimingStats {
pub(crate) fn from_nanos(values: impl IntoIterator<Item = u64>) -> Self {
let mut sorted: Vec<u64> = values.into_iter().collect();
if sorted.is_empty() {
return Self {
samples: 0,
min: Duration::ZERO,
max: Duration::ZERO,
mean: Duration::ZERO,
p95: Duration::ZERO,
};
}
sorted.sort_unstable();
let samples = sorted.len();
let min = sorted[0];
let max = sorted[samples - 1];
let sum = sorted
.iter()
.fold(0_u128, |acc, value| acc + u128::from(*value));
let mean = (sum / samples as u128).min(u64::MAX as u128) as u64;
let rank = ((samples * 95).div_ceil(100)).saturating_sub(1);
let p95 = sorted[rank];
Self {
samples,
min: Duration::from_nanos(min),
max: Duration::from_nanos(max),
mean: Duration::from_nanos(mean),
p95: Duration::from_nanos(p95),
}
}
}
#[cfg(test)]
mod tests {
use super::TimingStats;
use std::time::Duration;
#[test]
fn calculates_exact_nearest_rank_statistics() {
let stats = TimingStats::from_nanos(1..=100);
assert_eq!(stats.samples, 100);
assert_eq!(stats.min, Duration::from_nanos(1));
assert_eq!(stats.max, Duration::from_nanos(100));
assert_eq!(stats.mean, Duration::from_nanos(50));
assert_eq!(stats.p95, Duration::from_nanos(95));
}
#[test]
fn empty_input_returns_zeroed_stats() {
let stats = TimingStats::from_nanos(std::iter::empty());
assert_eq!(stats.samples, 0);
assert_eq!(stats.min, Duration::ZERO);
assert_eq!(stats.max, Duration::ZERO);
assert_eq!(stats.mean, Duration::ZERO);
assert_eq!(stats.p95, Duration::ZERO);
}
}