use core::fmt;
use crate::digest::StateHash;
use crate::schedule::FramePlan;
use crate::time::Nanos;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FrameStats {
frames: u64,
ticks: u64,
steps_dropped: u64,
hash: StateHash,
}
impl FrameStats {
#[must_use]
pub const fn new() -> Self {
Self {
frames: 0,
ticks: 0,
steps_dropped: 0,
hash: StateHash::new(),
}
}
pub fn absorb(&mut self, plan: &FramePlan) {
self.frames = self.frames.saturating_add(1);
self.ticks = self.ticks.saturating_add(u64::from(plan.step_count()));
self.steps_dropped = self.steps_dropped.saturating_add(plan.dropped());
self.hash = self.hash.absorb_plan(plan);
}
#[must_use]
pub const fn frames(&self) -> u64 {
self.frames
}
#[must_use]
pub const fn ticks(&self) -> u64 {
self.ticks
}
#[must_use]
pub const fn steps_dropped(&self) -> u64 {
self.steps_dropped
}
#[must_use]
pub const fn schedule_hash(&self) -> u64 {
self.hash.finish()
}
#[must_use]
pub const fn json(&self) -> FrameStatsJson<'_> {
FrameStatsJson(self)
}
}
impl Default for FrameStats {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, Debug)]
pub struct FrameStatsJson<'a>(&'a FrameStats);
impl fmt::Display for FrameStatsJson<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{{\"frames\":{},\"ticks\":{},\"steps_dropped\":{},\"schedule_hash\":\"{:#018x}\"}}",
self.0.frames,
self.0.ticks,
self.0.steps_dropped,
self.0.schedule_hash()
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FrameTiming {
count: u64,
min: u64,
max: u64,
sum: u64,
drawn: u64,
skipped: u64,
}
impl FrameTiming {
#[must_use]
pub const fn new() -> Self {
Self {
count: 0,
min: u64::MAX,
max: 0,
sum: 0,
drawn: 0,
skipped: 0,
}
}
pub fn record(&mut self, cpu_frame: Nanos, drawn: bool) {
let nanos = cpu_frame.get();
self.count = self.count.saturating_add(1);
self.min = self.min.min(nanos);
self.max = self.max.max(nanos);
self.sum = self.sum.saturating_add(nanos);
if drawn {
self.drawn = self.drawn.saturating_add(1);
} else {
self.skipped = self.skipped.saturating_add(1);
}
}
#[must_use]
pub const fn json(&self) -> FrameTimingJson<'_> {
FrameTimingJson(self)
}
const fn reported_min(&self) -> u64 {
if self.count == 0 { 0 } else { self.min }
}
}
impl Default for FrameTiming {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, Debug)]
pub struct FrameTimingJson<'a>(&'a FrameTiming);
impl fmt::Display for FrameTimingJson<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{{\"count\":{},\"min_ns\":{},\"max_ns\":{},\"sum_ns\":{},\"drawn\":{},\"skipped\":{}}}",
self.0.count,
self.0.reported_min(),
self.0.max,
self.0.sum,
self.0.drawn,
self.0.skipped
)
}
}
#[cfg(test)]
mod tests {
use super::{FrameStats, FrameTiming};
use crate::digest::StateHash;
use crate::schedule::FrameLoop;
use crate::time::{Nanos, StepBudget, Timestamp, Timestep};
fn stalling_loop() -> FrameLoop {
FrameLoop::new(
Timestep::HZ_60,
StepBudget::DEFAULT,
Timestamp::from_nanos(0),
)
}
#[test]
fn a_fresh_tally_is_empty_and_holds_the_untouched_digest() {
let stats = FrameStats::new();
assert_eq!(stats, FrameStats::default());
assert_eq!(stats.frames(), 0);
assert_eq!(stats.ticks(), 0);
assert_eq!(stats.steps_dropped(), 0);
assert_eq!(stats.schedule_hash(), StateHash::new().finish());
assert_eq!(
stats.json().to_string(),
"{\"frames\":0,\"ticks\":0,\"steps_dropped\":0,\"schedule_hash\":\"0xcbf29ce484222325\"}"
);
}
#[test]
fn absorbing_frames_tallies_steps_and_drops_and_moves_the_digest() {
let mut frame = stalling_loop();
let mut stats = FrameStats::new();
for now in [16_666_667_u64, 33_333_334, 233_333_334] {
stats.absorb(&frame.begin_frame(Timestamp::from_nanos(now)));
}
assert_eq!(stats.frames(), 3);
assert_eq!(stats.ticks(), 7, "one, one, then the budgeted five");
assert!(stats.steps_dropped() > 0, "the stall was refused");
assert_ne!(stats.schedule_hash(), StateHash::new().finish());
let json = stats.json().to_string();
let prefix = "{\"frames\":3,\"ticks\":7,\"steps_dropped\":6,\"schedule_hash\":\"0x";
assert!(json.starts_with(prefix), "{json}");
assert!(json.ends_with("\"}"), "{json}");
assert_eq!(
json.len(),
prefix.len() + 16 + 2,
"sixteen hex digits: {json}"
);
}
#[test]
fn two_runs_of_one_schedule_produce_identical_statistics() {
let run = || {
let mut frame = stalling_loop();
let mut stats = FrameStats::new();
for k in 1..=32u64 {
stats.absorb(&frame.begin_frame(Timestamp::from_nanos(k * 12_000_000)));
}
stats
};
assert_eq!(run(), run());
}
#[test]
fn a_fresh_timing_summary_reports_a_zero_minimum_rather_than_the_sentinel() {
let timing = FrameTiming::new();
assert_eq!(timing, FrameTiming::default());
assert_eq!(
timing.json().to_string(),
"{\"count\":0,\"min_ns\":0,\"max_ns\":0,\"sum_ns\":0,\"drawn\":0,\"skipped\":0}"
);
}
#[test]
fn recording_frames_tracks_the_extremes_the_total_and_the_presented_split() {
let mut timing = FrameTiming::new();
timing.record(Nanos::from_nanos(4_000_000), true);
timing.record(Nanos::from_nanos(1_000_000), true);
timing.record(Nanos::from_nanos(9_000_000), false);
assert_eq!(
timing.json().to_string(),
"{\"count\":3,\"min_ns\":1000000,\"max_ns\":9000000,\"sum_ns\":14000000,\
\"drawn\":2,\"skipped\":1}"
);
}
#[test]
fn the_measured_total_saturates_rather_than_wrapping() {
let mut timing = FrameTiming::new();
timing.record(Nanos::from_nanos(u64::MAX), true);
timing.record(Nanos::from_nanos(u64::MAX), true);
let json = timing.json().to_string();
assert!(json.contains(&format!("\"sum_ns\":{}", u64::MAX)), "{json}");
assert!(json.contains(&format!("\"min_ns\":{}", u64::MAX)), "{json}");
}
}