// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
// BBR (Bottleneck Bandwidth and Round-trip propagation time): the window is the path's
// bandwidth-delay product times a gain that walks through startup, drain, probe-bandwidth
// and probe-RTT. It models the pipe rather than counting acknowledgements, so a loss is not
// its congestion signal — a loss does not halve the window the way NewReno's does, and a
// connection whose bottleneck buffer is shallow keeps its rate. It implements the same
// `Control` contract as NewReno and CUBIC.
///|
/// Where a BBR sender is in its bandwidth-probing cycle.
#derive(Eq, Debug)
pub(all) enum Mode {
/// Grow the window 2.885× per round until the delivered rate stops rising.
Startup
/// Drain the queue startup built, down to the bandwidth-delay product.
Drain
/// Walk the gain around 1 to probe for more bandwidth, then yield it.
ProbeBw
/// Hold the window at the minimum for one round trip so the path drains and a fresh
/// round-trip minimum can be sampled.
ProbeRtt
}
///|
/// The startup gain, `2/ln(2)` (BBR's high gain), rounded.
let startup_gain : Double = 2.885
///|
/// The probe-bandwidth gain for a cycle position: 1.25 to probe, 0.75 to yield, then 1 while
/// the estimate is held. BBR's cycle is eight rounds long, one per position, and the two ends
/// are where the bandwidth estimate is tested (draft-cardwell-iccrg-bbr §4.3).
fn probe_gain(index : Int32) -> Double {
match index % 8 {
0 => 1.25
1 => 0.75
_ => 1.0
}
}
///|
/// How much of the bandwidth-delay product the window holds: BBR gives the window twice the
/// model while probing and once while draining, so a queue it built can empty. The pacing gain
/// and the window gain are different numbers on purpose — pacing says how fast to send, the
/// window says how much may be outstanding.
fn cwnd_gain(mode : Mode) -> Double {
match mode {
Startup => startup_gain
Drain => 1.0
ProbeBw => 2.0
ProbeRtt => 1.0
}
}
///|
/// How long BBR holds the window at its floor so the path can drain and a fresh round-trip
/// minimum can be sampled (draft-cardwell-iccrg-bbr §4.4). 200 milliseconds, as BBR uses.
let probe_rtt_period : Int64 = 200_000L
///|
/// How many datagrams the window holds while sampling the round trip. Four packets is BBR's
/// floor for the stay: enough for the peer's acknowledgement to come back, small enough that
/// the queue empties.
let probe_rtt_datagrams : Int64 = 4L
///|
/// A BBR congestion controller, an implementation of `Control`.
pub struct Bbr {
mut window : Int64
mut in_flight : Int64
/// The maximum delivery rate seen, in bytes per microsecond.
mut btl_bw : Double
/// The smallest round trip seen, in microseconds; the initial RTT until one arrives.
mut min_rtt : Int64
/// When `min_rtt` last improved, for the ten-second probe-RTT trigger.
mut min_rtt_at : Int64
mut mode : Mode
/// Bytes acknowledged in total, the numerator of a delivery-rate sample.
mut delivered : Int64
mut sample_at : Int64
mut sample_delivered : Int64
/// The bandwidth at the last startup round that still rose, and how many rounds since.
mut full_bw : Double
mut full_bw_count : Int32
mut cycle : Int32
mut cycle_at : Int64
mut probe_rtt_at : Int64
/// Whether a round-trip sample arrived during the probe-RTT stay, which is what lets it end.
mut probe_rtt_done : Bool
policy : Policy
}
///|
/// A fresh BBR controller at RFC 9002 §7.2's initial window, in startup.
pub fn Bbr::new(policy~ : Policy = policy) -> Bbr {
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,
in_flight: 0,
btl_bw: 0.0,
min_rtt: policy.initial_rtt,
min_rtt_at: 0,
mode: Startup,
delivered: 0,
sample_at: 0,
sample_delivered: 0,
full_bw: 0.0,
full_bw_count: 0,
cycle: 0,
cycle_at: 0,
probe_rtt_at: 0,
probe_rtt_done: false,
policy,
}
}
///|
/// The smallest window the controller will set (RFC 9002 §7.2).
pub fn Bbr::minimum(self : Bbr) -> Int64 {
2L * self.policy.datagram
}
///|
/// Where the controller is in its probing cycle.
pub fn Bbr::mode(self : Bbr) -> Mode {
self.mode
}
///|
/// The maximum delivery rate seen, in bytes per microsecond.
pub fn Bbr::bandwidth(self : Bbr) -> Double {
self.btl_bw
}
///|
/// The smallest round trip seen, in microseconds.
pub fn Bbr::min_rtt(self : Bbr) -> Int64 {
self.min_rtt
}
///|
/// The gain the current mode applies to the bandwidth-delay product when pacing.
fn Bbr::gain(self : Bbr) -> Double {
match self.mode {
Startup => startup_gain
Drain => 1.0 / startup_gain
ProbeBw => probe_gain(self.cycle)
ProbeRtt => 1.0
}
}
///|
/// The bandwidth-delay product in bytes: how much the path can hold in flight at once at the
/// best rate and the smallest round trip seen.
pub fn Bbr::bdp(self : Bbr) -> Int64 {
(self.btl_bw * self.min_rtt.to_double()).to_int64()
}
///|
/// The window the model asks for: the bandwidth-delay product times the mode's window gain,
/// three datagrams of headroom, floored at the minimum. A loss leaves it unchanged, which is
/// what tells BBR from NewReno. While the round trip is being sampled the window is held at
/// four datagrams whatever the model says, so the queue the last cycle built can drain.
pub fn Bbr::target(self : Bbr) -> Int64 {
if self.mode == ProbeRtt {
let floor = self.minimum()
let window = probe_rtt_datagrams * self.policy.datagram
return if window > floor { window } else { floor }
}
let scaled = (self.bdp().to_double() * cwnd_gain(self.mode)).to_int64()
let want = scaled + 3L * self.policy.datagram
let floor = self.minimum()
if want > floor {
want
} else {
floor
}
}
///|
/// The defaulted methods of `Control` this controller inherits, named so the promotion to
/// methods on `Bbr` is explicit rather than implied by the impl.
pub extend Bbr with Control::{on_new_path}
///|
pub impl Control for Bbr with fn window(self) {
self.window
}
///|
pub impl Control for Bbr with fn in_flight(self) {
self.in_flight
}
///|
pub impl Control for Bbr with fn on_sent(self, size) {
self.in_flight = self.in_flight + size
}
///|
pub impl Control for Bbr with fn on_ack(self, size) {
self.in_flight = if self.in_flight > size {
self.in_flight - size
} else {
0L
}
}
///|
pub impl Control for Bbr with fn on_lost(self, size) {
self.in_flight = if self.in_flight > size {
self.in_flight - size
} else {
0L
}
}
///|
/// A congestion signal. BBR does not treat loss as its signal: it leaves the window the
/// model computes alone, and a loss in startup only ends startup early.
pub impl Control for Bbr with fn on_congestion(self, at~, now~) {
ignore(at)
ignore(now)
if self.mode == Startup {
self.mode = Drain
}
}
///|
/// Persistent congestion is the one loss that resets the model: the path is not delivering,
/// so the window drops to the minimum and a fresh round-trip minimum is taken.
pub impl Control for Bbr with fn on_persistent(self) {
self.window = self.minimum()
self.btl_bw = 0.0
self.mode = ProbeRtt
self.probe_rtt_at = 0L
self.probe_rtt_done = false
}
///|
/// An acknowledgement at a known time: out of flight, into the delivery-rate sample, and the
/// window recomputed from the model.
pub impl Control for Bbr with fn on_ack_at(self, size, now~) {
self.in_flight = if self.in_flight > size {
self.in_flight - size
} else {
0L
}
self.delivered = self.delivered + size
let elapsed = now - self.sample_at
if elapsed >= self.min_rtt && elapsed > 0L {
let rate = (self.delivered - self.sample_delivered).to_double() /
elapsed.to_double()
if rate > self.btl_bw {
self.btl_bw = rate
}
self.sample_delivered = self.delivered
self.sample_at = now
if self.mode == Startup {
if self.btl_bw >= self.full_bw * 1.25 {
self.full_bw = self.btl_bw
self.full_bw_count = 0
} else {
self.full_bw_count = self.full_bw_count + 1
if self.full_bw_count >= 3 {
self.mode = Drain
}
}
}
}
// ProbeRTT: ten seconds without a fresher round-trip minimum means the path's queue has
// grown into every sample, so the window drops and waits for a clean one (§4.4).
if self.btl_bw > 0.0 &&
self.mode != ProbeRtt &&
now - self.min_rtt_at > 10_000_000L {
self.mode = ProbeRtt
self.probe_rtt_at = now
self.probe_rtt_done = false
}
match self.mode {
Startup => ()
Drain =>
// The queue startup built is gone once what is in flight fits the model.
if self.in_flight <= self.bdp() {
self.mode = ProbeBw
// Enter at a gain of 1: the bandwidth was just measured, so probe from there.
self.cycle = 2
self.cycle_at = now
}
ProbeBw =>
// One round per position: the cycle walks 1.25, 0.75, then six rounds at 1.
if now - self.cycle_at >= self.min_rtt {
self.cycle = self.cycle + 1
self.cycle_at = now
}
ProbeRtt =>
// It ends when the stay is over and a sample has arrived; a path that says nothing keeps
// the window small, which is the safe direction.
if now - self.probe_rtt_at >= probe_rtt_period && self.probe_rtt_done {
self.mode = ProbeBw
self.cycle = 2
self.cycle_at = now
}
}
self.window = self.target()
}
///|
/// A round-trip sample: keep the smallest, and note when it was taken so probe-RTT knows how
/// long the path has gone without a fresher one.
pub impl Control for Bbr with fn on_rtt(self, sample~, now~) {
if sample > 0L && sample < self.min_rtt {
self.min_rtt = sample
self.min_rtt_at = now
if self.mode == ProbeRtt {
// A clean sample during the stay is what it was waiting for; the stay still runs its
// 200 milliseconds, so the queue has time to empty.
self.probe_rtt_done = true
}
}
}
///|
/// What BBR would pace at: the bottleneck rate times the mode's gain, which is the rate the
/// model has decided the path can carry. The window gain is a different number on purpose —
/// pacing says how fast to send, the window says how much may be outstanding. Nothing is
/// offered before the first delivery-rate sample, so the connection is unpaced until it knows
/// something (RFC 9002 §7.7).
pub impl Control for Bbr with fn pacing_rate(self) {
if self.btl_bw <= 0.0 {
None
} else {
Some(self.btl_bw * self.gain())
}
}
///|
pub extend Bbr with Control::{
window,
in_flight,
on_sent,
on_ack,
on_ack_at,
on_lost,
on_congestion,
on_persistent,
on_rtt,
pacing_rate,
}
///|
pub extend Mode with Debug::{to_repr}
///|
pub extend Mode with Eq::{not_equal, equal}
///|
pub impl Control for Bbr with fn on_new_path(self) {
let fresh = Bbr::new(policy=self.policy)
self.window = fresh.window
self.btl_bw = fresh.btl_bw
self.min_rtt = fresh.min_rtt
self.min_rtt_at = fresh.min_rtt_at
self.mode = fresh.mode
self.delivered = fresh.delivered
self.sample_at = fresh.sample_at
self.sample_delivered = fresh.sample_delivered
self.full_bw = fresh.full_bw
self.full_bw_count = fresh.full_bw_count
self.cycle = fresh.cycle
self.cycle_at = fresh.cycle_at
self.probe_rtt_at = fresh.probe_rtt_at
self.probe_rtt_done = fresh.probe_rtt_done
}