noq_proto/congestion/
cubic.rs

1use std::any::Any;
2use std::cmp;
3use std::sync::Arc;
4
5use super::{BASE_DATAGRAM_SIZE, Controller, ControllerFactory};
6use crate::connection::RttEstimator;
7use crate::{Duration, Instant};
8
9/// CUBIC Constants.
10///
11/// These are recommended value in RFC8312.
12const BETA_CUBIC: f64 = 0.7;
13
14const C: f64 = 0.4;
15
16/// CUBIC State Variables.
17///
18/// We need to keep those variables across the connection.
19/// k, w_max are described in the RFC.
20#[derive(Debug, Default, Clone)]
21pub(super) struct State {
22    /// Time period that the cubic function takes to increase the window size to W_max.
23    k: f64,
24
25    /// Congestion window size when the last congestion event occurred.
26    w_max: f64,
27
28    /// Congestion window increment stored during congestion avoidance.
29    cwnd_inc: u64,
30
31    /// Maximum number of bytes in flight that may be sent.
32    window: u64,
33
34    /// Slow start threshold in bytes.
35    ///
36    /// When the congestion window is below ssthresh, the mode is slow start
37    /// and the window grows by the number of bytes acknowledged.
38    ssthresh: u64,
39
40    /// The time when QUIC first detects a loss, causing it to enter recovery. When a packet sent
41    /// after this time is acknowledged, QUIC exits recovery.
42    recovery_start_time: Option<Instant>,
43}
44
45/// CUBIC Functions.
46///
47/// Note that these calculations are based on a count of cwnd as bytes,
48/// not packets.
49/// Unit of t (duration) and RTT are based on seconds (f64).
50impl State {
51    // K = cbrt(w_max * (1 - beta_cubic) / C) (Eq. 2)
52    fn cubic_k(&self, max_datagram_size: u64) -> f64 {
53        let w_max = self.w_max / max_datagram_size as f64;
54        (w_max * (1.0 - BETA_CUBIC) / C).cbrt()
55    }
56
57    // W_cubic(t) = C * (t - K)^3 + w_max (Eq. 1)
58    fn w_cubic(&self, t: Duration, max_datagram_size: u64) -> f64 {
59        let w_max = self.w_max / max_datagram_size as f64;
60
61        (C * (t.as_secs_f64() - self.k).powi(3) + w_max) * max_datagram_size as f64
62    }
63
64    // W_est(t) = w_max * beta_cubic + 3 * (1 - beta_cubic) / (1 + beta_cubic) *
65    // (t / RTT) (Eq. 4)
66    fn w_est(&self, t: Duration, rtt: Duration, max_datagram_size: u64) -> f64 {
67        let w_max = self.w_max / max_datagram_size as f64;
68        (w_max * BETA_CUBIC
69            + 3.0 * (1.0 - BETA_CUBIC) / (1.0 + BETA_CUBIC) * t.as_secs_f64() / rtt.as_secs_f64())
70            * max_datagram_size as f64
71    }
72}
73
74/// The RFC8312 congestion controller, as widely used for TCP
75#[derive(Debug, Clone)]
76pub struct Cubic {
77    config: Arc<CubicConfig>,
78    current_mtu: u64,
79    state: State,
80    /// Copy of the controller state to restore when a spurious congestion event is detected.
81    pre_congestion_state: Option<State>,
82}
83
84impl Cubic {
85    /// Construct a state using the given `config` and current time `now`
86    pub fn new(config: Arc<CubicConfig>, _now: Instant, current_mtu: u16) -> Self {
87        Self {
88            state: State {
89                window: config.initial_window,
90                ssthresh: u64::MAX,
91                ..Default::default()
92            },
93            current_mtu: current_mtu as u64,
94            pre_congestion_state: None,
95            config,
96        }
97    }
98
99    fn minimum_window(&self) -> u64 {
100        2 * self.current_mtu
101    }
102}
103
104impl Controller for Cubic {
105    fn on_ack(
106        &mut self,
107        now: Instant,
108        sent: Instant,
109        bytes: u64,
110        _pn: u64,
111        app_limited: bool,
112        rtt: &RttEstimator,
113    ) {
114        if app_limited
115            || self
116                .state
117                .recovery_start_time
118                .map(|recovery_start_time| sent <= recovery_start_time)
119                .unwrap_or(false)
120        {
121            return;
122        }
123
124        if self.state.window < self.state.ssthresh {
125            // Slow start
126            self.state.window += bytes;
127        } else {
128            // Congestion avoidance.
129            let ca_start_time;
130
131            match self.state.recovery_start_time {
132                Some(t) => ca_start_time = t,
133                None => {
134                    // When we come here without congestion_event() triggered,
135                    // initialize congestion_recovery_start_time, w_max and k.
136                    ca_start_time = now;
137                    self.state.recovery_start_time = Some(now);
138
139                    self.state.w_max = self.state.window as f64;
140                    self.state.k = 0.0;
141                }
142            }
143
144            let t = now - ca_start_time;
145
146            // w_cubic(t + rtt)
147            let w_cubic = self.state.w_cubic(t + rtt.get(), self.current_mtu);
148
149            // w_est(t)
150            let w_est = self.state.w_est(t, rtt.get(), self.current_mtu);
151
152            let mut cubic_cwnd = self.state.window;
153
154            if w_cubic < w_est {
155                // TCP friendly region.
156                cubic_cwnd = cmp::max(cubic_cwnd, w_est as u64);
157            } else if cubic_cwnd < w_cubic as u64 {
158                // Concave region or convex region use same increment.
159                let cubic_inc =
160                    (w_cubic - cubic_cwnd as f64) / cubic_cwnd as f64 * self.current_mtu as f64;
161
162                cubic_cwnd += cubic_inc as u64;
163            }
164
165            // Update the increment and increase cwnd by MSS.
166            // Keep release builds from wrapping the retained credit.
167            self.state.cwnd_inc = self
168                .state
169                .cwnd_inc
170                .saturating_add(cubic_cwnd - self.state.window);
171
172            // cwnd_inc can be more than 1 MSS in the late stage of max probing.
173            // however RFC9002 ยง7.3.3 (Congestion Avoidance) limits
174            // the increase of cwnd to 1 max_datagram_size per cwnd acknowledged.
175            // Keep the excess credit for later ACKs instead of discarding it.
176            // https://www.rfc-editor.org/rfc/rfc9002.html#section-7.3.3
177            if self.state.cwnd_inc >= self.current_mtu {
178                self.state.window += self.current_mtu;
179                self.state.cwnd_inc -= self.current_mtu;
180            }
181        }
182    }
183
184    fn on_congestion_event(
185        &mut self,
186        now: Instant,
187        sent: Instant,
188        is_persistent_congestion: bool,
189        is_ecn: bool,
190        _lost_bytes: u64,
191        _largest_lost_pn: u64,
192    ) {
193        if self
194            .state
195            .recovery_start_time
196            .map(|recovery_start_time| sent <= recovery_start_time)
197            .unwrap_or(false)
198        {
199            return;
200        }
201
202        // Save state in case this event ends up being spurious
203        if !is_ecn {
204            self.pre_congestion_state = Some(self.state.clone());
205        }
206
207        self.state.recovery_start_time = Some(now);
208        let window = self.state.window as f64;
209
210        // Fast convergence lowers W_max first; the 0.7 loss reduction still
211        // applies to the old window, not to that already-reduced W_max.
212        // https://www.rfc-editor.org/rfc/rfc9438.html#section-4.7
213        // https://www.rfc-editor.org/rfc/rfc9438.html#section-4.6
214        if window < self.state.w_max {
215            self.state.w_max = window * (1.0 + BETA_CUBIC) / 2.0;
216        } else {
217            self.state.w_max = window;
218        }
219
220        self.state.ssthresh = cmp::max((window * BETA_CUBIC) as u64, self.minimum_window());
221        self.state.window = self.state.ssthresh;
222        self.state.k = self.state.cubic_k(self.current_mtu);
223
224        self.state.cwnd_inc = (self.state.cwnd_inc as f64 * BETA_CUBIC) as u64;
225
226        if is_persistent_congestion {
227            self.state.recovery_start_time = None;
228            self.state.w_max = self.state.window as f64;
229
230            // 4.7 Timeout - reduce ssthresh based on BETA_CUBIC
231            self.state.ssthresh = cmp::max(
232                (self.state.window as f64 * BETA_CUBIC) as u64,
233                self.minimum_window(),
234            );
235
236            self.state.cwnd_inc = 0;
237
238            self.state.window = self.minimum_window();
239        }
240    }
241
242    fn on_spurious_congestion_event(&mut self) {
243        if let Some(prior_state) = self.pre_congestion_state.take()
244            && self.state.window < prior_state.window
245        {
246            self.state = prior_state;
247        }
248    }
249
250    fn on_mtu_update(&mut self, new_mtu: u16) {
251        self.current_mtu = new_mtu as u64;
252        self.state.window = self.state.window.max(self.minimum_window());
253    }
254
255    fn window(&self) -> u64 {
256        self.state.window
257    }
258
259    fn metrics(&self) -> super::ControllerMetrics {
260        super::ControllerMetrics {
261            congestion_window: self.window(),
262            ssthresh: Some(self.state.ssthresh),
263            pacing_rate: None,
264            send_quantum: None,
265        }
266    }
267
268    fn clone_box(&self) -> Box<dyn Controller> {
269        Box::new(self.clone())
270    }
271
272    fn initial_window(&self) -> u64 {
273        self.config.initial_window
274    }
275
276    fn into_any(self: Box<Self>) -> Box<dyn Any> {
277        self
278    }
279}
280
281/// Configuration for the `Cubic` congestion controller
282#[derive(Debug, Clone)]
283pub struct CubicConfig {
284    initial_window: u64,
285}
286
287impl CubicConfig {
288    /// Default limit on the amount of outstanding data in bytes.
289    ///
290    /// Recommended value: `min(10 * max_datagram_size, max(2 * max_datagram_size, 14720))`
291    pub fn initial_window(&mut self, value: u64) -> &mut Self {
292        self.initial_window = value;
293        self
294    }
295}
296
297impl Default for CubicConfig {
298    fn default() -> Self {
299        Self {
300            initial_window: 14720.clamp(2 * BASE_DATAGRAM_SIZE, 10 * BASE_DATAGRAM_SIZE),
301        }
302    }
303}
304
305impl ControllerFactory for CubicConfig {
306    fn build(self: Arc<Self>, now: Instant, current_mtu: u16) -> Box<dyn Controller> {
307        Box::new(Cubic::new(self, now, current_mtu))
308    }
309}
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    #[test]
315    fn fast_convergence_reduces_w_max_without_double_reducing_window() {
316        let now = Instant::now();
317        let config = Arc::new(CubicConfig::default());
318        let mut cubic = Cubic::new(config, now, BASE_DATAGRAM_SIZE as u16);
319        let window = 8 * BASE_DATAGRAM_SIZE;
320
321        cubic.state.window = window;
322        cubic.state.ssthresh = window;
323        cubic.state.w_max = 12.0 * BASE_DATAGRAM_SIZE as f64;
324
325        cubic.on_congestion_event(now, now + Duration::from_millis(1), false, false, 0, 100);
326
327        assert_eq!(cubic.state.w_max, window as f64 * (1.0 + BETA_CUBIC) / 2.0);
328        assert_eq!(cubic.state.ssthresh, (window as f64 * BETA_CUBIC) as u64);
329        assert_eq!(cubic.state.window, cubic.state.ssthresh);
330    }
331
332    #[test]
333    fn congestion_avoidance_preserves_excess_cwnd_increment() {
334        let now = Instant::now();
335        let rtt = RttEstimator::new(Duration::from_millis(100));
336        let config = Arc::new(CubicConfig::default());
337        let mut cubic = Cubic::new(config, now, BASE_DATAGRAM_SIZE as u16);
338
339        // Put CUBIC directly into congestion avoidance.
340        cubic.state.ssthresh = cubic.state.window;
341        cubic.state.recovery_start_time = Some(now);
342        cubic.state.w_max = cubic.state.window as f64;
343
344        // Simulate accumulated credit from earlier ACKs. One ACK may only grow
345        // the window by one MTU, but the extra credit must remain for later ACKs.
346        cubic.state.cwnd_inc = 2 * BASE_DATAGRAM_SIZE + 1;
347        let window = cubic.state.window;
348
349        cubic.on_ack(
350            now,
351            now + Duration::from_millis(1),
352            BASE_DATAGRAM_SIZE,
353            0,
354            false,
355            &rtt,
356        );
357
358        // Before this fix, the window grew by one MTU and the remainder was
359        // reset to zero.
360        assert_eq!(cubic.state.window, window + BASE_DATAGRAM_SIZE);
361        assert_eq!(cubic.state.cwnd_inc, BASE_DATAGRAM_SIZE + 1);
362    }
363}