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>,
carry: Vec<u64>,
served: Vec<bool>,
}
impl GridTimer {
pub(crate) fn new(epoch: u64, periods: Vec<u64>) -> Self {
let len = periods.len();
let next = periods.iter().map(|p| epoch.saturating_add(*p)).collect();
Self {
epoch,
period_ns: periods,
next,
carry: vec![0; len],
served: vec![false; len],
}
}
#[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, u64, u64)>) {
due.clear();
for (i, next) in self.next.iter_mut().enumerate() {
if now >= *next {
let carry = std::mem::take(&mut self.carry[i]);
let period = self.period_ns[i];
if period == 0 {
due.push((i, carry, 0));
continue;
}
let stepped = next.saturating_add(period);
*next = if stepped > now {
stepped
} else {
let slots_passed = now.saturating_sub(self.epoch) / period;
let snapped = self
.epoch
.saturating_add(slots_passed.saturating_add(1).saturating_mul(period));
if self.served[i] {
self.carry[i] = snapped.saturating_sub(stepped) / period;
}
snapped
};
let late_by = now.saturating_sub(next.saturating_sub(period));
due.push((i, carry, late_by));
self.served[i] = true;
}
}
}
}
#[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, 0, 5)]);
assert_eq!(t.next_target(0), 2000);
t.take_due(2012, &mut due);
assert_eq!(due, vec![(0, 0, 12)]);
assert_eq!(t.next_target(0), 3000);
t.take_due(2999, &mut due);
assert_eq!(due, Vec::<(usize, u64, u64)>::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, 0, 500)]);
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, 0, 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, 0, 0)]);
assert_eq!(t.next_timeout(1000), Duration::from_nanos(1000));
t.take_due(2000, &mut due);
assert_eq!(due, vec![(0, 0, 0), (1, 0, 0)]);
assert_eq!(t.next_target(0), 3000);
assert_eq!(t.next_target(1), 4000);
}
#[test]
fn due_entries_carry_zero_skips_in_steady_state() {
let mut t = GridTimer::new(0, vec![1000]);
let mut due = Vec::new();
t.take_due(1002, &mut due);
assert_eq!(due, vec![(0, 0, 2)]);
t.take_due(2005, &mut due);
assert_eq!(due, vec![(0, 0, 5)]);
}
#[test]
fn realign_carries_abandoned_slots_to_the_next_dispatch_exactly_once() {
let mut t = GridTimer::new(0, vec![1000]);
let mut due = Vec::new();
t.take_due(1002, &mut due); assert_eq!(due, vec![(0, 0, 2)]);
t.take_due(3500, &mut due);
assert_eq!(due, vec![(0, 0, 500)]);
assert_eq!(t.next_target(0), 4000);
t.take_due(4000, &mut due);
assert_eq!(due, vec![(0, 1, 0)]);
t.take_due(5000, &mut due);
assert_eq!(due, vec![(0, 0, 0)]);
}
#[test]
fn realign_on_a_tasks_first_dispatch_sets_no_carry() {
let mut t = GridTimer::new(0, vec![1000]);
let mut due = Vec::new();
t.take_due(3500, &mut due); assert_eq!(due, vec![(0, 0, 500)]);
assert_eq!(t.next_target(0), 4000);
t.take_due(4000, &mut due); assert_eq!(due, vec![(0, 0, 0)]);
}
#[test]
fn due_entries_report_late_by_vs_the_last_passed_grid_point() {
let mut t = GridTimer::new(0, vec![1000]);
let mut due = Vec::new();
t.take_due(1700, &mut due);
assert_eq!(due, vec![(0, 0, 700)]);
t.take_due(3400, &mut due);
assert_eq!(due, vec![(0, 0, 400)]);
}
#[test]
fn multi_slot_starvation_carries_the_full_abandoned_count() {
let mut t = GridTimer::new(0, vec![1000]);
let mut due = Vec::new();
t.take_due(1000, &mut due); t.take_due(5500, &mut due);
assert_eq!(due, vec![(0, 0, 500)]);
assert_eq!(t.next_target(0), 6000);
t.take_due(6000, &mut due);
assert_eq!(due, vec![(0, 3, 0)]);
}
#[test]
fn back_to_back_realigns_hand_over_carry_without_loss_or_doubling() {
let mut t = GridTimer::new(0, vec![1000]);
let mut due = Vec::new();
t.take_due(1000, &mut due);
assert_eq!(due, vec![(0, 0, 0)]);
t.take_due(4500, &mut due);
assert_eq!(due, vec![(0, 0, 500)]);
assert_eq!(t.next_target(0), 5000);
t.take_due(7500, &mut due);
assert_eq!(due, vec![(0, 2, 500)]);
assert_eq!(t.next_target(0), 8000);
t.take_due(8000, &mut due);
assert_eq!(due, vec![(0, 2, 0)]);
t.take_due(9000, &mut due);
assert_eq!(due, vec![(0, 0, 0)]);
}
}