Skip to main content

clipper2_rust/utils/
timer.rs

1// Copyright 2025 - Clipper2 Rust port
2// Direct port of Timer.h by Angus Johnson
3// License: https://www.boost.org/LICENSE_1_0.txt
4//
5// Purpose: Performance timing utility
6
7use std::time::{Duration, Instant};
8
9/// A simple stopwatch timer that supports pause and resume.
10///
11/// Direct port from C++ Timer.h. The timer starts immediately on construction
12/// (unless `start_paused` is true). Use `pause()` and `resume()` any number
13/// of times; `elapsed()` returns the total time spent unpaused.
14///
15/// # Examples
16///
17/// ```
18/// use clipper2_rust::utils::timer::Timer;
19/// let timer = Timer::new(false);
20/// // ... do work ...
21/// let elapsed = timer.elapsed();
22/// println!("Took {}", Timer::format_duration(elapsed));
23/// ```
24pub struct Timer {
25    time_started: Instant,
26    duration: Duration,
27    paused: bool,
28}
29
30impl Timer {
31    /// Create a new timer. If `start_paused` is false, the timer starts immediately.
32    pub fn new(start_paused: bool) -> Self {
33        Self {
34            time_started: Instant::now(),
35            duration: Duration::ZERO,
36            paused: start_paused,
37        }
38    }
39
40    /// Restart the timer from zero.
41    pub fn restart(&mut self) {
42        self.paused = false;
43        self.duration = Duration::ZERO;
44        self.time_started = Instant::now();
45    }
46
47    /// Resume a paused timer. No-op if already running.
48    pub fn resume(&mut self) {
49        if !self.paused {
50            return;
51        }
52        self.paused = false;
53        self.time_started = Instant::now();
54    }
55
56    /// Pause a running timer. No-op if already paused.
57    pub fn pause(&mut self) {
58        if self.paused {
59            return;
60        }
61        self.duration += self.time_started.elapsed();
62        self.paused = true;
63    }
64
65    /// Return the total elapsed duration (excluding paused intervals).
66    ///
67    /// If the timer is currently running, includes time since last resume.
68    /// If the timer is paused, returns accumulated time only.
69    pub fn elapsed(&self) -> Duration {
70        if self.paused {
71            self.duration
72        } else {
73            self.duration + self.time_started.elapsed()
74        }
75    }
76
77    /// Return elapsed time in nanoseconds.
78    /// Direct port from C++ `elapsed_nano()`.
79    pub fn elapsed_nanos(&self) -> u128 {
80        self.elapsed().as_nanos()
81    }
82
83    /// Format a duration as a human-readable string.
84    /// Direct port from C++ `elapsed_str()`.
85    ///
86    /// Automatically selects appropriate units (microsecs, millisecs, secs).
87    pub fn format_duration(dur: Duration) -> String {
88        let nanos = dur.as_nanos() as f64;
89        if nanos < 1.0 {
90            return "0 microsecs".to_string();
91        }
92        let log10 = nanos.log10() as i32;
93        if log10 < 6 {
94            let precision = (2 - (log10 % 3)) as usize;
95            format!("{:.prec$} microsecs", nanos * 1.0e-3, prec = precision)
96        } else if log10 < 9 {
97            let precision = (2 - (log10 % 3)) as usize;
98            format!("{:.prec$} millisecs", nanos * 1.0e-6, prec = precision)
99        } else {
100            let precision = (2 - (log10 % 3)) as usize;
101            format!("{:.prec$} secs", nanos * 1.0e-9, prec = precision)
102        }
103    }
104
105    /// Return elapsed time as a human-readable string.
106    pub fn elapsed_str(&self) -> String {
107        Self::format_duration(self.elapsed())
108    }
109}
110
111impl Default for Timer {
112    fn default() -> Self {
113        Self::new(false)
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use std::thread;
121
122    #[test]
123    fn test_timer_starts_running() {
124        let timer = Timer::new(false);
125        thread::sleep(Duration::from_millis(10));
126        assert!(timer.elapsed() >= Duration::from_millis(5));
127    }
128
129    #[test]
130    fn test_timer_starts_paused() {
131        let timer = Timer::new(true);
132        thread::sleep(Duration::from_millis(10));
133        // Should have accumulated very little time since it was paused
134        assert!(timer.elapsed() < Duration::from_millis(1));
135    }
136
137    #[test]
138    fn test_timer_pause_resume() {
139        let mut timer = Timer::new(false);
140        thread::sleep(Duration::from_millis(20));
141        timer.pause();
142        let paused_elapsed = timer.elapsed();
143        thread::sleep(Duration::from_millis(20));
144        // Should not have changed while paused
145        assert_eq!(timer.elapsed(), paused_elapsed);
146        timer.resume();
147        thread::sleep(Duration::from_millis(20));
148        assert!(timer.elapsed() > paused_elapsed);
149    }
150
151    #[test]
152    fn test_timer_restart() {
153        let mut timer = Timer::new(false);
154        thread::sleep(Duration::from_millis(20));
155        timer.restart();
156        // After restart, elapsed should be very small
157        assert!(timer.elapsed() < Duration::from_millis(5));
158    }
159
160    #[test]
161    fn test_format_duration_microsecs() {
162        let s = Timer::format_duration(Duration::from_micros(500));
163        assert!(s.contains("microsecs"));
164    }
165
166    #[test]
167    fn test_format_duration_millisecs() {
168        let s = Timer::format_duration(Duration::from_millis(50));
169        assert!(s.contains("millisecs"));
170    }
171
172    #[test]
173    fn test_format_duration_secs() {
174        let s = Timer::format_duration(Duration::from_secs(2));
175        assert!(s.contains("secs"));
176    }
177
178    #[test]
179    fn test_format_duration_zero() {
180        let s = Timer::format_duration(Duration::ZERO);
181        assert!(s.contains("microsecs"));
182    }
183
184    #[test]
185    fn test_default_timer_starts_running() {
186        let timer = Timer::default();
187        thread::sleep(Duration::from_millis(10));
188        assert!(timer.elapsed() >= Duration::from_millis(5));
189    }
190
191    #[test]
192    fn test_elapsed_nanos() {
193        let timer = Timer::new(false);
194        thread::sleep(Duration::from_millis(10));
195        assert!(timer.elapsed_nanos() > 1_000_000); // > 1ms in nanos
196    }
197}