use core::fmt;
use crate::MeasurementSummary;
#[cfg(test)]
mod tests;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct HistogramBoundsError {
index: usize,
}
impl HistogramBoundsError {
#[must_use]
pub const fn index(self) -> usize {
self.index
}
}
impl fmt::Display for HistogramBoundsError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"histogram bound {} is not strictly increasing",
self.index
)
}
}
impl core::error::Error for HistogramBoundsError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HistogramQueryError {
BucketOutOfBounds {
index: usize,
buckets: usize,
},
InvalidQuantile,
SaturatedCount,
}
impl fmt::Display for HistogramQueryError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BucketOutOfBounds { index, buckets } => {
write!(
formatter,
"histogram bound index {index} is outside {buckets} bounds"
)
}
Self::InvalidQuantile => {
formatter.write_str("quantile must be greater than zero and at most one")
}
Self::SaturatedCount => formatter.write_str("histogram reporting count is saturated"),
}
}
}
impl core::error::Error for HistogramQueryError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct HistogramRange {
lower_exclusive: Option<u64>,
upper_inclusive: Option<u64>,
}
impl HistogramRange {
#[must_use]
pub const fn lower_exclusive(self) -> Option<u64> {
self.lower_exclusive
}
#[must_use]
pub const fn upper_inclusive(self) -> Option<u64> {
self.upper_inclusive
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MeasurementHistogram<const N: usize> {
upper_bounds: [u64; N],
bucket_counts: [u64; N],
overflow: u64,
summary: MeasurementSummary,
}
impl<const N: usize> MeasurementHistogram<N> {
pub const fn new(upper_bounds: [u64; N]) -> Result<Self, HistogramBoundsError> {
let mut index = 1;
while index < N {
if upper_bounds[index - 1] >= upper_bounds[index] {
return Err(HistogramBoundsError { index });
}
index += 1;
}
Ok(Self {
upper_bounds,
bucket_counts: [0; N],
overflow: 0,
summary: MeasurementSummary::EMPTY,
})
}
pub const fn record(&mut self, value: u64) {
self.summary.record(value);
let mut index = 0;
while index < N {
if value <= self.upper_bounds[index] {
self.bucket_counts[index] = self.bucket_counts[index].saturating_add(1);
return;
}
index += 1;
}
self.overflow = self.overflow.saturating_add(1);
}
#[must_use]
pub const fn upper_bounds(&self) -> &[u64; N] {
&self.upper_bounds
}
#[must_use]
pub const fn bucket_counts(&self) -> &[u64; N] {
&self.bucket_counts
}
#[must_use]
pub const fn overflow(&self) -> u64 {
self.overflow
}
#[must_use]
pub const fn summary(&self) -> MeasurementSummary {
self.summary
}
pub const fn cumulative_count(&self, index: usize) -> Result<u64, HistogramQueryError> {
if index >= N {
return Err(HistogramQueryError::BucketOutOfBounds { index, buckets: N });
}
let mut total = 0_u64;
let mut position = 0;
while position <= index {
total = total.saturating_add(self.bucket_counts[position]);
if total == u64::MAX {
return Err(HistogramQueryError::SaturatedCount);
}
position += 1;
}
Ok(total)
}
pub const fn quantile_bucket(
&self,
numerator: u64,
denominator: u64,
) -> Result<Option<HistogramRange>, HistogramQueryError> {
let rank = match quantile_rank(self.summary.samples(), numerator, denominator) {
Ok(Some(rank)) => rank,
Ok(None) => return Ok(None),
Err(error) => return Err(error),
};
let mut index = 0;
while index < N {
if self.bucket_counts[index] == u64::MAX {
return Err(HistogramQueryError::SaturatedCount);
}
index += 1;
}
if self.overflow == u64::MAX {
return Err(HistogramQueryError::SaturatedCount);
}
let mut cumulative = 0_u128;
index = 0;
while index < N {
cumulative += self.bucket_counts[index] as u128;
if cumulative >= rank {
return Ok(Some(HistogramRange {
lower_exclusive: if index == 0 {
None
} else {
Some(self.upper_bounds[index - 1])
},
upper_inclusive: Some(self.upper_bounds[index]),
}));
}
index += 1;
}
Ok(Some(HistogramRange {
lower_exclusive: if N == 0 {
None
} else {
Some(self.upper_bounds[N - 1])
},
upper_inclusive: None,
}))
}
}
const fn quantile_rank(
samples: u64,
numerator: u64,
denominator: u64,
) -> Result<Option<u128>, HistogramQueryError> {
if numerator == 0 || numerator > denominator {
return Err(HistogramQueryError::InvalidQuantile);
}
if samples == u64::MAX {
return Err(HistogramQueryError::SaturatedCount);
}
if samples == 0 {
return Ok(None);
}
Ok(Some(
((samples as u128) * (numerator as u128)).div_ceil(denominator as u128),
))
}