Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 122 additions & 57 deletions neqo-transport/src/cc/cubic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.

//! CUBIC congestion control
//! CUBIC congestion control (RFC 9438)

use std::{
fmt::{self, Display},
Expand All @@ -29,25 +29,43 @@
/// <https://datatracker.ietf.org/doc/html/rfc8312#section-4.2>
pub const CUBIC_ALPHA: f64 = 3.0 * (1.0 - 0.7) / (1.0 + 0.7);

/// `CUBIC_BETA` = 0.7;
/// > CUBIC multiplicative decrease factor
///
/// > Principle 4: To balance between the scalability and convergence speed,
/// > CUBIC sets the multiplicative window decrease factor to 0.7 while Standard
/// > TCP uses 0.5. While this improves the scalability of CUBIC, a side effect
/// > of this decision is slower convergence, especially under low statistical
/// <https://datatracker.ietf.org/doc/html/rfc9438#name-constants-of-interest>
///
/// > To balance between the scalability and convergence speed, CUBIC sets the multiplicative window
/// > decrease factor to 0.7 while Standard TCP uses 0.5. While this improves the scalability of
/// > CUBIC, a side effect of this decision is slower convergence, especially under low statistical
/// > multiplexing environments.
///
/// <https://datatracker.ietf.org/doc/html/rfc8312#section-3>
/// <https://datatracker.ietf.org/doc/html/rfc9438#name-principle-4-for-the-cubic-d>
///
/// For implementation reasons neqo uses a dividend and divisor approach with `usize` typing to
/// construct `CUBIC_BETA = 0.7`.
pub const CUBIC_BETA_USIZE_DIVIDEND: usize = 7;
/// > CUBIC multiplicative decrease factor
///
/// <https://datatracker.ietf.org/doc/html/rfc9438#name-constants-of-interest>
///
/// > To balance between the scalability and convergence speed, CUBIC sets the multiplicative window
/// > decrease factor to 0.7 while Standard TCP uses 0.5. While this improves the scalability of
/// > CUBIC, a side effect of this decision is slower convergence, especially under low statistical
/// > multiplexing environments.
///
/// <https://datatracker.ietf.org/doc/html/rfc9438#name-principle-4-for-the-cubic-d>
///
/// For implementation reasons neqo uses a dividend and divisor approach with `usize` typing to
/// construct `CUBIC_BETA = 0.7`
pub const CUBIC_BETA_USIZE_DIVISOR: usize = 10;

/// The fast convergence ratio further reduces the congestion window when a
/// congestion event occurs before reaching the previous `W_max`.
/// This is the factor that is used by fast convergence to further reduce the next `W_max` when a
/// congestion event occurs while `cwnd < W_max`. This speeds up the bandwidth release for when a
/// new flow joins the network.
///
/// See formula defined below.
/// The calculation assumes `CUBIC_BETA = 0.7`.
///
/// <https://www.rfc-editor.org/rfc/rfc8312#section-4.6>
pub const CUBIC_FAST_CONVERGENCE: f64 = 0.85; // (1.0 + CUBIC_BETA) / 2.0;
/// <https://datatracker.ietf.org/doc/html/rfc9438#name-fast-convergence>
pub const CUBIC_FAST_CONVERGENCE_FACTOR: f64 = (1.0 + 0.7) / 2.0;

Check warning on line 68 in neqo-transport/src/cc/cubic.rs

View workflow job for this annotation

GitHub Actions / Find mutants

Missed mutant

replace + with *

Check warning on line 68 in neqo-transport/src/cc/cubic.rs

View workflow job for this annotation

GitHub Actions / Find mutants

Missed mutant

replace / with *

Check warning on line 68 in neqo-transport/src/cc/cubic.rs

View workflow job for this annotation

GitHub Actions / Find mutants

Missed mutant

replace + with -

Check warning on line 68 in neqo-transport/src/cc/cubic.rs

View workflow job for this annotation

GitHub Actions / Find mutants

Missed mutant

replace / with %

/// The minimum number of multiples of the datagram size that need
/// to be received to cause an increase in the congestion window.
Expand All @@ -70,15 +88,6 @@

#[derive(Debug, Default)]
pub struct Cubic {
/// Maximum Window size two congestion events ago.
///
/// > With fast convergence, when a congestion event occurs, before the
/// > window reduction of the congestion window, a flow remembers the last
/// > value of W_max before it updates W_max for the current congestion
/// > event.
///
/// <https://datatracker.ietf.org/doc/html/rfc8312#section-4.6>
last_max_cwnd: f64,
/// Estimate of Standard TCP congestion window for Cubic's TCP-friendly
/// Region.
///
Expand All @@ -94,28 +103,45 @@
///
/// <https://datatracker.ietf.org/doc/html/rfc8312#section-4.1>
k: f64,
/// > W_max is the window size just before the window is reduced in the last
/// > congestion event.
/// > Size of `cwnd` in \[bytes\] just before `cwnd` was reduced in the last congestion
/// > event \[...\]. \[With\] fast convergence enabled, `w_max` may be further reduced based on
/// > the current saturation point.
///
/// <https://datatracker.ietf.org/doc/html/rfc8312#section-4.1>
/// <https://datatracker.ietf.org/doc/html/rfc9438#name-variables-of-interest>
///
/// `w_max` acts as the plateau for the cubic function where it switches from the concave to
/// the convex region.
///
/// It is calculated with the following logic:
///
/// ```pseudo
/// if (w_max > cwnd) {
/// w_max = cwnd * FAST_CONVERGENCE_FACTOR;
/// } else {
/// w_max = cwnd;
/// }
/// ```
///
/// <https://datatracker.ietf.org/doc/html/rfc9438#name-fast-convergence>
w_max: f64,
/// > the elapsed time from the beginning of the current congestion
/// > avoidance
/// > The time in seconds at which the current congestion avoidance stage started.
///
/// <https://datatracker.ietf.org/doc/html/rfc8312#section-4.1>
ca_epoch_start: Option<Instant>,
/// <https://datatracker.ietf.org/doc/html/rfc9438#name-variables-of-interest>
///
/// This also is reset on being application limited.
t_epoch: Option<Instant>,
/// Number of bytes acked since the last Standard TCP congestion window increase.
tcp_acked_bytes: f64,
}

impl Display for Cubic {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Cubic [last_max_cwnd: {}, k: {}, w_max: {}, ca_epoch_start: {:?}]",
self.last_max_cwnd, self.k, self.w_max, self.ca_epoch_start
"Cubic [w_max: {}, k: {}, t_epoch: {:?}]",
self.w_max, self.k, self.t_epoch
)?;
Ok(())

Check warning on line 144 in neqo-transport/src/cc/cubic.rs

View workflow job for this annotation

GitHub Actions / Find mutants

Missed mutant

replace <impl Display for Cubic>::fmt -> fmt::Result with Ok(Default::default())
}
}

