Skip to main content

ic_metrics/counter/
mod.rs

1//! Tracked scalar accumulation without observation statistics or attribution.
2
3use core::fmt;
4
5use crate::{MeasurementStateError, add_total};
6
7#[cfg(test)]
8mod tests;
9
10/// Why a scalar counter cannot supply an exact total or difference.
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum MeasurementCounterError {
13    /// A required counter total has actually overflowed.
14    TotalOverflow,
15    /// The current total is below the previous total.
16    DecreasingTotal,
17}
18
19impl fmt::Display for MeasurementCounterError {
20    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
21        formatter.write_str(match self {
22            Self::TotalOverflow => "counter total has overflowed",
23            Self::DecreasingTotal => "counter total decreased",
24        })
25    }
26}
27
28impl core::error::Error for MeasurementCounterError {}
29
30/// A scalar total in one consumer-selected unit with tracked actual overflow.
31///
32/// Use for diagnostic event counts, item counts or quantities such as bytes.
33/// Each addition increases the total without retaining a sample count, maximum,
34/// latest value or distribution. Zero is an exact total, not an absent metric;
35/// consumers own whether a counter exists and which events may increment it.
36///
37/// Totals clip at `u128::MAX` with a sticky flag for actual overflow. An exactly
38/// reached cap remains exact. Inputs must already be exact: adding a clamped
39/// quantity cannot restore its lost quality. Consumers own units, identities,
40/// reset boundaries and reporting. No allocation, platform reads or logging.
41/// Diagnostic totals do not replace authoritative budgets, balances or debt.
42///
43/// ```
44/// use ic_metrics::MeasurementCounter;
45/// # fn main() -> Result<(), ic_metrics::MeasurementCounterError> {
46/// let mut retries = MeasurementCounter::ZERO;
47/// retries.add(1);
48/// retries.add(1);
49/// assert_eq!(retries.exact_total()?, 2);
50/// let mut bytes = MeasurementCounter::ZERO;
51/// let before = bytes;
52/// bytes.add(32_768);
53/// bytes.add(0);
54/// assert_eq!(bytes.delta_since(before)?, 32_768);
55/// # Ok(())
56/// # }
57/// ```
58#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
59pub struct MeasurementCounter {
60    total: u128,
61    total_overflowed: bool,
62}
63
64impl MeasurementCounter {
65    /// Zero total with no actual overflow; zero additions remain exact.
66    pub const ZERO: Self = Self {
67        total: 0,
68        total_overflowed: false,
69    };
70
71    /// Restore a total and its actual-overflow flag after numeric admission.
72    ///
73    /// A cap with a false flag remains exact. The caller owns provenance, units
74    /// and identity; missing or previously discarded quality cannot be inferred.
75    ///
76    /// # Errors
77    /// Returns [`MeasurementStateError::InvalidOverflowFlag`] when a true flag
78    /// accompanies a total below its clipped cap.
79    pub const fn from_parts(
80        total: u128,
81        total_overflowed: bool,
82    ) -> Result<Self, MeasurementStateError> {
83        if total_overflowed && total != u128::MAX {
84            return Err(MeasurementStateError::InvalidOverflowFlag);
85        }
86        Ok(Self {
87            total,
88            total_overflowed,
89        })
90    }
91
92    /// Add an admitted exact quantity, including zero, in the existing unit.
93    ///
94    /// Use one for an event; larger quantities can account for a whole batch.
95    /// This never wraps or clears an existing overflow flag.
96    pub const fn add(&mut self, amount: u128) {
97        add_total(&mut self.total, &mut self.total_overflowed, amount);
98    }
99
100    /// Add a counter's total while preserving any source overflow.
101    ///
102    /// Consumers establish matching units and deliberate attribution. This is
103    /// addition, not deduplication: merging the same batch twice counts it twice.
104    pub const fn merge(&mut self, other: Self) {
105        self.add(other.total);
106        self.total_overflowed |= other.total_overflowed;
107    }
108
109    /// Total clipped at `u128::MAX`; check [`Self::total_overflowed`].
110    #[must_use]
111    pub const fn total(self) -> u128 {
112        self.total
113    }
114
115    /// Whether addition or merging has actually overflowed the total.
116    #[must_use]
117    pub const fn total_overflowed(self) -> bool {
118        self.total_overflowed
119    }
120
121    /// Exact total, including zero and an exactly reached cap.
122    ///
123    /// Use a checked conversion when a consumer report requires a narrower type;
124    /// a narrowing failure is distinct from this accumulator's overflow.
125    ///
126    /// # Errors
127    /// Returns [`MeasurementCounterError::TotalOverflow`] on actual overflow.
128    pub const fn exact_total(self) -> Result<u128, MeasurementCounterError> {
129        if self.total_overflowed {
130            Err(MeasurementCounterError::TotalOverflow)
131        } else {
132            Ok(self.total)
133        }
134    }
135
136    /// Exact difference from an earlier snapshot of this counter, including zero.
137    ///
138    /// Consumers must first establish matching units, counter/window identity and
139    /// append-only continuity. Numeric success cannot detect a reset followed by
140    /// larger values, changed attribution or an unrelated counter.
141    ///
142    /// # Errors
143    /// Rejects actual overflow in either snapshot before a decreasing total.
144    pub const fn delta_since(self, previous: Self) -> Result<u128, MeasurementCounterError> {
145        if self.total_overflowed || previous.total_overflowed {
146            return Err(MeasurementCounterError::TotalOverflow);
147        }
148        match self.total.checked_sub(previous.total) {
149            Some(delta) => Ok(delta),
150            None => Err(MeasurementCounterError::DecreasingTotal),
151        }
152    }
153}