khora_core/utils/
timer.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Provides simple timer utilities for performance measurement.
16
17use std::time::{Duration, Instant};
18
19/// A simple stopwatch for measuring elapsed time.
20///
21/// The stopwatch starts automatically upon creation.
22///
23/// # Examples
24///
25/// ```
26/// use khora_core::utils::timer::Stopwatch;
27/// use std::thread;
28/// use std::time::Duration;
29///
30/// let watch = Stopwatch::new();
31/// thread::sleep(Duration::from_millis(50));
32/// let elapsed_ms = watch.elapsed_ms().unwrap_or(0);
33///
34/// assert!(elapsed_ms >= 50);
35/// ```
36#[derive(Debug, Clone)]
37pub struct Stopwatch {
38    start_time: Option<Instant>,
39}
40
41impl Stopwatch {
42    /// Creates and starts a new `Stopwatch`.
43    #[inline]
44    pub fn new() -> Self {
45        Self {
46            start_time: Some(Instant::now()),
47        }
48    }
49
50    /// Returns the elapsed time since the stopwatch was created.
51    ///
52    /// Returns `None` if the stopwatch was not properly initialized.
53    #[inline]
54    pub fn elapsed(&self) -> Option<Duration> {
55        self.start_time.map(|start| start.elapsed())
56    }
57
58    /// Returns the elapsed time in whole milliseconds.
59    #[inline]
60    pub fn elapsed_ms(&self) -> Option<u64> {
61        self.elapsed().map(|d| d.as_millis() as u64)
62    }
63
64    /// Returns the elapsed time in whole microseconds.
65    #[inline]
66    pub fn elapsed_us(&self) -> Option<u64> {
67        self.elapsed().map(|d| d.as_micros() as u64)
68    }
69
70    /// Returns the elapsed time in fractional seconds as an `f64`.
71    #[inline]
72    pub fn elapsed_secs_f64(&self) -> Option<f64> {
73        self.elapsed().map(|d| d.as_secs_f64())
74    }
75}
76
77impl Default for Stopwatch {
78    /// Creates and starts a new `Stopwatch`.
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use std::thread;
88
89    const SMALL_DURATION_MS: u64 = 15;
90    const SLEEP_DURATION_MS: u64 = 100;
91    const SLEEP_MARGIN_MS: u64 = 200;
92
93    /// A test to check if the Stopwatch struct is created correctly and starts the timer.
94    /// It verifies that the elapsed time is not None after creation and that it is very small.
95    #[test]
96    fn stopwatch_creation_starts_timer() {
97        let watch = Stopwatch::new();
98        // Since ::new() guarantees start_time is Some, elapsed() should also be Some.
99        assert!(
100            watch.elapsed().is_some(),
101            "Elapsed should return Some after creation"
102        );
103        assert!(
104            watch.elapsed_ms().is_some(),
105            "Elapsed_ms should return Some after creation"
106        );
107        assert!(
108            watch.elapsed_us().is_some(),
109            "Elapsed_us should return Some after creation"
110        );
111        assert!(
112            watch.elapsed_secs_f64().is_some(),
113            "Elapsed_secs_f64 should return Some after creation"
114        );
115    }
116
117    /// A test to check if the Stopwatch struct correctly reports elapsed time after a short delay.
118    /// It verifies that the elapsed time is greater than or equal to the sleep duration and less than the sleep duration plus a margin.
119    #[test]
120    fn stopwatch_elapsed_time_near_zero_initially() {
121        let watch = Stopwatch::new();
122
123        // Check elapsed Duration
124        let elapsed_duration = watch.elapsed().expect("Should have elapsed duration");
125        assert!(
126            elapsed_duration < Duration::from_millis(SMALL_DURATION_MS),
127            "Initial elapsed duration ({elapsed_duration:?}) should be very small"
128        );
129
130        // Check elapsed milliseconds
131        let elapsed_ms = watch.elapsed_ms().expect("Should have elapsed ms");
132        assert!(
133            elapsed_ms < SMALL_DURATION_MS,
134            "Initial elapsed ms ({elapsed_ms}) should be very small"
135        );
136
137        // Check elapsed microseconds
138        let elapsed_us = watch.elapsed_us().expect("Should have elapsed us");
139        let small_duration_us = SMALL_DURATION_MS * 1000;
140        assert!(
141            elapsed_us < small_duration_us,
142            "Initial elapsed us ({elapsed_us}) should be very small"
143        );
144
145        let elapsed_secs_f64 = watch
146            .elapsed_secs_f64()
147            .expect("Should have elapsed seconds as f64");
148        assert!(
149            elapsed_secs_f64 < SMALL_DURATION_MS as f64 / 1000.0,
150            "Initial elapsed seconds ({elapsed_secs_f64}) should be very small"
151        );
152    }
153
154    /// A test to check if the Stopwatch struct correctly reports elapsed time after a sleep duration.
155    /// It verifies that the elapsed time is greater than or equal to the sleep duration and less than the sleep duration plus a margin.
156    #[test]
157    fn stopwatch_elapsed_time_after_delay() {
158        let watch = Stopwatch::new();
159        let sleep_duration = Duration::from_millis(SLEEP_DURATION_MS);
160        let margin_duration = Duration::from_millis(SLEEP_MARGIN_MS);
161        let min_expected_duration = sleep_duration;
162        let max_expected_duration = sleep_duration + margin_duration;
163
164        thread::sleep(sleep_duration);
165
166        // Check elapsed Duration
167        let elapsed_duration = watch
168            .elapsed()
169            .expect("Should have elapsed duration after sleep");
170        assert!(
171            elapsed_duration >= min_expected_duration,
172            "Elapsed duration ({elapsed_duration:?}) should be >= sleep duration ({min_expected_duration:?})"
173        );
174        assert!(
175            elapsed_duration < max_expected_duration,
176            "Elapsed duration ({elapsed_duration:?}) should be < sleep duration + margin ({max_expected_duration:?})"
177        );
178
179        // Check elapsed milliseconds
180        let elapsed_ms = watch
181            .elapsed_ms()
182            .expect("Should have elapsed ms after sleep");
183        let min_expected_ms = SLEEP_DURATION_MS;
184        let max_expected_ms = SLEEP_DURATION_MS + SLEEP_MARGIN_MS;
185        assert!(
186            elapsed_ms >= min_expected_ms,
187            "Elapsed ms ({elapsed_ms}) should be >= sleep duration ms ({min_expected_ms})"
188        );
189        assert!(
190            elapsed_ms < max_expected_ms,
191            "Elapsed ms ({elapsed_ms}) should be < sleep duration ms + margin ({max_expected_ms})"
192        );
193
194        // Check elapsed microseconds
195        let elapsed_us = watch
196            .elapsed_us()
197            .expect("Should have elapsed us after sleep");
198        let min_expected_us = SLEEP_DURATION_MS * 1000;
199        let max_expected_us = (SLEEP_DURATION_MS + SLEEP_MARGIN_MS) * 1000;
200        assert!(
201            elapsed_us >= min_expected_us,
202            "Elapsed us ({elapsed_us}) should be >= sleep duration us ({min_expected_us})"
203        );
204        assert!(
205            elapsed_us < max_expected_us,
206            "Elapsed us ({elapsed_us}) should be < sleep duration us + margin ({max_expected_us})"
207        );
208
209        // Check elapsed seconds as f64
210        let elapsed_secs_f64 = watch
211            .elapsed_secs_f64()
212            .expect("Should have elapsed seconds as f64 after sleep");
213        let min_expected_secs_f64 = SLEEP_DURATION_MS as f64 / 1000.0;
214        let max_expected_secs_f64 = (SLEEP_DURATION_MS + SLEEP_MARGIN_MS) as f64 / 1000.0;
215        assert!(
216            elapsed_secs_f64 >= min_expected_secs_f64,
217            "Elapsed seconds ({elapsed_secs_f64}) should be >= sleep duration seconds ({min_expected_secs_f64})"
218        );
219        assert!(
220            elapsed_secs_f64 < max_expected_secs_f64,
221            "Elapsed seconds ({elapsed_secs_f64}) should be < sleep duration seconds + margin ({max_expected_secs_f64})"
222        );
223    }
224
225    /// A test to check if the Stopwatch struct implements the Default trait.
226    /// It verifies that the default stopwatch has a valid elapsed time.
227    #[test]
228    fn stopwatch_implements_default() {
229        let watch = Stopwatch::default();
230        assert!(watch.elapsed().is_some());
231    }
232
233    /// A test to check if the Stopwatch struct implements the Clone trait.
234    /// It verifies that the elapsed time of the original and cloned stopwatch are roughly equal.
235    #[test]
236    fn stopwatch_clone() {
237        let watch1 = Stopwatch::new();
238        thread::sleep(Duration::from_millis(10));
239        let watch2 = watch1.clone(); // Clone the stopwatch
240
241        // Both clones should report roughly the same elapsed time,
242        // relative to the *original* start time.
243        let elapsed1 = watch1.elapsed_us().unwrap();
244        let elapsed2 = watch2.elapsed_us().unwrap();
245
246        // They should be very close, allow a small difference for the clone operation itself
247        let difference = if elapsed1 > elapsed2 {
248            elapsed1.abs_diff(elapsed2)
249        } else {
250            elapsed2.abs_diff(elapsed1)
251        };
252        assert!(
253            difference < 1000,
254            "Elapsed time of clones should be very close (diff: {difference} us)"
255        ); // Allow 1ms diff
256    }
257}