Skip to main content

rtc_interceptor/gcc/
rate_calc.rs

1//! What the receiver is actually getting, over a sliding window.
2
3use std::collections::VecDeque;
4use std::time::{Duration, Instant};
5
6/// How far back the received rate is measured over.
7///
8/// Long enough that one late group does not halve the reading, short enough to follow a real
9/// change within a round trip or two.
10pub const DEFAULT_WINDOW: Duration = Duration::from_millis(500);
11
12/// The rate the far end is receiving, from acknowledged bytes over a sliding window.
13///
14/// # Why the controller needs this and not the target
15///
16/// On a decrease, AIMD backs off to a fraction of what the path is *delivering*, not of what the
17/// sender was *aiming for*. Those differ exactly when it matters: a sender aiming at 2 Mb/s over a
18/// path carrying 600 kb/s must drop to about 500 kb/s, not to 1.7 Mb/s. Backing off from the target
19/// would take several rounds to reach the same place, queueing the whole way down.
20#[derive(Debug, Clone)]
21pub struct RateCalculator {
22    window: Duration,
23    /// Acknowledged packets in the window: when they arrived here, and how big they were.
24    samples: VecDeque<(Instant, usize)>,
25    bytes_in_window: usize,
26}
27
28impl Default for RateCalculator {
29    fn default() -> Self {
30        Self::new(DEFAULT_WINDOW)
31    }
32}
33
34impl RateCalculator {
35    /// A calculator over `window`.
36    pub fn new(window: Duration) -> Self {
37        Self {
38            window,
39            samples: VecDeque::new(),
40            bytes_in_window: 0,
41        }
42    }
43
44    /// Record `size` bytes acknowledged at `now`.
45    pub fn add(&mut self, now: Instant, size: usize) {
46        self.samples.push_back((now, size));
47        self.bytes_in_window += size;
48        self.expire(now);
49    }
50
51    /// The received rate in bits per second, or `None` when the window holds too little to say.
52    ///
53    /// `None` rather than zero: a controller that reads an empty window as "the path is delivering
54    /// nothing" would back off to its floor on the first feedback gap.
55    pub fn rate_bits_per_second(&mut self, now: Instant) -> Option<f64> {
56        self.expire(now);
57
58        // One sample measures nothing — a rate needs a span.
59        if self.samples.len() < 2 {
60            return None;
61        }
62
63        let oldest = self.samples.front()?.0;
64        let span = now.saturating_duration_since(oldest);
65        if span.is_zero() {
66            return None;
67        }
68
69        Some((self.bytes_in_window * 8) as f64 / span.as_secs_f64())
70    }
71
72    fn expire(&mut self, now: Instant) {
73        let cutoff = now.checked_sub(self.window).unwrap_or(now);
74        while let Some((at, size)) = self.samples.front().copied() {
75            if at >= cutoff {
76                break;
77            }
78            self.samples.pop_front();
79            self.bytes_in_window -= size;
80        }
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    /// A steady stream reads its own rate back.
89    #[test]
90    fn it_measures_a_steady_stream() {
91        let epoch = Instant::now();
92        let mut calculator = RateCalculator::default();
93
94        // 1500 bytes every 10 ms = 1.2 Mb/s.
95        for step in 0..50u64 {
96            calculator.add(epoch + Duration::from_millis(step * 10), 1500);
97        }
98
99        let rate = calculator
100            .rate_bits_per_second(epoch + Duration::from_millis(490))
101            .expect("a full window has a rate");
102        assert!(
103            (rate - 1_200_000.0).abs() < 100_000.0,
104            "expected about 1.2 Mb/s, got {rate}"
105        );
106    }
107
108    /// Too little to go on reads `None`, not zero — a controller must not mistake a feedback gap
109    /// for a dead path and back off to its floor.
110    #[test]
111    fn an_empty_window_has_no_rate() {
112        let epoch = Instant::now();
113        let mut calculator = RateCalculator::default();
114
115        assert_eq!(None, calculator.rate_bits_per_second(epoch));
116        calculator.add(epoch, 1500);
117        assert_eq!(
118            None,
119            calculator.rate_bits_per_second(epoch),
120            "one sample is not a rate"
121        );
122    }
123
124    /// Samples fall out of the window, so the reading follows a change rather than averaging over
125    /// all history.
126    #[test]
127    fn old_samples_expire() {
128        let epoch = Instant::now();
129        let mut calculator = RateCalculator::new(Duration::from_millis(200));
130
131        for step in 0..20u64 {
132            calculator.add(epoch + Duration::from_millis(step * 10), 1500);
133        }
134        let busy = calculator
135            .rate_bits_per_second(epoch + Duration::from_millis(190))
136            .expect("rate");
137
138        // Nothing for a while, then two packets: the busy period must have aged out.
139        calculator.add(epoch + Duration::from_millis(1_000), 1500);
140        calculator.add(epoch + Duration::from_millis(1_100), 1500);
141        let quiet = calculator
142            .rate_bits_per_second(epoch + Duration::from_millis(1_100))
143            .expect("rate");
144
145        assert!(
146            quiet < busy / 2.0,
147            "the window should have forgotten the busy period: {busy} then {quiet}"
148        );
149    }
150
151    /// Halving the offered rate halves the reading, which is the property the controller relies on
152    /// when it backs off to a fraction of what is being delivered.
153    #[test]
154    fn it_follows_a_halved_rate() {
155        let epoch = Instant::now();
156        let mut fast = RateCalculator::default();
157        let mut slow = RateCalculator::default();
158
159        for step in 0..50u64 {
160            fast.add(epoch + Duration::from_millis(step * 10), 1500);
161        }
162        for step in 0..25u64 {
163            slow.add(epoch + Duration::from_millis(step * 20), 1500);
164        }
165
166        let at = epoch + Duration::from_millis(490);
167        let fast_rate = fast.rate_bits_per_second(at).expect("rate");
168        let slow_rate = slow.rate_bits_per_second(at).expect("rate");
169
170        assert!(
171            (fast_rate / slow_rate - 2.0).abs() < 0.2,
172            "one should be twice the other: {fast_rate} vs {slow_rate}"
173        );
174    }
175}