Skip to main content

ic_timers/snapshot/
metrics.rs

1//! Saturating epoch-local counters and performance aggregates.
2
3use super::{TimerCompletion, TimerCompletionOutcome, TimerEpoch, TimerOutcomeSnapshot};
4
5/// Epoch-local timer event counters.
6///
7/// Fields are private so all mutation preserves saturation and the completion
8/// partition. Consumer adapters read values through the accessors.
9#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
10pub struct TimerCounters {
11    requested: u64,
12    armed: u64,
13    started: u64,
14    completed: u64,
15    succeeded: u64,
16    no_work: u64,
17    retryable_failure: u64,
18    invariant_failure: u64,
19    cancelled: u64,
20    stale: u64,
21    coalesced: u64,
22    interrupted: u64,
23}
24
25impl TimerCounters {
26    /// Record a validated schedule or reconciliation request.
27    pub const fn record_request(&mut self) {
28        self.requested = self.requested.saturating_add(1);
29    }
30
31    /// Record an actual one-shot provider arm.
32    pub const fn record_arm(&mut self) {
33        self.armed = self.armed.saturating_add(1);
34    }
35
36    /// Record a non-stale callback entering logical execution.
37    pub const fn record_start(&mut self) {
38        self.started = self.started.saturating_add(1);
39    }
40
41    /// Record one returned callback and its single completion class.
42    pub const fn record_completion(&mut self, outcome: TimerCompletionOutcome) {
43        self.completed = self.completed.saturating_add(1);
44        match outcome {
45            TimerCompletionOutcome::Success => {
46                self.succeeded = self.succeeded.saturating_add(1);
47            }
48            TimerCompletionOutcome::NoWork => {
49                self.no_work = self.no_work.saturating_add(1);
50            }
51            TimerCompletionOutcome::RetryableFailure => {
52                self.retryable_failure = self.retryable_failure.saturating_add(1);
53            }
54            TimerCompletionOutcome::InvariantFailure => {
55                self.invariant_failure = self.invariant_failure.saturating_add(1);
56            }
57        }
58    }
59
60    /// Record a logical cancellation that wins arbitration.
61    pub const fn record_cancellation(&mut self) {
62        self.cancelled = self.cancelled.saturating_add(1);
63    }
64
65    /// Record a provider callback or completion rejected as stale.
66    pub const fn record_stale(&mut self) {
67        self.stale = self.stale.saturating_add(1);
68    }
69
70    /// Record scheduling demand merged into existing work.
71    pub const fn record_coalesced(&mut self) {
72        self.coalesced = self.coalesced.saturating_add(1);
73    }
74
75    /// Record a started generation established not to have completed.
76    ///
77    /// The interruption belongs to the epoch in which it is observed. It can
78    /// therefore describe a generation started in an earlier epoch and has no
79    /// arithmetic invariant with this epoch's `started` count.
80    pub const fn record_interruption(&mut self) {
81        self.interrupted = self.interrupted.saturating_add(1);
82    }
83
84    /// Return validated scheduling requests.
85    #[must_use]
86    pub const fn requested(self) -> u64 {
87        self.requested
88    }
89
90    /// Return actual provider arm operations.
91    #[must_use]
92    pub const fn armed(self) -> u64 {
93        self.armed
94    }
95
96    /// Return callbacks that entered logical execution.
97    #[must_use]
98    pub const fn started(self) -> u64 {
99        self.started
100    }
101
102    /// Return callbacks that completed accounting.
103    #[must_use]
104    pub const fn completed(self) -> u64 {
105        self.completed
106    }
107
108    /// Return successful-work completions.
109    #[must_use]
110    pub const fn succeeded(self) -> u64 {
111        self.succeeded
112    }
113
114    /// Return valid no-work completions.
115    #[must_use]
116    pub const fn no_work(self) -> u64 {
117        self.no_work
118    }
119
120    /// Return retryable expected-failure completions.
121    #[must_use]
122    pub const fn retryable_failure(self) -> u64 {
123        self.retryable_failure
124    }
125
126    /// Return invariant or terminal-failure completions.
127    #[must_use]
128    pub const fn invariant_failure(self) -> u64 {
129        self.invariant_failure
130    }
131
132    /// Return logical cancellations that won arbitration.
133    #[must_use]
134    pub const fn cancelled(self) -> u64 {
135        self.cancelled
136    }
137
138    /// Return callbacks or completions rejected as stale.
139    #[must_use]
140    pub const fn stale(self) -> u64 {
141        self.stale
142    }
143
144    /// Return scheduling demands merged into existing work.
145    #[must_use]
146    pub const fn coalesced(self) -> u64 {
147        self.coalesced
148    }
149
150    /// Return incomplete generations observed in this epoch.
151    #[must_use]
152    pub const fn interrupted(self) -> u64 {
153        self.interrupted
154    }
155
156    /// Check that every completion belongs to exactly one outcome class.
157    #[must_use]
158    pub const fn completion_partition_is_valid(self) -> bool {
159        self.completed
160            == self
161                .succeeded
162                .saturating_add(self.no_work)
163                .saturating_add(self.retryable_failure)
164                .saturating_add(self.invariant_failure)
165    }
166}
167
168/// One callback's completed performance measurement.
169#[derive(Clone, Copy, Debug, Eq, PartialEq)]
170pub struct TimerMeasurement {
171    /// Instructions consumed by the callback.
172    pub instructions: u64,
173    /// Wall-clock callback duration in nanoseconds.
174    pub elapsed_ns: u64,
175}
176
177/// Saturating aggregate for one non-negative measurement.
178#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
179pub struct MeasurementSummary {
180    samples: u64,
181    total: u64,
182    latest: Option<u64>,
183    maximum: Option<u64>,
184}
185
186impl MeasurementSummary {
187    /// Record one completed sample.
188    pub const fn record(&mut self, value: u64) {
189        self.samples = self.samples.saturating_add(1);
190        self.total = self.total.saturating_add(value);
191        self.latest = Some(value);
192        self.maximum = Some(match self.maximum {
193            Some(current) if current > value => current,
194            Some(_) | None => value,
195        });
196    }
197
198    /// Return the number of completed samples.
199    #[must_use]
200    pub const fn samples(self) -> u64 {
201        self.samples
202    }
203
204    /// Return the saturating sum of all samples.
205    #[must_use]
206    pub const fn total(self) -> u64 {
207        self.total
208    }
209
210    /// Return the latest sample, if one exists.
211    #[must_use]
212    pub const fn latest(self) -> Option<u64> {
213        self.latest
214    }
215
216    /// Return the largest sample, if one exists.
217    #[must_use]
218    pub const fn maximum(self) -> Option<u64> {
219        self.maximum
220    }
221}
222
223/// Completed callback performance aggregates for one runtime epoch.
224#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
225pub struct TimerPerformance {
226    instructions: MeasurementSummary,
227    elapsed_ns: MeasurementSummary,
228}
229
230impl TimerPerformance {
231    /// Record one callback with valid end measurements.
232    pub const fn record(&mut self, measurement: TimerMeasurement) {
233        self.instructions.record(measurement.instructions);
234        self.elapsed_ns.record(measurement.elapsed_ns);
235    }
236
237    /// Return the instruction aggregate.
238    #[must_use]
239    pub const fn instructions(self) -> MeasurementSummary {
240        self.instructions
241    }
242
243    /// Return the elapsed-nanosecond aggregate.
244    #[must_use]
245    pub const fn elapsed_ns(self) -> MeasurementSummary {
246        self.elapsed_ns
247    }
248}
249
250/// Epoch-scoped outcomes, counters, and performance for one timer.
251#[derive(Clone, Copy, Debug, Eq, PartialEq)]
252pub struct TimerObservabilitySnapshot {
253    epoch: TimerEpoch,
254    outcomes: TimerOutcomeSnapshot,
255    counters: TimerCounters,
256    performance: TimerPerformance,
257}
258
259impl TimerObservabilitySnapshot {
260    /// Begin empty observation state for one runtime epoch.
261    #[must_use]
262    pub fn new(epoch: TimerEpoch) -> Self {
263        Self {
264            epoch,
265            outcomes: TimerOutcomeSnapshot::default(),
266            counters: TimerCounters::default(),
267            performance: TimerPerformance::default(),
268        }
269    }
270
271    /// Replace all epoch-scoped observations with a new empty epoch.
272    ///
273    /// This resets outcomes, timestamps, the expected-failure streak,
274    /// counters, and performance aggregates. Persistent scheduling state is
275    /// owned outside this observation value.
276    pub fn begin_epoch(&mut self, epoch: TimerEpoch) {
277        *self = Self::new(epoch);
278    }
279
280    /// Record one returned callback atomically across outcome and counters.
281    ///
282    /// Performance changes only when a valid end measurement is supplied.
283    pub const fn record_completion(
284        &mut self,
285        completion: TimerCompletion,
286        completed_at_ns: u64,
287        measurement: Option<TimerMeasurement>,
288    ) {
289        self.outcomes.record_completion(completion, completed_at_ns);
290        self.counters.record_completion(completion.outcome);
291        if let Some(measurement) = measurement {
292            self.performance.record(measurement);
293        }
294    }
295
296    /// Record an interruption in the epoch where it becomes observable.
297    pub const fn record_interruption(&mut self, observed_at_ns: u64) {
298        self.outcomes.record_interruption(observed_at_ns);
299        self.counters.record_interruption();
300    }
301
302    /// Record a validated schedule or reconciliation request.
303    pub const fn record_request(&mut self) {
304        self.counters.record_request();
305    }
306
307    /// Record an actual one-shot provider arm.
308    pub const fn record_arm(&mut self) {
309        self.counters.record_arm();
310    }
311
312    /// Record a non-stale callback entering logical execution.
313    pub const fn record_start(&mut self) {
314        self.counters.record_start();
315    }
316
317    /// Record a logical cancellation that wins arbitration.
318    pub const fn record_cancellation(&mut self) {
319        self.counters.record_cancellation();
320    }
321
322    /// Record a provider callback or completion rejected as stale.
323    pub const fn record_stale(&mut self) {
324        self.counters.record_stale();
325    }
326
327    /// Record scheduling demand merged into existing work.
328    pub const fn record_coalesced(&mut self) {
329        self.counters.record_coalesced();
330    }
331
332    /// Return the observation epoch.
333    #[must_use]
334    pub const fn epoch(self) -> TimerEpoch {
335        self.epoch
336    }
337
338    /// Return latest outcomes and functional failure state.
339    #[must_use]
340    pub const fn outcomes(self) -> TimerOutcomeSnapshot {
341        self.outcomes
342    }
343
344    /// Return epoch-local event counters.
345    #[must_use]
346    pub const fn counters(self) -> TimerCounters {
347        self.counters
348    }
349
350    /// Return completed callback performance aggregates.
351    #[must_use]
352    pub const fn performance(self) -> TimerPerformance {
353        self.performance
354    }
355
356    /// Return functional expected-failure state without rebuilding a snapshot.
357    #[must_use]
358    pub const fn consecutive_expected_failures(self) -> u64 {
359        self.outcomes.consecutive_expected_failures()
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    #[test]
368    fn all_counters_saturate() {
369        let mut counters = TimerCounters {
370            requested: u64::MAX,
371            armed: u64::MAX,
372            started: u64::MAX,
373            completed: u64::MAX,
374            succeeded: u64::MAX,
375            no_work: u64::MAX,
376            retryable_failure: u64::MAX,
377            invariant_failure: u64::MAX,
378            cancelled: u64::MAX,
379            stale: u64::MAX,
380            coalesced: u64::MAX,
381            interrupted: u64::MAX,
382        };
383
384        counters.record_request();
385        counters.record_arm();
386        counters.record_start();
387        counters.record_completion(TimerCompletionOutcome::Success);
388        counters.record_cancellation();
389        counters.record_stale();
390        counters.record_coalesced();
391        counters.record_interruption();
392
393        assert_eq!(counters.requested(), u64::MAX);
394        assert_eq!(counters.armed(), u64::MAX);
395        assert_eq!(counters.started(), u64::MAX);
396        assert_eq!(counters.completed(), u64::MAX);
397        assert_eq!(counters.succeeded(), u64::MAX);
398        assert_eq!(counters.cancelled(), u64::MAX);
399        assert_eq!(counters.stale(), u64::MAX);
400        assert_eq!(counters.coalesced(), u64::MAX);
401        assert_eq!(counters.interrupted(), u64::MAX);
402        assert!(counters.completion_partition_is_valid());
403    }
404
405    #[test]
406    fn measurement_count_and_total_saturate_while_latest_and_maximum_advance() {
407        let mut summary = MeasurementSummary {
408            samples: u64::MAX,
409            total: u64::MAX,
410            latest: Some(10),
411            maximum: Some(20),
412        };
413
414        summary.record(30);
415
416        assert_eq!(summary.samples(), u64::MAX);
417        assert_eq!(summary.total(), u64::MAX);
418        assert_eq!(summary.latest(), Some(30));
419        assert_eq!(summary.maximum(), Some(30));
420    }
421}