ic_metrics/aggregate/
mod.rs1use core::fmt;
4
5use crate::{MeasurementMeanError, MeasurementStateError, add_count, add_total, mean_total};
6
7#[cfg(test)]
8mod tests;
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum MeasurementIntervalError {
13 SaturatedSamples,
15 SaturatedTotal,
17 DecreasingSamples,
19 DecreasingTotal,
21 TotalWithoutSamples,
23}
24
25impl fmt::Display for MeasurementIntervalError {
26 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
27 formatter.write_str(match self {
28 Self::SaturatedSamples => "interval sample count has overflowed",
29 Self::SaturatedTotal => "interval total has overflowed",
30 Self::DecreasingSamples => "interval sample count decreased",
31 Self::DecreasingTotal => "interval total decreased",
32 Self::TotalWithoutSamples => "interval total changed without samples",
33 })
34 }
35}
36
37impl core::error::Error for MeasurementIntervalError {}
38
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub struct MeasurementInterval {
46 samples: u64,
47 total: u128,
48}
49
50impl MeasurementInterval {
51 #[must_use]
53 pub const fn samples(self) -> u64 {
54 self.samples
55 }
56
57 #[must_use]
59 pub const fn total(self) -> u128 {
60 self.total
61 }
62
63 #[must_use]
65 pub const fn mean(self) -> Option<u128> {
66 mean_total(self.samples, self.total)
67 }
68}
69
70#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
78pub struct MeasurementAggregate {
79 samples: u64,
80 total: u128,
81 maximum: u64,
82 samples_saturated: bool,
83 total_saturated: bool,
84}
85
86impl MeasurementAggregate {
87 pub const fn from_parts(
96 samples: u64,
97 total: u128,
98 maximum: Option<u64>,
99 samples_saturated: bool,
100 total_saturated: bool,
101 ) -> Result<Self, MeasurementStateError> {
102 if samples == 0 {
103 return if total == 0 && maximum.is_none() && !samples_saturated && !total_saturated {
104 Ok(Self::EMPTY)
105 } else {
106 Err(MeasurementStateError::InvalidEmpty)
107 };
108 }
109 let Some(maximum) = maximum else {
110 return Err(MeasurementStateError::MissingMaximum);
111 };
112 if (samples_saturated && samples != u64::MAX)
113 || (total_saturated && (total != u128::MAX || !samples_saturated))
114 {
115 return Err(MeasurementStateError::InvalidSaturation);
116 }
117 if total < maximum as u128
118 || (maximum == 0 && total != 0)
119 || (!samples_saturated && total > samples as u128 * maximum as u128)
120 {
121 return Err(MeasurementStateError::InvalidTotal);
122 }
123 Ok(Self {
124 samples,
125 total,
126 maximum,
127 samples_saturated,
128 total_saturated,
129 })
130 }
131
132 pub const EMPTY: Self = Self {
134 samples: 0,
135 total: 0,
136 maximum: 0,
137 samples_saturated: false,
138 total_saturated: false,
139 };
140
141 pub const fn record(&mut self, value: u64) {
143 add_count(&mut self.samples, &mut self.samples_saturated, 1);
144 add_total(&mut self.total, &mut self.total_saturated, value as u128);
145 if value > self.maximum {
146 self.maximum = value;
147 }
148 }
149
150 pub const fn merge(&mut self, other: Self) {
156 add_count(
157 &mut self.samples,
158 &mut self.samples_saturated,
159 other.samples,
160 );
161 add_total(&mut self.total, &mut self.total_saturated, other.total);
162 self.samples_saturated |= other.samples_saturated;
163 self.total_saturated |= other.total_saturated;
164 if other.maximum > self.maximum {
165 self.maximum = other.maximum;
166 }
167 }
168
169 #[must_use]
171 pub const fn samples(self) -> u64 {
172 self.samples
173 }
174
175 #[must_use]
177 pub const fn total(self) -> u128 {
178 self.total
179 }
180
181 #[must_use]
183 pub const fn samples_saturated(self) -> bool {
184 self.samples_saturated
185 }
186
187 #[must_use]
189 pub const fn total_saturated(self) -> bool {
190 self.total_saturated
191 }
192
193 #[must_use]
195 pub const fn maximum(self) -> Option<u64> {
196 if self.samples == 0 {
197 None
198 } else {
199 Some(self.maximum)
200 }
201 }
202
203 pub const fn mean(self) -> Result<Option<u128>, MeasurementMeanError> {
209 if self.samples_saturated {
210 return Err(MeasurementMeanError::SaturatedSamples);
211 }
212 if self.total_saturated {
213 return Err(MeasurementMeanError::SaturatedTotal);
214 }
215 Ok(mean_total(self.samples, self.total))
216 }
217
218 pub const fn interval_since(
229 self,
230 previous: Self,
231 ) -> Result<MeasurementInterval, MeasurementIntervalError> {
232 if self.samples_saturated || previous.samples_saturated {
233 return Err(MeasurementIntervalError::SaturatedSamples);
234 }
235 if self.total_saturated || previous.total_saturated {
236 return Err(MeasurementIntervalError::SaturatedTotal);
237 }
238 let Some(samples) = self.samples.checked_sub(previous.samples) else {
239 return Err(MeasurementIntervalError::DecreasingSamples);
240 };
241 let Some(total) = self.total.checked_sub(previous.total) else {
242 return Err(MeasurementIntervalError::DecreasingTotal);
243 };
244 if samples == 0 && total != 0 {
245 return Err(MeasurementIntervalError::TotalWithoutSamples);
246 }
247 Ok(MeasurementInterval { samples, total })
248 }
249}