use core::fmt;
use crate::{MeasurementAggregate, MeasurementStateError, add_count};
#[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 HistogramMergeError {
index: usize,
}
impl HistogramMergeError {
#[must_use]
pub const fn index(self) -> usize {
self.index
}
}
impl fmt::Display for HistogramMergeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "histogram merge bound {} differs", self.index)
}
}
impl core::error::Error for HistogramMergeError {}
#[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,
bucket_saturation: [bool; N],
overflow_saturated: bool,
aggregate: MeasurementAggregate,
}
impl<const N: usize> MeasurementHistogram<N> {
pub const fn from_parts(
upper_bounds: [u64; N],
bucket_counts: [u64; N],
overflow: u64,
bucket_saturation: [bool; N],
overflow_saturated: bool,
aggregate: MeasurementAggregate,
) -> Result<Self, MeasurementStateError> {
if Self::new(upper_bounds).is_err() {
return Err(MeasurementStateError::InvalidBounds);
}
if overflow_saturated && overflow != u64::MAX {
return Err(MeasurementStateError::InvalidSaturation);
}
let mut count = overflow;
let mut saturated = overflow_saturated;
let mut index = 0;
let mut highest = None;
while index < N {
if bucket_saturation[index] && bucket_counts[index] != u64::MAX {
return Err(MeasurementStateError::InvalidSaturation);
}
add_count(&mut count, &mut saturated, bucket_counts[index]);
saturated |= bucket_saturation[index];
if bucket_counts[index] != 0 {
highest = Some(index);
}
index += 1;
}
if count != aggregate.samples() || saturated != aggregate.samples_saturated() {
return Err(MeasurementStateError::InvalidDistribution);
}
if let Some(maximum) = aggregate.maximum() {
if overflow != 0 {
if N != 0 && maximum <= upper_bounds[N - 1] {
return Err(MeasurementStateError::InvalidDistribution);
}
} else if let Some(index) = highest {
if maximum > upper_bounds[index]
|| (index != 0 && maximum <= upper_bounds[index - 1])
{
return Err(MeasurementStateError::InvalidDistribution);
}
} else {
return Err(MeasurementStateError::InvalidDistribution);
}
if !valid_distribution_total(
&upper_bounds,
&bucket_counts,
overflow,
&bucket_saturation,
overflow_saturated,
aggregate,
) {
return Err(MeasurementStateError::InvalidDistribution);
}
}
Ok(Self {
upper_bounds,
bucket_counts,
overflow,
bucket_saturation,
overflow_saturated,
aggregate,
})
}
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,
bucket_saturation: [false; N],
overflow_saturated: false,
aggregate: MeasurementAggregate::EMPTY,
})
}
pub const fn record(&mut self, value: u64) {
self.aggregate.record(value);
let mut index = 0;
while index < N {
if value <= self.upper_bounds[index] {
add_count(
&mut self.bucket_counts[index],
&mut self.bucket_saturation[index],
1,
);
return;
}
index += 1;
}
add_count(&mut self.overflow, &mut self.overflow_saturated, 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 bucket_saturation(&self) -> &[bool; N] {
&self.bucket_saturation
}
#[must_use]
pub const fn overflow_saturated(&self) -> bool {
self.overflow_saturated
}
#[must_use]
pub const fn aggregate(&self) -> MeasurementAggregate {
self.aggregate
}
pub const fn merge(&mut self, other: &Self) -> Result<(), HistogramMergeError> {
let mut index = 0;
while index < N {
if self.upper_bounds[index] != other.upper_bounds[index] {
return Err(HistogramMergeError { index });
}
index += 1;
}
index = 0;
while index < N {
add_count(
&mut self.bucket_counts[index],
&mut self.bucket_saturation[index],
other.bucket_counts[index],
);
self.bucket_saturation[index] |= other.bucket_saturation[index];
index += 1;
}
add_count(
&mut self.overflow,
&mut self.overflow_saturated,
other.overflow,
);
self.overflow_saturated |= other.overflow_saturated;
self.aggregate.merge(other.aggregate);
Ok(())
}
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 {
if self.bucket_saturation[position] {
return Err(HistogramQueryError::SaturatedCount);
}
total = match total.checked_add(self.bucket_counts[position]) {
Some(next) => next,
None => 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.aggregate.samples(), numerator, denominator) {
Ok(Some(rank)) => rank,
Ok(None) => return Ok(None),
Err(error) => return Err(error),
};
if self.aggregate.samples_saturated() {
return Err(HistogramQueryError::SaturatedCount);
}
let mut index = 0;
while index < N {
if self.bucket_saturation[index] {
return Err(HistogramQueryError::SaturatedCount);
}
index += 1;
}
if self.overflow_saturated {
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 == 0 {
return Ok(None);
}
if let Some(product) = samples.checked_mul(numerator) {
return Ok(Some(product.div_ceil(denominator) as u128));
}
Ok(Some(
((samples as u128) * (numerator as u128)).div_ceil(denominator as u128),
))
}
const fn valid_distribution_total<const N: usize>(
upper_bounds: &[u64; N],
bucket_counts: &[u64; N],
overflow: u64,
bucket_saturation: &[bool; N],
overflow_saturated: bool,
aggregate: MeasurementAggregate,
) -> bool {
let Some(maximum) = aggregate.maximum() else {
return true;
};
let mut minimum_total = 0_u128;
let mut maximum_total = 0_u128;
let mut maximum_overflow = false;
let mut minimum_overflow = false;
let mut maximum_lower = 0_u64;
let mut index = 0;
while index <= N {
let (count, lost, lower, upper) = if index < N {
let lower = if index == 0 {
0
} else {
upper_bounds[index - 1].saturating_add(1)
};
(
bucket_counts[index],
bucket_saturation[index],
lower,
if upper_bounds[index] < maximum {
upper_bounds[index]
} else {
maximum
},
)
} else {
let lower = if N == 0 {
0
} else {
upper_bounds[N - 1].saturating_add(1)
};
(overflow, overflow_saturated, lower, maximum)
};
if count != 0 {
maximum_lower = lower;
let minimum_count = count as u128 + lost as u128;
let minimum = minimum_count * lower as u128;
if let Some(next) = minimum_total.checked_add(minimum) {
minimum_total = next;
} else {
minimum_overflow = true;
}
if lost && upper != 0 {
maximum_overflow = true;
}
let upper_total = count as u128 * upper as u128;
if let Some(next) = maximum_total.checked_add(upper_total) {
maximum_total = next;
} else {
maximum_overflow = true;
}
}
index += 1;
}
if let Some(next) = minimum_total.checked_add(maximum.saturating_sub(maximum_lower) as u128) {
minimum_total = next;
} else {
minimum_overflow = true;
}
if aggregate.total_saturated() {
maximum_overflow
} else {
!minimum_overflow
&& aggregate.total() >= minimum_total
&& (maximum_overflow || aggregate.total() <= maximum_total)
}
}