1
//! Round Trip Time measurement (ยง 2.1)
2

            
3
use std::cmp::{max, min};
4
use std::collections::VecDeque;
5
use std::sync::atomic::{AtomicBool, Ordering};
6
use web_time_compat::{Duration, Instant};
7

            
8
use super::params::RoundTripEstimatorParams;
9
use super::{CongestionWindow, State};
10

            
11
use thiserror::Error;
12
use tor_error::{ErrorKind, HasKind};
13

            
14
/// An error originating from the tor-congestion crate.
15
#[derive(Error, Debug, Clone)]
16
#[non_exhaustive]
17
pub(crate) enum Error {
18
    /// A call to `RoundtripTimeEstimator::sendme_received` was made without calling
19
    /// `RoundtripTimeEstimator::expect_sendme` first.
20
    #[error("Informed of a SENDME we weren't expecting")]
21
    MismatchedEstimationCall,
22
}
23

            
24
impl HasKind for Error {
25
    fn kind(&self) -> ErrorKind {
26
        use Error as E;
27
        match self {
28
            E::MismatchedEstimationCall => ErrorKind::TorProtocolViolation,
29
        }
30
    }
31
}
32

            
33
/// Provides an estimate of the round-trip time (RTT) of a Tor circuit.
34
#[derive(Debug)]
35
#[allow(dead_code)]
36
pub(crate) struct RoundtripTimeEstimator {
37
    /// A queue of times we sent a cell that we'd expect a SENDME for.
38
    ///
39
    /// When a data cell is sent and for which we expect a SENDME next, the timestamp at the send
40
    /// is kept in this queue so we can use it to measure the RTT when the SENDME is received.
41
    ///
42
    /// A queue is used here because the protocol allows to send all pending SENDMEs at once as
43
    /// long as it is within one congestion window.
44
    sendme_expected_from: VecDeque<Instant>,
45
    /// The last *measured* round-trip time.
46
    ///
47
    /// This is `None` iff we have not managed to get any estimate yet.
48
    last_rtt: Option<Duration>,
49
    /// The current smoothed *estimate* of what the round-trip time is.
50
    ///
51
    /// This is `None` iff we have not managed to get any estimate yet.
52
    ewma_rtt: Option<Duration>,
53
    /// The minimum observed value of `last_rtt`.
54
    ///
55
    /// This is `None` iff we have not managed to get any estimate yet.
56
    min_rtt: Option<Duration>,
57
    /// The maximum observed value of `last_rtt`.
58
    ///
59
    /// This is `None` iff we have not managed to get any estimate yet.
60
    max_rtt: Option<Duration>,
61
    /// The network parameters we're using.
62
    params: RoundTripEstimatorParams,
63
    /// A reference to a shared boolean for storing if the clock is stalled or not.
64
    /// Spec: CLOCK_HEURISTICS from prop324. See is_clock_stalled() for the implementation.
65
    clock_stalled: AtomicBool,
66
}
67

            
68
#[allow(dead_code)]
69
impl RoundtripTimeEstimator {
70
    /// Create a new `RoundtripTimeEstimator`, using a set of `NetParameters` and a shared boolean
71
    /// to cache clock stalled state in.
72
554
    pub(crate) fn new(params: &RoundTripEstimatorParams) -> Self {
73
554
        Self {
74
554
            sendme_expected_from: Default::default(),
75
554
            last_rtt: None,
76
554
            ewma_rtt: None,
77
554
            min_rtt: None,
78
554
            max_rtt: None,
79
554
            params: params.clone(),
80
554
            clock_stalled: AtomicBool::default(),
81
554
        }
82
554
    }
83

            
84
    /// Return true iff the estimator is ready to be used or read.
85
156
    pub(crate) fn is_ready(&self) -> bool {
86
156
        !self.clock_stalled() && self.last_rtt.is_some()
87
156
    }
88

            
89
    /// Return the state of the clock stalled indicator.
90
234
    pub(crate) fn clock_stalled(&self) -> bool {
91
234
        self.clock_stalled.load(Ordering::SeqCst)
92
234
    }
93

            
94
    /// Return the EWMA RTT or `None` if we don't have an estimate yet.
95
1338
    pub(crate) fn ewma_rtt(&self) -> Option<Duration> {
96
1338
        self.ewma_rtt
97
1338
    }
98

            
99
    /// Return the Minimum RTT or `None` if we don't have an estimate yet.
100
138
    pub(crate) fn min_rtt(&self) -> Option<Duration> {
101
138
        self.min_rtt
102
138
    }
103

            
104
    /// Return the maximum observed RTT in usec or `None` if we don't have an estimate yet.
105
38
    pub(crate) fn max_rtt(&self) -> Option<Duration> {
106
38
        self.max_rtt
107
38
    }
108

            
109
    /// Inform the estimator that we did (at time `now`) something that we'll expect a SENDME to
110
    /// be received for.
111
87
    pub(crate) fn expect_sendme(&mut self, now: Instant) {
112
87
        self.sendme_expected_from.push_back(now);
113
87
    }
114

            
115
    /// Return whether we can use heuristics to sanity-check RTT values against our EWMA value.
116
    /// Spec: 2.1.1. Clock Jump Heuristics CLOCK_HEURISTICS
117
    ///
118
    /// Used in [`is_clock_stalled`](RoundtripTimeEstimator::is_clock_stalled), to check the sanity of
119
    /// a newly measured RTT value.
120
87
    fn can_crosscheck_with_current_estimate(&self, in_slow_start: bool) -> bool {
121
        // If we're in slow start, we don't perform any sanity checks, as per spec. If we don't
122
        // have a current estimate, we can't use it for sanity checking, because it doesn't
123
        // exist.
124
87
        !in_slow_start && self.ewma_rtt.is_some()
125
87
    }
126

            
127
    /// Given a raw RTT value we just observed, compute whether or not we think the clock has
128
    /// stalled or jumped, and we should throw it out as a result.
129
87
    fn is_clock_stalled(&self, raw_rtt: Duration, in_slow_start: bool) -> bool {
130
87
        if raw_rtt.is_zero() {
131
            // Clock is stalled.
132
            self.clock_stalled.store(true, Ordering::SeqCst);
133
            true
134
87
        } else if self.can_crosscheck_with_current_estimate(in_slow_start) {
135
17
            let ewma_rtt = self
136
17
                .ewma_rtt
137
17
                .expect("ewma_rtt was not checked by can_crosscheck_with_current_estimate?!");
138

            
139
            /// Discrepancy ratio of a new RTT value that we allow against the current RTT in order
140
            /// to declare if the clock has stalled or not. This value is taken from proposal 324
141
            /// section 2.1.1 CLOCK_HEURISTICS and has the same name as in C-tor.
142
            const DELTA_DISCREPANCY_RATIO_MAX: u32 = 5000;
143
            // If we have enough data, check the sanity of our measurement against our EWMA value.
144
17
            if raw_rtt > ewma_rtt * DELTA_DISCREPANCY_RATIO_MAX {
145
                // The clock significantly jumped forward.
146
                //
147
                // Don't update the global cache, though, since this is triggerable over the
148
                // network.
149
                //
150
                // FIXME(eta): We should probably log something here?
151
                true
152
17
            } else if ewma_rtt > raw_rtt * DELTA_DISCREPANCY_RATIO_MAX {
153
                // The clock might have stalled. We can't really make a decision just off this
154
                // one measurement, though, so we'll use the stored stall value.
155
                self.clock_stalled.load(Ordering::SeqCst)
156
            } else {
157
                // If we got here, we're not stalled.
158
17
                self.clock_stalled.store(false, Ordering::SeqCst);
159
17
                false
160
            }
161
        } else {
162
            // If we don't have enough measurements to sanity check, assume it's okay.
163
70
            false
164
        }
165
87
    }
166

            
167
    /// Update the estimator on time `now` and at the congestion window `cwnd`.
168
    ///
169
    /// # Errors
170
    ///
171
    /// Each call to this function removes an entry from `sendme_expected_from` (the entries are
172
    /// added using [`sendme_expected_from`](Self::sendme_expected_from)).
173
    ///
174
    /// Returns an error if are not expecting any SENDMEs at this time (if `expect_sendme` was
175
    /// never called, or if we have exhausted all `sendme_expected_from` added by previous
176
    /// `expect_sendme` calls).
177
    ///
178
    /// Spec: prop324 section 2.1 C-tor: congestion_control_update_circuit_rtt() in
179
    /// congestion_control_common.c
180
87
    pub(crate) fn update(
181
87
        &mut self,
182
87
        now: Instant,
183
87
        state: &State,
184
87
        cwnd: &CongestionWindow,
185
87
    ) -> Result<ClockStall, Error> {
186
87
        let data_sent_at = self
187
87
            .sendme_expected_from
188
87
            .pop_front()
189
87
            .ok_or(Error::MismatchedEstimationCall)?;
190
87
        let raw_rtt = now.saturating_duration_since(data_sent_at);
191

            
192
87
        if self.is_clock_stalled(raw_rtt, state.in_slow_start()) {
193
            return Ok(ClockStall::Detected);
194
87
        }
195

            
196
87
        self.max_rtt = self.max_rtt.max(Some(raw_rtt));
197
87
        self.last_rtt = Some(raw_rtt);
198

            
199
        // This is the "N" for N-EWMA.
200
87
        let ewma_n = u64::from(if state.in_slow_start() {
201
70
            self.params.ewma_ss_max()
202
        } else {
203
17
            min(
204
17
                (cwnd.update_rate(state) * (self.params.ewma_cwnd_pct().as_percent())) / 100,
205
17
                self.params.ewma_max(),
206
            )
207
        });
208
87
        let ewma_n = max(ewma_n, 2);
209

            
210
        // Get the USEC values.
211
87
        let raw_rtt_usec = raw_rtt.as_micros() as u64;
212
87
        let prev_ewma_rtt_usec = self.ewma_rtt.map(|rtt| rtt.as_micros() as u64);
213

            
214
        // This is the actual EWMA calculation.
215
        // C-tor simplifies this as follows for rounding error reasons:
216
        //
217
        // EWMA = value*2/(N+1) + EMA_prev*(N-1)/(N+1)
218
        //      = (value*2 + EWMA_prev*(N-1))/(N+1)
219
        //
220
        // Spec: prop324 section 2.1.2 (N_EWMA_SMOOTHING)
221
87
        let new_ewma_rtt_usec = match prev_ewma_rtt_usec {
222
9
            None => raw_rtt_usec,
223
78
            Some(prev_ewma_rtt_usec) => {
224
78
                ((raw_rtt_usec * 2) + ((ewma_n - 1) * prev_ewma_rtt_usec)) / (ewma_n + 1)
225
            }
226
        };
227
87
        let ewma_rtt = Duration::from_micros(new_ewma_rtt_usec);
228
87
        self.ewma_rtt = Some(ewma_rtt);
229

            
230
87
        let Some(min_rtt) = self.min_rtt else {
231
9
            self.min_rtt = self.ewma_rtt;
232
9
            return Ok(ClockStall::NotDetected);
233
        };
234

            
235
78
        if cwnd.get() == cwnd.min() && !state.in_slow_start() {
236
2
            // The cast is OK even if lossy, we only care about the usec level.
237
2
            let max = max(ewma_rtt, min_rtt).as_micros() as u64;
238
2
            let min = min(ewma_rtt, min_rtt).as_micros() as u64;
239
2
            let rtt_reset_pct = u64::from(self.params.rtt_reset_pct().as_percent());
240
2
            let min_rtt = Duration::from_micros(
241
2
                (rtt_reset_pct * max / 100) + (100 - rtt_reset_pct) * min / 100,
242
2
            );
243
2

            
244
2
            self.min_rtt = Some(min_rtt);
245
76
        } else if self.ewma_rtt < self.min_rtt {
246
17
            self.min_rtt = self.ewma_rtt;
247
59
        }
248

            
249
78
        Ok(ClockStall::NotDetected)
250
87
    }
251
}
252

            
253
/// Whether a clock stall or jump was detected.
254
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
255
pub(crate) enum ClockStall {
256
    /// Clock stall or jump was detected.
257
    Detected,
258
    /// No clock stall or jump detected.
259
    NotDetected,
260
}
261

            
262
#[cfg(test)]
263
mod test {
264
    // @@ begin test lint list maintained by maint/add_warning @@
265
    #![allow(clippy::bool_assert_comparison)]
266
    #![allow(clippy::clone_on_copy)]
267
    #![allow(clippy::dbg_macro)]
268
    #![allow(clippy::mixed_attributes_style)]
269
    #![allow(clippy::print_stderr)]
270
    #![allow(clippy::print_stdout)]
271
    #![allow(clippy::single_char_pattern)]
272
    #![allow(clippy::unwrap_used)]
273
    #![allow(clippy::unchecked_time_subtraction)]
274
    #![allow(clippy::useless_vec)]
275
    #![allow(clippy::needless_pass_by_value)]
276
    #![allow(clippy::string_slice)] // See arti#2571
277
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
278

            
279
    use web_time_compat::{Duration, Instant, InstantExt};
280

            
281
    use crate::congestion::test_utils::{new_cwnd, new_rtt_estimator};
282

            
283
    use super::*;
284

            
285
    #[derive(Debug)]
286
    struct RttTestSample {
287
        sent_usec_in: u64,
288
        sendme_received_usec_in: u64,
289
        cwnd_in: u32,
290
        ss_in: bool,
291
        last_rtt_usec_out: u64,
292
        ewma_rtt_usec_out: u64,
293
        min_rtt_usec_out: u64,
294
    }
295

            
296
    impl From<[u64; 7]> for RttTestSample {
297
        fn from(arr: [u64; 7]) -> Self {
298
            Self {
299
                sent_usec_in: arr[0],
300
                sendme_received_usec_in: arr[1],
301
                cwnd_in: arr[2] as u32,
302
                ss_in: arr[3] == 1,
303
                last_rtt_usec_out: arr[4],
304
                ewma_rtt_usec_out: arr[5],
305
                min_rtt_usec_out: arr[6],
306
            }
307
        }
308
    }
309
    impl RttTestSample {
310
        fn test(&self, estimator: &mut RoundtripTimeEstimator, start: Instant) {
311
            let state = if self.ss_in {
312
                State::SlowStart
313
            } else {
314
                State::Steady
315
            };
316
            let mut cwnd = new_cwnd();
317
            cwnd.set(self.cwnd_in);
318
            let sent = start + Duration::from_micros(self.sent_usec_in);
319
            let sendme_received = start + Duration::from_micros(self.sendme_received_usec_in);
320

            
321
            estimator.expect_sendme(sent);
322
            estimator
323
                .update(sendme_received, &state, &cwnd)
324
                .expect("Error on RTT update");
325
            assert_eq!(
326
                estimator.last_rtt,
327
                Some(Duration::from_micros(self.last_rtt_usec_out))
328
            );
329
            assert_eq!(
330
                estimator.ewma_rtt,
331
                Some(Duration::from_micros(self.ewma_rtt_usec_out))
332
            );
333
            assert_eq!(
334
                estimator.min_rtt,
335
                Some(Duration::from_micros(self.min_rtt_usec_out))
336
            );
337
        }
338
    }
339

            
340
    #[test]
341
    fn test_vectors() {
342
        let mut rtt = new_rtt_estimator();
343
        let now = Instant::get();
344
        // from C-tor src/test/test_congestion_control.c
345
        let vectors = [
346
            [100000, 200000, 124, 1, 100000, 100000, 100000],
347
            [200000, 300000, 124, 1, 100000, 100000, 100000],
348
            [350000, 500000, 124, 1, 150000, 133333, 100000],
349
            [500000, 550000, 124, 1, 50000, 77777, 77777],
350
            [600000, 700000, 124, 1, 100000, 92592, 77777],
351
            [700000, 750000, 124, 1, 50000, 64197, 64197],
352
            [750000, 875000, 124, 0, 125000, 104732, 104732],
353
            [875000, 900000, 124, 0, 25000, 51577, 104732],
354
            [900000, 950000, 200, 0, 50000, 50525, 50525],
355
        ];
356
        for vect in vectors {
357
            let vect = RttTestSample::from(vect);
358
            eprintln!("Testing vector: {:?}", vect);
359
            vect.test(&mut rtt, now);
360
        }
361
    }
362
}