use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
const DEFAULT_BUCKETS_US: &[u64] = &[
100, 500, 1_000, 5_000, 10_000, 25_000, 50_000, 100_000, 250_000, 500_000, 1_000_000, 2_500_000, 5_000_000, 10_000_000, ];
#[derive(Debug)]
pub struct HistogramBucket {
pub upper_bound_us: u64,
count: AtomicU64,
}
impl HistogramBucket {
pub fn new(upper_bound_us: u64) -> Self {
Self {
upper_bound_us,
count: AtomicU64::new(0),
}
}
pub fn increment(&self) {
self.count.fetch_add(1, Ordering::Relaxed);
}
pub fn count(&self) -> u64 {
self.count.load(Ordering::Relaxed)
}
}
pub struct LatencyHistogram {
buckets: Vec<HistogramBucket>,
overflow: AtomicU64,
total_count: AtomicU64,
total_sum_us: AtomicU64,
}
impl LatencyHistogram {
pub fn new() -> Self {
Self::with_buckets(DEFAULT_BUCKETS_US)
}
pub fn with_buckets(boundaries_us: &[u64]) -> Self {
let buckets = boundaries_us
.iter()
.map(|&bound| HistogramBucket::new(bound))
.collect();
Self {
buckets,
overflow: AtomicU64::new(0),
total_count: AtomicU64::new(0),
total_sum_us: AtomicU64::new(0),
}
}
pub fn record(&self, duration: Duration) {
let value_us = duration.as_micros() as u64;
self.total_count.fetch_add(1, Ordering::Relaxed);
self.total_sum_us.fetch_add(value_us, Ordering::Relaxed);
let mut recorded = false;
for bucket in &self.buckets {
if value_us < bucket.upper_bound_us {
bucket.increment();
recorded = true;
break;
}
}
if !recorded {
self.overflow.fetch_add(1, Ordering::Relaxed);
}
}
pub fn record_us(&self, value_us: u64) {
self.record(Duration::from_micros(value_us));
}
pub fn count(&self) -> u64 {
self.total_count.load(Ordering::Relaxed)
}
pub fn mean(&self) -> Duration {
let count = self.total_count.load(Ordering::Relaxed);
if count == 0 {
return Duration::ZERO;
}
let sum = self.total_sum_us.load(Ordering::Relaxed);
Duration::from_micros(sum / count)
}
pub fn percentile(&self, p: f64) -> Duration {
let p = p.clamp(0.0, 1.0);
let total = self.total_count.load(Ordering::Relaxed);
if total == 0 {
return Duration::ZERO;
}
let target = (total as f64 * p).ceil() as u64;
let mut cumulative = 0u64;
for bucket in &self.buckets {
cumulative += bucket.count();
if cumulative >= target {
return Duration::from_micros(bucket.upper_bound_us);
}
}
if let Some(last) = self.buckets.last() {
Duration::from_micros(last.upper_bound_us)
} else {
Duration::ZERO
}
}
pub fn p50(&self) -> Duration {
self.percentile(0.50)
}
pub fn p90(&self) -> Duration {
self.percentile(0.90)
}
pub fn p95(&self) -> Duration {
self.percentile(0.95)
}
pub fn p99(&self) -> Duration {
self.percentile(0.99)
}
pub fn snapshot(&self) -> HistogramSnapshot {
let buckets: Vec<_> = self
.buckets
.iter()
.map(|b| BucketSnapshot {
upper_bound_us: b.upper_bound_us,
count: b.count(),
})
.collect();
HistogramSnapshot {
buckets,
overflow: self.overflow.load(Ordering::Relaxed),
total_count: self.total_count.load(Ordering::Relaxed),
total_sum_us: self.total_sum_us.load(Ordering::Relaxed),
}
}
pub fn reset(&self) {
for bucket in &self.buckets {
bucket.count.store(0, Ordering::Relaxed);
}
self.overflow.store(0, Ordering::Relaxed);
self.total_count.store(0, Ordering::Relaxed);
self.total_sum_us.store(0, Ordering::Relaxed);
}
}
impl Default for LatencyHistogram {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct BucketSnapshot {
pub upper_bound_us: u64,
pub count: u64,
}
#[derive(Debug, Clone)]
pub struct HistogramSnapshot {
pub buckets: Vec<BucketSnapshot>,
pub overflow: u64,
pub total_count: u64,
pub total_sum_us: u64,
}
impl HistogramSnapshot {
pub fn mean(&self) -> Duration {
if self.total_count == 0 {
return Duration::ZERO;
}
Duration::from_micros(self.total_sum_us / self.total_count)
}
pub fn percentile(&self, p: f64) -> Duration {
let p = p.clamp(0.0, 1.0);
if self.total_count == 0 {
return Duration::ZERO;
}
let target = (self.total_count as f64 * p).ceil() as u64;
let mut cumulative = 0u64;
for bucket in &self.buckets {
cumulative += bucket.count;
if cumulative >= target {
return Duration::from_micros(bucket.upper_bound_us);
}
}
if let Some(last) = self.buckets.last() {
Duration::from_micros(last.upper_bound_us)
} else {
Duration::ZERO
}
}
pub fn format_ascii(&self, width: usize) -> String {
let max_count = self.buckets.iter().map(|b| b.count).max().unwrap_or(1);
let mut output = String::new();
for bucket in &self.buckets {
let label = format_duration(bucket.upper_bound_us);
let bar_len = if max_count > 0 {
(bucket.count as f64 / max_count as f64 * width as f64) as usize
} else {
0
};
let bar: String = std::iter::repeat('#').take(bar_len).collect();
output.push_str(&format!("{:>8} | {:6} | {}\n", label, bucket.count, bar));
}
if self.overflow > 0 {
output.push_str(&format!("{:>8} | {:6} | (overflow)\n", ">max", self.overflow));
}
output
}
}
fn format_duration(us: u64) -> String {
if us < 1_000 {
format!("{}µs", us)
} else if us < 1_000_000 {
format!("{}ms", us / 1_000)
} else {
format!("{:.1}s", us as f64 / 1_000_000.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_histogram_new() {
let hist = LatencyHistogram::new();
assert_eq!(hist.count(), 0);
assert_eq!(hist.mean(), Duration::ZERO);
}
#[test]
fn test_histogram_record() {
let hist = LatencyHistogram::new();
hist.record(Duration::from_micros(500));
hist.record(Duration::from_millis(5));
hist.record(Duration::from_millis(50));
assert_eq!(hist.count(), 3);
}
#[test]
fn test_histogram_mean() {
let hist = LatencyHistogram::new();
hist.record(Duration::from_millis(10));
hist.record(Duration::from_millis(20));
hist.record(Duration::from_millis(30));
let mean = hist.mean();
assert_eq!(mean, Duration::from_millis(20));
}
#[test]
fn test_histogram_percentiles() {
let hist = LatencyHistogram::new();
for i in 1..=100 {
hist.record(Duration::from_millis(i));
}
let p50 = hist.p50();
assert!(p50 >= Duration::from_millis(50));
let p99 = hist.p99();
assert!(p99 >= Duration::from_millis(100));
}
#[test]
fn test_histogram_snapshot() {
let hist = LatencyHistogram::new();
hist.record(Duration::from_millis(1));
hist.record(Duration::from_millis(10));
let snapshot = hist.snapshot();
assert_eq!(snapshot.total_count, 2);
}
#[test]
fn test_histogram_reset() {
let hist = LatencyHistogram::new();
hist.record(Duration::from_millis(10));
assert_eq!(hist.count(), 1);
hist.reset();
assert_eq!(hist.count(), 0);
}
#[test]
fn test_custom_buckets() {
let hist = LatencyHistogram::with_buckets(&[100, 1000, 10000]);
hist.record(Duration::from_micros(50)); hist.record(Duration::from_micros(500)); hist.record(Duration::from_micros(5000)); hist.record(Duration::from_micros(50000));
let snapshot = hist.snapshot();
assert_eq!(snapshot.buckets[0].count, 1);
assert_eq!(snapshot.buckets[1].count, 1);
assert_eq!(snapshot.buckets[2].count, 1);
assert_eq!(snapshot.overflow, 1);
}
#[test]
fn test_format_duration() {
assert_eq!(format_duration(500), "500µs");
assert_eq!(format_duration(5_000), "5ms");
assert_eq!(format_duration(5_000_000), "5.0s");
}
#[test]
fn test_snapshot_format_ascii() {
let hist = LatencyHistogram::with_buckets(&[1000, 10000, 100000]);
hist.record(Duration::from_micros(500));
hist.record(Duration::from_micros(500));
hist.record(Duration::from_micros(5000));
let snapshot = hist.snapshot();
let ascii = snapshot.format_ascii(20);
assert!(ascii.contains("1ms"));
assert!(ascii.contains("10ms"));
}
}