pub struct PerfStat {
name: String,
window: usize,
warmup: usize,
total_count: u64,
ring: Vec<f64>,
head: usize,
filled: usize,
sum: f64,
sum_sq: f64,
last_val: f64,
}
impl PerfStat {
pub fn new(name: &str, window: usize, warmup: usize) -> Self {
let window = window.max(1);
Self {
name: name.to_owned(),
window,
warmup,
total_count: 0,
ring: vec![0.0; window],
head: 0,
filled: 0,
sum: 0.0,
sum_sq: 0.0,
last_val: 0.0,
}
}
pub fn push(&mut self, value: f64) {
self.last_val = value;
self.total_count += 1;
if self.total_count <= self.warmup as u64 {
return; }
let slot = self.head % self.window;
if self.filled == self.window {
let old = self.ring[slot];
self.sum -= old;
self.sum_sq -= old * old;
} else {
self.filled += 1;
}
self.ring[slot] = value;
self.sum += value;
self.sum_sq += value * value;
self.head = slot + 1;
}
pub fn is_ready(&self) -> bool {
self.filled > 0
}
pub fn mean(&self) -> f64 {
if self.filled == 0 {
return 0.0;
}
self.sum / self.filled as f64
}
pub fn stddev(&self) -> f64 {
if self.filled < 2 {
return 0.0;
}
let n = self.filled as f64;
let var = (self.sum_sq / n) - (self.sum / n).powi(2);
var.max(0.0).sqrt()
}
pub fn min(&self) -> f64 {
self
.window_slice()
.iter()
.cloned()
.fold(f64::INFINITY, f64::min)
}
pub fn max(&self) -> f64 {
self
.window_slice()
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max)
}
pub fn p50(&self) -> f64 {
self.percentile(0.50)
}
pub fn p95(&self) -> f64 {
self.percentile(0.95)
}
pub fn p99(&self) -> f64 {
self.percentile(0.99)
}
pub fn last(&self) -> f64 {
self.last_val
}
pub fn count(&self) -> u64 {
self.total_count
}
pub fn name(&self) -> &str {
&self.name
}
pub fn reset(&mut self) {
self.total_count = 0;
self.filled = 0;
self.head = 0;
self.sum = 0.0;
self.sum_sq = 0.0;
self.last_val = 0.0;
self.ring.fill(0.0);
}
fn window_slice(&self) -> Vec<f64> {
if self.filled == 0 {
return Vec::new();
}
let mut out = Vec::with_capacity(self.filled);
let start = if self.filled == self.window {
self.head % self.window
} else {
0
};
for i in 0..self.filled {
out.push(self.ring[(start + i) % self.window]);
}
out
}
fn percentile(&self, p: f64) -> f64 {
if self.filled == 0 {
return 0.0;
}
let mut sorted = self.window_slice();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let idx = ((p * (sorted.len() - 1) as f64).round() as usize).min(sorted.len() - 1);
sorted[idx]
}
}