use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, AtomicUsize, Ordering};
use std::time::Duration;
use crossbeam_queue::SegQueue;
static ON: AtomicU8 = AtomicU8::new(0);
#[must_use]
pub fn on() -> bool {
match ON.load(Ordering::Relaxed) {
1 => false,
2 => true,
_ => {
let on = std::env::var_os("TREX_TRACE").is_some();
ON.store(if on { 2 } else { 1 }, Ordering::Relaxed);
on
}
}
}
static RECORDING: AtomicBool = AtomicBool::new(false);
static HELD: AtomicUsize = AtomicUsize::new(0);
pub struct Recording {
_private: (),
}
impl Recording {
#[must_use]
pub fn start() -> Self {
HELD.fetch_add(1, Ordering::SeqCst);
Recording { _private: () }
}
}
impl Drop for Recording {
fn drop(&mut self) {
HELD.fetch_sub(1, Ordering::SeqCst);
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Rung {
pub ladder: String,
pub rung: String,
pub bytes: usize,
}
static RECORDED: SegQueue<Rung> = SegQueue::new();
static ANSWERED: SegQueue<Rung> = SegQueue::new();
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RungTotal {
pub ladder: String,
pub rung: String,
pub calls: u64,
pub bytes: u64,
}
static LEXED_TOKENS: AtomicU64 = AtomicU64::new(0);
static LEXED_NANOS: AtomicU64 = AtomicU64::new(0);
#[must_use]
pub fn take_totals() -> Vec<RungTotal> {
let mut out: Vec<RungTotal> = Vec::new();
while let Some(kept) = ANSWERED.pop() {
let bytes = kept.bytes as u64;
match out.iter_mut().find(|t| t.ladder == kept.ladder && t.rung == kept.rung) {
Some(t) => {
t.calls += 1;
t.bytes += bytes;
}
None => out.push(RungTotal { ladder: kept.ladder, rung: kept.rung, calls: 1, bytes }),
}
}
out
}
pub fn lexed(tokens: usize, elapsed: Duration) {
if !keeping() {
return;
}
let tokens = u64::try_from(tokens).expect("a token count within the counter's width");
let nanos = u64::try_from(elapsed.as_nanos())
.expect("a lex shorter than the five hundred years the counter holds");
LEXED_TOKENS.fetch_add(tokens, Ordering::Relaxed);
LEXED_NANOS.fetch_add(nanos, Ordering::Relaxed);
}
#[must_use]
pub fn keeping() -> bool {
RECORDING.load(Ordering::Relaxed) || HELD.load(Ordering::Relaxed) > 0
}
pub type Tally = (&'static str, u64, Duration);
static PHASES: SegQueue<(&'static str, Duration)> = SegQueue::new();
pub struct Phase {
name: &'static str,
began: std::time::Instant,
}
impl Drop for Phase {
fn drop(&mut self) {
PHASES.push((self.name, self.began.elapsed()));
}
}
#[must_use]
pub fn phase(name: &'static str) -> Option<Phase> {
keeping().then(|| Phase { name, began: std::time::Instant::now() })
}
static COUNTS: SegQueue<(&'static str, u64)> = SegQueue::new();
pub fn counted(name: &'static str, n: u64) {
if keeping() {
COUNTS.push((name, n));
}
}
#[must_use]
pub fn take_counts() -> Vec<(&'static str, u64)> {
let mut out: Vec<(&'static str, u64)> = Vec::new();
while let Some((name, n)) = COUNTS.pop() {
match out.iter_mut().find(|(held, _)| *held == name) {
Some((_, seen)) => *seen += n,
None => out.push((name, n)),
}
}
out
}
#[must_use]
pub fn take_phases() -> Vec<Tally> {
let mut out: Vec<Tally> = Vec::new();
while let Some((name, took)) = PHASES.pop() {
match out.iter_mut().find(|(held, _, _)| *held == name) {
Some((_, calls, spent)) => {
*calls += 1;
*spent += took;
}
None => out.push((name, 1, took)),
}
}
out
}
#[must_use]
pub fn take_lexing() -> (u64, Duration) {
let tokens = LEXED_TOKENS.swap(0, Ordering::SeqCst);
let nanos = LEXED_NANOS.swap(0, Ordering::SeqCst);
(tokens, Duration::from_nanos(nanos))
}
pub fn clear() {
LEXED_TOKENS.store(0, Ordering::SeqCst);
LEXED_NANOS.store(0, Ordering::SeqCst);
while RECORDED.pop().is_some() {}
while ANSWERED.pop().is_some() {}
while PHASES.pop().is_some() {}
while COUNTS.pop().is_some() {}
}
pub fn record() {
RECORDING.store(true, Ordering::SeqCst);
}
pub fn stop() {
RECORDING.store(false, Ordering::SeqCst);
}
#[must_use]
pub fn take_recorded() -> Vec<Rung> {
let mut out = Vec::new();
while let Some(kept) = RECORDED.pop() {
out.push(kept);
}
out
}
pub fn rung(ladder: &str, rung: &str, bytes: usize) {
if on() {
eprintln!("trex route: {ladder} over {bytes} bytes -> {rung}");
}
if keeping() {
let named = Rung { ladder: ladder.to_string(), rung: rung.to_string(), bytes };
ANSWERED.push(named.clone());
RECORDED.push(named);
}
}
#[cfg(test)]
mod tests {
use super::{Recording, counted, keeping, on, phase, rung, take_counts, take_phases, take_recorded, take_totals};
#[test]
fn the_trace_is_off_unless_the_environment_asks_for_it() {
assert!(!on(), "TREX_TRACE is set, so this process cannot check the default");
rung("a ladder", "a rung", 0);
}
#[test]
fn what_many_threads_keep_at_once_is_all_taken_and_added_up() {
const THREADS: usize = 8;
const EACH: usize = 1000;
let outer = Recording::start();
let inner = Recording::start();
drop(inner);
assert!(keeping(), "one holder's drop must not stop another's keeping");
std::thread::scope(|s| {
for _ in 0..THREADS {
s.spawn(|| {
for k in 0..EACH {
rung("the trace test's ladder", if k % 2 == 0 { "even" } else { "odd" }, 3);
counted("the trace test's count", 2);
let timed = phase("the trace test's phase");
drop(timed);
}
});
}
});
drop(outer);
let totals: Vec<_> = take_totals().into_iter().filter(|t| t.ladder == "the trace test's ladder").collect();
let calls: u64 = totals.iter().map(|t| t.calls).sum();
assert_eq!(calls, (THREADS * EACH) as u64, "{totals:?}");
assert_eq!(totals.len(), 2, "{totals:?}");
assert!(totals.iter().all(|t| t.calls == (THREADS * EACH / 2) as u64 && t.bytes == t.calls * 3), "{totals:?}");
let counted_up: Vec<_> = take_counts().into_iter().filter(|(n, _)| *n == "the trace test's count").collect();
assert_eq!(counted_up, vec![("the trace test's count", (THREADS * EACH * 2) as u64)]);
let phases: Vec<_> = take_phases().into_iter().filter(|(n, _, _)| *n == "the trace test's phase").collect();
assert_eq!(phases.len(), 1, "{phases:?}");
assert_eq!(phases[0].1, (THREADS * EACH) as u64);
let kept = take_recorded().into_iter().filter(|r| r.ladder == "the trace test's ladder").count();
assert_eq!(kept, THREADS * EACH);
assert!(take_totals().iter().all(|t| t.ladder != "the trace test's ladder"), "a take leaves nothing to take twice");
}
}