use std::time::Duration;
use crate::time::MonotonicTime;
pub trait Ticker: Send + 'static {
fn next_tick(&mut self, time: MonotonicTime) -> MonotonicTime;
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct PeriodicTicker {
period: Duration,
reference: Option<MonotonicTime>,
}
impl PeriodicTicker {
pub fn new(period: Duration) -> Self {
assert!(!period.is_zero(), "the tick period must be non-zero");
Self {
period,
reference: None,
}
}
pub fn anchored_at(period: Duration, reference: MonotonicTime) -> Self {
assert!(!period.is_zero(), "the tick period must be non-zero");
Self {
period,
reference: Some(reference),
}
}
}
impl Ticker for PeriodicTicker {
fn next_tick(&mut self, time: MonotonicTime) -> MonotonicTime {
let reference = *self.reference.get_or_insert(time);
let tick = if time >= reference {
let elapsed = time.duration_since(reference);
if elapsed < self.period {
reference + self.period
} else {
let period_ns = self.period.as_nanos();
let remainder_ns = elapsed.as_nanos() % period_ns;
let time_to_next_ns = period_ns - remainder_ns;
time + duration_from_nanos_u128(time_to_next_ns)
}
} else {
let period_ns = self.period.as_nanos();
let gap = reference.duration_since(time);
let dist_to_next_ns = gap.as_nanos() % period_ns;
if dist_to_next_ns == 0 {
time + self.period
} else {
time + duration_from_nanos_u128(dist_to_next_ns)
}
};
self.reference = Some(tick);
tick
}
}
fn duration_from_nanos_u128(nanos: u128) -> Duration {
const NANOS_PER_SEC: u128 = 1_000_000_000;
let secs = u64::try_from(nanos / NANOS_PER_SEC).unwrap();
let subsec_nanos = (nanos % NANOS_PER_SEC) as u32;
Duration::new(secs, subsec_nanos)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ticker_basic_stepping() {
const PERIOD: Duration = Duration::from_secs(1);
const START: MonotonicTime = MonotonicTime::new(123, 456_789_012).unwrap();
let mut ticker = PeriodicTicker::new(PERIOD);
assert_eq!(ticker.next_tick(START), START + PERIOD);
assert_eq!(
ticker.next_tick(MonotonicTime::new(0, 500_000_000).unwrap()),
MonotonicTime::new(1, 456_789_012).unwrap()
);
assert_eq!(
ticker.next_tick(MonotonicTime::new(1, 456_789_012).unwrap()),
MonotonicTime::new(2, 456_789_012).unwrap()
);
}
#[test]
fn ticker_sub_tick_increment() {
const PERIOD: Duration = Duration::from_secs(10);
const START: MonotonicTime = MonotonicTime::new(123, 456_789_012).unwrap();
let mut ticker = PeriodicTicker::anchored_at(PERIOD, START);
let next = ticker.next_tick(START + PERIOD / 2);
assert_eq!(next, START + PERIOD);
}
#[test]
fn ticker_exact_tick_increment() {
const PERIOD: Duration = Duration::from_secs(10);
const START: MonotonicTime = MonotonicTime::new(123, 456_789_012).unwrap();
let mut ticker = PeriodicTicker::anchored_at(PERIOD, START);
let next = ticker.next_tick(START + PERIOD);
assert_eq!(next, START + PERIOD * 2);
}
#[test]
fn ticker_past_reference() {
const PERIOD: Duration = Duration::from_secs(10);
const FUTURE_ANCHOR: MonotonicTime = MonotonicTime::new(100, 0).unwrap();
let mut ticker = PeriodicTicker::anchored_at(PERIOD, FUTURE_ANCHOR);
assert_eq!(
ticker.next_tick(MonotonicTime::new(95, 0).unwrap()),
MonotonicTime::new(100, 0).unwrap()
);
assert_eq!(
ticker.next_tick(MonotonicTime::new(85, 0).unwrap()),
MonotonicTime::new(90, 0).unwrap()
);
assert_eq!(
ticker.next_tick(MonotonicTime::new(80, 0).unwrap()),
MonotonicTime::new(90, 0).unwrap()
);
}
#[test]
fn ticker_very_large_step() {
const PERIOD: Duration = Duration::from_secs(2);
const START: MonotonicTime = MonotonicTime::new(123, 456_789_012).unwrap();
let mut ticker = PeriodicTicker::new(PERIOD);
ticker.next_tick(START);
assert_eq!(
ticker.next_tick(START + Duration::new(1001, 0)),
START + Duration::new(1002, 0)
);
assert_eq!(
ticker.next_tick(START + Duration::new(1002, 0)),
START + Duration::new(1004, 0)
);
assert_eq!(
ticker.next_tick(START + Duration::new(1200, 0)),
START + Duration::new(1202, 0)
);
}
}