use crate::clock_sync::ClockSyncEstimator;
use crate::rtt::RttEstimator;
const CLOCK_WINDOW: usize = 32;
#[derive(Debug, Clone, Copy)]
pub struct Probe {
epoch: u32,
pub sent_at: u64,
}
#[derive(Debug, Clone)]
pub struct Timeline {
pub rtt: RttEstimator,
pub clock: ClockSyncEstimator,
epoch: u32,
stamp: Option<(u64, u64)>,
}
impl Timeline {
pub fn new() -> Self {
Self::with_estimators(RttEstimator::default(), ClockSyncEstimator::new(CLOCK_WINDOW))
}
pub fn with_estimators(rtt: RttEstimator, clock: ClockSyncEstimator) -> Self {
Self {
rtt,
clock,
epoch: 0,
stamp: None,
}
}
pub fn begin(&self, now: u64) -> Probe {
Probe {
epoch: self.epoch,
sent_at: now,
}
}
pub fn complete(&mut self, probe: Probe, now: u64, responder: Option<u64>) -> bool {
if probe.epoch != self.epoch {
return false;
}
self.rtt.observe_pong(probe.sent_at, now);
if let Some(responder) = responder {
self
.clock
.observe_exchange(probe.sent_at as f64, responder as f64, now as f64);
}
true
}
pub fn on_reconnect(&mut self) {
self.epoch = self.epoch.wrapping_add(1);
}
pub fn on_resume(&mut self) {
self.epoch = self.epoch.wrapping_add(1);
self.rtt.clear();
self.clock.clear();
}
pub fn epoch(&self) -> u32 {
self.epoch
}
pub fn note_stamp(&mut self, stamp_ms: u64, now_ms: u64) {
if stamp_ms >= self.stamp.map_or(0, |(newest, _)| newest) {
self.stamp = Some((stamp_ms, now_ms));
}
}
pub fn newest_stamp_ms(&self) -> u64 {
self.stamp.map_or(0, |(newest, _)| newest)
}
pub fn server_time_ms(&self, now_ms: u64) -> u64 {
let fitted = self.clock.server_time_at(now_ms as f64).unwrap_or(now_ms as f64).max(0.0) as u64;
match self.stamp {
Some((newest, at_local)) => fitted.max(newest + now_ms.saturating_sub(at_local)),
None => fitted,
}
}
}
impl Default for Timeline {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_completed_probe_feeds_both_estimators() {
let mut t = Timeline::new();
let probe = t.begin(1000);
assert!(t.complete(probe, 1100, Some(5550)));
assert_eq!(t.rtt.rtt(), Some(100.0));
assert_eq!(t.clock.offset_at(1100.0), Some(4500.0));
}
#[test]
fn without_a_responder_only_the_round_trip_is_learned() {
let mut t = Timeline::new();
let probe = t.begin(1000);
assert!(t.complete(probe, 1100, None));
assert_eq!(t.rtt.rtt(), Some(100.0));
assert_eq!(t.clock.sample_count(), 0, "no reading to compare against");
}
#[test]
fn a_probe_from_before_a_reconnect_is_refused() {
let mut t = Timeline::new();
let stale = t.begin(1000);
t.on_reconnect();
assert!(!t.complete(stale, 400_000, None), "discarded rather than recorded");
assert_eq!(t.rtt.rtt(), None);
}
#[test]
fn a_reconnect_keeps_what_was_learned_and_a_resume_does_not() {
let mut t = Timeline::new();
let probe = t.begin(1000);
t.complete(probe, 1100, Some(5550));
t.on_reconnect();
assert_eq!(t.rtt.rtt(), Some(100.0));
t.on_resume();
assert_eq!(t.rtt.rtt(), None);
assert_eq!(t.clock.sample_count(), 0);
}
#[test]
fn the_newest_stamp_floors_the_estimate_and_is_carried_at_wall_rate() {
let mut t = Timeline::new();
t.note_stamp(100_000, 500);
assert_eq!(t.newest_stamp_ms(), 100_000);
assert_eq!(t.server_time_ms(500), 100_000);
assert_eq!(t.server_time_ms(700), 100_200, "carried forward at wall rate between messages");
t.note_stamp(99_000, 800);
assert_eq!(t.newest_stamp_ms(), 100_000, "an older stamp moves nothing");
}
#[test]
fn a_converged_fit_ahead_of_the_stamp_wins() {
let mut t = Timeline::new();
let probe = t.begin(1000);
t.complete(probe, 1100, Some(5550));
t.note_stamp(3000, 1100);
assert_eq!(t.server_time_ms(1100), 5600, "the fit (offset 4500) is past the carried stamp");
}
#[test]
fn probes_started_after_the_bump_count_again() {
let mut t = Timeline::new();
t.on_resume();
let fresh = t.begin(2000);
assert!(t.complete(fresh, 2050, None));
assert_eq!(t.rtt.rtt(), Some(50.0));
}
}