use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
const DEFAULT_BUCKET_COUNT: usize = 64;
const MIN_LATENCY_NS: u64 = 1;
const MAX_LATENCY_NS: u64 = 60_000_000_000;
#[derive(Debug)]
pub struct LatencyHistogram {
buckets: Vec<AtomicU64>,
boundaries: Vec<u64>,
count: AtomicU64,
sum_nanos: AtomicU64,
min_nanos: AtomicU64,
max_nanos: AtomicU64,
}
impl LatencyHistogram {
pub fn new() -> Self {
Self::with_buckets(DEFAULT_BUCKET_COUNT)
}
pub fn with_buckets(bucket_count: usize) -> Self {
let boundaries = Self::compute_boundaries(bucket_count);
let buckets = (0..bucket_count)
.map(|_| AtomicU64::new(0))
.collect();
Self {
buckets,
boundaries,
count: AtomicU64::new(0),
sum_nanos: AtomicU64::new(0),
min_nanos: AtomicU64::new(u64::MAX),
max_nanos: AtomicU64::new(0),
}
}
fn compute_boundaries(count: usize) -> Vec<u64> {
let log_min = (MIN_LATENCY_NS as f64).ln();
let log_max = (MAX_LATENCY_NS as f64).ln();
let log_step = (log_max - log_min) / (count as f64);
(0..count)
.map(|i| {
let log_value = log_min + (i as f64) * log_step;
log_value.exp() as u64
})
.collect()
}
fn find_bucket(&self, nanos: u64) -> usize {
match self.boundaries.binary_search(&nanos) {
Ok(idx) => idx,
Err(0) => 0,
Err(idx) => idx - 1,
}
}
pub fn record(&self, latency: Duration) {
let nanos = latency.as_nanos() as u64;
self.count.fetch_add(1, Ordering::Relaxed);
self.sum_nanos.fetch_add(nanos, Ordering::Relaxed);
self.update_min(nanos);
self.update_max(nanos);
let bucket = self.find_bucket(nanos);
if bucket < self.buckets.len() {
self.buckets[bucket].fetch_add(1, Ordering::Relaxed);
}
}
fn update_min(&self, nanos: u64) {
let mut current = self.min_nanos.load(Ordering::Relaxed);
while nanos < current {
match self.min_nanos.compare_exchange_weak(
current,
nanos,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(x) => current = x,
}
}
}
fn update_max(&self, nanos: u64) {
let mut current = self.max_nanos.load(Ordering::Relaxed);
while nanos > current {
match self.max_nanos.compare_exchange_weak(
current,
nanos,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(x) => current = x,
}
}
}
pub fn count(&self) -> u64 {
self.count.load(Ordering::Relaxed)
}
pub fn mean(&self) -> Duration {
let count = self.count();
if count == 0 {
return Duration::ZERO;
}
let sum = self.sum_nanos.load(Ordering::Relaxed);
Duration::from_nanos(sum / count)
}
pub fn min(&self) -> Duration {
let min = self.min_nanos.load(Ordering::Relaxed);
if min == u64::MAX {
Duration::ZERO
} else {
Duration::from_nanos(min)
}
}
pub fn max(&self) -> Duration {
Duration::from_nanos(self.max_nanos.load(Ordering::Relaxed))
}
pub fn percentile(&self, p: f64) -> Duration {
assert!((0.0..=1.0).contains(&p), "Percentile must be between 0.0 and 1.0");
let count = self.count();
if count == 0 {
return Duration::ZERO;
}
let target_count = (count as f64 * p) as u64;
let mut cumulative = 0u64;
for (i, bucket) in self.buckets.iter().enumerate() {
let bucket_count = bucket.load(Ordering::Relaxed);
cumulative += bucket_count;
if cumulative >= target_count {
let boundary = self.boundaries[i];
let next_boundary = self.boundaries.get(i + 1)
.copied()
.unwrap_or(MAX_LATENCY_NS);
let ratio = if bucket_count > 0 {
(target_count - (cumulative - bucket_count)) as f64 / bucket_count as f64
} else {
0.5
};
let interpolated = boundary as f64 +
ratio * (next_boundary - boundary) as f64;
return Duration::from_nanos(interpolated as u64);
}
}
self.max()
}
pub fn snapshot(&self) -> LatencyStats {
LatencyStats {
count: self.count(),
mean: self.mean(),
min: self.min(),
max: self.max(),
p50: self.percentile(0.50),
p90: self.percentile(0.90),
p95: self.percentile(0.95),
p99: self.percentile(0.99),
p999: self.percentile(0.999),
}
}
pub fn reset(&self) {
for bucket in &self.buckets {
bucket.store(0, Ordering::Relaxed);
}
self.count.store(0, Ordering::Relaxed);
self.sum_nanos.store(0, Ordering::Relaxed);
self.min_nanos.store(u64::MAX, Ordering::Relaxed);
self.max_nanos.store(0, Ordering::Relaxed);
}
}
impl Default for LatencyHistogram {
fn default() -> Self {
Self::new()
}
}
impl Clone for LatencyHistogram {
fn clone(&self) -> Self {
let buckets = self.buckets.iter()
.map(|b| AtomicU64::new(b.load(Ordering::Relaxed)))
.collect();
Self {
buckets,
boundaries: self.boundaries.clone(),
count: AtomicU64::new(self.count.load(Ordering::Relaxed)),
sum_nanos: AtomicU64::new(self.sum_nanos.load(Ordering::Relaxed)),
min_nanos: AtomicU64::new(self.min_nanos.load(Ordering::Relaxed)),
max_nanos: AtomicU64::new(self.max_nanos.load(Ordering::Relaxed)),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct LatencyStats {
pub count: u64,
pub mean: Duration,
pub min: Duration,
pub max: Duration,
pub p50: Duration,
pub p90: Duration,
pub p95: Duration,
pub p99: Duration,
pub p999: Duration,
}
impl LatencyStats {
pub fn percentile(&self, p: f64) -> Duration {
match p {
x if x <= 0.50 => self.p50,
x if x <= 0.90 => self.p90,
x if x <= 0.95 => self.p95,
x if x <= 0.99 => self.p99,
x if x <= 0.999 => self.p999,
_ => self.max,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_histogram() {
let histogram = LatencyHistogram::new();
assert_eq!(histogram.count(), 0);
assert_eq!(histogram.mean(), Duration::ZERO);
}
#[test]
fn test_record_single() {
let histogram = LatencyHistogram::new();
histogram.record(Duration::from_micros(100));
assert_eq!(histogram.count(), 1);
assert_eq!(histogram.mean(), Duration::from_micros(100));
assert_eq!(histogram.min(), Duration::from_micros(100));
assert_eq!(histogram.max(), Duration::from_micros(100));
}
#[test]
fn test_record_multiple() {
let histogram = LatencyHistogram::new();
histogram.record(Duration::from_micros(100));
histogram.record(Duration::from_micros(200));
histogram.record(Duration::from_micros(300));
assert_eq!(histogram.count(), 3);
assert_eq!(histogram.mean(), Duration::from_micros(200));
assert_eq!(histogram.min(), Duration::from_micros(100));
assert_eq!(histogram.max(), Duration::from_micros(300));
}
#[test]
fn test_percentile_calculation() {
let histogram = LatencyHistogram::new();
for i in 1..=100 {
histogram.record(Duration::from_micros(i));
}
let p50 = histogram.percentile(0.50);
let p90 = histogram.percentile(0.90);
let p99 = histogram.percentile(0.99);
assert!(p50.as_micros() >= 35 && p50.as_micros() <= 65,
"P50 {} not in expected range [35, 65]", p50.as_micros());
assert!(p90.as_micros() >= 70 && p90.as_micros() <= 110,
"P90 {} not in expected range [70, 110]", p90.as_micros());
assert!(p99.as_micros() >= 80 && p99.as_micros() <= 120,
"P99 {} not in expected range [80, 120]", p99.as_micros());
}
#[test]
fn test_snapshot() {
let histogram = LatencyHistogram::new();
for i in 1..=100 {
histogram.record(Duration::from_micros(i));
}
let stats = histogram.snapshot();
assert_eq!(stats.count, 100);
assert_eq!(stats.mean.as_micros(), 50); assert_eq!(stats.min.as_micros(), 1);
assert_eq!(stats.max.as_micros(), 100);
}
#[test]
fn test_reset() {
let histogram = LatencyHistogram::new();
histogram.record(Duration::from_micros(100));
assert_eq!(histogram.count(), 1);
histogram.reset();
assert_eq!(histogram.count(), 0);
assert_eq!(histogram.mean(), Duration::ZERO);
}
#[test]
fn test_wide_range() {
let histogram = LatencyHistogram::new();
histogram.record(Duration::from_nanos(100));
histogram.record(Duration::from_micros(100));
histogram.record(Duration::from_millis(100));
assert_eq!(histogram.count(), 3);
assert_eq!(histogram.min(), Duration::from_nanos(100));
assert_eq!(histogram.max(), Duration::from_millis(100));
}
#[test]
fn test_concurrent_recording() {
use std::sync::Arc;
use std::thread;
let histogram = Arc::new(LatencyHistogram::new());
let mut handles = vec![];
for i in 0..10 {
let histogram_clone = Arc::clone(&histogram);
let handle = thread::spawn(move || {
for j in 0..100 {
histogram_clone.record(Duration::from_micros((i * 100 + j) as u64));
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(histogram.count(), 1000);
}
#[test]
fn test_custom_bucket_count() {
let histogram = LatencyHistogram::with_buckets(128);
histogram.record(Duration::from_micros(100));
assert_eq!(histogram.count(), 1);
}
#[test]
#[should_panic(expected = "Percentile must be between 0.0 and 1.0")]
fn test_invalid_percentile() {
let histogram = LatencyHistogram::new();
histogram.percentile(1.5);
}
#[test]
fn test_empty_histogram_percentile() {
let histogram = LatencyHistogram::new();
let p50 = histogram.percentile(0.50);
assert_eq!(p50, Duration::ZERO);
}
#[test]
fn test_clone() {
let histogram = LatencyHistogram::new();
histogram.record(Duration::from_micros(100));
let cloned = histogram.clone();
assert_eq!(cloned.count(), 1);
assert_eq!(cloned.mean(), Duration::from_micros(100));
}
#[test]
fn test_latency_stats_percentile_method() {
let stats = LatencyStats {
count: 100,
mean: Duration::from_micros(50),
min: Duration::from_micros(1),
max: Duration::from_micros(100),
p50: Duration::from_micros(50),
p90: Duration::from_micros(90),
p95: Duration::from_micros(95),
p99: Duration::from_micros(99),
p999: Duration::from_micros(100),
};
assert_eq!(stats.percentile(0.50), stats.p50);
assert_eq!(stats.percentile(0.90), stats.p90);
assert_eq!(stats.percentile(0.99), stats.p99);
}
}