use std::time::{Duration, SystemTime, UNIX_EPOCH};
const STEP_THRESHOLD_MS: i128 = 5_000;
fn wall_now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
.try_into()
.unwrap_or(u64::MAX)
}
fn monotonic_now() -> Duration {
subc_uptime::suspend_inclusive_now()
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct StartClock {
started_monotonic: Duration,
}
impl StartClock {
pub(crate) fn capture() -> Self {
Self {
started_monotonic: monotonic_now(),
}
}
pub(crate) fn started_at_ms(self) -> u64 {
self.derive(wall_now_ms(), monotonic_now())
}
fn derive(self, wall_ms: u64, monotonic: Duration) -> u64 {
let elapsed_ms = monotonic.saturating_sub(self.started_monotonic).as_millis();
wall_ms.saturating_sub(elapsed_ms.try_into().unwrap_or(u64::MAX))
}
}
#[derive(Debug)]
pub(crate) struct ClockStepDetector {
previous_offset_ms: Option<i128>,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct ClockStep {
pub(crate) delta_ms: i128,
pub(crate) old_offset_ms: i128,
pub(crate) new_offset_ms: i128,
}
impl ClockStep {
pub(crate) fn old_clock_reading(&self, wall_now: SystemTime) -> SystemTime {
let delta =
Duration::from_millis(self.delta_ms.unsigned_abs().try_into().unwrap_or(u64::MAX));
if self.delta_ms > 0 {
wall_now.checked_sub(delta).unwrap_or(UNIX_EPOCH)
} else {
wall_now.checked_add(delta).unwrap_or(wall_now)
}
}
}
impl ClockStepDetector {
pub(crate) fn new() -> Self {
Self {
previous_offset_ms: None,
}
}
pub(crate) fn check_now(&mut self) -> Option<ClockStep> {
self.observe(wall_now_ms(), monotonic_now())
}
fn observe(&mut self, wall_ms: u64, monotonic: Duration) -> Option<ClockStep> {
let offset = i128::from(wall_ms) - monotonic.as_millis() as i128;
let previous = self.previous_offset_ms.replace(offset)?;
let delta = offset - previous;
(delta.abs() > STEP_THRESHOLD_MS).then_some(ClockStep {
delta_ms: delta,
old_offset_ms: previous,
new_offset_ms: offset,
})
}
}
#[cfg(test)]
mod tests {
#[test]
fn old_clock_reading_undoes_the_step_in_both_directions() {
let now = UNIX_EPOCH + Duration::from_millis(1_790_000_000_000);
let backward = ClockStep {
delta_ms: -7_200_000,
old_offset_ms: 0,
new_offset_ms: -7_200_000,
};
assert_eq!(
backward.old_clock_reading(now),
now + Duration::from_millis(7_200_000)
);
let forward = ClockStep {
delta_ms: 9_000,
old_offset_ms: 0,
new_offset_ms: 9_000,
};
assert_eq!(
forward.old_clock_reading(now),
now - Duration::from_millis(9_000)
);
}
use super::*;
fn seconds(value: u64) -> Duration {
Duration::from_secs(value)
}
#[test]
fn backward_wall_step_moves_derived_start_by_exact_step() {
let start = StartClock {
started_monotonic: seconds(100),
};
assert_eq!(start.derive(1_000_000, seconds(140)), 960_000);
assert_eq!(start.derive(992_800, seconds(140)), 952_800);
}
#[test]
fn advancing_monotonic_and_wall_leaves_derived_start_stable() {
let start = StartClock {
started_monotonic: seconds(100),
};
assert_eq!(start.derive(1_000_000, seconds(140)), 960_000);
assert_eq!(start.derive(1_020_000, seconds(160)), 960_000);
}
#[test]
fn detector_ignores_noise_and_reports_each_step_only_once() {
let mut detector = ClockStepDetector::new();
assert_eq!(detector.observe(1_000_000, seconds(100)), None);
assert_eq!(detector.observe(1_003_000, seconds(102)), None);
assert_eq!(
detector.observe(1_013_000, seconds(103)),
Some(ClockStep {
delta_ms: 9_000,
old_offset_ms: 901_000,
new_offset_ms: 910_000,
})
);
assert_eq!(detector.observe(1_014_000, seconds(104)), None);
assert_eq!(
detector.observe(1_006_000, seconds(105)),
Some(ClockStep {
delta_ms: -9_000,
old_offset_ms: 910_000,
new_offset_ms: 901_000,
})
);
assert_eq!(detector.observe(1_007_000, seconds(106)), None);
}
}