pub(super) mod history;
use crate::{
Abacus,
BrunchError,
MIN_SAMPLES,
};
use dactyl::{
NiceFloat,
NicePercent,
traits::SaturatingFrom,
};
use std::{
cmp::Ordering,
num::NonZeroU32,
time::Duration,
};
#[derive(Debug, Clone, Copy)]
pub(crate) struct Stats {
total: NonZeroU32,
valid: NonZeroU32,
deviation: f64,
mean: f64,
}
impl TryFrom<Vec<Duration>> for Stats {
type Error = BrunchError;
fn try_from(samples: Vec<Duration>) -> Result<Self, Self::Error> {
let total = u32::saturating_from(samples.len());
let total = NonZeroU32::new(total).ok_or(BrunchError::TooSmall(total))?;
if total < MIN_SAMPLES {
return Err(BrunchError::TooSmall(total.get()));
}
let mut calc = Abacus::from(samples);
calc.prune_outliers();
let valid = u32::saturating_from(calc.len());
let valid = NonZeroU32::new(valid).ok_or(BrunchError::TooWild)?;
if valid < MIN_SAMPLES {
return Err(BrunchError::TooWild);
}
let mean = calc.mean();
let deviation = calc.deviation();
let out = Self { total, valid, deviation, mean };
if out.is_valid() { Ok(out) }
else { Err(BrunchError::Overflow) }
}
}
impl Stats {
pub(crate) fn is_deviant(self, other: Self) -> Option<String> {
let lo = self.deviation.mul_add(-2.0, self.mean);
let hi = self.deviation.mul_add(2.0, self.mean);
if other.mean.total_cmp(&lo).is_lt() || other.mean.total_cmp(&hi).is_gt() {
return match self.mean.total_cmp(&other.mean) {
Ordering::Less => Some(format!(
"\x1b[92m-{}\x1b[0m",
NicePercent::from((other.mean - self.mean) / other.mean)
)),
Ordering::Equal => None,
Ordering::Greater => Some(format!(
"\x1b[91m+{}\x1b[0m",
NicePercent::from((self.mean - other.mean) / other.mean)
)),
};
}
None
}
pub(crate) fn nice_mean(self) -> String {
let (mean, unit) =
if self.mean.total_cmp(&0.000_001).is_lt() {
(self.mean * 1_000_000_000.0, "ns")
}
else if self.mean.total_cmp(&0.001).is_lt() {
(self.mean * 1_000_000.0, "\u{3bc}s")
}
else if self.mean.total_cmp(&1.0).is_lt() {
(self.mean * 1_000.0, "ms")
}
else {
(self.mean, "s ")
};
format!(
"\x1b[0;1m{} {}\x1b[0m",
NiceFloat::from(mean).precise_str(2),
unit,
)
}
pub(crate) const fn samples(self) -> (NonZeroU32, NonZeroU32) {
(self.valid, self.total)
}
fn is_valid(self) -> bool {
MIN_SAMPLES <= self.valid &&
self.valid <= self.total &&
self.deviation.is_finite() &&
self.deviation.total_cmp(&0.0).is_ge() &&
self.mean.is_finite() &&
self.mean.total_cmp(&0.0).is_ge()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn t_stats_valid() {
let mut stat = Stats {
total: NonZeroU32::new(2500).unwrap(),
valid: NonZeroU32::new(2496).unwrap(),
deviation: 0.000_000_123,
mean: 0.000_002_2,
};
assert!(stat.is_valid(), "Stat should be valid.");
stat.total = NonZeroU32::new(100).unwrap();
assert!(! stat.is_valid(), "Insufficient total.");
stat.valid = NonZeroU32::new(100).unwrap();
assert!(stat.is_valid(), "Stat should be valid.");
stat.valid = NonZeroU32::new(30).unwrap();
assert!(! stat.is_valid(), "Insufficient samples.");
stat.valid = NonZeroU32::new(100).unwrap();
assert!(stat.is_valid(), "Stat should be valid.");
stat.deviation = f64::NAN;
assert!(! stat.is_valid(), "NaN deviation.");
stat.deviation = -0.003;
assert!(! stat.is_valid(), "Negative deviation.");
stat.deviation = 0.003;
assert!(stat.is_valid(), "Stat should be valid.");
stat.mean = f64::NAN;
assert!(! stat.is_valid(), "NaN mean.");
stat.mean = -0.003;
assert!(! stat.is_valid(), "Negative mean.");
}
}