nomad_protocol/transport/
timing.rs

1//! RTT estimation and timing utilities.
2//!
3//! Implements RFC 6298 RTT estimation algorithm as specified in 2-TRANSPORT.md.
4
5use std::time::{Duration, Instant};
6
7/// RTT timing constants from the protocol specification.
8pub mod constants {
9    use std::time::Duration;
10
11    /// Initial retransmission timeout before first RTT sample.
12    pub const INITIAL_RTO: Duration = Duration::from_millis(1000);
13
14    /// Minimum retransmission timeout.
15    pub const MIN_RTO: Duration = Duration::from_millis(100);
16
17    /// Maximum retransmission timeout.
18    pub const MAX_RTO: Duration = Duration::from_millis(60000);
19
20    /// Alpha for SRTT smoothing (0.125 = 1/8).
21    pub const SRTT_ALPHA: f64 = 0.125;
22
23    /// Beta for RTTVAR smoothing (0.25 = 1/4).
24    pub const RTTVAR_BETA: f64 = 0.25;
25
26    /// K multiplier for RTO calculation (4.0 per RFC 6298).
27    pub const RTO_K: f64 = 4.0;
28
29    /// Minimum RTT granularity for RTO calculation.
30    pub const MIN_RTO_GRANULARITY_MS: f64 = 100.0;
31}
32
33/// RTT estimator implementing RFC 6298.
34///
35/// This struct maintains smoothed RTT (SRTT) and RTT variance (RTTVAR) values,
36/// and computes an adaptive Retransmission Timeout (RTO).
37#[derive(Debug, Clone)]
38pub struct RttEstimator {
39    /// Smoothed RTT in milliseconds.
40    srtt: f64,
41    /// RTT variance in milliseconds.
42    rttvar: f64,
43    /// Current retransmission timeout.
44    rto: Duration,
45    /// Whether we've received the first RTT sample.
46    initialized: bool,
47}
48
49impl Default for RttEstimator {
50    fn default() -> Self {
51        Self::new()
52    }
53}
54
55impl RttEstimator {
56    /// Create a new RTT estimator with initial values.
57    pub fn new() -> Self {
58        Self {
59            srtt: 0.0,
60            rttvar: 0.0,
61            rto: constants::INITIAL_RTO,
62            initialized: false,
63        }
64    }
65
66    /// Update RTT estimate with a new sample.
67    ///
68    /// Implements RFC 6298 RTT calculation:
69    /// - First measurement: SRTT = sample, RTTVAR = sample / 2
70    /// - Subsequent: RTTVAR = 0.75 * RTTVAR + 0.25 * |SRTT - sample|
71    /// - SRTT = 0.875 * SRTT + 0.125 * sample
72    pub fn update(&mut self, sample: Duration) {
73        let sample_ms = sample.as_secs_f64() * 1000.0;
74
75        if !self.initialized {
76            // First measurement
77            self.srtt = sample_ms;
78            self.rttvar = sample_ms / 2.0;
79            self.initialized = true;
80        } else {
81            // Subsequent measurements (RFC 6298 algorithm)
82            // RTTVAR = (1 - beta) * RTTVAR + beta * |SRTT - R|
83            self.rttvar = (1.0 - constants::RTTVAR_BETA) * self.rttvar
84                + constants::RTTVAR_BETA * (self.srtt - sample_ms).abs();
85            // SRTT = (1 - alpha) * SRTT + alpha * R
86            self.srtt =
87                (1.0 - constants::SRTT_ALPHA) * self.srtt + constants::SRTT_ALPHA * sample_ms;
88        }
89
90        // RTO = SRTT + max(G, K * RTTVAR)
91        // where G is the clock granularity (we use 100ms minimum)
92        let rto_ms =
93            self.srtt + f64::max(constants::MIN_RTO_GRANULARITY_MS, constants::RTO_K * self.rttvar);
94
95        // Clamp to [MIN_RTO, MAX_RTO]
96        let rto_ms = rto_ms.clamp(
97            constants::MIN_RTO.as_millis() as f64,
98            constants::MAX_RTO.as_millis() as f64,
99        );
100
101        self.rto = Duration::from_millis(rto_ms as u64);
102    }
103
104    /// Get the current smoothed RTT.
105    pub fn srtt(&self) -> Duration {
106        Duration::from_secs_f64(self.srtt / 1000.0)
107    }
108
109    /// Get the current smoothed RTT in milliseconds.
110    pub fn srtt_ms(&self) -> f64 {
111        self.srtt
112    }
113
114    /// Get the current RTT variance.
115    pub fn rttvar(&self) -> Duration {
116        Duration::from_secs_f64(self.rttvar / 1000.0)
117    }
118
119    /// Get the current retransmission timeout.
120    pub fn rto(&self) -> Duration {
121        self.rto
122    }
123
124    /// Check if the estimator has been initialized with at least one sample.
125    pub fn is_initialized(&self) -> bool {
126        self.initialized
127    }
128
129    /// Apply exponential backoff to RTO (used after timeout).
130    ///
131    /// Returns the new RTO after doubling (capped at MAX_RTO).
132    pub fn backoff(&mut self) -> Duration {
133        let new_rto_ms = (self.rto.as_millis() as u64).saturating_mul(2);
134        self.rto = Duration::from_millis(new_rto_ms).min(constants::MAX_RTO);
135        self.rto
136    }
137
138    /// Reset RTO to initial value (e.g., after successful transmission).
139    pub fn reset_backoff(&mut self) {
140        if self.initialized {
141            // Recalculate RTO from current SRTT/RTTVAR
142            let rto_ms = self.srtt
143                + f64::max(constants::MIN_RTO_GRANULARITY_MS, constants::RTO_K * self.rttvar);
144            let rto_ms = rto_ms.clamp(
145                constants::MIN_RTO.as_millis() as f64,
146                constants::MAX_RTO.as_millis() as f64,
147            );
148            self.rto = Duration::from_millis(rto_ms as u64);
149        } else {
150            self.rto = constants::INITIAL_RTO;
151        }
152    }
153}
154
155/// Timestamp tracker for RTT measurement via timestamp echo.
156///
157/// Each frame carries a timestamp and echoes the peer's timestamp.
158/// When we receive an echo of our timestamp, we can compute RTT.
159#[derive(Debug, Clone)]
160pub struct TimestampTracker {
161    /// Session start time (all timestamps are relative to this).
162    session_start: Instant,
163    /// Most recent timestamp we received from peer (for echoing).
164    last_peer_timestamp: u32,
165    /// Our timestamp that we're waiting to be echoed.
166    pending_timestamp: Option<u32>,
167    /// When we sent the frame with pending_timestamp.
168    pending_send_time: Option<Instant>,
169}
170
171impl TimestampTracker {
172    /// Create a new timestamp tracker.
173    pub fn new() -> Self {
174        Self {
175            session_start: Instant::now(),
176            last_peer_timestamp: 0,
177            pending_timestamp: None,
178            pending_send_time: None,
179        }
180    }
181
182    /// Create a timestamp tracker with a specific start time.
183    pub fn with_start(start: Instant) -> Self {
184        Self {
185            session_start: start,
186            last_peer_timestamp: 0,
187            pending_timestamp: None,
188            pending_send_time: None,
189        }
190    }
191
192    /// Get the current timestamp (ms since session start).
193    pub fn now(&self) -> u32 {
194        self.session_start.elapsed().as_millis() as u32
195    }
196
197    /// Get the timestamp echo value (peer's last timestamp).
198    pub fn timestamp_echo(&self) -> u32 {
199        self.last_peer_timestamp
200    }
201
202    /// Record that we're sending a frame with the given timestamp.
203    pub fn on_send(&mut self, timestamp: u32) {
204        self.pending_timestamp = Some(timestamp);
205        self.pending_send_time = Some(Instant::now());
206    }
207
208    /// Process a received frame's timestamps.
209    ///
210    /// Returns an RTT sample if the echo matches our pending timestamp.
211    pub fn on_receive(&mut self, peer_timestamp: u32, echo: u32) -> Option<Duration> {
212        // Update the timestamp we'll echo back
213        self.last_peer_timestamp = peer_timestamp;
214
215        // Check if this echoes our pending timestamp
216        if let (Some(pending), Some(send_time)) = (self.pending_timestamp, self.pending_send_time)
217            && echo == pending
218        {
219            let rtt = send_time.elapsed();
220            self.pending_timestamp = None;
221            self.pending_send_time = None;
222            return Some(rtt);
223        }
224
225        None
226    }
227
228    /// Clear the pending timestamp (e.g., on retransmission).
229    pub fn clear_pending(&mut self) {
230        self.pending_timestamp = None;
231        self.pending_send_time = None;
232    }
233}
234
235impl Default for TimestampTracker {
236    fn default() -> Self {
237        Self::new()
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[test]
246    fn test_rtt_estimator_initial() {
247        let estimator = RttEstimator::new();
248        assert!(!estimator.is_initialized());
249        assert_eq!(estimator.rto(), constants::INITIAL_RTO);
250    }
251
252    #[test]
253    fn test_rtt_estimator_first_sample() {
254        let mut estimator = RttEstimator::new();
255        estimator.update(Duration::from_millis(100));
256
257        assert!(estimator.is_initialized());
258        assert!((estimator.srtt_ms() - 100.0).abs() < 0.01);
259        assert!((estimator.rttvar - 50.0).abs() < 0.01); // sample / 2
260    }
261
262    #[test]
263    fn test_rtt_estimator_multiple_samples() {
264        let mut estimator = RttEstimator::new();
265
266        // First sample: 100ms
267        estimator.update(Duration::from_millis(100));
268        let srtt1 = estimator.srtt_ms();
269
270        // Second sample: 120ms
271        estimator.update(Duration::from_millis(120));
272        let srtt2 = estimator.srtt_ms();
273
274        // SRTT should move toward the new sample
275        assert!(srtt2 > srtt1);
276        assert!(srtt2 < 120.0);
277    }
278
279    #[test]
280    fn test_rtt_estimator_backoff() {
281        let mut estimator = RttEstimator::new();
282        estimator.update(Duration::from_millis(100));
283
284        let rto1 = estimator.rto();
285        let rto2 = estimator.backoff();
286
287        // RTO should double
288        assert!(rto2 > rto1);
289        assert!(rto2 <= constants::MAX_RTO);
290    }
291
292    #[test]
293    fn test_rtt_estimator_max_rto() {
294        let mut estimator = RttEstimator::new();
295        estimator.update(Duration::from_millis(100));
296
297        // Keep backing off until we hit max
298        for _ in 0..20 {
299            estimator.backoff();
300        }
301
302        assert_eq!(estimator.rto(), constants::MAX_RTO);
303    }
304
305    #[test]
306    fn test_rtt_estimator_min_rto() {
307        let mut estimator = RttEstimator::new();
308
309        // Very small RTT sample
310        estimator.update(Duration::from_micros(100));
311
312        // RTO should still be at least MIN_RTO
313        assert!(estimator.rto() >= constants::MIN_RTO);
314    }
315
316    #[test]
317    fn test_timestamp_tracker_echo() {
318        let start = Instant::now();
319        let mut tracker = TimestampTracker::with_start(start);
320
321        // Send a frame
322        tracker.on_send(1000);
323
324        // Receive a frame with echo of our timestamp
325        std::thread::sleep(Duration::from_millis(10));
326        let rtt = tracker.on_receive(2000, 1000);
327
328        assert!(rtt.is_some());
329        let rtt = rtt.unwrap();
330        assert!(rtt >= Duration::from_millis(10));
331    }
332
333    #[test]
334    fn test_timestamp_tracker_no_match() {
335        let start = Instant::now();
336        let mut tracker = TimestampTracker::with_start(start);
337
338        // Send a frame
339        tracker.on_send(1000);
340
341        // Receive a frame with different echo (not our timestamp)
342        let rtt = tracker.on_receive(2000, 999);
343
344        assert!(rtt.is_none());
345        // Pending timestamp should still be waiting
346        assert!(tracker.pending_timestamp.is_some());
347    }
348
349    #[test]
350    fn test_timestamp_tracker_peer_timestamp() {
351        let mut tracker = TimestampTracker::new();
352
353        // Initially no peer timestamp
354        assert_eq!(tracker.timestamp_echo(), 0);
355
356        // Receive frame from peer
357        tracker.on_receive(5000, 0);
358
359        // Now we have peer's timestamp to echo
360        assert_eq!(tracker.timestamp_echo(), 5000);
361    }
362}