use std::collections::HashMap;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy)]
struct Span {
start: Instant,
end: Option<Instant>,
}
#[derive(Debug)]
pub struct TimeUse {
global_start: Instant,
global_end: Option<Instant>,
spans: HashMap<String, Span>,
}
impl TimeUse {
pub fn new() -> Self {
Self {
global_start: Instant::now(),
global_end: None,
spans: HashMap::new(),
}
}
pub fn start(&mut self, tag: Option<&str>) {
let now = Instant::now();
match tag {
None => {
self.global_start = now;
self.global_end = None;
}
Some(t) => {
self.spans.insert(
t.to_string(),
Span {
start: now,
end: None,
},
);
}
}
}
pub fn stop(&mut self, tag: Option<&str>) -> Duration {
let now = Instant::now();
match tag {
None => {
let end = *self.global_end.get_or_insert(now);
end.saturating_duration_since(self.global_start)
}
Some(t) => {
let global_start = self.global_start;
let span = self.spans.entry(t.to_string()).or_insert(Span {
start: global_start,
end: None,
});
let end = *span.end.get_or_insert(now);
end.saturating_duration_since(span.start)
}
}
}
pub fn elapsed(&self, tag: Option<&str>) -> Duration {
let now = Instant::now();
match tag {
None => {
let end = self.global_end.unwrap_or(now);
end.saturating_duration_since(self.global_start)
}
Some(t) => match self.spans.get(t) {
Some(span) => span
.end
.unwrap_or(now)
.saturating_duration_since(span.start),
None => now.saturating_duration_since(self.global_start),
},
}
}
pub fn restart(&mut self, tag: Option<&str>) -> Duration {
let elapsed = self.stop(tag);
self.start(tag);
elapsed
}
pub fn tag_count(&self) -> usize {
self.spans.len()
}
}
impl Default for TimeUse {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread::sleep;
#[test]
fn global_elapsed_grows() {
let t = TimeUse::new();
sleep(Duration::from_millis(5));
let a = t.elapsed(None);
sleep(Duration::from_millis(5));
let b = t.elapsed(None);
assert!(b >= a);
assert!(b.as_millis() >= 10);
}
#[test]
fn stop_is_idempotent() {
let mut t = TimeUse::new();
sleep(Duration::from_millis(10));
let first = t.stop(None);
sleep(Duration::from_millis(10));
let second = t.stop(None);
assert_eq!(first, second);
}
#[test]
fn elapsed_after_stop_returns_cached() {
let mut t = TimeUse::new();
sleep(Duration::from_millis(10));
let stopped = t.stop(None);
sleep(Duration::from_millis(10));
assert_eq!(t.elapsed(None), stopped);
}
#[test]
fn independent_tags() {
let mut t = TimeUse::new();
t.start(Some("a"));
sleep(Duration::from_millis(10));
let a = t.stop(Some("a"));
t.start(Some("b"));
sleep(Duration::from_millis(20));
let b = t.stop(Some("b"));
assert!(a.as_millis() >= 10);
assert!(b.as_millis() >= 20);
assert_eq!(t.tag_count(), 2);
}
#[test]
fn stop_tag_is_idempotent() {
let mut t = TimeUse::new();
t.start(Some("k"));
sleep(Duration::from_millis(5));
let first = t.stop(Some("k"));
sleep(Duration::from_millis(20));
let second = t.stop(Some("k"));
assert_eq!(first, second);
}
#[test]
fn stop_unknown_tag_falls_back_to_global_start() {
let mut t = TimeUse::new();
sleep(Duration::from_millis(10));
let elapsed = t.stop(Some("never_started"));
assert!(elapsed.as_millis() >= 10);
}
#[test]
fn elapsed_unknown_tag_falls_back_to_global_start() {
let t = TimeUse::new();
sleep(Duration::from_millis(10));
let elapsed = t.elapsed(Some("never_started"));
assert!(elapsed.as_millis() >= 10);
}
#[test]
fn stop_global_does_not_reset() {
let mut t = TimeUse::new();
sleep(Duration::from_millis(10));
let first = t.stop(None);
sleep(Duration::from_millis(20));
assert_eq!(t.stop(None), first);
}
#[test]
fn restart_global_returns_previous_duration() {
let mut t = TimeUse::new();
sleep(Duration::from_millis(10));
let prev = t.restart(None);
assert!(prev.as_millis() >= 10);
}
#[test]
fn restart_tag_returns_previous_duration() {
let mut t = TimeUse::new();
t.start(Some("loop"));
sleep(Duration::from_millis(10));
let first = t.restart(Some("loop"));
assert!(first.as_millis() >= 10);
}
}