use std::time::{Duration, Instant};
pub trait CyclicClock: Send + Sync + 'static {
fn now_nanos(&self) -> u64;
}
#[derive(Debug)]
pub struct MonotonicCyclicClock {
epoch: Instant,
}
impl MonotonicCyclicClock {
#[must_use]
pub fn new() -> Self {
Self {
epoch: Instant::now(),
}
}
}
impl Default for MonotonicCyclicClock {
fn default() -> Self {
Self::new()
}
}
impl CyclicClock for MonotonicCyclicClock {
fn now_nanos(&self) -> u64 {
u64::try_from(self.epoch.elapsed().as_nanos()).unwrap_or(u64::MAX)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DispatchMode {
Grid,
Legacy,
}
impl Default for DispatchMode {
fn default() -> Self {
if cfg!(target_os = "linux") {
Self::Grid
} else {
Self::Legacy
}
}
}
#[allow(clippy::redundant_pub_crate)]
#[derive(Debug)]
pub(crate) struct GridTimer {
epoch: u64,
period_ns: Vec<u64>,
next: Vec<u64>,
}
impl GridTimer {
pub(crate) fn new(epoch: u64, periods: Vec<u64>) -> Self {
let next = periods.iter().map(|p| epoch.saturating_add(*p)).collect();
Self {
epoch,
period_ns: periods,
next,
}
}
#[cfg_attr(target_os = "linux", allow(dead_code))]
pub(crate) fn next_timeout(&self, now: u64) -> Duration {
let Some(earliest) = self.next.iter().copied().min() else {
return Duration::MAX;
};
Duration::from_nanos(earliest.saturating_sub(now))
}
#[cfg(test)]
pub(crate) fn next_target(&self, i: usize) -> u64 {
self.next[i]
}
pub(crate) fn take_due(&mut self, now: u64, due: &mut Vec<usize>) {
due.clear();
for (i, next) in self.next.iter_mut().enumerate() {
if now >= *next {
due.push(i);
let period = self.period_ns[i];
if period == 0 {
continue;
}
let stepped = next.saturating_add(period);
*next = if stepped > now {
stepped
} else {
let slots_passed = now.saturating_sub(self.epoch) / period;
self.epoch
.saturating_add(slots_passed.saturating_add(1).saturating_mul(period))
};
}
}
}
}
#[allow(clippy::redundant_pub_crate)]
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub(crate) fn base_period(periods: &[u64]) -> u64 {
periods.iter().copied().filter(|p| *p != 0).fold(0, gcd)
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
const fn gcd(a: u64, b: u64) -> u64 {
if b == 0 { a } else { gcd(b, a % b) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn monotonic_cyclic_clock_is_non_decreasing() {
let c = MonotonicCyclicClock::new();
let a = c.now_nanos();
let b = c.now_nanos();
assert!(
b >= a,
"CLOCK_MONOTONIC must not go backwards: {a} then {b}"
);
}
#[test]
fn dispatch_mode_default_is_grid_on_linux_legacy_elsewhere() {
#[cfg(target_os = "linux")]
assert_eq!(DispatchMode::default(), DispatchMode::Grid);
#[cfg(not(target_os = "linux"))]
assert_eq!(DispatchMode::default(), DispatchMode::Legacy);
}
#[test]
fn single_period_advances_on_absolute_grid_with_zero_drift() {
let mut t = GridTimer::new(0, vec![1000]);
let mut due = Vec::new();
t.take_due(1005, &mut due);
assert_eq!(due, vec![0]);
assert_eq!(t.next_target(0), 2000);
t.take_due(2012, &mut due);
assert_eq!(due, vec![0]);
assert_eq!(t.next_target(0), 3000);
t.take_due(2999, &mut due);
assert_eq!(due, Vec::<usize>::new());
assert_eq!(t.next_target(0), 3000);
}
#[test]
fn stall_skips_whole_slots_and_dispatches_once() {
let mut t = GridTimer::new(0, vec![1000]);
let mut due = Vec::new();
t.take_due(3500, &mut due);
assert_eq!(due, vec![0]);
assert_eq!(t.next_target(0), 4000);
assert!(
t.next_target(0) > 3500,
"target must be strictly in the future"
);
}
#[test]
fn stall_realign_is_exact_on_a_slot_boundary() {
let mut t = GridTimer::new(0, vec![1000]);
let mut due = Vec::new();
t.take_due(3000, &mut due);
assert_eq!(due, vec![0]);
assert_eq!(t.next_target(0), 4000);
}
#[test]
#[allow(clippy::duration_suboptimal_units)]
fn next_timeout_is_distance_to_earliest_target() {
let t = GridTimer::new(0, vec![1000]);
assert_eq!(t.next_timeout(0), Duration::from_nanos(1000));
assert_eq!(t.next_timeout(250), Duration::from_nanos(750));
assert_eq!(t.next_timeout(1500), Duration::from_nanos(0));
}
#[test]
fn empty_grid_next_timeout_is_duration_max() {
let t = GridTimer::new(0, vec![]);
assert_eq!(t.next_timeout(0), Duration::MAX);
assert_eq!(t.next_timeout(12_345), Duration::MAX);
}
#[test]
fn base_period_is_gcd_of_declared_periods() {
assert_eq!(base_period(&[1_000_000]), 1_000_000); assert_eq!(base_period(&[2_000_000, 4_000_000]), 2_000_000); assert_eq!(base_period(&[2_000_000, 3_000_000]), 1_000_000); assert_eq!(base_period(&[1_000_000, 1_000_000]), 1_000_000); assert_eq!(base_period(&[0, 2_000_000]), 2_000_000); assert_eq!(base_period(&[]), 0); }
#[test]
#[allow(clippy::duration_suboptimal_units)]
fn multi_period_picks_earliest_and_coalesces_coincident_slots() {
let mut t = GridTimer::new(0, vec![1000, 2000]);
let mut due = Vec::new();
assert_eq!(t.next_timeout(0), Duration::from_nanos(1000));
t.take_due(1000, &mut due);
assert_eq!(due, vec![0]);
assert_eq!(t.next_timeout(1000), Duration::from_nanos(1000));
t.take_due(2000, &mut due);
assert_eq!(due, vec![0, 1]);
assert_eq!(t.next_target(0), 3000);
assert_eq!(t.next_target(1), 4000);
}
}