Skip to main content

ic_timers/snapshot/
metrics.rs

1//! Saturating epoch-local counters and instruction aggregates.
2
3use super::{TimerCompletion, TimerCompletionOutcome, TimerEpoch, TimerOutcomeSnapshot};
4
5/// Epoch-local timer event counters.
6///
7/// Fields are private so mutation preserves saturation and the completion
8/// partition. The registry is the sole writer.
9#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
10pub struct TimerCounters {
11    schedule_requests: u64,
12    wakeups_armed: u64,
13    work_dispatched: u64,
14    scheduler_started: u64,
15    work_started: u64,
16    work_completed: u64,
17    succeeded: u64,
18    no_work: u64,
19    retryable_failure: u64,
20    invariant_failure: u64,
21    cancelled: u64,
22    stale_wakeups: u64,
23    stale_work: u64,
24    coalesced: u64,
25    unacknowledged: u64,
26}
27
28impl TimerCounters {
29    pub(crate) const fn record_schedule_request(&mut self) {
30        self.schedule_requests = self.schedule_requests.saturating_add(1);
31    }
32
33    pub(crate) const fn record_wakeup_armed(&mut self) {
34        self.wakeups_armed = self.wakeups_armed.saturating_add(1);
35    }
36
37    pub(crate) const fn record_work_dispatched(&mut self) {
38        self.work_dispatched = self.work_dispatched.saturating_add(1);
39    }
40
41    pub(crate) const fn record_scheduler_started(&mut self) {
42        self.scheduler_started = self.scheduler_started.saturating_add(1);
43    }
44
45    pub(crate) const fn record_work_started(&mut self) {
46        self.work_started = self.work_started.saturating_add(1);
47    }
48
49    pub(crate) const fn record_completion(&mut self, outcome: TimerCompletionOutcome) {
50        self.work_completed = self.work_completed.saturating_add(1);
51        match outcome {
52            TimerCompletionOutcome::Success => {
53                self.succeeded = self.succeeded.saturating_add(1);
54            }
55            TimerCompletionOutcome::NoWork => {
56                self.no_work = self.no_work.saturating_add(1);
57            }
58            TimerCompletionOutcome::RetryableFailure => {
59                self.retryable_failure = self.retryable_failure.saturating_add(1);
60            }
61            TimerCompletionOutcome::InvariantFailure => {
62                self.invariant_failure = self.invariant_failure.saturating_add(1);
63            }
64        }
65    }
66
67    pub(crate) const fn record_cancellation(&mut self) {
68        self.cancelled = self.cancelled.saturating_add(1);
69    }
70
71    pub(crate) const fn record_stale_wakeup(&mut self) {
72        self.stale_wakeups = self.stale_wakeups.saturating_add(1);
73    }
74
75    pub(crate) const fn record_stale_work(&mut self) {
76        self.stale_work = self.stale_work.saturating_add(1);
77    }
78
79    pub(crate) const fn record_coalesced(&mut self) {
80        self.coalesced = self.coalesced.saturating_add(1);
81    }
82
83    pub(crate) const fn record_unacknowledged(&mut self) {
84        self.unacknowledged = self.unacknowledged.saturating_add(1);
85    }
86
87    /// Return validated activation and reconciliation requests.
88    #[must_use]
89    pub const fn schedule_requests(self) -> u64 {
90        self.schedule_requests
91    }
92
93    /// Return ordinary or scheduler provider one-shots armed.
94    #[must_use]
95    pub const fn wakeups_armed(self) -> u64 {
96        self.wakeups_armed
97    }
98
99    /// Return immediate watchdog work one-shots dispatched.
100    #[must_use]
101    pub const fn work_dispatched(self) -> u64 {
102        self.work_dispatched
103    }
104
105    /// Return actual provider arms across both callback roles.
106    #[must_use]
107    pub const fn provider_arms(self) -> u64 {
108        self.wakeups_armed.saturating_add(self.work_dispatched)
109    }
110
111    /// Return accepted watchdog scheduler callbacks.
112    #[must_use]
113    pub const fn scheduler_started(self) -> u64 {
114        self.scheduler_started
115    }
116
117    /// Return accepted consumer-work callbacks.
118    #[must_use]
119    pub const fn work_started(self) -> u64 {
120        self.work_started
121    }
122
123    /// Return consumer work whose completion accounting committed.
124    #[must_use]
125    pub const fn work_completed(self) -> u64 {
126        self.work_completed
127    }
128
129    /// Return successful-work completions.
130    #[must_use]
131    pub const fn succeeded(self) -> u64 {
132        self.succeeded
133    }
134
135    /// Return valid no-work completions.
136    #[must_use]
137    pub const fn no_work(self) -> u64 {
138        self.no_work
139    }
140
141    /// Return retryable expected-failure completions.
142    #[must_use]
143    pub const fn retryable_failure(self) -> u64 {
144        self.retryable_failure
145    }
146
147    /// Return invariant or terminal-failure completions.
148    #[must_use]
149    pub const fn invariant_failure(self) -> u64 {
150        self.invariant_failure
151    }
152
153    /// Return logical cancellations that changed authoritative state.
154    #[must_use]
155    pub const fn cancelled(self) -> u64 {
156        self.cancelled
157    }
158
159    /// Return rejected ordinary or scheduler callback generations.
160    #[must_use]
161    pub const fn stale_wakeups(self) -> u64 {
162        self.stale_wakeups
163    }
164
165    /// Return rejected watchdog work generations.
166    #[must_use]
167    pub const fn stale_work(self) -> u64 {
168        self.stale_work
169    }
170
171    /// Return scheduling demand satisfied without another logical arm.
172    #[must_use]
173    pub const fn coalesced(self) -> u64 {
174        self.coalesced
175    }
176
177    /// Return committed watchdog dispatches retired without completion.
178    #[must_use]
179    pub const fn unacknowledged(self) -> u64 {
180        self.unacknowledged
181    }
182
183    /// Check that every completion belongs to exactly one outcome class.
184    #[must_use]
185    pub const fn completion_partition_is_valid(self) -> bool {
186        self.work_completed
187            == self
188                .succeeded
189                .saturating_add(self.no_work)
190                .saturating_add(self.retryable_failure)
191                .saturating_add(self.invariant_failure)
192    }
193}
194
195/// Saturating aggregate for one instruction measurement role.
196#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
197pub struct MeasurementSummary {
198    samples: u64,
199    total: u64,
200    latest: Option<u64>,
201    maximum: Option<u64>,
202}
203
204impl MeasurementSummary {
205    pub(crate) const fn record(&mut self, value: u64) {
206        self.samples = self.samples.saturating_add(1);
207        self.total = self.total.saturating_add(value);
208        self.latest = Some(value);
209        self.maximum = Some(match self.maximum {
210            Some(current) if current > value => current,
211            Some(_) | None => value,
212        });
213    }
214
215    /// Return the number of completed samples.
216    #[must_use]
217    pub const fn samples(self) -> u64 {
218        self.samples
219    }
220
221    /// Return the saturating sum of all samples.
222    #[must_use]
223    pub const fn total(self) -> u64 {
224        self.total
225    }
226
227    /// Return the latest sample, if one exists.
228    #[must_use]
229    pub const fn latest(self) -> Option<u64> {
230        self.latest
231    }
232
233    /// Return the largest sample, if one exists.
234    #[must_use]
235    pub const fn maximum(self) -> Option<u64> {
236        self.maximum
237    }
238}
239
240/// Completed instruction aggregates split by callback role.
241#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
242pub struct TimerPerformance {
243    scheduler_instructions: MeasurementSummary,
244    work_instructions: MeasurementSummary,
245}
246
247impl TimerPerformance {
248    pub(crate) const fn record_scheduler(&mut self, instructions: u64) {
249        self.scheduler_instructions.record(instructions);
250    }
251
252    pub(crate) const fn record_work(&mut self, instructions: u64) {
253        self.work_instructions.record(instructions);
254    }
255
256    /// Return normally completed scheduler instruction measurements.
257    #[must_use]
258    pub const fn scheduler_instructions(self) -> MeasurementSummary {
259        self.scheduler_instructions
260    }
261
262    /// Return normally completed consumer-work instruction measurements.
263    #[must_use]
264    pub const fn work_instructions(self) -> MeasurementSummary {
265        self.work_instructions
266    }
267}
268
269/// Epoch-scoped outcomes, counters, and performance for one timer.
270#[derive(Clone, Copy, Debug, Eq, PartialEq)]
271pub struct TimerObservabilitySnapshot {
272    epoch: TimerEpoch,
273    outcomes: TimerOutcomeSnapshot,
274    counters: TimerCounters,
275    performance: TimerPerformance,
276}
277
278impl TimerObservabilitySnapshot {
279    pub(crate) const fn new(epoch: TimerEpoch) -> Self {
280        Self {
281            epoch,
282            outcomes: TimerOutcomeSnapshot::new(),
283            counters: TimerCounters {
284                schedule_requests: 0,
285                wakeups_armed: 0,
286                work_dispatched: 0,
287                scheduler_started: 0,
288                work_started: 0,
289                work_completed: 0,
290                succeeded: 0,
291                no_work: 0,
292                retryable_failure: 0,
293                invariant_failure: 0,
294                cancelled: 0,
295                stale_wakeups: 0,
296                stale_work: 0,
297                coalesced: 0,
298                unacknowledged: 0,
299            },
300            performance: TimerPerformance {
301                scheduler_instructions: MeasurementSummary {
302                    samples: 0,
303                    total: 0,
304                    latest: None,
305                    maximum: None,
306                },
307                work_instructions: MeasurementSummary {
308                    samples: 0,
309                    total: 0,
310                    latest: None,
311                    maximum: None,
312                },
313            },
314        }
315    }
316
317    pub(crate) const fn record_completion(
318        &mut self,
319        completion: TimerCompletion,
320        completed_at_ns: u64,
321    ) {
322        self.outcomes.record_completion(completion, completed_at_ns);
323        self.counters.record_completion(completion.outcome());
324    }
325
326    pub(crate) const fn record_unacknowledged(&mut self, observed_at_ns: u64) {
327        self.outcomes.record_unacknowledged(observed_at_ns);
328        self.counters.record_unacknowledged();
329    }
330
331    pub(crate) const fn counters_mut(&mut self) -> &mut TimerCounters {
332        &mut self.counters
333    }
334
335    pub(crate) const fn record_scheduler_instructions(&mut self, instructions: u64) {
336        self.performance.record_scheduler(instructions);
337    }
338
339    pub(crate) const fn record_work_instructions(&mut self, instructions: u64) {
340        self.performance.record_work(instructions);
341    }
342
343    /// Return the observation epoch.
344    #[must_use]
345    pub const fn epoch(self) -> TimerEpoch {
346        self.epoch
347    }
348
349    /// Return latest outcomes and functional failure state.
350    #[must_use]
351    pub const fn outcomes(self) -> TimerOutcomeSnapshot {
352        self.outcomes
353    }
354
355    /// Return epoch-local event counters.
356    #[must_use]
357    pub const fn counters(self) -> TimerCounters {
358        self.counters
359    }
360
361    /// Return completed instruction aggregates.
362    #[must_use]
363    pub const fn performance(self) -> TimerPerformance {
364        self.performance
365    }
366
367    /// Return functional expected-failure state without rebuilding a snapshot.
368    #[must_use]
369    pub const fn consecutive_expected_failures(self) -> u64 {
370        self.outcomes.consecutive_expected_failures()
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377
378    #[test]
379    fn all_counters_saturate() {
380        let mut counters = TimerCounters {
381            schedule_requests: u64::MAX,
382            wakeups_armed: u64::MAX,
383            work_dispatched: u64::MAX,
384            scheduler_started: u64::MAX,
385            work_started: u64::MAX,
386            work_completed: u64::MAX,
387            succeeded: u64::MAX,
388            no_work: u64::MAX,
389            retryable_failure: u64::MAX,
390            invariant_failure: u64::MAX,
391            cancelled: u64::MAX,
392            stale_wakeups: u64::MAX,
393            stale_work: u64::MAX,
394            coalesced: u64::MAX,
395            unacknowledged: u64::MAX,
396        };
397
398        counters.record_schedule_request();
399        counters.record_wakeup_armed();
400        counters.record_work_dispatched();
401        counters.record_scheduler_started();
402        counters.record_work_started();
403        counters.record_completion(TimerCompletionOutcome::Success);
404        counters.record_cancellation();
405        counters.record_stale_wakeup();
406        counters.record_stale_work();
407        counters.record_coalesced();
408        counters.record_unacknowledged();
409
410        assert_eq!(counters.schedule_requests(), u64::MAX);
411        assert_eq!(counters.provider_arms(), u64::MAX);
412        assert_eq!(counters.work_completed(), u64::MAX);
413        assert_eq!(counters.cancelled(), u64::MAX);
414        assert_eq!(counters.stale_wakeups(), u64::MAX);
415        assert_eq!(counters.stale_work(), u64::MAX);
416        assert_eq!(counters.coalesced(), u64::MAX);
417        assert_eq!(counters.unacknowledged(), u64::MAX);
418        assert!(counters.completion_partition_is_valid());
419    }
420
421    #[test]
422    fn instruction_roles_are_separate_and_saturating() {
423        let mut performance = TimerPerformance::default();
424        performance.record_scheduler(20);
425        performance.record_work(30);
426        performance.record_work(10);
427
428        assert_eq!(performance.scheduler_instructions().total(), 20);
429        assert_eq!(performance.work_instructions().samples(), 2);
430        assert_eq!(performance.work_instructions().total(), 40);
431        assert_eq!(performance.work_instructions().latest(), Some(10));
432        assert_eq!(performance.work_instructions().maximum(), Some(30));
433    }
434}