@@ -15,14 +15,14 @@ use neqo_common::qtrace;
1515
1616use crate :: cc:: classic_cc:: WindowAdjustment ;
1717
18- /// > C is a constant fixed to determine the aggressiveness of window
19- /// > increase in high BDP networks.
18+ /// > Constant that determines the aggressiveness of CUBIC in competing with other congestion
19+ /// > control algorithms in high- BDP networks.
2020///
21- /// <https://datatracker.ietf.org/doc/html/rfc8312#section-4.1 >
21+ /// <https://datatracker.ietf.org/doc/html/rfc9438#name-constants-of-interest >
2222///
23- /// See discussion for rational for concrete value.
23+ /// See section 5.1 of RFC9438 for discussion on how to set the concrete value:
2424///
25- /// <https://datatracker.ietf.org/doc/html/rfc8312#section-5.1 >
25+ /// <https://datatracker.ietf.org/doc/html/rfc9438#name-fairness-to-reno >
2626pub const CUBIC_C : f64 = 0.4 ;
2727/// > CUBIC additive increase factor used in the Reno-friendly region \[to achieve approximately the
2828/// > same average congestion window size as Reno\].
@@ -78,13 +78,6 @@ pub const CUBIC_BETA_USIZE_DIVISOR: usize = 10;
7878/// <https://datatracker.ietf.org/doc/html/rfc9438#name-fast-convergence>
7979pub const CUBIC_FAST_CONVERGENCE_FACTOR : f64 = ( 1.0 + 0.7 ) / 2.0 ;
8080
81- /// The minimum number of multiples of the datagram size that need
82- /// to be received to cause an increase in the congestion window.
83- /// When there is no loss, Cubic can return to exponential increase, but
84- /// this value reduces the magnitude of the resulting growth by a constant factor.
85- /// A value of 1.0 would mean a return to the rate used in slow start.
86- const EXPONENTIAL_GROWTH_REDUCTION : f64 = 2.0 ;
87-
8881/// Convert an integer congestion window value into a floating point value.
8982/// This has the effect of reducing larger values to `1<<53`.
9083/// If you have a congestion window that large, something is probably wrong.
@@ -110,10 +103,16 @@ pub struct Cubic {
110103 ///
111104 /// <https://datatracker.ietf.org/doc/html/rfc9438#name-reno-friendly-region>
112105 w_est : f64 ,
113- /// > K is the time period that the above function takes to increase the
114- /// > current window size to W_max if there are no further congestion events
106+ /// > The time period in seconds it takes to increase the congestion window size
107+ /// > at the beginning of the current congestion avoidance stage to `w_max`.
108+ ///
109+ /// <https://datatracker.ietf.org/doc/html/rfc9438#name-variables-of-interest>
110+ ///
111+ /// Formula:
112+ ///
113+ /// `k = cubic_root((w_max - cwnd_epoch) / C)`
115114 ///
116- /// <https://datatracker.ietf.org/doc/html/rfc8312#section-4.1 >
115+ /// <https://datatracker.ietf.org/doc/html/rfc9438#name-window-increase-function >
117116 k : f64 ,
118117 /// > Size of `cwnd` in \[bytes\] just before `cwnd` was reduced in the last congestion
119118 /// > event \[...\]. \[With\] fast convergence enabled, `w_max` may be further reduced based on
@@ -157,28 +156,34 @@ impl Display for Cubic {
157156 }
158157}
159158
160- #[ expect( clippy:: doc_markdown, reason = "Not doc items; names from RFC." ) ]
161159impl Cubic {
162- /// Original equations is:
163- /// K = cubic_root(W_max*(1-beta_cubic)/C) (Eq. 2 RFC8312)
164- /// W_max is number of segments of the maximum segment size (MSS).
160+ /// Original equation is:
165161 ///
166- /// K is actually the time that W_cubic(t) = C*(t-K)^3 + W_max (Eq. 1) would
167- /// take to increase to W_max. We use bytes not MSS units, therefore this
168- /// equation will be: W_cubic(t) = C*MSS*(t-K)^3 + W_max.
162+ /// `k = cubic_root((w_max - cwnd_epoch)/C)`
169163 ///
170- /// From that equation we can calculate K as:
171- /// K = cubic_root((W_max - W_cubic) / C / MSS);
164+ /// with `cwnd_epoch` being the congestion window at the start of the current congestion
165+ /// avoidance stage (so at time `t_epoch`).
172166 ///
173- /// <https://www.rfc-editor.org/rfc/rfc8312#section-4.1>
174- fn calc_k ( & self , curr_cwnd : f64 , max_datagram_size : f64 ) -> f64 {
175- ( ( self . w_max - curr_cwnd) / CUBIC_C / max_datagram_size) . cbrt ( )
167+ /// <https://datatracker.ietf.org/doc/html/rfc9438#figure-2>
168+ ///
169+ /// Taking into account that neqo is using bytes but the formula assumes segments for both
170+ /// `w_max` and `cwnd_epoch` it becomes:
171+ ///
172+ /// `k = cubic_root((w_max - cwnd_epoch)/SMSS/C)`
173+ fn calc_k ( & self , cwnd_epoch : f64 , max_datagram_size : f64 ) -> f64 {
174+ ( ( self . w_max - cwnd_epoch) / max_datagram_size / CUBIC_C ) . cbrt ( )
176175 }
177176
178- /// W_cubic(t) = C*(t-K)^3 + W_max (Eq. 1)
179- /// t is relative to the start of the congestion avoidance phase and it is in seconds.
177+ /// `w_cubic(t) = C*(t-K)^3 + w_max`
178+ ///
179+ /// with `t = t_current - t_epoch`.
180180 ///
181- /// <https://www.rfc-editor.org/rfc/rfc8312#section-4.1>
181+ /// <https://datatracker.ietf.org/doc/html/rfc9438#figure-1>
182+ ///
183+ /// Taking into account that neqo is using bytes and the formula returns segments and that
184+ /// `w_max` already is in bytes the formula becomes:
185+ ///
186+ /// `w_cubic(t) = (C*(t-K)^3) * SMSS + w_max`
182187 fn w_cubic ( & self , t : f64 , max_datagram_size : f64 ) -> f64 {
183188 ( CUBIC_C * ( t - self . k ) . powi ( 3 ) ) . mul_add ( max_datagram_size, self . w_max )
184189 }
@@ -265,12 +270,24 @@ impl WindowAdjustment for Cubic {
265270 . expect ( "unwrapping `None` value -- it should've been set by `start_epoch`" )
266271 } ;
267272
268- // Cubic concave or convex region
273+ // Calculate `target_cubic` for the concave or convex region
274+ //
275+ // > Upon receiving a new ACK during congestion avoidance, CUBIC computes the target
276+ // > congestion window size after the next RTT [...], where RTT is the
277+ // > smoothed round-trip time. The lower and upper bounds below ensure that CUBIC's
278+ // > congestion window increase rate is non-decreasing and is less than the increase rate of
279+ // > slow start.
269280 //
270- // <https://datatracker.ietf.org/doc/html/rfc8312#section-4.3>
271- // <https://datatracker.ietf.org/doc/html/rfc8312#section-4.4>
281+ // <https://datatracker.ietf.org/doc/html/rfc9438#section-4.2-10>
282+ //
283+ // In neqo the target congestion window is in bytes.
272284 let t = now. saturating_duration_since ( t_epoch) ;
273- let target_cubic = self . w_cubic ( ( t + min_rtt) . as_secs_f64 ( ) , max_datagram_size) ;
285+ // cwnd <= target_cubic <= cwnd * 1.5
286+ let target_cubic = f64:: clamp (
287+ self . w_cubic ( ( t + min_rtt) . as_secs_f64 ( ) , max_datagram_size) ,
288+ curr_cwnd,
289+ curr_cwnd * 1.5 ,
290+ ) ;
274291
275292 // Calculate w_est for the Reno-friendly region with a slightly adjusted formula per the
276293 // below:
@@ -285,6 +302,7 @@ impl WindowAdjustment for Cubic {
285302
286303 // We first calculate the increase in segments and floor it to only include whole segments.
287304 let increase = ( CUBIC_ALPHA * self . reno_acked_bytes / curr_cwnd) . floor ( ) ;
305+
288306 // Only apply the increase if it is at least by one segment.
289307 if increase > 0.0 {
290308 self . w_est += increase * max_datagram_size;
@@ -294,30 +312,50 @@ impl WindowAdjustment for Cubic {
294312 self . reno_acked_bytes -= acked_bytes_used;
295313 }
296314
297- // Take the larger cwnd of Cubic concave or convex and Cubic
298- // TCP-friendly region.
315+ // > When receiving a new ACK in congestion avoidance (where cwnd could be greater than
316+ // > or less than w_max), CUBIC checks whether W_cubic(t) is less than w_est. If so, CUBIC
317+ // > is in the Reno-friendly region and cwnd SHOULD be set to w_est at each reception of a
318+ // > new ACK.
319+ //
320+ // <https://datatracker.ietf.org/doc/html/rfc9438#section-4.3-8>
299321 //
300- // > When receiving an ACK in congestion avoidance (cwnd could be
301- // > greater than or less than W_max), CUBIC checks whether W_cubic(t) is
302- // > less than W_est(t). If so, CUBIC is in the TCP-friendly region and
303- // > cwnd SHOULD be set to W_est(t) at each reception of an ACK .
322+ // While the RFC specifies that we should compare `w_cubic(t)` with `w_est` we are rather
323+ // comparing the previously calculated `target` here (`w_cubic(t + min_rtt)` with clamping
324+ // to `cwnd <= target <= cwnd * 1.5` applied), since that is the value that would actually
325+ // be used if we are in the cubic region .
304326 //
305- // <https://datatracker.ietf.org/doc/html/rfc8312#section-4.2>
306- let target_cwnd = target_cubic. max ( self . w_est ) ;
327+ // That is in line with what e.g. the Linux Kernel CUBIC implementation is doing.
328+ //
329+ // <https://github.com/torvalds/linux/blob/d7ee5bdce7892643409dea7266c34977e651b479/net/ipv4/tcp_cubic.c#L313>
330+ let target = target_cubic. max ( self . w_est ) ;
307331
308- // Calculate the number of bytes that would need to be acknowledged for an increase
309- // of `max_datagram_size` to match the increase of `target - cwnd / cwnd` as defined
310- // in the specification (Sections 4.4 and 4.5).
311- // The amount of data required therefore reduces asymptotically as the target increases.
312- // If the target is not significantly higher than the congestion window, require a very
313- // large amount of acknowledged data (effectively block increases).
314- let mut acked_to_increase =
315- max_datagram_size * curr_cwnd / ( target_cwnd - curr_cwnd) . max ( 1.0 ) ;
332+ let cwnd_increase = target - curr_cwnd;
316333
317- // Limit increase to max 1 MSS per EXPONENTIAL_GROWTH_REDUCTION ack packets.
318- // This effectively limits target_cwnd to (1 + 1 / EXPONENTIAL_GROWTH_REDUCTION) cwnd.
319- acked_to_increase = acked_to_increase. max ( EXPONENTIAL_GROWTH_REDUCTION * max_datagram_size) ;
320- acked_to_increase as usize
334+ // Calculate the number of bytes that would need to be acknowledged for an increase
335+ // of `max_datagram_size` to match `cwnd_increase`, that is the increase from the current
336+ // congestion window to `target`.
337+ // The amount of acked data required therefore reduces asymptotically as the target
338+ // increases.
339+ //
340+ // RFC 9438 tells us to increase cwnd by `cwnd_increase/cwnd` which would amount to the
341+ // increase in segments per congestion window acked.
342+ //
343+ // (https://datatracker.ietf.org/doc/html/rfc9438#section-4.4-2.1)
344+ //
345+ // Since we want to know how much we need to ack to increase by 1 segment we need the
346+ // inverse of that, which would be `cwnd/cwnd_increase`.
347+ // (E.g. if we'd increase by `1/4 * mss` per cwnd acked then we need to ack `4 * cwnd` to
348+ // increase by `1 * mss`)
349+ //
350+ // The RFC only applies this increase per acked cwnd to the Cubic (concave/convex) region.
351+ // We also apply it to the Reno region, as that is what the Linux Kernel CUBIC
352+ // implementation does, too.
353+ //
354+ // <https://github.com/torvalds/linux/blob/d7ee5bdce7892643409dea7266c34977e651b479/net/ipv4/tcp_cubic.c#L311-L315>
355+ //
356+ // We multiply by `max_datagram_size` as our `curr_cwnd` value is in bytes and prevent
357+ // division by zero by setting `cwnd_increase` to `1` for the `target == curr_cwnd` case.
358+ ( max_datagram_size * curr_cwnd / cwnd_increase. max ( 1.0 ) ) as usize
321359 }
322360
323361 // CUBIC RFC 9438 changes the logic for multiplicative decrease, most notably setting the
0 commit comments