1use super::{TimerCompletion, TimerCompletionOutcome, TimerEpoch, TimerOutcomeSnapshot};
4use ic_metrics::MeasurementSummary;
5
6#[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 #[must_use]
108 pub const fn schedule_requests(self) -> u64 {
109 self.schedule_requests
110 }
111
112 #[must_use]
114 pub const fn wakeups_armed(self) -> u64 {
115 self.wakeups_armed
116 }
117
118 #[must_use]
120 pub const fn work_dispatched(self) -> u64 {
121 self.work_dispatched
122 }
123
124 #[must_use]
126 pub const fn scheduler_started(self) -> u64 {
127 self.scheduler_started
128 }
129
130 #[must_use]
132 pub const fn work_started(self) -> u64 {
133 self.work_started
134 }
135
136 #[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 #[must_use]
149 pub const fn succeeded(self) -> u64 {
150 self.succeeded
151 }
152
153 #[must_use]
155 pub const fn no_work(self) -> u64 {
156 self.no_work
157 }
158
159 #[must_use]
161 pub const fn retryable_failure(self) -> u64 {
162 self.retryable_failure
163 }
164
165 #[must_use]
167 pub const fn invariant_failure(self) -> u64 {
168 self.invariant_failure
169 }
170
171 #[must_use]
173 pub const fn cancelled(self) -> u64 {
174 self.cancelled
175 }
176
177 #[must_use]
179 pub const fn stale_wakeups(self) -> u64 {
180 self.stale_wakeups
181 }
182
183 #[must_use]
185 pub const fn stale_work(self) -> u64 {
186 self.stale_work
187 }
188
189 #[must_use]
191 pub const fn coalesced(self) -> u64 {
192 self.coalesced
193 }
194
195 #[must_use]
198 pub const fn unacknowledged(self) -> u64 {
199 self.unacknowledged
200 }
201}
202
203#[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 #[must_use]
223 pub const fn wasm_pages(self) -> u64 {
224 self.wasm
225 }
226
227 #[must_use]
229 pub const fn stable_pages(self) -> u64 {
230 self.stable
231 }
232}
233
234#[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 #[must_use]
254 pub const fn start(self) -> MemoryPageExtent {
255 self.start
256 }
257
258 #[must_use]
260 pub const fn end(self) -> MemoryPageExtent {
261 self.end
262 }
263
264 #[must_use]
269 pub const fn wasm_growth_pages(self) -> u64 {
270 self.end.wasm.saturating_sub(self.start.wasm)
271 }
272
273 #[must_use]
278 pub const fn stable_growth_pages(self) -> u64 {
279 self.end.stable.saturating_sub(self.start.stable)
280 }
281}
282
283#[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 #[must_use]
314 pub const fn samples(self) -> u64 {
315 self.samples
316 }
317
318 #[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 #[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 #[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#[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 #[must_use]
397 pub const fn scheduler_instructions(self) -> MeasurementSummary {
398 self.scheduler_instructions
399 }
400
401 #[must_use]
403 pub const fn work_instructions(self) -> MeasurementSummary {
404 self.work_instructions
405 }
406
407 #[must_use]
409 pub const fn scheduler_memory_pages(self) -> MemoryPageSummary {
410 self.scheduler_memory_pages
411 }
412
413 #[must_use]
415 pub const fn work_memory_pages(self) -> MemoryPageSummary {
416 self.work_memory_pages
417 }
418}
419
420#[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 #[must_use]
475 pub const fn epoch(self) -> TimerEpoch {
476 self.epoch
477 }
478
479 #[must_use]
481 pub const fn outcomes(self) -> TimerOutcomeSnapshot {
482 self.outcomes
483 }
484
485 #[must_use]
487 pub const fn counters(self) -> TimerCounters {
488 self.counters
489 }
490
491 #[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}