use std::future::poll_fn;
use std::task::{Context, Poll};
use std::time::Duration;
use tokio::time::Instant;
use crate::sys;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum MissedTickBehavior {
#[default]
Burst,
Delay,
Skip,
}
impl MissedTickBehavior {
fn next_deadline(self, prev: Instant, now: Instant, period: Duration) -> Instant {
match self {
MissedTickBehavior::Burst => prev + period,
MissedTickBehavior::Delay => now + period,
MissedTickBehavior::Skip => {
if now <= prev + period {
prev + period
} else {
let elapsed_ns = now.saturating_duration_since(prev).as_nanos();
let period_ns = period.as_nanos().max(1);
let n = u32::try_from(elapsed_ns.div_ceil(period_ns)).unwrap_or(u32::MAX);
prev + period * n
}
}
}
}
}
#[must_use]
pub fn interval(period: Duration) -> OsInterval {
interval_at(Instant::now(), period)
}
#[must_use]
pub fn interval_at(start: Instant, period: Duration) -> OsInterval {
assert!(period > Duration::ZERO, "`period` must be non-zero");
OsInterval {
period,
behavior: MissedTickBehavior::default(),
next_deadline: start,
armed_for: None,
timer: sys::Timer::new(),
}
}
pub struct OsInterval {
period: Duration,
behavior: MissedTickBehavior,
next_deadline: Instant,
armed_for: Option<Instant>,
timer: sys::Timer,
}
impl OsInterval {
#[must_use]
pub fn period(&self) -> Duration {
self.period
}
#[must_use]
pub fn missed_tick_behavior(&self) -> MissedTickBehavior {
self.behavior
}
pub fn set_missed_tick_behavior(&mut self, behavior: MissedTickBehavior) {
self.behavior = behavior;
}
pub fn reset(&mut self) {
self.set_next_deadline(Instant::now() + self.period);
}
pub fn reset_immediately(&mut self) {
self.set_next_deadline(Instant::now());
}
pub fn reset_after(&mut self, after: Duration) {
self.set_next_deadline(Instant::now() + after);
}
pub fn reset_at(&mut self, deadline: Instant) {
self.set_next_deadline(deadline);
}
fn set_next_deadline(&mut self, deadline: Instant) {
self.next_deadline = deadline;
self.armed_for = None;
self.timer.disarm();
}
pub async fn tick(&mut self) -> Instant {
poll_fn(|cx| self.poll_tick(cx)).await
}
pub fn poll_tick(&mut self, cx: &mut Context<'_>) -> Poll<Instant> {
let target = self.next_deadline;
let now = Instant::now();
if now >= target {
self.advance_after_tick(target, now);
return Poll::Ready(target);
}
if self.armed_for != Some(target) {
self.timer.arm(target);
self.armed_for = Some(target);
}
match self.timer.poll_expired(cx) {
Poll::Ready(()) => {
let now = Instant::now();
self.advance_after_tick(target, now);
Poll::Ready(target)
}
Poll::Pending => Poll::Pending,
}
}
fn advance_after_tick(&mut self, fired: Instant, now: Instant) {
self.next_deadline = self.behavior.next_deadline(fired, now, self.period);
self.armed_for = None;
}
}
impl std::fmt::Debug for OsInterval {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OsInterval")
.field("period", &self.period)
.field("missed_tick_behavior", &self.behavior)
.field("next_deadline", &self.next_deadline)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn skip_advances_to_next_multiple() {
let prev = Instant::now();
let period = Duration::from_millis(100);
let now = prev + Duration::from_millis(350);
let next = MissedTickBehavior::Skip.next_deadline(prev, now, period);
assert_eq!(next, prev + Duration::from_millis(400));
}
#[test]
fn burst_uses_period_only() {
let prev = Instant::now();
let period = Duration::from_millis(100);
let now = prev + Duration::from_millis(500);
let next = MissedTickBehavior::Burst.next_deadline(prev, now, period);
assert_eq!(next, prev + period);
}
#[test]
fn delay_uses_now_plus_period() {
let prev = Instant::now();
let period = Duration::from_millis(100);
let now = prev + Duration::from_millis(500);
let next = MissedTickBehavior::Delay.next_deadline(prev, now, period);
assert_eq!(next, now + period);
}
}