Skip to main content

ic_timers/snapshot/
metrics.rs

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