use std::cell::Cell;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum TickStage {
Poll,
Fingerprint,
Compare,
Schedule,
Decide,
Act,
}
impl TickStage {
pub const ALL: [Self; 6] = [
Self::Poll,
Self::Fingerprint,
Self::Compare,
Self::Schedule,
Self::Decide,
Self::Act,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Poll => "poll",
Self::Fingerprint => "fingerprint",
Self::Compare => "compare",
Self::Schedule => "schedule",
Self::Decide => "decide",
Self::Act => "act",
}
}
}
impl core::fmt::Display for TickStage {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct TickProfile {
poll: Duration,
fingerprint: Duration,
compare: Duration,
schedule: Duration,
decide: Duration,
act: Duration,
ticks: u64,
}
impl TickProfile {
#[must_use]
pub const fn new() -> Self {
Self {
poll: Duration::ZERO,
fingerprint: Duration::ZERO,
compare: Duration::ZERO,
schedule: Duration::ZERO,
decide: Duration::ZERO,
act: Duration::ZERO,
ticks: 0,
}
}
#[must_use]
pub const fn ticks(&self) -> u64 {
self.ticks
}
#[must_use]
pub const fn stage(&self, stage: TickStage) -> Duration {
match stage {
TickStage::Poll => self.poll,
TickStage::Fingerprint => self.fingerprint,
TickStage::Compare => self.compare,
TickStage::Schedule => self.schedule,
TickStage::Decide => self.decide,
TickStage::Act => self.act,
}
}
#[must_use]
pub fn per_tick(&self, stage: TickStage) -> Option<Duration> {
let mean = self
.stage(stage)
.as_nanos()
.checked_div(u128::from(self.ticks))?;
Some(Duration::from_nanos_u128(mean))
}
#[must_use]
pub const fn total(&self) -> Duration {
Duration::new(0, 0)
.saturating_add(self.poll)
.saturating_add(self.fingerprint)
.saturating_add(self.compare)
.saturating_add(self.schedule)
.saturating_add(self.decide)
.saturating_add(self.act)
}
#[must_use]
pub fn share(&self, stage: TickStage) -> f64 {
let total = self.total().as_secs_f64();
if total <= 0.0 {
return 0.0;
}
self.stage(stage).as_secs_f64() / total
}
pub fn absorb(&mut self, other: Self) {
self.poll = self.poll.saturating_add(other.poll);
self.fingerprint = self.fingerprint.saturating_add(other.fingerprint);
self.compare = self.compare.saturating_add(other.compare);
self.schedule = self.schedule.saturating_add(other.schedule);
self.decide = self.decide.saturating_add(other.decide);
self.act = self.act.saturating_add(other.act);
self.ticks = self.ticks.saturating_add(other.ticks);
}
}
thread_local! {
static LIVE: Cell<bool> = const { Cell::new(false) };
static LAST: Cell<Option<Instant>> = const { Cell::new(None) };
static POLL: Cell<Duration> = const { Cell::new(Duration::ZERO) };
static FINGERPRINT: Cell<Duration> = const { Cell::new(Duration::ZERO) };
static COMPARE: Cell<Duration> = const { Cell::new(Duration::ZERO) };
static SCHEDULE: Cell<Duration> = const { Cell::new(Duration::ZERO) };
static DECIDE: Cell<Duration> = const { Cell::new(Duration::ZERO) };
static ACT: Cell<Duration> = const { Cell::new(Duration::ZERO) };
static TICKS: Cell<u64> = const { Cell::new(0) };
}
fn charge(stage: TickStage) {
let now = Instant::now();
let since = LAST
.get()
.map_or(Duration::ZERO, |last| now.duration_since(last));
LAST.set(Some(now));
match stage {
TickStage::Poll => POLL.set(POLL.get().saturating_add(since)),
TickStage::Fingerprint => FINGERPRINT.set(FINGERPRINT.get().saturating_add(since)),
TickStage::Compare => COMPARE.set(COMPARE.get().saturating_add(since)),
TickStage::Schedule => SCHEDULE.set(SCHEDULE.get().saturating_add(since)),
TickStage::Decide => DECIDE.set(DECIDE.get().saturating_add(since)),
TickStage::Act => ACT.set(ACT.get().saturating_add(since)),
}
}
pub(crate) struct Charge(TickStage);
impl Charge {
pub(crate) const fn new(stage: TickStage) -> Self {
Self(stage)
}
}
impl Drop for Charge {
fn drop(&mut self) {
if LIVE.get() {
charge(self.0);
}
}
}
pub(crate) fn begin() {
LAST.set(Some(Instant::now()));
POLL.set(Duration::ZERO);
FINGERPRINT.set(Duration::ZERO);
COMPARE.set(Duration::ZERO);
SCHEDULE.set(Duration::ZERO);
DECIDE.set(Duration::ZERO);
ACT.set(Duration::ZERO);
TICKS.set(0);
LIVE.set(true);
}
pub(crate) fn end() -> TickProfile {
LAST.set(None);
TICKS.set(TICKS.get().saturating_add(1));
LIVE.set(false);
TickProfile {
poll: POLL.get(),
fingerprint: FINGERPRINT.get(),
compare: COMPARE.get(),
schedule: SCHEDULE.get(),
decide: DECIDE.get(),
act: ACT.get(),
ticks: TICKS.get(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_profile_with_no_ticks_has_no_mean() {
let empty = TickProfile::new();
assert_eq!(empty.ticks(), 0, "a fresh profile has measured nothing");
for stage in TickStage::ALL {
assert_eq!(
empty.per_tick(stage),
None,
"the mean of no ticks is not a number: {}",
stage
);
assert_eq!(
empty.share(stage),
0.0,
"an unmeasured stage holds no share of nothing: {}",
stage
);
}
}
#[test]
fn the_stages_partition_the_measured_ticks() {
let profile = TickProfile {
poll: Duration::from_nanos(10),
fingerprint: Duration::from_nanos(20),
compare: Duration::from_nanos(30),
schedule: Duration::from_nanos(40),
decide: Duration::from_nanos(50),
act: Duration::from_nanos(50),
ticks: 2,
};
assert_eq!(
profile.total(),
Duration::from_nanos(200),
"the six stages are the whole of a tick, so they add up"
);
assert_eq!(
profile.total(),
TickStage::ALL
.iter()
.map(|stage| profile.stage(*stage))
.sum::<Duration>(),
"every stage is reachable through the enum a caller matches on"
);
let shares: f64 = TickStage::ALL
.iter()
.map(|stage| profile.share(*stage))
.sum();
assert!(
(shares - 1.0).abs() < 1e-12,
"the shares must sum to one or the breakdown double-counts: {shares}"
);
assert_eq!(
profile.per_tick(TickStage::Poll),
Some(Duration::from_nanos(5)),
"20 ns over two ticks is 5 ns per tick"
);
}
#[test]
fn a_count_past_a_u32_divides_exactly() {
let ticks = u64::from(u32::MAX).saturating_add(1);
let profile = TickProfile {
poll: Duration::from_nanos(ticks.saturating_mul(3)),
ticks,
..TickProfile::new()
};
assert_eq!(
profile.per_tick(TickStage::Poll),
Some(Duration::from_nanos(3)),
"a narrowed divisor would report 3 ns/tick as more than 3"
);
}
#[test]
fn an_absorbed_profile_adds_both_the_figures_and_the_count() {
let mut first = TickProfile {
poll: Duration::from_nanos(10),
ticks: 1,
..TickProfile::new()
};
first.absorb(TickProfile {
poll: Duration::from_nanos(30),
ticks: 3,
..TickProfile::new()
});
assert_eq!(
first.ticks(),
4,
"the count is what a per-tick figure divides"
);
assert_eq!(
first.per_tick(TickStage::Poll),
Some(Duration::from_nanos(10)),
"40 ns over four ticks is 10 ns per tick, not 40"
);
}
#[test]
fn every_stage_has_its_own_stable_name() {
let names: Vec<&str> = TickStage::ALL.iter().map(|stage| stage.as_str()).collect();
assert_eq!(
names,
vec![
"poll",
"fingerprint",
"compare",
"schedule",
"decide",
"act",
],
"the report is keyed on these spellings, so they are part of the surface"
);
for stage in TickStage::ALL {
assert_eq!(
stage.to_string(),
stage.as_str(),
"Display and the stable name are one spelling, not two"
);
}
}
}