//! Bounded-memory online statistics: fixed histogram + Welford mean/variance. //! //! Unlike the original implementation (which pushed every sample into a //! `Vec` and sorted at the end — ~30 MB/hour of RAM per metric at 1 kHz), //! memory use here is constant, so multi-hour stability runs are safe. pub struct OnlineStats { name: &'static str, /// Histogram bin width in ns. bin_ns: i64, /// Values below `low` or above `high` go to under/overflow counters but /// still feed min/max/mean/variance. low: i64, high: i64, bins: Vec, under: u64, over: u64, n: u64, min: i64, max: i64, mean: f64, m2: f64, } impl OnlineStats { /// Track values in `[low, high]` with `bin_ns` resolution (all in ns). pub fn new(name: &'static str, bin_ns: i64, low: i64, high: i64) -> Self { let n_bins = ((high - low) / bin_ns + 1).max(1) as usize; Self { name, bin_ns, low, high, bins: vec![0; n_bins], under: 0, over: 0, n: 0, min: i64::MAX, max: i64::MIN, mean: 0.0, m2: 0.0, } } pub fn push(&mut self, v: i64) { self.n += 1; if v < self.min { self.min = v; } if v > self.max { self.max = v; } // Welford let d = v as f64 - self.mean; self.mean += d / self.n as f64; self.m2 += d * (v as f64 - self.mean); if v < self.low { self.under += 1; } else if v > self.high { self.over += 1; } else { let idx = ((v - self.low) / self.bin_ns) as usize; let last = self.bins.len() - 1; self.bins[idx.min(last)] += 1; } } pub fn len(&self) -> u64 { self.n } fn percentile(&self, p: f64) -> i64 { if self.n == 0 { return 0; } let mut target = (self.n as f64 * p).ceil() as u64; target = target.max(1); // Mass below the histogram window counts first. if target <= self.under { return self.low; } target -= self.under; let mut acc = 0u64; for (i, &c) in self.bins.iter().enumerate() { acc += c; if acc >= target { return self.low + (i as i64 + 1) * self.bin_ns; } } self.high // fell into overflow region } pub fn report(&self, unit_div: i64, unit: &str) { if self.n == 0 { println!("{}: no samples", self.name); return; } let std = (self.m2 / self.n as f64).sqrt(); let d = unit_div as f64; println!( "{}: n={} min={:.1} p50={:.1} p95={:.1} p99={:.1} p99.9={:.1} max={:.1} mean={:.2} std={:.2} ({}) under={} over={}", self.name, self.n, self.min as f64 / d, self.percentile(0.50) as f64 / d, self.percentile(0.95) as f64 / d, self.percentile(0.99) as f64 / d, self.percentile(0.999) as f64 / d, self.max as f64 / d, self.mean / d, std / d, unit, self.under, self.over, ); } }