use std::sync::atomic::{AtomicU64, Ordering};
const CACHE_LINE: usize = 64;
#[repr(C, align(64))]
pub struct LatencyHistogram<const BUCKETS: usize> {
counts: [AtomicU64; BUCKETS],
min: AtomicU64,
max: AtomicU64,
sum: AtomicU64,
total: AtomicU64,
_pad: [u8; CACHE_LINE - 32],
}
impl<const BUCKETS: usize> LatencyHistogram<BUCKETS> {
pub fn new() -> Self {
Self {
counts: std::array::from_fn(|_| AtomicU64::new(0)),
min: AtomicU64::new(u64::MAX),
max: AtomicU64::new(0),
sum: AtomicU64::new(0),
total: AtomicU64::new(0),
_pad: [0; CACHE_LINE - 32],
}
}
fn bucket_index(value: u64) -> usize {
if value == 0 {
0
} else {
let leading = value.leading_zeros();
let bucket = (64 - leading) as usize;
bucket.min(BUCKETS - 1)
}
}
#[inline]
pub fn record(&self, value: u64) {
let idx = Self::bucket_index(value);
if idx < BUCKETS {
self.counts[idx].fetch_add(1, Ordering::Relaxed);
}
self.sum.fetch_add(value, Ordering::Relaxed);
self.total.fetch_add(1, Ordering::Relaxed);
let mut current_min = self.min.load(Ordering::Relaxed);
while value < current_min {
match self.min.compare_exchange_weak(
current_min,
value,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(v) => current_min = v,
}
}
let mut current_max = self.max.load(Ordering::Relaxed);
while value > current_max {
match self.max.compare_exchange_weak(
current_max,
value,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(v) => current_max = v,
}
}
}
#[inline]
pub fn record_cycles(&self, start: u64, end: u64) {
self.record(end.saturating_sub(start));
}
pub fn percentile(&self, p: f64) -> u64 {
let total = self.total.load(Ordering::Relaxed);
if total == 0 {
return 0;
}
let target = ((total as f64) * p / 100.0).ceil() as u64;
let mut cumulative = 0u64;
for (i, count) in self.counts.iter().enumerate() {
cumulative += count.load(Ordering::Relaxed);
if cumulative >= target {
return 1u64 << i;
}
}
self.max.load(Ordering::Relaxed)
}
#[inline]
pub fn p50(&self) -> u64 {
self.percentile(50.0)
}
#[inline]
pub fn p90(&self) -> u64 {
self.percentile(90.0)
}
#[inline]
pub fn p99(&self) -> u64 {
self.percentile(99.0)
}
#[inline]
pub fn p999(&self) -> u64 {
self.percentile(99.9)
}
pub fn mean(&self) -> f64 {
let total = self.total.load(Ordering::Relaxed);
if total == 0 {
return 0.0;
}
self.sum.load(Ordering::Relaxed) as f64 / total as f64
}
#[inline]
pub fn min(&self) -> u64 {
let min = self.min.load(Ordering::Relaxed);
if min == u64::MAX {
0
} else {
min
}
}
#[inline]
pub fn max(&self) -> u64 {
self.max.load(Ordering::Relaxed)
}
#[inline]
pub fn count(&self) -> u64 {
self.total.load(Ordering::Relaxed)
}
#[inline]
pub fn sum(&self) -> u64 {
self.sum.load(Ordering::Relaxed)
}
pub fn reset(&self) {
for count in &self.counts {
count.store(0, Ordering::Relaxed);
}
self.min.store(u64::MAX, Ordering::Relaxed);
self.max.store(0, Ordering::Relaxed);
self.sum.store(0, Ordering::Relaxed);
self.total.store(0, Ordering::Relaxed);
}
pub fn snapshot(&self) -> HistogramSnapshot {
HistogramSnapshot {
min: self.min(),
max: self.max(),
mean: self.mean(),
p50: self.p50(),
p90: self.p90(),
p99: self.p99(),
p999: self.p999(),
count: self.count(),
sum: self.sum(),
}
}
}
impl<const BUCKETS: usize> Default for LatencyHistogram<BUCKETS> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy)]
pub struct HistogramSnapshot {
pub min: u64,
pub max: u64,
pub mean: f64,
pub p50: u64,
pub p90: u64,
pub p99: u64,
pub p999: u64,
pub count: u64,
pub sum: u64,
}