Skip to content

Commit 7334a18

Browse files
authored
feat(transport/cubic): cubic region RFC9438 updates (#2983)
* feat(transport/cubic): cubic region RFC9438 updates - comments and docs - new `target` formula clamping to min and max value (`cwnd<=target<=cwnd*1.5`) - removing `EXPONENTIAL_GROWTH_REDUCTION` since it's use is now covered by the max value mentioned above * adressing review comments - linux reference links - renaming variables - `debug_assert!` to check exponential growth of `w_est` * fixes - comments - moved debug assert * fix the assert - add predicate for small cwnd values - move assert and change the message * remove assert
1 parent b90ded6 commit 7334a18

1 file changed

Lines changed: 92 additions & 54 deletions

File tree

neqo-transport/src/cc/cubic.rs

Lines changed: 92 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ use neqo_common::qtrace;
1515

1616
use 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>
2626
pub 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>
7979
pub 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.")]
161159
impl 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

Comments
 (0)