use std::fmt;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::OnceLock;
use std::time::{Duration, Instant};
use crate::style;
const PREFIX: &str = "pmpx debug: ";
static ENABLED: AtomicBool = AtomicBool::new(false);
static START: OnceLock<Instant> = OnceLock::new();
pub fn mark_start() {
let _ = START.set(Instant::now());
}
pub fn enable() {
ENABLED.store(true, Ordering::Relaxed);
}
fn is_on() -> bool {
ENABLED.load(Ordering::Relaxed)
}
pub fn enabled() -> bool {
is_on()
}
pub fn now() -> Instant {
Instant::now()
}
pub fn done<D: fmt::Display>(phase: &str, since: Instant, detail: impl FnOnce() -> D) {
if !is_on() {
return;
}
emit(phase, since.elapsed(), detail());
}
pub fn phase(name: &str, micros: u128, detail: impl fmt::Display) {
if !is_on() {
return;
}
emit(name, Duration::from_micros(micros as u64), detail);
}
pub fn note(text: impl fmt::Display) {
if !is_on() {
return;
}
anstream::eprintln!("{}", style::paint(style::DIM, format!("{PREFIX}{text}")));
}
pub fn header() {
note(format_args!(
"pmpx {} ({}) in {}",
env!("CARGO_PKG_VERSION"),
std::env::consts::OS,
cwd()
));
}
pub fn total(code: u8) {
if !is_on() {
return;
}
emit("total", elapsed(), format!("exit {code}"));
}
fn elapsed() -> Duration {
START.get().map(Instant::elapsed).unwrap_or_default()
}
fn cwd() -> String {
std::env::current_dir()
.map(|p| p.display().to_string())
.unwrap_or_else(|_| "?".to_string())
}
fn emit(phase: &str, took: Duration, detail: impl fmt::Display) {
anstream::eprintln!("{}", style::paint(style::DIM, line(phase, took, detail)));
}
fn line(phase: &str, took: Duration, detail: impl fmt::Display) -> String {
format!("{PREFIX}{:>8.2}ms {phase:<20} {detail}", millis(took))
}
fn millis(took: Duration) -> f64 {
took.as_secs_f64() * 1000.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_line_carries_the_marker_the_phase_and_the_detail() {
let text = line("detect.score", Duration::from_micros(240), "2 plugins");
assert!(text.starts_with(PREFIX), "{text}");
assert!(text.contains("0.24ms"), "{text}");
assert!(text.contains("detect.score"), "{text}");
assert!(text.contains("2 plugins"), "{text}");
}
#[test]
fn the_columns_hold_still_across_phases_of_very_different_sizes() {
let fast = line("a", Duration::from_millis(1), "x");
let slow = line("detect.decide", Duration::from_secs(1), "y");
assert_eq!(
fast.find(" x"),
slow.find(" y"),
"the detail column moved:\n{fast}\n{slow}"
);
}
#[test]
fn elapsed_is_measured_from_the_first_mark_start() {
mark_start();
let first = elapsed();
mark_start();
let second = elapsed();
assert!(second >= first);
assert!(second < Duration::from_secs(60), "a clock, not a stopwatch");
}
}