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