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