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}