use std::{
sync::{
Mutex,
atomic::{AtomicU64, Ordering},
},
time::{Duration, Instant},
};
pub struct Clock {
state: Mutex<State>,
interrupt_epoch: AtomicU64,
}
#[derive(Clone, Copy)]
enum State {
Unset,
Running {
start: Instant,
},
Paused {
start: Instant,
paused_at: Instant,
},
}
impl Default for Clock {
fn default() -> Self {
Self::new()
}
}
impl Clock {
pub fn new() -> Self {
Self {
state: Mutex::new(State::Unset),
interrupt_epoch: AtomicU64::new(0),
}
}
pub(crate) fn interrupt(&self) {
self.interrupt_epoch.fetch_add(1, Ordering::Release);
}
pub(crate) fn interrupt_epoch(&self) -> u64 {
self.interrupt_epoch.load(Ordering::Acquire)
}
pub fn start(&self) -> Instant {
let mut state = self.state.lock().unwrap();
match *state {
State::Unset => {
let now = Instant::now();
*state = State::Running { start: now };
now
}
State::Running { start } => start,
State::Paused { start, .. } => start,
}
}
pub(crate) fn elapsed(&self) -> Duration {
let state = self.state.lock().unwrap();
match *state {
State::Unset => Duration::ZERO,
State::Running { start } => Instant::now().saturating_duration_since(start),
State::Paused { start, paused_at } => paused_at.saturating_duration_since(start),
}
}
pub fn pause(&self) {
let mut state = self.state.lock().unwrap();
if let State::Running { start } = *state {
*state = State::Paused {
start,
paused_at: Instant::now(),
};
}
}
pub fn resume(&self) {
let mut state = self.state.lock().unwrap();
if let State::Paused { start, paused_at } = *state {
let shift = Instant::now().saturating_duration_since(paused_at);
*state = State::Running {
start: start + shift,
};
}
}
pub fn reset(&self) {
let mut state = self.state.lock().unwrap();
*state = State::Unset;
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
#[test]
fn pause_shifts_start_forward_by_the_pause_duration() {
let clock = Clock::new();
let first = clock.start();
clock.pause();
std::thread::sleep(Duration::from_millis(30));
clock.resume();
let after = clock.start();
assert!(
after >= first + Duration::from_millis(20),
"expected start() to shift forward by roughly the pause duration"
);
}
#[test]
fn reset_makes_the_next_start_anchor_to_a_fresh_now() {
let clock = Clock::new();
let original = clock.start();
std::thread::sleep(Duration::from_millis(30));
clock.reset();
let after_reset = clock.start();
assert!(
after_reset >= original + Duration::from_millis(20),
"expected start() after reset() to anchor to a fresh instant, \
not keep returning the original one"
);
}
}