// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
// CUBIC (RFC 9438): the congestion window follows a cubic function of the time since the
// last reduction, C·(t−K)³ + W_max, instead of NewReno's one datagram per round. On a long
// fat pipe that climbs back to the old rate quickly without over-running a shallow buffer,
// and it falls to β = 0.7 of the window rather than NewReno's half. It answers the same
// `Control` questions; only the growth rule and the reduction factor differ.
///|
/// The multiplicative decrease factor β (RFC 9438 §4.6): a CUBIC window falls to seven
/// tenths of itself, not to half.
let beta : Double = 0.7
///|
/// The cubic scaling constant C, in segments per second cubed (RFC 9438 §4.2).
let scaling : Double = 0.4
///|
/// A CUBIC congestion controller (RFC 9438), an implementation of `Control`.
pub struct Cubic {
mut window : Int64
mut ssthresh : Int64
mut in_flight : Int64
mut recovery : Int64
/// The window at the last reduction, the W_max the cubic climbs back to (bytes).
mut w_max : Int64
/// When the current congestion-avoidance epoch began, in microseconds.
mut epoch : Int64
/// K, the seconds the cubic takes to return to W_max, fixed at the reduction.
mut k : Double
/// The smallest round trip seen, for the Reno-friendly estimate; the initial RTT until
/// one arrives.
mut rtt : Int64
policy : Policy
}
///|
/// A fresh CUBIC controller at RFC 9002 §7.2's initial window: the same start as NewReno,
/// since how a controller begins is not what makes it CUBIC.
pub fn Cubic::new(policy~ : Policy = policy) -> Cubic {
let two = 2L * policy.datagram
let ten = 10L * policy.datagram
let floor = if two > 14_720L { two } else { 14_720L }
let initial = if ten < floor { ten } else { floor }
{
window: initial,
ssthresh: 1L << 60,
in_flight: 0,
recovery: -1L,
w_max: 0,
epoch: 0,
k: 0.0,
rtt: policy.initial_rtt,
policy,
}
}
///|
/// The smallest window the controller will reduce to (RFC 9002 §7.2).
pub fn Cubic::minimum(self : Cubic) -> Int64 {
2L * self.policy.datagram
}
///|
/// The slow-start threshold in bytes.
pub fn Cubic::ssthresh(self : Cubic) -> Int64 {
self.ssthresh
}
///|
/// Whether a packet sent at `at` falls inside the recovery period already under way.
pub fn Cubic::recovering(self : Cubic, at : Int64) -> Bool {
self.recovery >= 0L && at <= self.recovery
}
///|
/// The window the cubic and the Reno-friendly estimate ask for at `now`, in bytes — the
/// larger of the two (RFC 9438 §4.2, §4.3). A test reproduces this arithmetic itself so a
/// change to the curve is visible as a changed window.
pub fn Cubic::target(self : Cubic, now : Int64) -> Int64 {
let seg = self.policy.datagram.to_double()
let t = (now - self.epoch).to_double() / 1_000_000.0
let w_max = self.w_max.to_double() / seg
let cubic = scaling * (t - self.k) * (t - self.k) * (t - self.k) + w_max
let rtt = self.rtt.to_double() / 1_000_000.0
let est = w_max * beta +
3.0 * (1.0 - beta) / (1.0 + beta) * (if rtt > 0.0 { t / rtt } else { 0.0 })
let want = if cubic > est { cubic } else { est }
(want * seg).to_int64()
}
///|
/// One acknowledgement's growth: below the threshold every acknowledged byte is added as in
/// slow start; above it the window follows the curve, never falling back.
fn Cubic::grow(self : Cubic, size : Int64, now : Int64) -> Unit {
if self.window < self.ssthresh {
self.window = self.window + size
} else {
let want = self.target(now)
if want > self.window {
self.window = want
}
}
}
///|
/// The defaulted methods of `Control` this controller inherits, named so the promotion to
/// methods on `Cubic` is explicit rather than implied by the impl.
pub extend Cubic with Control::{on_new_path}
///|
pub impl Control for Cubic with fn window(self) {
self.window
}
///|
pub impl Control for Cubic with fn in_flight(self) {
self.in_flight
}
///|
pub impl Control for Cubic with fn on_sent(self, size) {
self.in_flight = self.in_flight + size
}
///|
/// Without a clock this can only take the bytes out of flight; the growth on an
/// acknowledgement comes through `on_ack_at`, which has the time.
pub impl Control for Cubic with fn on_ack(self, size) {
self.in_flight = if self.in_flight > size {
self.in_flight - size
} else {
0L
}
}
///|
pub impl Control for Cubic with fn on_ack_at(self, size, now~) {
self.in_flight = if self.in_flight > size {
self.in_flight - size
} else {
0L
}
self.grow(size, now)
}
///|
pub impl Control for Cubic with fn on_lost(self, size) {
self.in_flight = if self.in_flight > size {
self.in_flight - size
} else {
0L
}
}
///|
/// A congestion signal (RFC 9438 §4.6): W_max remembers the window it fell from, the window
/// drops to β of it (with fast convergence when it was already below the last W_max), a new
/// epoch starts, and K is recomputed as the time it takes the cubic to climb back.
pub impl Control for Cubic with fn on_congestion(self, at~, now~) {
if self.recovering(at) {
return
}
let previous = self.window
if self.w_max > 0L && previous < self.w_max {
self.w_max = (previous.to_double() * (1.0 + beta) / 2.0).to_int64()
} else {
self.w_max = previous
}
let reduced = (previous.to_double() * beta).to_int64()
let floor = self.minimum()
self.window = if reduced > floor { reduced } else { floor }
self.ssthresh = self.window
self.recovery = now
self.epoch = now
let seg = self.policy.datagram.to_double()
let gap = (self.w_max.to_double() - self.window.to_double()) / seg
self.k = @math.cbrt(if gap > 0.0 { gap / scaling } else { 0.0 })
}
///|
/// Persistent congestion collapses the window to the minimum and clears the recovery period,
/// as NewReno's does; the ssthresh left above it puts the next acknowledgement in slow start.
pub impl Control for Cubic with fn on_persistent(self) {
self.window = self.minimum()
self.recovery = -1L
}
///|
pub impl Control for Cubic with fn on_rtt(self, sample~, now~) {
ignore(now)
if sample > 0L && sample < self.rtt {
self.rtt = sample
}
}
///|
pub extend Cubic with Control::{
window,
in_flight,
on_sent,
on_ack,
on_ack_at,
on_lost,
on_congestion,
on_persistent,
on_rtt,
pacing_rate,
}
///|
pub impl Control for Cubic with fn on_new_path(self) {
let fresh = Cubic::new(policy=self.policy)
self.window = fresh.window
self.ssthresh = fresh.ssthresh
self.recovery = fresh.recovery
self.w_max = fresh.w_max
self.epoch = fresh.epoch
self.k = fresh.k
self.rtt = fresh.rtt
}