Skip to main content

ic_metrics/histogram/
mod.rs

1//! Fixed-size distributions without sampling or reporting policy.
2
3use core::fmt;
4
5use crate::MeasurementSummary;
6
7#[cfg(test)]
8mod tests;
9
10/// A bound is not strictly greater than the preceding bound.
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub struct HistogramBoundsError {
13    index: usize,
14}
15
16impl HistogramBoundsError {
17    /// Zero-based index of the first invalid bound, always at least one.
18    #[must_use]
19    pub const fn index(self) -> usize {
20        self.index
21    }
22}
23
24impl fmt::Display for HistogramBoundsError {
25    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
26        write!(
27            formatter,
28            "histogram bound {} is not strictly increasing",
29            self.index
30        )
31    }
32}
33
34impl core::error::Error for HistogramBoundsError {}
35
36/// Fixed-size histogram of observations in one consumer-selected unit.
37///
38/// `N` strictly increasing inclusive upper bounds define `N` disjoint buckets.
39/// The first bucket includes zero; later buckets exclude the preceding bound.
40/// Values above the last bound go into a separate overflow bucket. With no
41/// bounds, every observation goes into overflow. A final bound of `u64::MAX`
42/// is valid and leaves overflow empty.
43///
44/// Each observation updates one bucket and the accompanying
45/// [`MeasurementSummary`]. Bucket counts saturate independently at `u64::MAX`.
46/// Treat a count at that value as unavailable for exact arithmetic, even when
47/// reached exactly. Unsaturated bucket counts remain useful after the summary's
48/// total saturates. Saturation can prevent bucket counts from summing to the
49/// summary's sample count. Buckets describe ranges, not exact percentiles.
50///
51/// Storage is fixed arrays, one overflow count and one summary, with no heap
52/// allocation. Recording searches at most `N` bounds. Consumers choose bounds,
53/// units, sample admission, identity and reset boundaries. Bounds are immutable
54/// after construction; cumulative reporting and persistence remain consumer-owned.
55///
56/// ```
57/// use ic_metrics::MeasurementHistogram;
58///
59/// # fn main() -> Result<(), ic_metrics::HistogramBoundsError> {
60/// let mut histogram = MeasurementHistogram::new([10, 100])?;
61/// for value in [0, 10, 11, 101] {
62///     histogram.record(value);
63/// }
64/// assert_eq!(histogram.bucket_counts(), &[2, 1]);
65/// assert_eq!(histogram.overflow(), 1);
66/// assert_eq!(histogram.summary().samples(), 4);
67/// # Ok(())
68/// # }
69/// ```
70#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub struct MeasurementHistogram<const N: usize> {
72    upper_bounds: [u64; N],
73    bucket_counts: [u64; N],
74    overflow: u64,
75    summary: MeasurementSummary,
76}
77
78impl<const N: usize> MeasurementHistogram<N> {
79    /// Construct an empty histogram with inclusive upper bounds.
80    ///
81    /// Bounds and observations must use the same unit. A bound of zero and an
82    /// empty bounds array are valid.
83    ///
84    /// # Errors
85    ///
86    /// Returns [`HistogramBoundsError`] for the first duplicate or descending
87    /// bound. The error identifies the right-hand bound in that pair.
88    pub const fn new(upper_bounds: [u64; N]) -> Result<Self, HistogramBoundsError> {
89        let mut index = 1;
90        while index < N {
91            // Both indices are within the array, including when N is zero.
92            if upper_bounds[index - 1] >= upper_bounds[index] {
93                return Err(HistogramBoundsError { index });
94            }
95            index += 1;
96        }
97        Ok(Self {
98            upper_bounds,
99            bucket_counts: [0; N],
100            overflow: 0,
101            summary: MeasurementSummary::EMPTY,
102        })
103    }
104
105    /// Record one completed observation, including zero.
106    ///
107    /// Updates the summary and exactly one disjoint bucket. Every observation
108    /// must use the same unit as the bounds and earlier observations.
109    pub const fn record(&mut self, value: u64) {
110        self.summary.record(value);
111        let mut index = 0;
112        while index < N {
113            // Both arrays have length N; the guard establishes valid indices.
114            if value <= self.upper_bounds[index] {
115                self.bucket_counts[index] = self.bucket_counts[index].saturating_add(1);
116                return;
117            }
118            index += 1;
119        }
120        self.overflow = self.overflow.saturating_add(1);
121    }
122
123    /// Immutable inclusive upper bounds corresponding to [`Self::bucket_counts`].
124    #[must_use]
125    pub const fn upper_bounds(&self) -> &[u64; N] {
126        &self.upper_bounds
127    }
128
129    /// Disjoint bucket counts, each saturating independently at `u64::MAX`.
130    ///
131    /// These exclude overflow and are not cumulative counts.
132    #[must_use]
133    pub const fn bucket_counts(&self) -> &[u64; N] {
134        &self.bucket_counts
135    }
136
137    /// Count above the last bound, saturating independently at `u64::MAX`.
138    ///
139    /// With no bounds, this counts all observations.
140    #[must_use]
141    pub const fn overflow(&self) -> u64 {
142        self.overflow
143    }
144
145    /// Summary of all observations, including overflow.
146    #[must_use]
147    pub const fn summary(&self) -> MeasurementSummary {
148        self.summary
149    }
150}