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