use super::{TimerCompletion, TimerCompletionOutcome, TimerOutcomeSnapshot};
use ic_metrics::{MeasurementCounter, MeasurementSummary};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TimerCounters {
schedule_requests: MeasurementCounter,
wakeups_armed: MeasurementCounter,
work_dispatched: MeasurementCounter,
scheduler_started: MeasurementCounter,
work_started: MeasurementCounter,
succeeded: MeasurementCounter,
no_work: MeasurementCounter,
retryable_failure: MeasurementCounter,
invariant_failure: MeasurementCounter,
cancelled: MeasurementCounter,
stale_wakeups: MeasurementCounter,
stale_work: MeasurementCounter,
coalesced: MeasurementCounter,
unacknowledged: MeasurementCounter,
}
impl TimerCounters {
const EMPTY: Self = Self {
schedule_requests: MeasurementCounter::ZERO,
wakeups_armed: MeasurementCounter::ZERO,
work_dispatched: MeasurementCounter::ZERO,
scheduler_started: MeasurementCounter::ZERO,
work_started: MeasurementCounter::ZERO,
succeeded: MeasurementCounter::ZERO,
no_work: MeasurementCounter::ZERO,
retryable_failure: MeasurementCounter::ZERO,
invariant_failure: MeasurementCounter::ZERO,
cancelled: MeasurementCounter::ZERO,
stale_wakeups: MeasurementCounter::ZERO,
stale_work: MeasurementCounter::ZERO,
coalesced: MeasurementCounter::ZERO,
unacknowledged: MeasurementCounter::ZERO,
};
pub(crate) const fn record_schedule_request(&mut self) {
self.schedule_requests.add(1);
}
pub(crate) const fn record_wakeup_armed(&mut self) {
self.wakeups_armed.add(1);
}
pub(crate) const fn record_work_dispatched(&mut self) {
self.work_dispatched.add(1);
}
pub(crate) const fn record_scheduler_started(&mut self) {
self.scheduler_started.add(1);
}
pub(crate) const fn record_work_started(&mut self) {
self.work_started.add(1);
}
pub(crate) const fn record_completion(&mut self, outcome: TimerCompletionOutcome) {
match outcome {
TimerCompletionOutcome::Success => {
self.succeeded.add(1);
}
TimerCompletionOutcome::NoWork => {
self.no_work.add(1);
}
TimerCompletionOutcome::RetryableFailure => {
self.retryable_failure.add(1);
}
TimerCompletionOutcome::InvariantFailure => {
self.invariant_failure.add(1);
}
}
}
pub(crate) const fn record_cancellation(&mut self) {
self.cancelled.add(1);
}
pub(crate) const fn record_stale_wakeup(&mut self) {
self.stale_wakeups.add(1);
}
pub(crate) const fn record_stale_work(&mut self) {
self.stale_work.add(1);
}
pub(crate) const fn record_coalesced(&mut self) {
self.coalesced.add(1);
}
pub(crate) const fn record_unacknowledged(&mut self) {
self.unacknowledged.add(1);
}
#[must_use]
pub const fn schedule_requests(self) -> MeasurementCounter {
self.schedule_requests
}
#[must_use]
pub const fn wakeups_armed(self) -> MeasurementCounter {
self.wakeups_armed
}
#[must_use]
pub const fn work_dispatched(self) -> MeasurementCounter {
self.work_dispatched
}
#[must_use]
pub const fn scheduler_started(self) -> MeasurementCounter {
self.scheduler_started
}
#[must_use]
pub const fn work_started(self) -> MeasurementCounter {
self.work_started
}
#[must_use]
pub const fn work_completed(self) -> MeasurementCounter {
let mut completed = self.succeeded;
completed.merge(self.no_work);
completed.merge(self.retryable_failure);
completed.merge(self.invariant_failure);
completed
}
#[must_use]
pub const fn succeeded(self) -> MeasurementCounter {
self.succeeded
}
#[must_use]
pub const fn no_work(self) -> MeasurementCounter {
self.no_work
}
#[must_use]
pub const fn retryable_failure(self) -> MeasurementCounter {
self.retryable_failure
}
#[must_use]
pub const fn invariant_failure(self) -> MeasurementCounter {
self.invariant_failure
}
#[must_use]
pub const fn cancelled(self) -> MeasurementCounter {
self.cancelled
}
#[must_use]
pub const fn stale_wakeups(self) -> MeasurementCounter {
self.stale_wakeups
}
#[must_use]
pub const fn stale_work(self) -> MeasurementCounter {
self.stale_work
}
#[must_use]
pub const fn coalesced(self) -> MeasurementCounter {
self.coalesced
}
#[must_use]
pub const fn unacknowledged(self) -> MeasurementCounter {
self.unacknowledged
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MemoryPageExtent {
wasm: u64,
stable: u64,
}
impl MemoryPageExtent {
const EMPTY: Self = Self { wasm: 0, stable: 0 };
pub(crate) const fn new(wasm: u64, stable: u64) -> Self {
Self { wasm, stable }
}
#[must_use]
pub const fn wasm_pages(self) -> u64 {
self.wasm
}
#[must_use]
pub const fn stable_pages(self) -> u64 {
self.stable
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MemoryPageSample {
start: MemoryPageExtent,
end: MemoryPageExtent,
}
impl MemoryPageSample {
const EMPTY: Self = Self {
start: MemoryPageExtent::EMPTY,
end: MemoryPageExtent::EMPTY,
};
pub(crate) const fn new(start: MemoryPageExtent, end: MemoryPageExtent) -> Self {
Self { start, end }
}
#[must_use]
pub const fn start(self) -> MemoryPageExtent {
self.start
}
#[must_use]
pub const fn end(self) -> MemoryPageExtent {
self.end
}
#[must_use]
pub const fn wasm_growth_pages(self) -> u64 {
self.end.wasm.saturating_sub(self.start.wasm)
}
#[must_use]
pub const fn stable_growth_pages(self) -> u64 {
self.end.stable.saturating_sub(self.start.stable)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MemoryPageSummary {
samples: u64,
latest: MemoryPageSample,
maximum_wasm_growth: u64,
maximum_stable_growth: u64,
}
impl MemoryPageSummary {
const EMPTY: Self = Self {
samples: 0,
latest: MemoryPageSample::EMPTY,
maximum_wasm_growth: 0,
maximum_stable_growth: 0,
};
const fn record(&mut self, sample: MemoryPageSample) {
self.samples = self.samples.saturating_add(1);
self.latest = sample;
self.maximum_wasm_growth = max_u64(self.maximum_wasm_growth, sample.wasm_growth_pages());
self.maximum_stable_growth =
max_u64(self.maximum_stable_growth, sample.stable_growth_pages());
}
#[must_use]
pub const fn samples(self) -> u64 {
self.samples
}
#[must_use]
pub const fn latest(self) -> Option<MemoryPageSample> {
if self.samples == 0 {
None
} else {
Some(self.latest)
}
}
#[must_use]
pub const fn maximum_wasm_growth_pages(self) -> Option<u64> {
if self.samples == 0 {
None
} else {
Some(self.maximum_wasm_growth)
}
}
#[must_use]
pub const fn maximum_stable_growth_pages(self) -> Option<u64> {
if self.samples == 0 {
None
} else {
Some(self.maximum_stable_growth)
}
}
}
const fn max_u64(left: u64, right: u64) -> u64 {
if left > right { left } else { right }
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TimerPerformance {
scheduler_instructions: MeasurementSummary,
work_instructions: MeasurementSummary,
scheduler_memory_pages: MemoryPageSummary,
work_memory_pages: MemoryPageSummary,
}
impl TimerPerformance {
const EMPTY: Self = Self {
scheduler_instructions: MeasurementSummary::EMPTY,
work_instructions: MeasurementSummary::EMPTY,
scheduler_memory_pages: MemoryPageSummary::EMPTY,
work_memory_pages: MemoryPageSummary::EMPTY,
};
pub(crate) const fn record_scheduler(&mut self, instructions: u64, memory: MemoryPageSample) {
self.scheduler_instructions.record(instructions);
self.scheduler_memory_pages.record(memory);
}
pub(crate) const fn record_work(&mut self, instructions: u64, memory: MemoryPageSample) {
self.work_instructions.record(instructions);
self.work_memory_pages.record(memory);
}
#[must_use]
pub const fn scheduler_instructions(self) -> MeasurementSummary {
self.scheduler_instructions
}
#[must_use]
pub const fn work_instructions(self) -> MeasurementSummary {
self.work_instructions
}
#[must_use]
pub const fn scheduler_memory_pages(self) -> MemoryPageSummary {
self.scheduler_memory_pages
}
#[must_use]
pub const fn work_memory_pages(self) -> MemoryPageSummary {
self.work_memory_pages
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TimerObservabilitySnapshot {
outcomes: TimerOutcomeSnapshot,
counters: TimerCounters,
performance: TimerPerformance,
}
impl TimerObservabilitySnapshot {
pub(crate) const EMPTY: Self = Self {
outcomes: TimerOutcomeSnapshot::EMPTY,
counters: TimerCounters::EMPTY,
performance: TimerPerformance::EMPTY,
};
pub(crate) const fn record_completion(
&mut self,
completion: TimerCompletion,
completed_at_ns: u64,
) {
self.outcomes.record_completion(completion, completed_at_ns);
self.counters.record_completion(completion.outcome());
}
pub(crate) const fn record_unacknowledged(&mut self, observed_at_ns: u64) {
self.outcomes.record_unacknowledged(observed_at_ns);
self.counters.record_unacknowledged();
}
pub(crate) const fn counters_mut(&mut self) -> &mut TimerCounters {
&mut self.counters
}
pub(crate) const fn record_scheduler_measurements(
&mut self,
instructions: u64,
memory: MemoryPageSample,
) {
self.performance.record_scheduler(instructions, memory);
}
pub(crate) const fn record_work_measurements(
&mut self,
instructions: u64,
memory: MemoryPageSample,
) {
self.performance.record_work(instructions, memory);
}
#[must_use]
pub const fn outcomes(self) -> TimerOutcomeSnapshot {
self.outcomes
}
#[must_use]
pub const fn counters(self) -> TimerCounters {
self.counters
}
#[must_use]
pub const fn performance(self) -> TimerPerformance {
self.performance
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_counters_saturate() {
let exact_cap = MeasurementCounter::from_parts(u128::MAX, false).unwrap();
let mut counters = TimerCounters {
schedule_requests: exact_cap,
wakeups_armed: exact_cap,
work_dispatched: exact_cap,
scheduler_started: exact_cap,
work_started: exact_cap,
succeeded: exact_cap,
no_work: exact_cap,
retryable_failure: exact_cap,
invariant_failure: exact_cap,
cancelled: exact_cap,
stale_wakeups: exact_cap,
stale_work: exact_cap,
coalesced: exact_cap,
unacknowledged: exact_cap,
};
assert!(!counters.schedule_requests().total_overflowed());
counters.record_schedule_request();
counters.record_wakeup_armed();
counters.record_work_dispatched();
counters.record_scheduler_started();
counters.record_work_started();
for outcome in [
TimerCompletionOutcome::Success,
TimerCompletionOutcome::NoWork,
TimerCompletionOutcome::RetryableFailure,
TimerCompletionOutcome::InvariantFailure,
] {
counters.record_completion(outcome);
}
counters.record_cancellation();
counters.record_stale_wakeup();
counters.record_stale_work();
counters.record_coalesced();
counters.record_unacknowledged();
assert_eq!(counters.schedule_requests().total(), u128::MAX);
assert!(counters.schedule_requests().total_overflowed());
assert!(counters.schedule_requests().exact_total().is_err());
assert_eq!(counters.wakeups_armed().total(), u128::MAX);
assert!(counters.wakeups_armed().total_overflowed());
assert!(counters.wakeups_armed().exact_total().is_err());
assert_eq!(counters.work_dispatched().total(), u128::MAX);
assert!(counters.work_dispatched().total_overflowed());
assert!(counters.work_dispatched().exact_total().is_err());
assert_eq!(counters.scheduler_started().total(), u128::MAX);
assert!(counters.scheduler_started().total_overflowed());
assert!(counters.scheduler_started().exact_total().is_err());
assert_eq!(counters.work_started().total(), u128::MAX);
assert!(counters.work_started().total_overflowed());
assert!(counters.work_started().exact_total().is_err());
assert_eq!(counters.succeeded().total(), u128::MAX);
assert!(counters.succeeded().total_overflowed());
assert!(counters.succeeded().exact_total().is_err());
assert_eq!(counters.no_work().total(), u128::MAX);
assert!(counters.no_work().total_overflowed());
assert!(counters.no_work().exact_total().is_err());
assert_eq!(counters.retryable_failure().total(), u128::MAX);
assert!(counters.retryable_failure().total_overflowed());
assert!(counters.retryable_failure().exact_total().is_err());
assert_eq!(counters.invariant_failure().total(), u128::MAX);
assert!(counters.invariant_failure().total_overflowed());
assert!(counters.invariant_failure().exact_total().is_err());
assert_eq!(counters.cancelled().total(), u128::MAX);
assert!(counters.cancelled().total_overflowed());
assert!(counters.cancelled().exact_total().is_err());
assert_eq!(counters.stale_wakeups().total(), u128::MAX);
assert!(counters.stale_wakeups().total_overflowed());
assert!(counters.stale_wakeups().exact_total().is_err());
assert_eq!(counters.stale_work().total(), u128::MAX);
assert!(counters.stale_work().total_overflowed());
assert!(counters.stale_work().exact_total().is_err());
assert_eq!(counters.coalesced().total(), u128::MAX);
assert!(counters.coalesced().total_overflowed());
assert!(counters.coalesced().exact_total().is_err());
assert_eq!(counters.unacknowledged().total(), u128::MAX);
assert!(counters.unacknowledged().total_overflowed());
assert!(counters.unacknowledged().exact_total().is_err());
}
#[test]
fn completion_totals_follow_classifications_and_saturate_across_outcomes() {
let mut counters = TimerCounters::EMPTY;
assert_eq!(counters.work_completed().exact_total(), Ok(0));
for (outcome, total) in [
(TimerCompletionOutcome::Success, 1),
(TimerCompletionOutcome::NoWork, 2),
(TimerCompletionOutcome::RetryableFailure, 3),
(TimerCompletionOutcome::InvariantFailure, 4),
] {
counters.record_completion(outcome);
assert_eq!(counters.work_completed().exact_total(), Ok(total));
}
assert_eq!(counters.succeeded().exact_total(), Ok(1));
assert_eq!(counters.no_work().exact_total(), Ok(1));
assert_eq!(counters.retryable_failure().exact_total(), Ok(1));
assert_eq!(counters.invariant_failure().exact_total(), Ok(1));
assert_eq!(counters.work_started().exact_total(), Ok(0));
let mut counters = TimerCounters {
succeeded: MeasurementCounter::from_parts(u128::MAX - 2, false).unwrap(),
..TimerCounters::EMPTY
};
counters.record_completion(TimerCompletionOutcome::NoWork);
assert_eq!(counters.work_completed().exact_total(), Ok(u128::MAX - 1));
counters.record_completion(TimerCompletionOutcome::RetryableFailure);
assert_eq!(counters.work_completed().exact_total(), Ok(u128::MAX));
counters.record_completion(TimerCompletionOutcome::InvariantFailure);
assert!(counters.work_completed().exact_total().is_err());
counters = TimerCounters {
succeeded: MeasurementCounter::from_parts(u128::MAX, true).unwrap(),
..TimerCounters::EMPTY
};
assert!(counters.work_completed().total_overflowed());
}
#[test]
fn counter_copies_cross_the_old_cap_without_mutating_registration_state() {
let mut counters = TimerCounters {
schedule_requests: MeasurementCounter::from_parts(u128::from(u64::MAX), false).unwrap(),
..TimerCounters::EMPTY
};
let previous = counters.schedule_requests();
counters.record_schedule_request();
assert_eq!(
counters.schedule_requests().exact_total(),
Ok(u128::from(u64::MAX) + 1)
);
assert_eq!(counters.schedule_requests().delta_since(previous), Ok(1));
assert!(previous.delta_since(counters.schedule_requests()).is_err());
let mut copied = counters.schedule_requests();
copied.add(10);
assert_eq!(counters.schedule_requests().delta_since(previous), Ok(1));
}
#[test]
fn callback_measurements_are_role_specific_bounded_and_saturating() {
let mut performance = TimerPerformance::EMPTY;
performance.record_scheduler(
20,
MemoryPageSample::new(MemoryPageExtent::new(1, 2), MemoryPageExtent::new(2, 4)),
);
performance.record_work(
30,
MemoryPageSample::new(MemoryPageExtent::new(2, 4), MemoryPageExtent::new(5, 5)),
);
performance.record_work(
10,
MemoryPageSample::new(MemoryPageExtent::new(5, 5), MemoryPageExtent::new(6, 9)),
);
assert_eq!(performance.scheduler_instructions().aggregate().total(), 20);
assert_eq!(performance.work_instructions().aggregate().samples(), 2);
assert_eq!(performance.work_instructions().aggregate().total(), 40);
assert_eq!(performance.work_instructions().latest(), Some(10));
assert_eq!(
performance.work_instructions().aggregate().maximum(),
Some(30)
);
let scheduler_memory = performance.scheduler_memory_pages();
assert_eq!(
scheduler_memory.samples(),
performance.scheduler_instructions().aggregate().samples()
);
assert_eq!(scheduler_memory.samples(), 1);
let scheduler_latest = scheduler_memory
.latest()
.expect("scheduler sample should exist");
assert_eq!(scheduler_latest.start().wasm_pages(), 1);
assert_eq!(scheduler_latest.start().stable_pages(), 2);
assert_eq!(scheduler_latest.end().wasm_pages(), 2);
assert_eq!(scheduler_latest.end().stable_pages(), 4);
assert_eq!(scheduler_latest.wasm_growth_pages(), 1);
assert_eq!(scheduler_latest.stable_growth_pages(), 2);
let work_memory = performance.work_memory_pages();
assert_eq!(
work_memory.samples(),
performance.work_instructions().aggregate().samples()
);
assert_eq!(work_memory.samples(), 2);
let work_latest = work_memory.latest().expect("work sample should exist");
assert_eq!(work_latest.wasm_growth_pages(), 1);
assert_eq!(work_latest.stable_growth_pages(), 4);
assert_eq!(work_memory.maximum_wasm_growth_pages(), Some(3));
assert_eq!(work_memory.maximum_stable_growth_pages(), Some(4));
}
#[test]
fn instruction_summaries_keep_empty_zero_and_role_boundaries() {
let mut performance = TimerPerformance::EMPTY;
let mut expected = MeasurementSummary::EMPTY;
assert_eq!(performance.scheduler_instructions(), expected);
assert_eq!(performance.work_instructions(), expected);
for instructions in [0, u64::MAX, 1, u64::MAX] {
let previous_work = performance.work_instructions();
expected.record(instructions);
performance.record_scheduler(instructions, MemoryPageSample::EMPTY);
assert_eq!(performance.scheduler_instructions(), expected);
assert_eq!(performance.work_instructions(), previous_work);
performance.record_work(instructions, MemoryPageSample::EMPTY);
assert_eq!(performance.work_instructions(), expected);
assert_eq!(performance.scheduler_instructions(), expected);
}
}
#[test]
fn memory_sample_count_saturates_while_latest_and_maximum_continue() {
let mut summary = MemoryPageSummary {
samples: u64::MAX,
latest: MemoryPageSample::EMPTY,
maximum_wasm_growth: 1,
maximum_stable_growth: 1,
};
let sample =
MemoryPageSample::new(MemoryPageExtent::new(10, 20), MemoryPageExtent::new(13, 25));
summary.record(sample);
assert_eq!(summary.samples(), u64::MAX);
assert_eq!(summary.latest(), Some(sample));
assert_eq!(summary.maximum_wasm_growth_pages(), Some(3));
assert_eq!(summary.maximum_stable_growth_pages(), Some(5));
}
}