Skip to main content

rtc_interceptor/gcc/
rate_control.rs

1//! AIMD: what the target bitrate does in each state.
2
3use super::overuse::Usage;
4use super::state::RateControlState;
5use std::time::{Duration, Instant};
6
7/// Multiplier applied to the *received* rate when backing off.
8pub const DEFAULT_DECREASE_FACTOR: f64 = 0.85;
9
10/// Multiplicative growth per second while climbing well below the last known ceiling.
11pub const DEFAULT_INCREASE_FACTOR: f64 = 1.08;
12
13/// How long to wait between changes, so each one is observed before the next.
14pub const DEFAULT_RATE_CONTROL_INTERVAL: Duration = Duration::from_millis(200);
15
16/// The AIMD controller: a usage signal and a received rate in, a target bitrate out.
17///
18/// # Additive versus multiplicative increase
19///
20/// Climbing multiplicatively is fast but overshoots, which on a path already known to be near its
21/// limit means congesting it again immediately. So the controller climbs multiplicatively only
22/// while it is *far* from the rate that last caused a backoff, and switches to additive — one
23/// packet per round trip — as it approaches. Upstream does the same; it is the difference between
24/// probing for capacity and hammering at a known ceiling.
25#[derive(Debug, Clone)]
26pub struct RateController {
27    state: RateControlState,
28    /// Current target, in bits per second.
29    target: f64,
30    min: f64,
31    max: f64,
32    decrease_factor: f64,
33    increase_factor: f64,
34    interval: Duration,
35    /// The received rate at the last backoff, which is the ceiling to approach carefully.
36    last_decrease_rate: Option<f64>,
37    last_change: Option<Instant>,
38}
39
40impl RateController {
41    /// A controller starting at `initial`, clamped to `min..=max`.
42    ///
43    /// **One clamp, from configuration** — see D3. Upstream clamps in two places with two different
44    /// hard-coded ranges, and they disagree.
45    pub fn new(initial: f64, min: f64, max: f64) -> Self {
46        Self {
47            state: RateControlState::default(),
48            target: initial.clamp(min, max),
49            min,
50            max,
51            decrease_factor: DEFAULT_DECREASE_FACTOR,
52            increase_factor: DEFAULT_INCREASE_FACTOR,
53            interval: DEFAULT_RATE_CONTROL_INTERVAL,
54            last_decrease_rate: None,
55            last_change: None,
56        }
57    }
58
59    /// How hard to back off, as a fraction of the received rate.
60    pub fn with_decrease_factor(mut self, decrease_factor: f64) -> Self {
61        self.decrease_factor = decrease_factor;
62        self
63    }
64
65    /// The current target, in bits per second.
66    pub fn target(&self) -> f64 {
67        self.target
68    }
69
70    /// What the controller is doing.
71    pub fn state(&self) -> RateControlState {
72        self.state
73    }
74
75    /// Fold in a usage signal and the rate the far end is receiving.
76    ///
77    /// `received` is `None` when the window holds too little to say; the controller then holds
78    /// rather than guessing, because every action it could take needs a rate to compute from.
79    pub fn update(&mut self, now: Instant, usage: Usage, received: Option<f64>) -> f64 {
80        self.state = self.state.next(usage);
81
82        // Each change is given time to take effect before the next. Without this the controller
83        // acts several times on the same round trip's worth of evidence.
84        if let Some(last) = self.last_change
85            && now.saturating_duration_since(last) < self.interval
86            && self.state != RateControlState::Decrease
87        {
88            return self.target;
89        }
90
91        match self.state {
92            RateControlState::Hold => {}
93
94            RateControlState::Decrease => {
95                // Back off from what the path is *delivering*, not from what we were aiming for.
96                let Some(received) = received else {
97                    return self.target;
98                };
99                self.target = (received * self.decrease_factor).clamp(self.min, self.max);
100                self.last_decrease_rate = Some(received);
101                self.last_change = Some(now);
102            }
103
104            RateControlState::Increase => {
105                let elapsed = self
106                    .last_change
107                    .map_or(self.interval, |last| now.saturating_duration_since(last));
108
109                let near_the_ceiling = self
110                    .last_decrease_rate
111                    .is_some_and(|ceiling| self.target > ceiling * 0.85);
112
113                self.target = if near_the_ceiling {
114                    // Additive: one MTU per round trip, so the last known ceiling is approached
115                    // rather than blown through.
116                    (self.target + 12_000.0).clamp(self.min, self.max)
117                } else {
118                    let growth = self.increase_factor.powf(elapsed.as_secs_f64().min(1.0));
119                    (self.target * growth).clamp(self.min, self.max)
120                };
121                self.last_change = Some(now);
122            }
123        }
124
125        self.target
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    const MIN: f64 = 100_000.0;
134    const MAX: f64 = 10_000_000.0;
135
136    fn controller() -> RateController {
137        RateController::new(1_000_000.0, MIN, MAX)
138    }
139
140    /// Overuse backs off to a fraction of what is being **received**, not of the target. On a path
141    /// delivering far less than the sender is aiming for, those differ by the whole overshoot.
142    #[test]
143    fn overuse_backs_off_from_the_received_rate() {
144        let epoch = Instant::now();
145        let mut controller = RateController::new(2_000_000.0, MIN, MAX);
146
147        let target = controller.update(epoch, Usage::Over, Some(600_000.0));
148
149        assert!(
150            (target - 510_000.0).abs() < 1.0,
151            "expected 0.85 × 600 kb/s, got {target}"
152        );
153    }
154
155    /// A quiet path climbs.
156    #[test]
157    fn a_quiet_path_increases() {
158        let epoch = Instant::now();
159        let mut controller = controller();
160
161        let mut at = epoch;
162        let before = controller.target();
163        for _ in 0..10 {
164            at += DEFAULT_RATE_CONTROL_INTERVAL;
165            controller.update(at, Usage::Normal, Some(1_000_000.0));
166        }
167
168        assert!(
169            controller.target() > before,
170            "a healthy path should be probed for more: {before} → {}",
171            controller.target()
172        );
173    }
174
175    /// And climbs **carefully** once near a ceiling it has already hit, rather than overshooting
176    /// straight back into congestion.
177    #[test]
178    fn it_climbs_carefully_near_a_known_ceiling() {
179        let epoch = Instant::now();
180        let mut controller = controller();
181
182        // Establish a ceiling.
183        let mut at = epoch;
184        controller.update(at, Usage::Over, Some(1_000_000.0));
185        let after_backoff = controller.target();
186
187        // Climb back towards it.
188        let mut steps = 0;
189        while controller.target() < after_backoff * 1.5 && steps < 200 {
190            at += DEFAULT_RATE_CONTROL_INTERVAL;
191            controller.update(at, Usage::Normal, Some(1_000_000.0));
192            steps += 1;
193        }
194
195        assert!(
196            steps > 5,
197            "approaching a known ceiling should take several steps, took {steps}"
198        );
199    }
200
201    /// Without a received rate the controller holds. Guessing here is how an estimate collapses on
202    /// a feedback gap.
203    #[test]
204    fn it_holds_when_the_received_rate_is_unknown() {
205        let epoch = Instant::now();
206        let mut controller = controller();
207        let before = controller.target();
208
209        let target = controller.update(epoch, Usage::Over, None);
210
211        assert_eq!(before, target, "no rate to back off from means no change");
212    }
213
214    /// One clamp, from configuration (D3). Upstream clamps twice with two different hard-coded
215    /// ranges that disagree.
216    #[test]
217    fn the_target_stays_within_configured_bounds() {
218        let epoch = Instant::now();
219        let mut controller = RateController::new(MIN, MIN, 500_000.0);
220
221        let mut at = epoch;
222        for _ in 0..500 {
223            at += DEFAULT_RATE_CONTROL_INTERVAL;
224            controller.update(at, Usage::Normal, Some(10_000_000.0));
225        }
226        assert!(
227            controller.target() <= 500_000.0,
228            "ceiling breached: {}",
229            controller.target()
230        );
231
232        for _ in 0..50 {
233            at += DEFAULT_RATE_CONTROL_INTERVAL;
234            controller.update(at, Usage::Over, Some(1.0));
235        }
236        assert!(
237            controller.target() >= MIN,
238            "floor breached: {}",
239            controller.target()
240        );
241    }
242
243    /// Changes are spaced, so the controller is not acting several times on one round trip's
244    /// evidence. A decrease is exempt: congestion is urgent.
245    #[test]
246    fn increases_are_paced_but_a_decrease_is_not() {
247        let epoch = Instant::now();
248        let mut controller = controller();
249
250        controller.update(epoch, Usage::Normal, Some(1_000_000.0));
251        let after_first = controller.target();
252        // Immediately again: too soon to act.
253        controller.update(
254            epoch + Duration::from_millis(1),
255            Usage::Normal,
256            Some(1_000_000.0),
257        );
258        assert_eq!(
259            after_first,
260            controller.target(),
261            "a second increase in the same interval must be ignored"
262        );
263
264        let before_backoff = controller.target();
265        controller.update(
266            epoch + Duration::from_millis(2),
267            Usage::Over,
268            Some(500_000.0),
269        );
270        assert!(
271            controller.target() < before_backoff,
272            "a decrease must not be delayed by the pacing interval"
273        );
274    }
275}