Skip to main content

rusty_time_core/
vclock.rs

1//! The virtual clock: a disciplined *view* of time for hosts where the OS clock
2//! cannot be adjusted (wasm, unprivileged processes) — and the holdover model
3//! everywhere else.
4//!
5//! It never steps its output backwards: corrections that would reverse reported
6//! time are absorbed by freezing until real time catches up.
7
8#[derive(Clone, Copy, Debug)]
9pub struct VirtualClock {
10    /// Seconds to add to the raw wall clock.
11    offset: f64,
12    /// Raw clock's frequency error, ppm (positive = raw clock runs fast).
13    skew_ppm: f64,
14    /// Monotonic time of the last update.
15    last_update_mono: Option<f64>,
16    /// Error bound at the last update, seconds.
17    err_at_update: f64,
18    /// How fast the error bound grows with no updates, ppm.
19    err_growth_ppm: f64,
20    /// Highest corrected time handed out (monotonicity guard).
21    high_water: f64,
22}
23
24impl VirtualClock {
25    pub fn new() -> Self {
26        VirtualClock {
27            offset: 0.0,
28            skew_ppm: 0.0,
29            last_update_mono: None,
30            err_at_update: f64::INFINITY,
31            err_growth_ppm: 15.0, // an undisciplined crystal's typical wander budget
32            high_water: f64::NEG_INFINITY,
33        }
34    }
35
36    /// Feed a fresh measurement.
37    ///
38    /// * `mono` — monotonic seconds now.
39    /// * `offset` — seconds to ADD to the raw wall clock, measured now.
40    /// * `skew_ppm` — raw clock frequency error if known.
41    /// * `error_bound` — measurement error, seconds.
42    pub fn update(&mut self, mono: f64, offset: f64, skew_ppm: Option<f64>, error_bound: f64) {
43        self.offset = offset;
44        if let Some(s) = skew_ppm {
45            self.skew_ppm = s;
46        }
47        self.last_update_mono = Some(mono);
48        self.err_at_update = error_bound.max(0.0);
49    }
50
51    /// Corrected wall time. `raw_wall` is the platform clock reading at the same
52    /// instant `mono` was read.
53    pub fn now(&mut self, mono: f64, raw_wall: f64) -> f64 {
54        let corrected = match self.last_update_mono {
55            Some(t0) => {
56                let dt = (mono - t0).max(0.0);
57                // The raw clock has been drifting since the measurement.
58                raw_wall + self.offset - self.skew_ppm * 1e-6 * dt
59            }
60            None => raw_wall,
61        };
62        // Monotonicity guard: never hand out a time earlier than we already did.
63        if corrected < self.high_water {
64            self.high_water
65        } else {
66            self.high_water = corrected;
67            corrected
68        }
69    }
70
71    /// Current error bound, seconds — grows while no updates arrive. `INFINITY`
72    /// until the first update: callers gate decisions on this, so it must not
73    /// pretend precision it does not have.
74    pub fn confidence(&self, mono: f64) -> f64 {
75        match self.last_update_mono {
76            Some(t0) => {
77                let dt = (mono - t0).max(0.0);
78                self.err_at_update + self.err_growth_ppm * 1e-6 * dt
79            }
80            None => f64::INFINITY,
81        }
82    }
83
84    pub fn is_synchronized(&self) -> bool {
85        self.last_update_mono.is_some()
86    }
87}
88
89impl Default for VirtualClock {
90    fn default() -> Self {
91        Self::new()
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn unsynchronized_reports_infinite_error() {
101        let vc = VirtualClock::new();
102        assert!(vc.confidence(100.0).is_infinite());
103    }
104
105    #[test]
106    fn correction_and_skew_are_applied() {
107        let mut vc = VirtualClock::new();
108        // Raw clock 2 ms behind, running 10 ppm fast.
109        vc.update(1000.0, 0.002, Some(10.0), 1e-4);
110        let t = vc.now(1000.0, 5000.0);
111        assert!((t - 5000.002).abs() < 1e-9);
112        // 100 s later the raw clock gained another 1 ms; the model removes it.
113        let t = vc.now(1100.0, 5100.0);
114        assert!((t - 5100.001).abs() < 1e-9, "{t}");
115    }
116
117    #[test]
118    fn output_never_steps_backwards() {
119        let mut vc = VirtualClock::new();
120        vc.update(0.0, 0.0, None, 1e-4);
121        let t1 = vc.now(10.0, 100.0);
122        // A later measurement says we were 50 ms fast: raw correction would
123        // report an earlier time.
124        vc.update(10.0, -0.050, None, 1e-4);
125        let t2 = vc.now(10.001, 100.001);
126        assert!(t2 >= t1, "stepped backwards: {t1} -> {t2}");
127    }
128
129    #[test]
130    fn error_bound_grows_over_time() {
131        let mut vc = VirtualClock::new();
132        vc.update(0.0, 0.0, None, 1e-4);
133        let e0 = vc.confidence(0.0);
134        let e1 = vc.confidence(3600.0);
135        assert!(e1 > e0);
136        // 15 ppm for an hour ≈ 54 ms.
137        assert!((e1 - (1e-4 + 0.054)).abs() < 1e-3, "{e1}");
138    }
139}