Skip to main content

ic_timers/snapshot/
metrics.rs

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