use std::io::{self, Write};
use std::sync::OnceLock;
use std::time::Instant;
use crate::bench::format_ns;
#[derive(Copy, Clone, Debug)]
#[repr(transparent)]
pub struct SubMsTick(Instant);
impl SubMsTick {
#[inline]
pub fn elapsed_ns(self) -> u64 {
self.0.elapsed().as_nanos() as u64
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SubMsTimerCheckpoint {
pub label: String,
pub since_last_ns: u64,
pub since_start_ns: u64,
pub is_stop: bool,
}
pub struct SubMsTimer {
name: String,
started_at: Instant,
last_at: Instant,
stopped_at: Option<Instant>,
checkpoints: Vec<SubMsTimerCheckpoint>,
}
impl SubMsTimer {
#[inline]
pub fn nanos_now() -> u64 {
static EPOCH: OnceLock<Instant> = OnceLock::new();
let epoch = EPOCH.get_or_init(Instant::now);
epoch.elapsed().as_nanos() as u64
}
#[inline]
pub fn tick() -> SubMsTick {
SubMsTick(Instant::now())
}
#[inline]
pub fn measure_ns<F, T>(f: F) -> (T, u64)
where
F: FnOnce() -> T,
{
let t0 = Instant::now();
let r = f();
let elapsed = t0.elapsed().as_nanos() as u64;
(r, elapsed)
}
pub fn unnamed() -> Self {
Self::new("")
}
pub fn new(name: &str) -> Self {
let now = Instant::now();
Self {
name: name.to_string(),
started_at: now,
last_at: now,
stopped_at: None,
checkpoints: Vec::new(),
}
}
pub fn start(&mut self) -> &mut Self {
let now = Instant::now();
self.started_at = now;
self.last_at = now;
self.stopped_at = None;
self.checkpoints.clear();
self
}
pub fn reset(&mut self) -> &mut Self {
self.start()
}
pub fn mark(&mut self, label: &str) -> u64 {
let now = Instant::now();
let since_start = now.duration_since(self.started_at).as_nanos() as u64;
let since_last = now.duration_since(self.last_at).as_nanos() as u64;
self.last_at = now;
self.checkpoints.push(SubMsTimerCheckpoint {
label: label.to_string(),
since_last_ns: since_last,
since_start_ns: since_start,
is_stop: false,
});
since_start
}
pub fn lap(&mut self, label: &str) -> u64 {
self.mark(label)
}
pub fn stop(&mut self, label: &str) -> u64 {
let now = Instant::now();
let since_start = now.duration_since(self.started_at).as_nanos() as u64;
let since_last = now.duration_since(self.last_at).as_nanos() as u64;
self.last_at = now;
self.stopped_at = Some(now);
self.checkpoints.push(SubMsTimerCheckpoint {
label: label.to_string(),
since_last_ns: since_last,
since_start_ns: since_start,
is_stop: true,
});
since_start
}
pub fn is_stopped(&self) -> bool {
self.stopped_at.is_some()
}
pub fn elapsed_ns(&self) -> u64 {
let end = self.stopped_at.unwrap_or_else(Instant::now);
end.duration_since(self.started_at).as_nanos() as u64
}
pub fn name(&self) -> &str {
&self.name
}
pub fn checkpoints(&self) -> &[SubMsTimerCheckpoint] {
&self.checkpoints
}
pub fn print<W: Write>(&self, out: &mut W) -> io::Result<()> {
writeln!(
out,
"timer \"{}\" total={}",
self.name,
format_ns(self.elapsed_ns())
)?;
for cp in &self.checkpoints {
let label = if cp.is_stop {
format!("{} *", cp.label)
} else {
cp.label.clone()
};
writeln!(
out,
" {:<18} +{:>8} {:>8}",
label,
format_ns(cp.since_last_ns),
format_ns(cp.since_start_ns)
)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
#[test]
fn autostart_and_mark_captures_increasing_since_start() {
let mut t = SubMsTimer::new("x");
thread::sleep(Duration::from_millis(1));
let a = t.mark("a");
thread::sleep(Duration::from_millis(1));
let b = t.mark("b");
assert!(a > 0);
assert!(b > a);
assert_eq!(t.checkpoints().len(), 2);
assert_eq!(t.checkpoints()[0].label, "a");
assert_eq!(t.checkpoints()[1].label, "b");
assert!(!t.checkpoints()[0].is_stop);
}
#[test]
fn stop_marks_is_stop_and_freezes_elapsed() {
let mut t = SubMsTimer::new("x");
thread::sleep(Duration::from_millis(1));
t.stop("done");
assert!(t.is_stopped());
let e1 = t.elapsed_ns();
thread::sleep(Duration::from_millis(2));
let e2 = t.elapsed_ns();
assert_eq!(e1, e2, "elapsed should freeze after stop");
assert!(t.checkpoints().last().unwrap().is_stop);
}
#[test]
fn reset_clears_checkpoints() {
let mut t = SubMsTimer::new("x");
t.mark("a");
t.mark("b");
t.reset();
assert!(t.checkpoints().is_empty());
assert!(!t.is_stopped());
}
#[test]
fn lap_is_alias_of_mark() {
let mut t = SubMsTimer::new("x");
t.lap("a");
assert_eq!(t.checkpoints().len(), 1);
assert_eq!(t.checkpoints()[0].label, "a");
}
#[test]
fn print_emits_header_and_checkpoints() {
let mut t = SubMsTimer::new("parse");
t.mark("a");
t.stop("done");
let mut buf = Vec::new();
t.print(&mut buf).unwrap();
let out = String::from_utf8(buf).unwrap();
assert!(out.contains("timer \"parse\""));
assert!(out.contains("a"));
assert!(out.contains("done *"));
}
#[test]
fn nanos_now_returns_positive_increasing() {
let a = SubMsTimer::nanos_now();
thread::sleep(Duration::from_millis(1));
let b = SubMsTimer::nanos_now();
assert!(b > a, "monotonic: {} -> {}", a, b);
}
#[test]
fn tick_and_elapsed_ns_capture_positive_interval() {
let t = SubMsTimer::tick();
thread::sleep(Duration::from_millis(2));
let ns = t.elapsed_ns();
assert!(ns >= 1_000_000, "should be >= 1ms after sleep: {}", ns);
assert!(
ns < 100_000_000,
"shouldn't be > 100ms on a healthy box: {}",
ns
);
}
#[test]
fn tick_is_reusable_for_multiple_reads() {
let t = SubMsTimer::tick();
thread::sleep(Duration::from_millis(1));
let a = t.elapsed_ns();
thread::sleep(Duration::from_millis(1));
let b = t.elapsed_ns();
assert!(b >= a, "second read >= first: {} -> {}", a, b);
}
#[test]
fn measure_ns_returns_elapsed_and_runs_closure() {
let mut counter = 0;
let ((), elapsed) = SubMsTimer::measure_ns(|| {
counter += 1;
thread::sleep(Duration::from_millis(1));
});
assert_eq!(counter, 1, "closure should run exactly once");
assert!(
elapsed >= 500_000,
"elapsed should be at least 0.5ms: {}",
elapsed
);
}
#[test]
fn measure_ns_propagates_closure_return_value() {
let (val, _ns) = SubMsTimer::measure_ns(|| 42);
assert_eq!(val, 42);
}
#[test]
fn measure_ns_returns_zero_or_positive_for_noop() {
let (_, elapsed) = SubMsTimer::measure_ns(|| {});
assert!(
elapsed < 1_000_000,
"no-op shouldn't take >= 1ms: {}",
elapsed
);
}
}