Expand Down Expand Up @@ -145,35 +171,49 @@
(CUBIC_C * (t - self.k).powi(3)).mul_add(max_datagram_size, self.w_max)
}

/// Sets `estimated_tcp_cwnd`, `k`, `t_epoch` and `tcp_acked_bytes` at the start of a new
/// epoch (new congestion avoidance stage) according to RFC 9438. The `w_max` variable has
/// been set in `reduce_cwnd()` prior to this call.
///
/// > `w_est` is set equal to `cwnd_epoch` at the start of the congestion avoidance stage.
///
/// <https://datatracker.ietf.org/doc/html/rfc9438#section-4.3-9>
///
/// Also initializes `k` and `w_max` if we start an epoch without having ever had
/// a congestion event, which can happen upon exiting slow start.
fn start_epoch(
&mut self,
curr_cwnd: f64,
new_acked: f64,
max_datagram_size: f64,
now: Instant,
) {
self.ca_epoch_start = Some(now);
// reset tcp_acked_bytes and estimated_tcp_cwnd;
self.t_epoch = Some(now);
self.tcp_acked_bytes = new_acked;
self.estimated_tcp_cwnd = curr_cwnd;
if self.last_max_cwnd <= curr_cwnd {
// If `w_max < cwnd_epoch` we take the cubic root from a negative value in `calc_k()`. That
// could only happen if somehow `cwnd` get's increased between calling `reduce_cwnd()` and
// `start_epoch()`. This could happen if we exit slow start without packet loss, thus never
// had a congestion event and called `reduce_cwnd()` which means `w_max` was never set and
// is still it's default `0.0` value. For those cases we reset/initialize `w_max` here and
// appropiately set `k` to `0.0` (`k` is the time for `cwnd` to reach `w_max`).
self.k = if self.w_max <= curr_cwnd {
self.w_max = curr_cwnd;
self.k = 0.0;
0.0
} else {
self.w_max = self.last_max_cwnd;
self.k = self.calc_k(curr_cwnd, max_datagram_size);
}
self.calc_k(curr_cwnd, max_datagram_size)
};
qtrace!("[{self}] New epoch");
}

