1pub struct ClockState {
12 alpha: f64,
13 offset_ns: f64,
14 rtt_ns: f64,
15 samples: u32,
16 bootstrap_count: u32,
17}
18
19impl ClockState {
20 pub fn new() -> Self {
21 Self {
22 alpha: 0.125,
23 offset_ns: 0.0,
24 rtt_ns: 0.0,
25 samples: 0,
26 bootstrap_count: 10,
27 }
28 }
29
30 pub fn update(&mut self, t1: u64, t2: u64, t3: u64, t4: u64) {
31 let offset = ((t2 as i128 - t1 as i128) + (t3 as i128 - t4 as i128)) as f64 / 2.0;
32 let rtt = ((t4 - t1) - (t3 - t2)) as f64;
33
34 if self.samples == 0 {
35 self.offset_ns = offset;
36 self.rtt_ns = rtt;
37 } else {
38 let alpha = if self.samples < self.bootstrap_count {
39 0.5
40 } else {
41 self.alpha
42 };
43 self.offset_ns = self.offset_ns * (1.0 - alpha) + offset * alpha;
44 self.rtt_ns = self.rtt_ns * (1.0 - alpha) + rtt * alpha;
45 }
46 self.samples += 1;
47 }
48
49 pub fn offset_ns(&self) -> i64 {
50 self.offset_ns.round() as i64
51 }
52
53 pub fn rtt_ns(&self) -> u64 {
54 self.rtt_ns.round() as u64
55 }
56
57 pub fn is_bootstrapped(&self) -> bool {
58 self.samples >= self.bootstrap_count
59 }
60
61 pub fn samples(&self) -> u32 {
62 self.samples
63 }
64
65 pub fn local_to_controller(&self, local_ns: u64) -> u64 {
67 (local_ns as i64 + self.offset_ns()) as u64
68 }
69
70 pub fn controller_to_local(&self, controller_ns: u64) -> u64 {
72 (controller_ns as i64 - self.offset_ns()) as u64
73 }
74}
75
76impl Default for ClockState {
77 fn default() -> Self {
78 Self::new()
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn zero_offset_symmetric() {
88 let mut clock = ClockState::new();
89 clock.update(1000, 1010, 1015, 1025);
91 assert_eq!(clock.offset_ns(), 0);
92 assert_eq!(clock.rtt_ns(), 20);
93 }
94
95 #[test]
96 fn positive_offset() {
97 let mut clock = ClockState::new();
98 clock.update(1000, 1110, 1115, 1025);
100 assert_eq!(clock.offset_ns(), 100);
101 }
102
103 #[test]
104 fn ema_convergence() {
105 let mut clock = ClockState::new();
106 for _ in 0..100 {
107 clock.update(1000, 1060, 1065, 1025);
109 }
110 let offset = clock.offset_ns();
111 assert!(
112 (offset - 50).abs() < 2,
113 "offset should converge to ~50, got {offset}"
114 );
115 }
116
117 #[test]
118 fn bootstrap_phase() {
119 let clock = ClockState::new();
120 assert!(!clock.is_bootstrapped());
121 }
122}