use std::cell::Cell;
use std::time::Instant;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Stage {
Read,
Decompress,
Decode,
Dictionary,
Assemble,
Fold,
Scatter,
Merge,
Emit,
}
const STAGES: usize = 9;
impl Stage {
pub const ALL: [Self; STAGES] = [
Self::Read,
Self::Decompress,
Self::Decode,
Self::Dictionary,
Self::Assemble,
Self::Fold,
Self::Scatter,
Self::Merge,
Self::Emit,
];
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Read => "read",
Self::Decompress => "decompress",
Self::Decode => "decode",
Self::Dictionary => "dictionary",
Self::Assemble => "assemble",
Self::Fold => "fold",
Self::Scatter => "scatter",
Self::Merge => "merge",
Self::Emit => "emit",
}
}
#[must_use]
pub const fn slot(self) -> usize {
match self {
Self::Read => 0,
Self::Decompress => 1,
Self::Decode => 2,
Self::Dictionary => 3,
Self::Assemble => 4,
Self::Fold => 5,
Self::Scatter => 6,
Self::Merge => 7,
Self::Emit => 8,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Spent {
nanos: [u64; STAGES],
bytes: [u64; STAGES],
}
impl Spent {
#[must_use]
pub const fn none() -> Self {
Self { nanos: [0; STAGES], bytes: [0; STAGES] }
}
#[must_use]
pub const fn nanos(&self, stage: Stage) -> u64 {
self.nanos[stage.slot()]
}
#[must_use]
pub const fn bytes(&self, stage: Stage) -> u64 {
self.bytes[stage.slot()]
}
#[must_use]
pub fn total(&self) -> u64 {
self.nanos.iter().fold(0, |sum, nanos| sum.saturating_add(*nanos))
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.nanos.iter().all(|nanos| *nanos == 0) && self.bytes.iter().all(|bytes| *bytes == 0)
}
pub fn taken(&self) -> impl Iterator<Item = (Stage, u64, u64)> + '_ {
Stage::ALL
.into_iter()
.map(|stage| (stage, self.nanos(stage), self.bytes(stage)))
.filter(|(_, nanos, bytes)| *nanos > 0 || *bytes > 0)
}
#[must_use]
pub fn worst(&self) -> Option<(Stage, u64)> {
self.taken()
.map(|(stage, nanos, _)| (stage, nanos))
.filter(|(_, nanos)| *nanos > 0)
.max_by_key(|(stage, nanos)| (*nanos, std::cmp::Reverse(stage.slot())))
}
#[must_use]
pub fn since(&self, before: Self) -> Self {
let mut out = Self::none();
for slot in 0..STAGES {
out.nanos[slot] = self.nanos[slot].saturating_sub(before.nanos[slot]);
out.bytes[slot] = self.bytes[slot].saturating_sub(before.bytes[slot]);
}
out
}
pub fn add(&mut self, other: Self) {
for slot in 0..STAGES {
self.nanos[slot] = self.nanos[slot].saturating_add(other.nanos[slot]);
self.bytes[slot] = self.bytes[slot].saturating_add(other.bytes[slot]);
}
}
#[must_use]
pub fn of(stage: Stage, nanos: u64, bytes: u64) -> Self {
let mut spent = Self::none();
spent.nanos[stage.slot()] = nanos;
spent.bytes[stage.slot()] = bytes;
spent
}
}
thread_local! {
static SPENT: Cell<Spent> = const { Cell::new(Spent::none()) };
}
pub fn took(stage: Stage, nanos: u64, bytes: u64) {
SPENT.with(|spent| {
let mut now = spent.get();
now.add(Spent::of(stage, nanos, bytes));
spent.set(now);
});
}
pub fn gained(spent: Spent) {
SPENT.with(|slot| {
let mut now = slot.get();
now.add(spent);
slot.set(now);
});
}
#[must_use]
pub fn here() -> Spent {
SPENT.with(Cell::get)
}
pub fn reset() {
SPENT.with(|spent| spent.set(Spent::none()));
}
#[derive(Debug)]
pub struct Timing {
stage: Stage,
at: Instant,
}
impl Timing {
#[must_use]
pub fn start(stage: Stage) -> Self {
Self { stage, at: Instant::now() }
}
pub fn stop(self, bytes: u64) {
let nanos = u64::try_from(self.at.elapsed().as_nanos()).unwrap_or(u64::MAX);
took(self.stage, nanos, bytes);
}
}
#[cfg(test)]
mod tests {
use super::{Spent, Stage, here, reset, took};
#[test]
fn time_lands_against_its_own_stage_and_leaves_the_rest_alone() {
reset();
took(Stage::Read, 100, 4096);
took(Stage::Read, 50, 1024);
took(Stage::Decompress, 700, 8192);
let spent = here();
assert_eq!(spent.nanos(Stage::Read), 150);
assert_eq!(spent.bytes(Stage::Read), 5120);
assert_eq!(spent.nanos(Stage::Decompress), 700);
assert_eq!(spent.nanos(Stage::Decode), 0);
assert_eq!(spent.total(), 850);
reset();
}
#[test]
fn a_difference_is_what_happened_between_the_two_readings_and_nothing_before_them() {
reset();
took(Stage::Decode, 900, 16);
let before = here();
took(Stage::Assemble, 12, 0);
let during = here().since(before);
assert_eq!(during.nanos(Stage::Assemble), 12);
assert_eq!(during.nanos(Stage::Decode), 0, "what happened before the reading is not in it");
assert_eq!(during.total(), 12);
reset();
}
#[test]
fn a_difference_taken_backwards_reports_nothing_rather_than_most_of_a_century() {
let later = Spent::of(Stage::Read, 900, 900);
assert!(Spent::none().since(later).is_empty());
}
#[test]
fn the_worst_stage_is_the_one_worth_working_on() {
let mut spent = Spent::of(Stage::Read, 40, 0);
spent.add(Spent::of(Stage::Decompress, 4000, 0));
spent.add(Spent::of(Stage::Decode, 900, 0));
assert_eq!(spent.worst(), Some((Stage::Decompress, 4000)));
assert_eq!(spent.taken().count(), 3);
assert_eq!(Spent::none().worst(), None);
}
#[test]
fn a_stage_that_only_moved_bytes_is_listed_and_is_not_the_worst() {
let mut spent = Spent::of(Stage::Read, 0, 8192);
spent.add(Spent::of(Stage::Decode, 5, 0));
let listed: Vec<&str> = spent.taken().map(|(stage, _, _)| stage.name()).collect();
assert_eq!(listed, ["read", "decode"]);
assert_eq!(spent.worst(), Some((Stage::Decode, 5)));
}
#[test]
fn one_thread_timing_is_invisible_to_another() {
reset();
took(Stage::Dictionary, 44, 0);
let elsewhere = std::thread::spawn(|| {
took(Stage::Dictionary, 1, 0);
here()
})
.join()
.expect("no timing thread panics");
assert_eq!(elsewhere.nanos(Stage::Dictionary), 1, "the other thread starts from nothing");
assert_eq!(here().nanos(Stage::Dictionary), 44, "and does not add to this one");
reset();
}
#[test]
fn every_stage_has_its_own_slot_and_its_own_name() {
let mut seen: Vec<&str> = Stage::ALL.iter().map(|stage| stage.name()).collect();
seen.sort_unstable();
seen.dedup();
assert_eq!(seen.len(), Stage::ALL.len());
for stage in Stage::ALL {
assert_eq!(Spent::of(stage, 7, 3).total(), 7);
assert_eq!(Spent::of(stage, 7, 3).bytes(stage), 3);
}
}
}