#[cfg(test)]
pub const fn last_max_cwnd(&self) -> f64 {
self.last_max_cwnd
pub const fn w_max(&self) -> f64 {
self.w_max
}

#[cfg(test)]
pub fn set_last_max_cwnd(&mut self, last_max_cwnd: f64) {
self.last_max_cwnd = last_max_cwnd;
pub fn set_w_max(&mut self, w_max: f64) {
self.w_max = w_max;
}
}

Expand All @@ -194,7 +234,7 @@
let curr_cwnd_f64 = convert_to_f64(curr_cwnd);
let new_acked_f64 = convert_to_f64(new_acked_bytes);
let max_datagram_size_f64 = convert_to_f64(max_datagram_size);
if self.ca_epoch_start.is_none() {
if self.t_epoch.is_none() {
// This is a start of a new congestion avoidance phase.
self.start_epoch(curr_cwnd_f64, new_acked_f64, max_datagram_size_f64, now);
} else {
Expand All @@ -206,7 +246,7 @@
// <https://datatracker.ietf.org/doc/html/rfc8312#section-4.3>
// <https://datatracker.ietf.org/doc/html/rfc8312#section-4.4>
let time_ca = self
.ca_epoch_start
.t_epoch
.map_or(min_rtt, |t| {
if now + min_rtt < t {
// This only happens when processing old packets
Expand Down Expand Up @@ -256,6 +296,22 @@
acked_to_increase as usize
}

// CUBIC RFC 9438 changes the logic for multiplicative decrease, most notably setting the
// minimum congestion window to 1*SMSS under some circumstances while keeping ssthresh at
// 2*SMSS.
//
// <https://datatracker.ietf.org/doc/html/rfc9438#section-4.6>
//
// QUIC has a minimum congestion window of 2*SMSS as per RFC 9002.
//
// <https://datatracker.ietf.org/doc/html/rfc9002#section-4.8>
//
// For that reason we diverge from CUBIC RFC 9438 here retaining the 2*SMSS minimum for the
// congestion window.
//
// This function only returns the value for `cwnd * CUBIC_BETA` and sets some variables for the
// start of a new congestion avoidance phase. Actually setting the congestion window happens in
// [`super::ClassicCongestionControl::on_congestion_event`] where this function is called.
fn reduce_cwnd(
&mut self,
curr_cwnd: usize,
Expand All @@ -265,28 +321,37 @@
let curr_cwnd_f64 = convert_to_f64(curr_cwnd);
// Fast Convergence
//
// If congestion event occurs before the maximum congestion window before the last
// congestion event, we reduce the the maximum congestion window and thereby W_max.
// check cwnd + MAX_DATAGRAM_SIZE instead of cwnd because with cwnd in bytes, cwnd may be
// slightly off.
// > During a congestion event, if the current cwnd is less than w_max, this indicates
// > that the saturation point experienced by this flow is getting reduced because of
// > a change in available bandwidth. This flow can then release more bandwidth by
// > reducing w_max further. This action effectively lengthens the time for this flow
// > to increase its congestion window, because the reduced w_max forces the flow to
// > plateau earlier. This allows more time for the new flow to catch up to its
// > congestion window size.
//
// <https://datatracker.ietf.org/doc/html/rfc9438#name-fast-convergence>
//
// <https://www.rfc-editor.org/rfc/rfc8312#section-4.6>
self.last_max_cwnd =
if curr_cwnd_f64 + convert_to_f64(max_datagram_size) < self.last_max_cwnd {
curr_cwnd_f64 * CUBIC_FAST_CONVERGENCE
} else {
curr_cwnd_f64
};
self.ca_epoch_start = None;
// From the old implementation:
//
// "Check cwnd + MAX_DATAGRAM_SIZE instead of cwnd because with cwnd in bytes, cwnd may be
// slightly off."
self.w_max = if curr_cwnd_f64 + convert_to_f64(max_datagram_size) < self.w_max {

Check warning on line 338 in neqo-transport/src/cc/cubic.rs

View workflow job for this annotation

GitHub Actions / Find mutants

Missed mutant

replace < with <= in <impl WindowAdjustment for Cubic>::reduce_cwnd
curr_cwnd_f64 * CUBIC_FAST_CONVERGENCE_FACTOR
} else {
curr_cwnd_f64
};

// Reducing the congestion window and resetting time
self.t_epoch = None;
(
curr_cwnd * CUBIC_BETA_USIZE_DIVIDEND / CUBIC_BETA_USIZE_DIVISOR,
acked_bytes * CUBIC_BETA_USIZE_DIVIDEND / CUBIC_BETA_USIZE_DIVISOR,
)
}

fn on_app_limited(&mut self) {
// Reset ca_epoch_start. Let it start again when the congestion controller
// Reset t_epoch. Let it start again when the congestion controller
// exits the app-limited period.
self.ca_epoch_start = None;
self.t_epoch = None;

Check warning on line 355 in neqo-transport/src/cc/cubic.rs

View workflow job for this annotation

GitHub Actions / Find mutants

Missed mutant

replace <impl WindowAdjustment for Cubic>::on_app_limited with ()
}
}
Loading
Loading