quinn_proto/congestion/
cubic.rs1use 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
9const BETA_CUBIC: f64 = 0.7;
13
14const C: f64 = 0.4;
15
16#[derive(Debug, Default, Clone)]
21pub(super) struct State {
22 k: f64,
23
24 w_max: f64,
25
26 cwnd_inc: u64,
28}
29
30impl State {
36 fn cubic_k(&self, max_datagram_size: u64) -> f64 {
38 let w_max = self.w_max / max_datagram_size as f64;
39 (w_max * (1.0 - BETA_CUBIC) / C).cbrt()
40 }
41
42 fn w_cubic(&self, t: Duration, max_datagram_size: u64) -> f64 {
44 let w_max = self.w_max / max_datagram_size as f64;
45
46 (C * (t.as_secs_f64() - self.k).powi(3) + w_max) * max_datagram_size as f64
47 }
48
49 fn w_est(&self, t: Duration, rtt: Duration, max_datagram_size: u64) -> f64 {
52 let w_max = self.w_max / max_datagram_size as f64;
53 (w_max * BETA_CUBIC
54 + 3.0 * (1.0 - BETA_CUBIC) / (1.0 + BETA_CUBIC) * t.as_secs_f64() / rtt.as_secs_f64())
55 * max_datagram_size as f64
56 }
57}
58
59#[derive(Debug, Clone)]
61pub struct Cubic {
62 config: Arc<CubicConfig>,
63 window: u64,
65 ssthresh: u64,
68 recovery_start_time: Option<Instant>,
71 cubic_state: State,
72 current_mtu: u64,
73}
74
75impl Cubic {
76 pub fn new(config: Arc<CubicConfig>, _now: Instant, current_mtu: u16) -> Self {
78 Self {
79 window: config.initial_window,
80 ssthresh: u64::MAX,
81 recovery_start_time: None,
82 config,
83 cubic_state: Default::default(),
84 current_mtu: current_mtu as u64,
85 }
86 }
87
88 fn minimum_window(&self) -> u64 {
89 2 * self.current_mtu
90 }
91}
92
93impl Controller for Cubic {
94 fn on_ack(
95 &mut self,
96 now: Instant,
97 sent: Instant,
98 bytes: u64,
99 app_limited: bool,
100 rtt: &RttEstimator,
101 ) {
102 if app_limited
103 || self
104 .recovery_start_time
105 .map(|recovery_start_time| sent <= recovery_start_time)
106 .unwrap_or(false)
107 {
108 return;
109 }
110
111 if self.window < self.ssthresh {
112 self.window += bytes;
114 } else {
115 let ca_start_time;
117
118 match self.recovery_start_time {
119 Some(t) => ca_start_time = t,
120 None => {
121 ca_start_time = now;
124 self.recovery_start_time = Some(now);
125
126 self.cubic_state.w_max = self.window as f64;
127 self.cubic_state.k = 0.0;
128 }
129 }
130
131 let t = now - ca_start_time;
132
133 let w_cubic = self.cubic_state.w_cubic(t + rtt.get(), self.current_mtu);
135
136 let w_est = self.cubic_state.w_est(t, rtt.get(), self.current_mtu);
138
139 let mut cubic_cwnd = self.window;
140
141 if w_cubic < w_est {
142 cubic_cwnd = cmp::max(cubic_cwnd, w_est as u64);
144 } else if cubic_cwnd < w_cubic as u64 {
145 let cubic_inc =
147 (w_cubic - cubic_cwnd as f64) / cubic_cwnd as f64 * self.current_mtu as f64;
148
149 cubic_cwnd = cubic_cwnd.saturating_add(cubic_inc as u64);
152 }
153
154 self.cubic_state.cwnd_inc += cubic_cwnd - self.window;
156
157 if self.cubic_state.cwnd_inc >= self.current_mtu {
161 self.window += self.current_mtu;
162 self.cubic_state.cwnd_inc = 0;
163 }
164 }
165 }
166
167 fn on_congestion_event(
168 &mut self,
169 now: Instant,
170 sent: Instant,
171 is_persistent_congestion: bool,
172 _lost_bytes: u64,
173 ) {
174 if self
175 .recovery_start_time
176 .map(|recovery_start_time| sent <= recovery_start_time)
177 .unwrap_or(false)
178 {
179 return;
180 }
181
182 self.recovery_start_time = Some(now);
183 let window = self.window as f64;
184
185 if window < self.cubic_state.w_max {
190 self.cubic_state.w_max = window * (1.0 + BETA_CUBIC) / 2.0;
191 } else {
192 self.cubic_state.w_max = window;
193 }
194
195 self.ssthresh = cmp::max((window * BETA_CUBIC) as u64, self.minimum_window());
196 self.window = self.ssthresh;
197 self.cubic_state.k = self.cubic_state.cubic_k(self.current_mtu);
198
199 self.cubic_state.cwnd_inc = (self.cubic_state.cwnd_inc as f64 * BETA_CUBIC) as u64;
200
201 if is_persistent_congestion {
202 self.recovery_start_time = None;
203 self.cubic_state.w_max = self.window as f64;
204
205 self.ssthresh = cmp::max(
207 (self.window as f64 * BETA_CUBIC) as u64,
208 self.minimum_window(),
209 );
210
211 self.cubic_state.cwnd_inc = 0;
212
213 self.window = self.minimum_window();
214 }
215 }
216
217 fn on_mtu_update(&mut self, new_mtu: u16) {
218 self.current_mtu = new_mtu as u64;
219 self.window = self.window.max(self.minimum_window());
220 }
221
222 fn window(&self) -> u64 {
223 self.window
224 }
225
226 fn metrics(&self) -> super::ControllerMetrics {
227 super::ControllerMetrics {
228 congestion_window: self.window(),
229 ssthresh: Some(self.ssthresh),
230 pacing_rate: None,
231 }
232 }
233
234 fn clone_box(&self) -> Box<dyn Controller> {
235 Box::new(self.clone())
236 }
237
238 fn initial_window(&self) -> u64 {
239 self.config.initial_window
240 }
241
242 fn into_any(self: Box<Self>) -> Box<dyn Any> {
243 self
244 }
245}
246
247#[derive(Debug, Clone)]
249pub struct CubicConfig {
250 initial_window: u64,
251}
252
253impl CubicConfig {
254 pub fn initial_window(&mut self, value: u64) -> &mut Self {
258 self.initial_window = value;
259 self
260 }
261}
262
263impl Default for CubicConfig {
264 fn default() -> Self {
265 Self {
266 initial_window: 14720.clamp(2 * BASE_DATAGRAM_SIZE, 10 * BASE_DATAGRAM_SIZE),
267 }
268 }
269}
270
271impl ControllerFactory for CubicConfig {
272 fn build(self: Arc<Self>, now: Instant, current_mtu: u16) -> Box<dyn Controller> {
273 Box::new(Cubic::new(self, now, current_mtu))
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn fast_convergence_reduces_w_max_without_double_reducing_window() {
283 let now = Instant::now();
284 let config = Arc::new(CubicConfig::default());
285 let mut cubic = Cubic::new(config, now, BASE_DATAGRAM_SIZE as u16);
286 let window = 8 * BASE_DATAGRAM_SIZE;
287
288 cubic.window = window;
289 cubic.ssthresh = window;
290 cubic.cubic_state.w_max = 12.0 * BASE_DATAGRAM_SIZE as f64;
291
292 cubic.on_congestion_event(now, now + Duration::from_millis(1), false, 0);
293
294 assert_eq!(
295 cubic.cubic_state.w_max,
296 window as f64 * (1.0 + BETA_CUBIC) / 2.0
297 );
298 assert_eq!(cubic.ssthresh, (window as f64 * BETA_CUBIC) as u64);
299 assert_eq!(cubic.window, cubic.ssthresh);
300 }
301
302 #[test]
303 fn congestion_avoidance_does_not_overflow_after_long_lossless_period() {
304 let now = Instant::now();
305 let rtt = RttEstimator::new(Duration::from_millis(100));
306 let config = Arc::new(CubicConfig::default());
307 let mut cubic = Cubic::new(config, now, BASE_DATAGRAM_SIZE as u16);
308
309 cubic.ssthresh = cubic.window;
311 cubic.recovery_start_time = Some(now);
312 let window = cubic.window;
313
314 let later = now + Duration::from_secs(10 * 24 * 60 * 60);
317 cubic.on_ack(later, later, BASE_DATAGRAM_SIZE, false, &rtt);
318
319 assert_eq!(cubic.window, window + BASE_DATAGRAM_SIZE);
320 }
321}