116 lines
4.1 KiB
Rust
116 lines
4.1 KiB
Rust
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
//! Measurement of the simulation loop's own timing.
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
/// Wall-clock cadence at which a measurement window closes and a report is produced.
|
|
///
|
|
/// One second is short enough to surface a stall promptly and long enough that the report costs nothing next to the ticks it summarises.
|
|
pub const REPORT_INTERVAL: Duration = Duration::from_secs(1);
|
|
|
|
/// The summary produced when a measurement window closes.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub struct TickWindow {
|
|
/// Ticks completed in the window, expressed per second.
|
|
pub measured_tps: f32,
|
|
/// Mean duration of a tick body in the window, in milliseconds.
|
|
pub mean_tick_ms: f32,
|
|
/// Longest tick body in the window, in milliseconds.
|
|
pub max_tick_ms: f32,
|
|
/// Share of the nominal tick period consumed by the mean tick body, in percent.
|
|
pub tick_budget_percent: f32,
|
|
}
|
|
|
|
/// Accumulates tick timings and closes a measurement window on a fixed cadence.
|
|
#[derive(Debug)]
|
|
pub struct TickMeter {
|
|
/// Nominal period one tick is budgeted, against which utilisation is computed.
|
|
period: Duration,
|
|
/// Instant the current window opened; the window closes once [`REPORT_INTERVAL`] has elapsed from here.
|
|
window_start: Instant,
|
|
/// Tick bodies recorded in the current window.
|
|
ticks: u32,
|
|
/// Summed duration of every tick body in the current window.
|
|
total: Duration,
|
|
/// Longest single tick body in the current window.
|
|
max: Duration,
|
|
}
|
|
|
|
impl TickMeter {
|
|
/// Opens the first measurement window at `now`, budgeting each tick `period`.
|
|
#[must_use]
|
|
pub fn new(now: Instant, period: Duration) -> Self {
|
|
Self {
|
|
period,
|
|
window_start: now,
|
|
ticks: 0,
|
|
total: Duration::ZERO,
|
|
max: Duration::ZERO,
|
|
}
|
|
}
|
|
|
|
/// Records one completed tick body of duration `elapsed`.
|
|
pub fn record(&mut self, elapsed: Duration) {
|
|
self.ticks = self.ticks.saturating_add(1);
|
|
self.total = self.total.saturating_add(elapsed);
|
|
self.max = self.max.max(elapsed);
|
|
}
|
|
|
|
/// Closes the window and returns its summary once [`REPORT_INTERVAL`] has elapsed since it opened, otherwise returns [`None`].
|
|
///
|
|
/// On close the accumulators reset and a fresh window opens at `now`, so windows tile the timeline without gaps or overlap.
|
|
pub fn take_window(&mut self, now: Instant) -> Option<TickWindow> {
|
|
let elapsed = now.saturating_duration_since(self.window_start);
|
|
if elapsed < REPORT_INTERVAL {
|
|
return None;
|
|
}
|
|
|
|
let window = summarise(self.ticks, self.total, self.max, elapsed, self.period);
|
|
self.window_start = now;
|
|
self.ticks = 0;
|
|
self.total = Duration::ZERO;
|
|
self.max = Duration::ZERO;
|
|
Some(window)
|
|
}
|
|
}
|
|
|
|
/// Derives a window summary from its raw accumulators.
|
|
///
|
|
/// Split out from [`TickMeter::take_window`] so the arithmetic is exercisable without driving a clock. A window containing no ticks reports zeroes throughout rather than dividing by zero, which is the correct reading of "nothing completed".
|
|
fn summarise(
|
|
ticks: u32,
|
|
total: Duration,
|
|
max: Duration,
|
|
elapsed: Duration,
|
|
period: Duration,
|
|
) -> TickWindow {
|
|
if ticks == 0 || elapsed.is_zero() {
|
|
return TickWindow {
|
|
measured_tps: 0.0,
|
|
mean_tick_ms: 0.0,
|
|
max_tick_ms: max.as_secs_f32() * 1000.0,
|
|
tick_budget_percent: 0.0,
|
|
};
|
|
}
|
|
|
|
let mean = total.as_secs_f32() / f32::from(u16::try_from(ticks).unwrap_or(u16::MAX));
|
|
let period_secs = period.as_secs_f32();
|
|
|
|
TickWindow {
|
|
measured_tps: f32::from(u16::try_from(ticks).unwrap_or(u16::MAX)) / elapsed.as_secs_f32(),
|
|
mean_tick_ms: mean * 1000.0,
|
|
max_tick_ms: max.as_secs_f32() * 1000.0,
|
|
// A zero period would mean no budget exists to consume, so utilisation is undefined and reported as zero.
|
|
tick_budget_percent: if period_secs > 0.0 {
|
|
mean / period_secs * 100.0
|
|
} else {
|
|
0.0
|
|
},
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "tests/tick_stats.rs"]
|
|
mod tests;
|