Skip to main content

ic_timers/snapshot/
metrics.rs

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