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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
412pub struct TimerPerformance {
413    scheduler_instructions: MeasurementSummary,
414    work_instructions: MeasurementSummary,
415    scheduler_memory_pages: MemoryPageSummary,
416    work_memory_pages: MemoryPageSummary,
417}
418
419impl TimerPerformance {
420    const EMPTY: Self = Self {
421        scheduler_instructions: MeasurementSummary::EMPTY,
422        work_instructions: MeasurementSummary::EMPTY,
423        scheduler_memory_pages: MemoryPageSummary::EMPTY,
424        work_memory_pages: MemoryPageSummary::EMPTY,
425    };
426
427    pub(crate) const fn record_scheduler(&mut self, instructions: u64, memory: MemoryPageSample) {
428        self.scheduler_instructions.record(instructions);
429        self.scheduler_memory_pages.record(memory);
430    }
431
432    pub(crate) const fn record_work(&mut self, instructions: u64, memory: MemoryPageSample) {
433        self.work_instructions.record(instructions);
434        self.work_memory_pages.record(memory);
435    }
436
437    /// Return normally completed scheduler instruction measurements.
438    #[must_use]
439    pub const fn scheduler_instructions(self) -> MeasurementSummary {
440        self.scheduler_instructions
441    }
442
443    /// Return normally completed consumer-work instruction measurements.
444    #[must_use]
445    pub const fn work_instructions(self) -> MeasurementSummary {
446        self.work_instructions
447    }
448
449    /// Return normally completed scheduler memory-page observations.
450    #[must_use]
451    pub const fn scheduler_memory_pages(self) -> MemoryPageSummary {
452        self.scheduler_memory_pages
453    }
454
455    /// Return normally completed consumer-work memory-page observations.
456    #[must_use]
457    pub const fn work_memory_pages(self) -> MemoryPageSummary {
458        self.work_memory_pages
459    }
460}
461
462/// Epoch-scoped outcomes, counters, and performance for one timer.
463#[derive(Clone, Copy, Debug, Eq, PartialEq)]
464pub struct TimerObservabilitySnapshot {
465    epoch: TimerEpoch,
466    outcomes: TimerOutcomeSnapshot,
467    counters: TimerCounters,
468    performance: TimerPerformance,
469}
470
471impl TimerObservabilitySnapshot {
472    pub(crate) const fn new(epoch: TimerEpoch) -> Self {
473        Self {
474            epoch,
475            outcomes: TimerOutcomeSnapshot::EMPTY,
476            counters: TimerCounters::EMPTY,
477            performance: TimerPerformance::EMPTY,
478        }
479    }
480
481    pub(crate) const fn record_completion(
482        &mut self,
483        completion: TimerCompletion,
484        completed_at_ns: u64,
485    ) {
486        self.outcomes.record_completion(completion, completed_at_ns);
487        self.counters.record_completion(completion.outcome());
488    }
489
490    pub(crate) const fn record_unacknowledged(&mut self, observed_at_ns: u64) {
491        self.outcomes.record_unacknowledged(observed_at_ns);
492        self.counters.record_unacknowledged();
493    }
494
495    pub(crate) const fn counters_mut(&mut self) -> &mut TimerCounters {
496        &mut self.counters
497    }
498
499    pub(crate) const fn record_scheduler_measurements(
500        &mut self,
501        instructions: u64,
502        memory: MemoryPageSample,
503    ) {
504        self.performance.record_scheduler(instructions, memory);
505    }
506
507    pub(crate) const fn record_work_measurements(
508        &mut self,
509        instructions: u64,
510        memory: MemoryPageSample,
511    ) {
512        self.performance.record_work(instructions, memory);
513    }
514
515    /// Return the observation epoch.
516    #[must_use]
517    pub const fn epoch(self) -> TimerEpoch {
518        self.epoch
519    }
520
521    /// Return latest outcomes and functional failure state.
522    #[must_use]
523    pub const fn outcomes(self) -> TimerOutcomeSnapshot {
524        self.outcomes
525    }
526
527    /// Return epoch-local event counters.
528    #[must_use]
529    pub const fn counters(self) -> TimerCounters {
530        self.counters
531    }
532
533    /// Return completed instruction and memory-page measurements.
534    #[must_use]
535    pub const fn performance(self) -> TimerPerformance {
536        self.performance
537    }
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543
544    #[test]
545    fn all_counters_saturate() {
546        let mut counters = TimerCounters {
547            schedule_requests: u64::MAX,
548            wakeups_armed: u64::MAX,
549            work_dispatched: u64::MAX,
550            scheduler_started: u64::MAX,
551            work_started: u64::MAX,
552            work_completed: u64::MAX,
553            succeeded: u64::MAX,
554            no_work: u64::MAX,
555            retryable_failure: u64::MAX,
556            invariant_failure: u64::MAX,
557            cancelled: u64::MAX,
558            stale_wakeups: u64::MAX,
559            stale_work: u64::MAX,
560            coalesced: u64::MAX,
561            unacknowledged: u64::MAX,
562        };
563
564        counters.record_schedule_request();
565        counters.record_wakeup_armed();
566        counters.record_work_dispatched();
567        counters.record_scheduler_started();
568        counters.record_work_started();
569        counters.record_completion(TimerCompletionOutcome::Success);
570        counters.record_cancellation();
571        counters.record_stale_wakeup();
572        counters.record_stale_work();
573        counters.record_coalesced();
574        counters.record_unacknowledged();
575
576        assert_eq!(counters.schedule_requests(), u64::MAX);
577        assert_eq!(counters.wakeups_armed(), u64::MAX);
578        assert_eq!(counters.work_dispatched(), u64::MAX);
579        assert_eq!(counters.work_completed(), u64::MAX);
580        assert_eq!(counters.cancelled(), u64::MAX);
581        assert_eq!(counters.stale_wakeups(), u64::MAX);
582        assert_eq!(counters.stale_work(), u64::MAX);
583        assert_eq!(counters.coalesced(), u64::MAX);
584        assert_eq!(counters.unacknowledged(), u64::MAX);
585        assert!(counters.completion_partition_is_valid());
586    }
587
588    #[test]
589    fn callback_measurements_are_role_specific_bounded_and_saturating() {
590        let mut performance = TimerPerformance::EMPTY;
591        performance.record_scheduler(
592            20,
593            MemoryPageSample::new(MemoryPageExtent::new(1, 2), MemoryPageExtent::new(2, 4)),
594        );
595        performance.record_work(
596            30,
597            MemoryPageSample::new(MemoryPageExtent::new(2, 4), MemoryPageExtent::new(5, 5)),
598        );
599        performance.record_work(
600            10,
601            MemoryPageSample::new(MemoryPageExtent::new(5, 5), MemoryPageExtent::new(6, 9)),
602        );
603
604        assert_eq!(performance.scheduler_instructions().total(), 20);
605        assert_eq!(performance.work_instructions().samples(), 2);
606        assert_eq!(performance.work_instructions().total(), 40);
607        assert_eq!(performance.work_instructions().latest(), Some(10));
608        assert_eq!(performance.work_instructions().maximum(), Some(30));
609
610        let scheduler_memory = performance.scheduler_memory_pages();
611        assert_eq!(
612            scheduler_memory.samples(),
613            performance.scheduler_instructions().samples()
614        );
615        assert_eq!(scheduler_memory.samples(), 1);
616        let scheduler_latest = scheduler_memory
617            .latest()
618            .expect("scheduler sample should exist");
619        assert_eq!(scheduler_latest.start().wasm_pages(), 1);
620        assert_eq!(scheduler_latest.start().stable_pages(), 2);
621        assert_eq!(scheduler_latest.end().wasm_pages(), 2);
622        assert_eq!(scheduler_latest.end().stable_pages(), 4);
623        assert_eq!(scheduler_latest.wasm_growth_pages(), 1);
624        assert_eq!(scheduler_latest.stable_growth_pages(), 2);
625
626        let work_memory = performance.work_memory_pages();
627        assert_eq!(
628            work_memory.samples(),
629            performance.work_instructions().samples()
630        );
631        assert_eq!(work_memory.samples(), 2);
632        let work_latest = work_memory.latest().expect("work sample should exist");
633        assert_eq!(work_latest.wasm_growth_pages(), 1);
634        assert_eq!(work_latest.stable_growth_pages(), 4);
635        assert_eq!(work_memory.maximum_wasm_growth_pages(), Some(3));
636        assert_eq!(work_memory.maximum_stable_growth_pages(), Some(4));
637    }
638
639    #[test]
640    fn memory_sample_count_saturates_while_latest_and_maximum_continue() {
641        let mut summary = MemoryPageSummary {
642            samples: u64::MAX,
643            latest: MemoryPageSample::EMPTY,
644            maximum_wasm_growth: 1,
645            maximum_stable_growth: 1,
646        };
647        let sample =
648            MemoryPageSample::new(MemoryPageExtent::new(10, 20), MemoryPageExtent::new(13, 25));
649
650        summary.record(sample);
651
652        assert_eq!(summary.samples(), u64::MAX);
653        assert_eq!(summary.latest(), Some(sample));
654        assert_eq!(summary.maximum_wasm_growth_pages(), Some(3));
655        assert_eq!(summary.maximum_stable_growth_pages(), Some(5));
656    }
657}