use std::cell::Cell;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Cause {
Flatten,
Compare,
Scalar,
Logic,
Cast,
Aggregate,
Select,
}
const KINDS: usize = 7;
impl Cause {
pub const ALL: [Self; KINDS] = [
Self::Flatten,
Self::Compare,
Self::Scalar,
Self::Logic,
Self::Cast,
Self::Aggregate,
Self::Select,
];
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Flatten => "flatten",
Self::Compare => "compare",
Self::Scalar => "scalar",
Self::Logic => "logic",
Self::Cast => "cast",
Self::Aggregate => "aggregate",
Self::Select => "select",
}
}
#[must_use]
pub const fn slot(self) -> usize {
match self {
Self::Flatten => 0,
Self::Compare => 1,
Self::Scalar => 2,
Self::Logic => 3,
Self::Cast => 4,
Self::Aggregate => 5,
Self::Select => 6,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Tally {
counts: [u64; KINDS],
}
impl Tally {
#[must_use]
pub const fn none() -> Self {
Self { counts: [0; KINDS] }
}
#[must_use]
pub const fn get(&self, cause: Cause) -> u64 {
self.counts[cause.slot()]
}
#[must_use]
pub fn total(&self) -> u64 {
self.counts.iter().fold(0, |sum, count| sum.saturating_add(*count))
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.total() == 0
}
pub fn taken(&self) -> impl Iterator<Item = (Cause, u64)> + '_ {
Cause::ALL.into_iter().map(|cause| (cause, self.get(cause))).filter(|(_, seen)| *seen > 0)
}
#[must_use]
pub fn worst(&self) -> Option<(Cause, u64)> {
self.taken().max_by_key(|(cause, seen)| (*seen, std::cmp::Reverse(cause.slot())))
}
#[must_use]
pub fn since(&self, before: Self) -> Self {
let mut counts = [0; KINDS];
for (slot, (now, then)) in
counts.iter_mut().zip(self.counts.iter().zip(before.counts.iter()))
{
*slot = now.saturating_sub(*then);
}
Self { counts }
}
pub fn add(&mut self, other: Self) {
for (slot, more) in self.counts.iter_mut().zip(other.counts.iter()) {
*slot = slot.saturating_add(*more);
}
}
#[must_use]
pub fn of(cause: Cause, times: u64) -> Self {
let mut tally = Self::none();
tally.counts[cause.slot()] = times;
tally
}
}
thread_local! {
static TAKEN: Cell<Tally> = const { Cell::new(Tally::none()) };
}
pub fn took(cause: Cause) {
took_many(cause, 1);
}
pub fn took_many(cause: Cause, times: u64) {
TAKEN.with(|taken| {
let mut tally = taken.get();
tally.add(Tally::of(cause, times));
taken.set(tally);
});
}
#[must_use]
pub fn here() -> Tally {
TAKEN.with(Cell::get)
}
pub fn reset() {
TAKEN.with(|taken| taken.set(Tally::none()));
}
#[cfg(test)]
mod tests {
use super::{Cause, Tally, here, reset, took, took_many};
#[test]
fn a_fall_back_lands_against_its_own_cause_and_leaves_the_rest_alone() {
reset();
took(Cause::Flatten);
took(Cause::Flatten);
took(Cause::Compare);
let tally = here();
assert_eq!(tally.get(Cause::Flatten), 2);
assert_eq!(tally.get(Cause::Compare), 1);
assert_eq!(tally.get(Cause::Cast), 0);
assert_eq!(tally.total(), 3);
reset();
}
#[test]
fn a_difference_is_what_happened_between_the_two_readings_and_nothing_before_them() {
reset();
took_many(Cause::Cast, 5);
let before = here();
took(Cause::Select);
took(Cause::Select);
let during = here().since(before);
assert_eq!(during.get(Cause::Select), 2);
assert_eq!(during.get(Cause::Cast), 0, "what happened before the reading is not in it");
assert_eq!(during.total(), 2);
reset();
}
#[test]
fn a_difference_taken_backwards_reports_nothing_rather_than_an_enormous_number() {
let later = Tally::of(Cause::Logic, 3);
assert!(Tally::none().since(later).is_empty());
}
#[test]
fn the_worst_cause_is_the_one_to_go_and_write_a_specialisation_for() {
let mut tally = Tally::of(Cause::Flatten, 2);
tally.add(Tally::of(Cause::Scalar, 90));
tally.add(Tally::of(Cause::Logic, 11));
assert_eq!(tally.worst(), Some((Cause::Scalar, 90)));
assert_eq!(tally.taken().count(), 3);
assert_eq!(Tally::none().worst(), None);
}
#[test]
fn the_causes_are_listed_in_one_order_however_big_the_numbers_are() {
let mut tally = Tally::of(Cause::Select, 1);
tally.add(Tally::of(Cause::Flatten, 1000));
let listed: Vec<&str> = tally.taken().map(|(cause, _)| cause.name()).collect();
assert_eq!(listed, ["flatten", "select"]);
}
#[test]
fn one_thread_counting_is_invisible_to_another() {
reset();
took_many(Cause::Aggregate, 4);
let elsewhere = std::thread::spawn(|| {
took(Cause::Aggregate);
here()
})
.join()
.expect("no counting thread panics");
assert_eq!(elsewhere.get(Cause::Aggregate), 1, "the other thread starts from nothing");
assert_eq!(here().get(Cause::Aggregate), 4, "and does not add to this one");
reset();
}
#[test]
fn every_cause_has_its_own_slot_and_its_own_name() {
let mut seen: Vec<&str> = Cause::ALL.iter().map(|cause| cause.name()).collect();
seen.sort_unstable();
seen.dedup();
assert_eq!(seen.len(), Cause::ALL.len());
for cause in Cause::ALL {
assert_eq!(Tally::of(cause, 7).total(), 7);
assert_eq!(Tally::of(cause, 7).get(cause), 7);
}
}
}