use std::time::Duration;
use super::{SchedulerTick, StepScheduler};
use crate::bus::{LocalInstant, RobotInstant, TimelineId};
use crate::participant::duration_nanos;
pub(crate) struct RealScheduler {
period: Option<Duration>,
timeline: TimelineId,
started_boot: LocalInstant,
started_timer: tokio::time::Instant,
started_ticks: u64,
}
impl RealScheduler {
pub(crate) fn new(period: Option<Duration>, now: RobotInstant) -> Option<Self> {
let started_boot = LocalInstant::try_now()?;
Some(RealScheduler {
period,
timeline: now.timeline(),
started_boot,
started_timer: tokio::time::Instant::now(),
started_ticks: now.ticks(),
})
}
fn timer_deadline_for(&self, target: RobotInstant) -> tokio::time::Instant {
let delta_ns = target.ticks().saturating_sub(self.started_ticks);
self.started_timer + Duration::from_nanos(delta_ns)
}
fn boot_elapsed(&self) -> Option<Duration> {
Some(LocalInstant::try_now()?.saturating_duration_since(self.started_boot))
}
fn resolve_tick(&self, elapsed: Duration, target_ticks: u64) -> (u64, u32) {
let fired_ticks = self
.started_ticks
.saturating_add(duration_nanos(elapsed))
.max(target_ticks);
let mut missed_ticks = 0u32;
if let Some(period) = self.period.filter(|period| !period.is_zero()) {
let period_ns = duration_nanos(period);
let overrun = fired_ticks.saturating_sub(target_ticks);
missed_ticks = u32::try_from(overrun / period_ns).unwrap_or(u32::MAX);
}
(fired_ticks, missed_ticks)
}
}
impl StepScheduler for RealScheduler {
async fn wait_until(&self, target: RobotInstant) -> SchedulerTick {
tokio::time::sleep_until(self.timer_deadline_for(target)).await;
let Some(elapsed) = self.boot_elapsed() else {
return SchedulerTick {
fired_at: target,
missed_ticks: 0,
};
};
let (fired_ticks, missed_ticks) = self.resolve_tick(elapsed, target.ticks());
SchedulerTick {
fired_at: RobotInstant::new(self.timeline, fired_ticks),
missed_ticks,
}
}
fn now(&self) -> Option<RobotInstant> {
Some(RobotInstant::new(
self.timeline,
self.started_ticks
.saturating_add(duration_nanos(self.boot_elapsed()?)),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn lt(ticks: u64) -> RobotInstant {
RobotInstant::new(
TimelineId::from_raw(1).expect("test timeline must be nonzero"),
ticks,
)
}
const PERIOD: Duration = Duration::from_millis(10);
fn anchored(period: Option<Duration>) -> RealScheduler {
RealScheduler::new(period, lt(0)).expect("test host clock")
}
#[tokio::test]
async fn real_scheduler_wakes_at_target_and_reports_no_miss_when_on_time() {
let start = tokio::time::Instant::now();
let scheduler = anchored(Some(PERIOD));
let period_ns = PERIOD.as_nanos() as u64;
let tick = scheduler.wait_until(lt(period_ns)).await;
assert_eq!(tick.missed_ticks, 0);
assert!(tick.fired_at.ticks() >= period_ns);
assert!(start.elapsed() >= PERIOD);
}
#[test]
fn real_cadence_collapses_a_missed_tick_instead_of_bursting() {
let period_ns = PERIOD.as_nanos() as u64;
let (fired, missed) =
anchored(Some(PERIOD)).resolve_tick(Duration::from_millis(50), period_ns);
assert_eq!(fired, 50_000_000);
assert_eq!(
missed, 4,
"a multi-period overrun collapses to one tick, reporting the skipped count"
);
}
#[test]
fn a_host_suspend_counts_as_missed_periods_rather_than_time_that_never_happened() {
let period_ns = PERIOD.as_nanos() as u64;
let (fired, missed) =
anchored(Some(PERIOD)).resolve_tick(Duration::from_secs(1), period_ns);
assert_eq!(
fired, 1_000_000_000,
"the released tick is where the host is"
);
assert_eq!(
missed, 99,
"one second of suspend is 99 further 10ms periods"
);
}
#[test]
fn a_tick_that_fires_on_time_reports_no_miss_and_no_period_means_no_collapse() {
let period_ns = PERIOD.as_nanos() as u64;
assert_eq!(
anchored(Some(PERIOD)).resolve_tick(PERIOD, period_ns),
(period_ns, 0)
);
assert_eq!(
anchored(None).resolve_tick(Duration::from_millis(50), period_ns),
(50_000_000, 0),
"a step-less participant has no period to collapse against"
);
}
}