Skip to main content

ic_timers/snapshot/
metrics.rs

1//! Saturating registration-local counters and bounded callback measurements.
2
3use super::{TimerCompletion, TimerCompletionOutcome, TimerEpoch, TimerOutcomeSnapshot};
4use ic_metrics::MeasurementSummary;
5
6/// Registration-local timer event counters.
7///
8/// Fields are private so mutation preserves saturation. Completed work is
9/// derived from its classified outcomes; the registry is the sole writer.
10/// A value equal to `u64::MAX` is conservatively saturated: do not derive an
11/// exact interval delta from that field. Compare registration identities before
12/// subtracting counters; runtime epoch equality alone does not prove continuity.
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub struct TimerCounters {
15    schedule_requests: u64,
16    wakeups_armed: u64,
17    work_dispatched: u64,
18    scheduler_started: u64,
19    work_started: u64,
20    succeeded: u64,
21    no_work: u64,
22    retryable_failure: u64,
23    invariant_failure: u64,
24    cancelled: u64,
25    stale_wakeups: u64,
26    stale_work: u64,
27    coalesced: u64,
28    unacknowledged: u64,
29}
30
31impl TimerCounters {
32    const EMPTY: Self = Self {
33        schedule_requests: 0,
34        wakeups_armed: 0,
35        work_dispatched: 0,
36        scheduler_started: 0,
37        work_started: 0,
38        succeeded: 0,
39        no_work: 0,
40        retryable_failure: 0,
41        invariant_failure: 0,
42        cancelled: 0,
43        stale_wakeups: 0,
44        stale_work: 0,
45        coalesced: 0,
46        unacknowledged: 0,
47    };
48
49    pub(crate) const fn record_schedule_request(&mut self) {
50        self.schedule_requests = self.schedule_requests.saturating_add(1);
51    }
52
53    pub(crate) const fn record_wakeup_armed(&mut self) {
54        self.wakeups_armed = self.wakeups_armed.saturating_add(1);
55    }
56
57    pub(crate) const fn record_work_dispatched(&mut self) {
58        self.work_dispatched = self.work_dispatched.saturating_add(1);
59    }
60
61    pub(crate) const fn record_scheduler_started(&mut self) {
62        self.scheduler_started = self.scheduler_started.saturating_add(1);
63    }
64
65    pub(crate) const fn record_work_started(&mut self) {
66        self.work_started = self.work_started.saturating_add(1);
67    }
68
69    pub(crate) const fn record_completion(&mut self, outcome: TimerCompletionOutcome) {
70        match outcome {
71            TimerCompletionOutcome::Success => {
72                self.succeeded = self.succeeded.saturating_add(1);
73            }
74            TimerCompletionOutcome::NoWork => {
75                self.no_work = self.no_work.saturating_add(1);
76            }
77            TimerCompletionOutcome::RetryableFailure => {
78                self.retryable_failure = self.retryable_failure.saturating_add(1);
79            }
80            TimerCompletionOutcome::InvariantFailure => {
81                self.invariant_failure = self.invariant_failure.saturating_add(1);
82            }
83        }
84    }
85
86    pub(crate) const fn record_cancellation(&mut self) {
87        self.cancelled = self.cancelled.saturating_add(1);
88    }
89
90    pub(crate) const fn record_stale_wakeup(&mut self) {
91        self.stale_wakeups = self.stale_wakeups.saturating_add(1);
92    }
93
94    pub(crate) const fn record_stale_work(&mut self) {
95        self.stale_work = self.stale_work.saturating_add(1);
96    }
97
98    pub(crate) const fn record_coalesced(&mut self) {
99        self.coalesced = self.coalesced.saturating_add(1);
100    }
101
102    pub(crate) const fn record_unacknowledged(&mut self) {
103        self.unacknowledged = self.unacknowledged.saturating_add(1);
104    }
105
106    /// Return validated activation and reconciliation requests.
107    #[must_use]
108    pub const fn schedule_requests(self) -> u64 {
109        self.schedule_requests
110    }
111
112    /// Return ordinary or scheduler provider one-shots armed.
113    #[must_use]
114    pub const fn wakeups_armed(self) -> u64 {
115        self.wakeups_armed
116    }
117
118    /// Return immediate watchdog work one-shots dispatched.
119    #[must_use]
120    pub const fn work_dispatched(self) -> u64 {
121        self.work_dispatched
122    }
123
124    /// Return accepted watchdog scheduler callbacks.
125    #[must_use]
126    pub const fn scheduler_started(self) -> u64 {
127        self.scheduler_started
128    }
129
130    /// Return accepted consumer-work callbacks.
131    #[must_use]
132    pub const fn work_started(self) -> u64 {
133        self.work_started
134    }
135
136    /// Return consumer work whose completion accounting committed.
137    ///
138    /// This is the saturating sum of the four classified completion counters.
139    #[must_use]
140    pub const fn work_completed(self) -> u64 {
141        self.succeeded
142            .saturating_add(self.no_work)
143            .saturating_add(self.retryable_failure)
144            .saturating_add(self.invariant_failure)
145    }
146
147    /// Return successful-work completions.
148    #[must_use]
149    pub const fn succeeded(self) -> u64 {
150        self.succeeded
151    }
152
153    /// Return valid no-work completions.
154    #[must_use]
155    pub const fn no_work(self) -> u64 {
156        self.no_work
157    }
158
159    /// Return retryable expected-failure completions.
160    #[must_use]
161    pub const fn retryable_failure(self) -> u64 {
162        self.retryable_failure
163    }
164
165    /// Return invariant or terminal-failure completions.
166    #[must_use]
167    pub const fn invariant_failure(self) -> u64 {
168        self.invariant_failure
169    }
170
171    /// Return logical cancellations that changed authoritative state.
172    #[must_use]
173    pub const fn cancelled(self) -> u64 {
174        self.cancelled
175    }
176
177    /// Return rejected ordinary or scheduler callback generations.
178    #[must_use]
179    pub const fn stale_wakeups(self) -> u64 {
180        self.stale_wakeups
181    }
182
183    /// Return rejected watchdog work generations.
184    #[must_use]
185    pub const fn stale_work(self) -> u64 {
186        self.stale_work
187    }
188
189    /// Return scheduling demand satisfied without another logical arm.
190    #[must_use]
191    pub const fn coalesced(self) -> u64 {
192        self.coalesced
193    }
194
195    /// Return confirmed ordinary deliveries dropped without completion and
196    /// committed Watchdog dispatches retired by their successor without completion.
197    #[must_use]
198    pub const fn unacknowledged(self) -> u64 {
199        self.unacknowledged
200    }
201}
202
203/// Wasm and stable memory extents in 64 KiB pages at one instant.
204///
205/// Within one runtime epoch these are monotonic page extents, not allocator
206/// liveness or exact live-byte measurements. Consumers that need live bytes
207/// must supply an owner-derived bound for allocations within the final page.
208#[derive(Clone, Copy, Debug, Eq, PartialEq)]
209pub struct MemoryPageExtent {
210    wasm: u64,
211    stable: u64,
212}
213
214impl MemoryPageExtent {
215    const EMPTY: Self = Self { wasm: 0, stable: 0 };
216
217    pub(crate) const fn new(wasm: u64, stable: u64) -> Self {
218        Self { wasm, stable }
219    }
220
221    /// Return the Wasm linear-memory extent in 64 KiB pages.
222    #[must_use]
223    pub const fn wasm_pages(self) -> u64 {
224        self.wasm
225    }
226
227    /// Return the stable-memory extent in 64 KiB pages.
228    #[must_use]
229    pub const fn stable_pages(self) -> u64 {
230        self.stable
231    }
232}
233
234/// Page extents observed at the start and end of one normally completed
235/// callback.
236#[derive(Clone, Copy, Debug, Eq, PartialEq)]
237pub struct MemoryPageSample {
238    start: MemoryPageExtent,
239    end: MemoryPageExtent,
240}
241
242impl MemoryPageSample {
243    const EMPTY: Self = Self {
244        start: MemoryPageExtent::EMPTY,
245        end: MemoryPageExtent::EMPTY,
246    };
247
248    pub(crate) const fn new(start: MemoryPageExtent, end: MemoryPageExtent) -> Self {
249        Self { start, end }
250    }
251
252    /// Return page extents sampled at callback start.
253    #[must_use]
254    pub const fn start(self) -> MemoryPageExtent {
255        self.start
256    }
257
258    /// Return page extents sampled after normal callback completion.
259    #[must_use]
260    pub const fn end(self) -> MemoryPageExtent {
261        self.end
262    }
263
264    /// Return non-negative Wasm-memory page growth observed between samples.
265    ///
266    /// For async ordinary work, the interval may include interleaved canister
267    /// activity while the callback future is awaiting.
268    #[must_use]
269    pub const fn wasm_growth_pages(self) -> u64 {
270        self.end.wasm.saturating_sub(self.start.wasm)
271    }
272
273    /// Return non-negative stable-memory page growth observed between samples.
274    ///
275    /// For async ordinary work, the interval may include interleaved canister
276    /// activity while the callback future is awaiting.
277    #[must_use]
278    pub const fn stable_growth_pages(self) -> u64 {
279        self.end.stable.saturating_sub(self.start.stable)
280    }
281}
282
283/// Bounded page-extent observations for one callback role.
284///
285/// Absolute page extents are retained only for the latest normal completion;
286/// they are never totaled. Maximums describe observed start-to-end page
287/// growth, which is not exclusive allocation attribution for async work.
288#[derive(Clone, Copy, Debug, Eq, PartialEq)]
289pub struct MemoryPageSummary {
290    samples: u64,
291    latest: MemoryPageSample,
292    maximum_wasm_growth: u64,
293    maximum_stable_growth: u64,
294}
295
296impl MemoryPageSummary {
297    const EMPTY: Self = Self {
298        samples: 0,
299        latest: MemoryPageSample::EMPTY,
300        maximum_wasm_growth: 0,
301        maximum_stable_growth: 0,
302    };
303
304    const fn record(&mut self, sample: MemoryPageSample) {
305        self.samples = self.samples.saturating_add(1);
306        self.latest = sample;
307        self.maximum_wasm_growth = max_u64(self.maximum_wasm_growth, sample.wasm_growth_pages());
308        self.maximum_stable_growth =
309            max_u64(self.maximum_stable_growth, sample.stable_growth_pages());
310    }
311
312    /// Return the number of normally completed callback samples.
313    #[must_use]
314    pub const fn samples(self) -> u64 {
315        self.samples
316    }
317
318    /// Return start/end page extents for the latest normal completion.
319    #[must_use]
320    pub const fn latest(self) -> Option<MemoryPageSample> {
321        if self.samples == 0 {
322            None
323        } else {
324            Some(self.latest)
325        }
326    }
327
328    /// Return the largest observed Wasm-memory page growth in one sample.
329    #[must_use]
330    pub const fn maximum_wasm_growth_pages(self) -> Option<u64> {
331        if self.samples == 0 {
332            None
333        } else {
334            Some(self.maximum_wasm_growth)
335        }
336    }
337
338    /// Return the largest observed stable-memory page growth in one sample.
339    #[must_use]
340    pub const fn maximum_stable_growth_pages(self) -> Option<u64> {
341        if self.samples == 0 {
342            None
343        } else {
344            Some(self.maximum_stable_growth)
345        }
346    }
347}
348
349const fn max_u64(left: u64, right: u64) -> u64 {
350    if left > right { left } else { right }
351}
352
353/// Completed instruction and memory-page measurements split by callback role.
354///
355/// Each interval covers the accepted `ic-timers` callback envelope: it starts
356/// before callback acceptance and ends after completion processing and any
357/// successor binding. It is not an exclusive measurement of consumer code.
358/// Start/end page reads bracket the instruction interval from outside.
359///
360/// A terminal [`DeclarationLifetime::RemoveWhenStopped`](crate::DeclarationLifetime::RemoveWhenStopped)
361/// callback can remove its declaration before this final record is retained;
362/// no timer then remains in the inventory to expose that sample.
363#[derive(Clone, Copy, Debug, Eq, PartialEq)]
364pub struct TimerPerformance {
365    scheduler_instructions: MeasurementSummary,
366    work_instructions: MeasurementSummary,
367    scheduler_memory_pages: MemoryPageSummary,
368    work_memory_pages: MemoryPageSummary,
369}
370
371impl TimerPerformance {
372    const EMPTY: Self = Self {
373        scheduler_instructions: MeasurementSummary::EMPTY,
374        work_instructions: MeasurementSummary::EMPTY,
375        scheduler_memory_pages: MemoryPageSummary::EMPTY,
376        work_memory_pages: MemoryPageSummary::EMPTY,
377    };
378
379    pub(crate) const fn record_scheduler(&mut self, instructions: u64, memory: MemoryPageSample) {
380        self.scheduler_instructions.record(instructions);
381        self.scheduler_memory_pages.record(memory);
382    }
383
384    pub(crate) const fn record_work(&mut self, instructions: u64, memory: MemoryPageSample) {
385        self.work_instructions.record(instructions);
386        self.work_memory_pages.record(memory);
387    }
388
389    /// Return normally completed accepted scheduler-envelope measurements.
390    #[must_use]
391    pub const fn scheduler_instructions(self) -> MeasurementSummary {
392        self.scheduler_instructions
393    }
394
395    /// Return normally completed accepted work-envelope measurements.
396    #[must_use]
397    pub const fn work_instructions(self) -> MeasurementSummary {
398        self.work_instructions
399    }
400
401    /// Return normally completed accepted scheduler-envelope page observations.
402    #[must_use]
403    pub const fn scheduler_memory_pages(self) -> MemoryPageSummary {
404        self.scheduler_memory_pages
405    }
406
407    /// Return normally completed accepted work-envelope page observations.
408    #[must_use]
409    pub const fn work_memory_pages(self) -> MemoryPageSummary {
410        self.work_memory_pages
411    }
412}
413
414/// Epoch-scoped outcomes, counters, and performance for one timer.
415#[derive(Clone, Copy, Debug, Eq, PartialEq)]
416pub struct TimerObservabilitySnapshot {
417    epoch: TimerEpoch,
418    outcomes: TimerOutcomeSnapshot,
419    counters: TimerCounters,
420    performance: TimerPerformance,
421}
422
423impl TimerObservabilitySnapshot {
424    pub(crate) const fn new(epoch: TimerEpoch) -> Self {
425        Self {
426            epoch,
427            outcomes: TimerOutcomeSnapshot::EMPTY,
428            counters: TimerCounters::EMPTY,
429            performance: TimerPerformance::EMPTY,
430        }
431    }
432
433    pub(crate) const fn record_completion(
434        &mut self,
435        completion: TimerCompletion,
436        completed_at_ns: u64,
437    ) {
438        self.outcomes.record_completion(completion, completed_at_ns);
439        self.counters.record_completion(completion.outcome());
440    }
441
442    pub(crate) const fn record_unacknowledged(&mut self, observed_at_ns: u64) {
443        self.outcomes.record_unacknowledged(observed_at_ns);
444        self.counters.record_unacknowledged();
445    }
446
447    pub(crate) const fn counters_mut(&mut self) -> &mut TimerCounters {
448        &mut self.counters
449    }
450
451    pub(crate) const fn record_scheduler_measurements(
452        &mut self,
453        instructions: u64,
454        memory: MemoryPageSample,
455    ) {
456        self.performance.record_scheduler(instructions, memory);
457    }
458
459    pub(crate) const fn record_work_measurements(
460        &mut self,
461        instructions: u64,
462        memory: MemoryPageSample,
463    ) {
464        self.performance.record_work(instructions, memory);
465    }
466
467    /// Return the observation epoch.
468    #[must_use]
469    pub const fn epoch(self) -> TimerEpoch {
470        self.epoch
471    }
472
473    /// Return latest outcomes and functional failure state.
474    #[must_use]
475    pub const fn outcomes(self) -> TimerOutcomeSnapshot {
476        self.outcomes
477    }
478
479    /// Return registration-local event counters within this epoch.
480    #[must_use]
481    pub const fn counters(self) -> TimerCounters {
482        self.counters
483    }
484
485    /// Return completed instruction and memory-page measurements.
486    #[must_use]
487    pub const fn performance(self) -> TimerPerformance {
488        self.performance
489    }
490}
491
492#[cfg(test)]
493mod tests {
494    use super::*;
495
496    #[test]
497    fn all_counters_saturate() {
498        let mut counters = TimerCounters {
499            schedule_requests: u64::MAX,
500            wakeups_armed: u64::MAX,
501            work_dispatched: u64::MAX,
502            scheduler_started: u64::MAX,
503            work_started: u64::MAX,
504            succeeded: u64::MAX,
505            no_work: u64::MAX,
506            retryable_failure: u64::MAX,
507            invariant_failure: u64::MAX,
508            cancelled: u64::MAX,
509            stale_wakeups: u64::MAX,
510            stale_work: u64::MAX,
511            coalesced: u64::MAX,
512            unacknowledged: u64::MAX,
513        };
514
515        counters.record_schedule_request();
516        counters.record_wakeup_armed();
517        counters.record_work_dispatched();
518        counters.record_scheduler_started();
519        counters.record_work_started();
520        for outcome in [
521            TimerCompletionOutcome::Success,
522            TimerCompletionOutcome::NoWork,
523            TimerCompletionOutcome::RetryableFailure,
524            TimerCompletionOutcome::InvariantFailure,
525        ] {
526            counters.record_completion(outcome);
527        }
528        counters.record_cancellation();
529        counters.record_stale_wakeup();
530        counters.record_stale_work();
531        counters.record_coalesced();
532        counters.record_unacknowledged();
533
534        assert_eq!(counters.schedule_requests(), u64::MAX);
535        assert_eq!(counters.wakeups_armed(), u64::MAX);
536        assert_eq!(counters.work_dispatched(), u64::MAX);
537        assert_eq!(counters.work_completed(), u64::MAX);
538        assert_eq!(counters.succeeded(), u64::MAX);
539        assert_eq!(counters.no_work(), u64::MAX);
540        assert_eq!(counters.retryable_failure(), u64::MAX);
541        assert_eq!(counters.invariant_failure(), u64::MAX);
542        assert_eq!(counters.cancelled(), u64::MAX);
543        assert_eq!(counters.stale_wakeups(), u64::MAX);
544        assert_eq!(counters.stale_work(), u64::MAX);
545        assert_eq!(counters.coalesced(), u64::MAX);
546        assert_eq!(counters.unacknowledged(), u64::MAX);
547    }
548
549    #[test]
550    fn completion_totals_follow_classifications_and_saturate_across_outcomes() {
551        let mut counters = TimerCounters::EMPTY;
552        assert_eq!(counters.work_completed(), 0);
553        for (outcome, total) in [
554            (TimerCompletionOutcome::Success, 1),
555            (TimerCompletionOutcome::NoWork, 2),
556            (TimerCompletionOutcome::RetryableFailure, 3),
557            (TimerCompletionOutcome::InvariantFailure, 4),
558        ] {
559            counters.record_completion(outcome);
560            assert_eq!(counters.work_completed(), total);
561        }
562        assert_eq!(counters.succeeded(), 1);
563        assert_eq!(counters.no_work(), 1);
564        assert_eq!(counters.retryable_failure(), 1);
565        assert_eq!(counters.invariant_failure(), 1);
566        assert_eq!(counters.work_started(), 0);
567
568        let mut counters = TimerCounters {
569            succeeded: u64::MAX - 2,
570            ..TimerCounters::EMPTY
571        };
572        counters.record_completion(TimerCompletionOutcome::NoWork);
573        assert_eq!(counters.work_completed(), u64::MAX - 1);
574        counters.record_completion(TimerCompletionOutcome::RetryableFailure);
575        assert_eq!(counters.work_completed(), u64::MAX);
576        counters.record_completion(TimerCompletionOutcome::InvariantFailure);
577        counters.record_completion(TimerCompletionOutcome::Success);
578        assert_eq!(counters.work_completed(), u64::MAX);
579        assert_eq!(counters.succeeded(), u64::MAX - 1);
580        assert_eq!(counters.no_work(), 1);
581        assert_eq!(counters.retryable_failure(), 1);
582        assert_eq!(counters.invariant_failure(), 1);
583    }
584
585    #[test]
586    fn callback_measurements_are_role_specific_bounded_and_saturating() {
587        let mut performance = TimerPerformance::EMPTY;
588        performance.record_scheduler(
589            20,
590            MemoryPageSample::new(MemoryPageExtent::new(1, 2), MemoryPageExtent::new(2, 4)),
591        );
592        performance.record_work(
593            30,
594            MemoryPageSample::new(MemoryPageExtent::new(2, 4), MemoryPageExtent::new(5, 5)),
595        );
596        performance.record_work(
597            10,
598            MemoryPageSample::new(MemoryPageExtent::new(5, 5), MemoryPageExtent::new(6, 9)),
599        );
600
601        assert_eq!(performance.scheduler_instructions().total(), 20);
602        assert_eq!(performance.work_instructions().samples(), 2);
603        assert_eq!(performance.work_instructions().total(), 40);
604        assert_eq!(performance.work_instructions().latest(), Some(10));
605        assert_eq!(performance.work_instructions().maximum(), Some(30));
606
607        let scheduler_memory = performance.scheduler_memory_pages();
608        assert_eq!(
609            scheduler_memory.samples(),
610            performance.scheduler_instructions().samples()
611        );
612        assert_eq!(scheduler_memory.samples(), 1);
613        let scheduler_latest = scheduler_memory
614            .latest()
615            .expect("scheduler sample should exist");
616        assert_eq!(scheduler_latest.start().wasm_pages(), 1);
617        assert_eq!(scheduler_latest.start().stable_pages(), 2);
618        assert_eq!(scheduler_latest.end().wasm_pages(), 2);
619        assert_eq!(scheduler_latest.end().stable_pages(), 4);
620        assert_eq!(scheduler_latest.wasm_growth_pages(), 1);
621        assert_eq!(scheduler_latest.stable_growth_pages(), 2);
622
623        let work_memory = performance.work_memory_pages();
624        assert_eq!(
625            work_memory.samples(),
626            performance.work_instructions().samples()
627        );
628        assert_eq!(work_memory.samples(), 2);
629        let work_latest = work_memory.latest().expect("work sample should exist");
630        assert_eq!(work_latest.wasm_growth_pages(), 1);
631        assert_eq!(work_latest.stable_growth_pages(), 4);
632        assert_eq!(work_memory.maximum_wasm_growth_pages(), Some(3));
633        assert_eq!(work_memory.maximum_stable_growth_pages(), Some(4));
634    }
635
636    #[test]
637    fn memory_sample_count_saturates_while_latest_and_maximum_continue() {
638        let mut summary = MemoryPageSummary {
639            samples: u64::MAX,
640            latest: MemoryPageSample::EMPTY,
641            maximum_wasm_growth: 1,
642            maximum_stable_growth: 1,
643        };
644        let sample =
645            MemoryPageSample::new(MemoryPageExtent::new(10, 20), MemoryPageExtent::new(13, 25));
646
647        summary.record(sample);
648
649        assert_eq!(summary.samples(), u64::MAX);
650        assert_eq!(summary.latest(), Some(sample));
651        assert_eq!(summary.maximum_wasm_growth_pages(), Some(3));
652        assert_eq!(summary.maximum_stable_growth_pages(), Some(5));
653    }
654}