use crossbeam_channel::{bounded, Receiver, Sender};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy)]
pub struct Tick {
pub frame: u64,
pub elapsed: Duration,
}
pub struct TickerActor {
handle: Option<JoinHandle<()>>,
shutdown: Arc<AtomicBool>,
tick_rx: Receiver<Tick>,
}
impl TickerActor {
#[allow(clippy::missing_panics_doc)]
pub fn spawn(interval: Duration) -> Self {
let shutdown = Arc::new(AtomicBool::new(false));
let shutdown_clone = shutdown.clone();
let (tick_tx, tick_rx) = bounded(2);
let handle = thread::Builder::new()
.name("flywheel-ticker".to_string())
.spawn(move || {
Self::run_loop(&tick_tx, &shutdown_clone, interval);
})
.expect("Failed to spawn ticker thread");
Self {
handle: Some(handle),
shutdown,
tick_rx,
}
}
#[inline]
pub const fn receiver(&self) -> &Receiver<Tick> {
&self.tick_rx
}
pub fn shutdown(&self) {
self.shutdown.store(true, Ordering::Relaxed);
}
pub fn join(mut self) {
self.shutdown();
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
fn run_loop(tick_tx: &Sender<Tick>, shutdown: &Arc<AtomicBool>, interval: Duration) {
let start = Instant::now();
let mut frame = 0u64;
let mut next_tick = start + interval;
loop {
if shutdown.load(Ordering::Relaxed) {
break;
}
let now = Instant::now();
if now >= next_tick {
let tick = Tick {
frame,
elapsed: now - start,
};
let _ = tick_tx.try_send(tick);
frame += 1;
next_tick += interval;
if next_tick < now {
next_tick = now + interval;
}
} else {
let sleep_duration = next_tick - now;
thread::sleep(sleep_duration.min(Duration::from_millis(1)));
}
}
}
}
impl Drop for TickerActor {
fn drop(&mut self) {
self.shutdown();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ticker_basic() {
let ticker = TickerActor::spawn(Duration::from_millis(10));
let tick = ticker.receiver().recv_timeout(Duration::from_millis(100));
assert!(tick.is_ok());
assert_eq!(tick.unwrap().frame, 0);
let tick2 = ticker.receiver().recv_timeout(Duration::from_millis(50));
assert!(tick2.is_ok());
ticker.join();
}
#[test]
fn test_ticker_shutdown() {
let ticker = TickerActor::spawn(Duration::from_millis(100));
ticker.shutdown();
thread::sleep(Duration::from_millis(50));
ticker.join();
}
}