use std::collections::VecDeque;
const DEFAULT_STARTUP_GAIN: f64 = 2.885;
const PROBE_RTT_PACING_GAIN: f64 = 1.0;
const MIN_CWND_BYTES: u64 = 4 * 1460;
const PROBE_BW_GAINS: [f64; 8] = [1.25, 0.75, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
const PROBE_RTT_DURATION_MS: u64 = 200;
#[derive(Debug, Clone)]
pub struct BbrConfig {
pub startup_gain: f64,
pub drain_gain: f64,
pub probe_bw_gain: f64,
pub rtprop_filter_len_ms: u64,
pub btlbw_filter_len: usize,
}
impl Default for BbrConfig {
fn default() -> Self {
Self {
startup_gain: DEFAULT_STARTUP_GAIN,
drain_gain: 1.0 / DEFAULT_STARTUP_GAIN,
probe_bw_gain: 1.25,
rtprop_filter_len_ms: 10_000,
btlbw_filter_len: 10,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BbrState {
Startup,
Drain,
ProbeBw,
ProbeRtt,
}
impl std::fmt::Display for BbrState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::Startup => "startup",
Self::Drain => "drain",
Self::ProbeBw => "probe_bw",
Self::ProbeRtt => "probe_rtt",
};
write!(f, "{s}")
}
}
#[derive(Debug, Clone, Copy)]
pub struct AckSample {
pub delivered: u64,
pub elapsed_secs: f64,
pub rtt_secs: f64,
pub is_app_limited: bool,
}
#[derive(Debug, Clone, Copy)]
struct BwSample {
rate_bps: f64,
#[allow(dead_code)]
round: u64,
}
#[derive(Debug, Clone, Copy)]
struct RttSample {
rtt_secs: f64,
timestamp_ms: u64,
}
pub struct BbrController {
config: BbrConfig,
state: BbrState,
btlbw: f64,
rtprop: f64,
rtprop_stamp_ms: u64,
rtprop_expired: bool,
pacing_rate: f64,
cwnd: u64,
now_ms: u64,
round_count: u64,
full_bw_count: u32,
full_bw: f64,
bw_samples: VecDeque<BwSample>,
rtt_samples: VecDeque<RttSample>,
probe_bw_cycle_idx: usize,
probe_bw_cycle_start_round: u64,
probe_rtt_start_ms: Option<u64>,
ack_count: u64,
}
impl BbrController {
#[must_use]
pub fn new(config: BbrConfig) -> Self {
let startup_gain = config.startup_gain;
let initial_bw: f64 = 125_000.0; let initial_rtt: f64 = 0.1;
let initial_cwnd = ((initial_bw * initial_rtt * startup_gain) as u64).max(MIN_CWND_BYTES);
Self {
config,
state: BbrState::Startup,
btlbw: initial_bw,
rtprop: initial_rtt,
rtprop_stamp_ms: 0,
rtprop_expired: false,
pacing_rate: initial_bw * startup_gain,
cwnd: initial_cwnd,
now_ms: 0,
round_count: 0,
full_bw_count: 0,
full_bw: 0.0,
bw_samples: VecDeque::new(),
rtt_samples: VecDeque::new(),
probe_bw_cycle_idx: 0,
probe_bw_cycle_start_round: 0,
probe_rtt_start_ms: None,
ack_count: 0,
}
}
pub fn on_ack(&mut self, sample: AckSample) {
if sample.elapsed_secs <= 0.0 || sample.rtt_secs <= 0.0 {
return;
}
self.ack_count += 1;
let elapsed_ms = (sample.elapsed_secs * 1_000.0) as u64;
self.now_ms = self.now_ms.saturating_add(elapsed_ms);
self.update_rtprop(sample.rtt_secs);
let delivery_rate = if sample.elapsed_secs > 0.0 {
sample.delivered as f64 / sample.elapsed_secs
} else {
0.0
};
if !sample.is_app_limited || delivery_rate > self.btlbw {
self.update_btlbw(delivery_rate);
}
self.round_count = self.round_count.saturating_add(1);
match self.state {
BbrState::Startup => self.handle_startup(),
BbrState::Drain => self.handle_drain(),
BbrState::ProbeBw => self.handle_probe_bw(),
BbrState::ProbeRtt => self.handle_probe_rtt(),
}
self.update_pacing_and_cwnd();
}
#[must_use]
pub fn pacing_rate(&self) -> f64 {
self.pacing_rate
}
#[must_use]
pub fn cwnd(&self) -> u64 {
self.cwnd
}
#[must_use]
pub fn state(&self) -> &BbrState {
&self.state
}
#[must_use]
pub fn inflight_target(&self) -> u64 {
let bdp = self.btlbw * self.rtprop;
let gain = self.current_cwnd_gain();
((bdp * gain) as u64).max(MIN_CWND_BYTES)
}
#[must_use]
pub fn btlbw(&self) -> f64 {
self.btlbw
}
#[must_use]
pub fn rtprop(&self) -> f64 {
self.rtprop
}
fn update_rtprop(&mut self, rtt_secs: f64) {
let window_ms = self.config.rtprop_filter_len_ms;
let age_ms = self.now_ms.saturating_sub(self.rtprop_stamp_ms);
self.rtprop_expired = age_ms > window_ms;
self.rtt_samples.push_back(RttSample {
rtt_secs,
timestamp_ms: self.now_ms,
});
while let Some(front) = self.rtt_samples.front() {
if self.now_ms.saturating_sub(front.timestamp_ms) > window_ms {
self.rtt_samples.pop_front();
} else {
break;
}
}
if let Some(min_sample) = self.rtt_samples.iter().min_by(|a, b| {
a.rtt_secs
.partial_cmp(&b.rtt_secs)
.unwrap_or(std::cmp::Ordering::Equal)
}) {
if min_sample.rtt_secs < self.rtprop || self.rtprop_expired {
self.rtprop = min_sample.rtt_secs;
self.rtprop_stamp_ms = self.now_ms;
self.rtprop_expired = false;
}
}
}
fn update_btlbw(&mut self, delivery_rate: f64) {
self.bw_samples.push_back(BwSample {
rate_bps: delivery_rate,
round: self.round_count,
});
while self.bw_samples.len() > self.config.btlbw_filter_len {
self.bw_samples.pop_front();
}
if let Some(max_sample) = self.bw_samples.iter().max_by(|a, b| {
a.rate_bps
.partial_cmp(&b.rate_bps)
.unwrap_or(std::cmp::Ordering::Equal)
}) {
self.btlbw = max_sample.rate_bps;
}
}
fn handle_startup(&mut self) {
const FULL_BW_GROWTH_THRESHOLD: f64 = 1.25;
const FULL_BW_COUNT_THRESHOLD: u32 = 3;
if self.btlbw >= self.full_bw * FULL_BW_GROWTH_THRESHOLD {
self.full_bw = self.btlbw;
self.full_bw_count = 0;
} else {
self.full_bw_count += 1;
}
if self.full_bw_count >= FULL_BW_COUNT_THRESHOLD {
self.state = BbrState::Drain;
}
}
fn handle_drain(&mut self) {
let bdp = self.btlbw * self.rtprop;
let inflight = self.cwnd; if (inflight as f64) <= bdp {
self.state = BbrState::ProbeBw;
self.probe_bw_cycle_idx = 0;
self.probe_bw_cycle_start_round = self.round_count;
}
}
fn handle_probe_bw(&mut self) {
let rounds_in_phase = self
.round_count
.saturating_sub(self.probe_bw_cycle_start_round);
if rounds_in_phase >= 1 {
self.probe_bw_cycle_idx = (self.probe_bw_cycle_idx + 1) % PROBE_BW_GAINS.len();
self.probe_bw_cycle_start_round = self.round_count;
}
if self.rtprop_expired {
self.state = BbrState::ProbeRtt;
self.probe_rtt_start_ms = Some(self.now_ms);
}
}
fn handle_probe_rtt(&mut self) {
let start_ms = self.probe_rtt_start_ms.unwrap_or(self.now_ms);
let elapsed_ms = self.now_ms.saturating_sub(start_ms);
if elapsed_ms >= PROBE_RTT_DURATION_MS {
self.probe_rtt_start_ms = None;
self.state = BbrState::ProbeBw;
self.probe_bw_cycle_idx = 0;
self.probe_bw_cycle_start_round = self.round_count;
self.rtprop_stamp_ms = self.now_ms;
self.rtprop_expired = false;
}
}
fn current_pacing_gain(&self) -> f64 {
match self.state {
BbrState::Startup => self.config.startup_gain,
BbrState::Drain => self.config.drain_gain,
BbrState::ProbeBw => PROBE_BW_GAINS[self.probe_bw_cycle_idx],
BbrState::ProbeRtt => PROBE_RTT_PACING_GAIN,
}
}
fn current_cwnd_gain(&self) -> f64 {
match self.state {
BbrState::Startup => self.config.startup_gain,
BbrState::Drain => self.config.drain_gain,
BbrState::ProbeBw => 2.0,
BbrState::ProbeRtt => 1.0,
}
}
fn update_pacing_and_cwnd(&mut self) {
let pacing_gain = self.current_pacing_gain();
self.pacing_rate = self.btlbw * pacing_gain;
let bdp = self.btlbw * self.rtprop;
let cwnd_gain = self.current_cwnd_gain();
let target = ((bdp * cwnd_gain) as u64).max(MIN_CWND_BYTES);
self.cwnd = target;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_sample(delivered: u64, elapsed_secs: f64, rtt_secs: f64) -> AckSample {
AckSample {
delivered,
elapsed_secs,
rtt_secs,
is_app_limited: false,
}
}
#[test]
fn test_default_config_values() {
let cfg = BbrConfig::default();
assert!((cfg.startup_gain - DEFAULT_STARTUP_GAIN).abs() < 1e-6);
assert!((cfg.drain_gain - 1.0 / DEFAULT_STARTUP_GAIN).abs() < 1e-6);
assert!((cfg.probe_bw_gain - 1.25).abs() < 1e-6);
assert_eq!(cfg.rtprop_filter_len_ms, 10_000);
assert_eq!(cfg.btlbw_filter_len, 10);
}
#[test]
fn test_initial_state_is_startup() {
let ctrl = BbrController::new(BbrConfig::default());
assert_eq!(*ctrl.state(), BbrState::Startup);
}
#[test]
fn test_initial_pacing_rate_positive() {
let ctrl = BbrController::new(BbrConfig::default());
assert!(
ctrl.pacing_rate() > 0.0,
"initial pacing rate should be positive"
);
}
#[test]
fn test_initial_cwnd_at_least_min() {
let ctrl = BbrController::new(BbrConfig::default());
assert!(ctrl.cwnd() >= MIN_CWND_BYTES, "initial cwnd below floor");
}
#[test]
fn test_startup_pacing_gain_applied() {
let cfg = BbrConfig::default();
let mut ctrl = BbrController::new(cfg.clone());
let sample = make_sample(125_000, 0.001, 0.01); ctrl.on_ack(sample);
assert_eq!(*ctrl.state(), BbrState::Startup);
let expected = ctrl.btlbw() * cfg.startup_gain;
assert!(
(ctrl.pacing_rate() - expected).abs() < 1.0,
"unexpected pacing rate in startup"
);
}
#[test]
fn test_startup_exits_after_full_bw_detected() {
let mut ctrl = BbrController::new(BbrConfig::default());
let flat_sample = make_sample(12_500, 0.001, 0.01); for _ in 0..20 {
ctrl.on_ack(flat_sample);
}
assert_ne!(
*ctrl.state(),
BbrState::Startup,
"should have exited Startup after flat bandwidth"
);
}
#[test]
fn test_startup_rate_increases_with_growing_bw() {
let mut ctrl = BbrController::new(BbrConfig::default());
let r0 = ctrl.pacing_rate();
ctrl.on_ack(make_sample(1_000_000, 0.001, 0.005));
assert!(
ctrl.pacing_rate() > r0,
"pacing rate should increase after high-bw sample"
);
}
#[test]
fn test_drain_uses_lower_pacing_gain() {
let cfg = BbrConfig::default();
let mut ctrl = BbrController::new(cfg.clone());
ctrl.state = BbrState::Drain;
ctrl.btlbw = 1_000_000.0;
ctrl.rtprop = 0.01;
ctrl.update_pacing_and_cwnd();
let expected = ctrl.btlbw() * cfg.drain_gain;
assert!(
(ctrl.pacing_rate() - expected).abs() < 1.0,
"drain pacing rate incorrect: {} vs {}",
ctrl.pacing_rate(),
expected
);
}
#[test]
fn test_probe_bw_gain_cycles() {
let mut ctrl = BbrController::new(BbrConfig::default());
ctrl.state = BbrState::ProbeBw;
ctrl.btlbw = 1_000_000.0;
ctrl.rtprop = 0.01;
let mut saw_high = false;
let mut saw_low = false;
for _ in 0..32 {
ctrl.on_ack(make_sample(1_000, 0.001, 0.01));
let rate = ctrl.pacing_rate() / ctrl.btlbw();
if rate > 1.1 {
saw_high = true;
}
if rate < 0.9 {
saw_low = true;
}
}
assert!(saw_high, "expected a high-gain phase in ProbeBw");
assert!(saw_low, "expected a drain-gain phase in ProbeBw");
}
#[test]
fn test_probe_rtt_reduces_cwnd() {
let mut ctrl = BbrController::new(BbrConfig::default());
ctrl.state = BbrState::ProbeBw;
ctrl.btlbw = 1_000_000.0;
ctrl.rtprop = 0.01;
ctrl.update_pacing_and_cwnd();
let cwnd_before = ctrl.cwnd();
ctrl.state = BbrState::ProbeRtt;
ctrl.probe_rtt_start_ms = Some(ctrl.now_ms);
ctrl.update_pacing_and_cwnd();
assert!(
ctrl.cwnd() <= cwnd_before,
"ProbeRtt should reduce cwnd: {} vs {}",
ctrl.cwnd(),
cwnd_before
);
}
#[test]
fn test_probe_rtt_exits_after_duration() {
let mut ctrl = BbrController::new(BbrConfig::default());
ctrl.state = BbrState::ProbeRtt;
ctrl.probe_rtt_start_ms = Some(0);
ctrl.now_ms = 0;
ctrl.on_ack(make_sample(1_000, 0.001, 0.01));
ctrl.now_ms = PROBE_RTT_DURATION_MS + 1;
ctrl.on_ack(make_sample(1_000, 0.001, 0.01));
assert_eq!(
*ctrl.state(),
BbrState::ProbeBw,
"should transition to ProbeBw after ProbeRtt duration"
);
}
#[test]
fn test_rtprop_tracks_minimum() {
let mut ctrl = BbrController::new(BbrConfig::default());
ctrl.on_ack(make_sample(1_000, 0.001, 0.050));
ctrl.on_ack(make_sample(1_000, 0.001, 0.010)); ctrl.on_ack(make_sample(1_000, 0.001, 0.030));
assert!(
ctrl.rtprop() <= 0.010 + 1e-9,
"RTprop should track minimum RTT, got {}",
ctrl.rtprop()
);
}
#[test]
fn test_rtprop_updates_on_better_rtt() {
let mut ctrl = BbrController::new(BbrConfig::default());
ctrl.on_ack(make_sample(1_000, 0.001, 0.100));
let rtt_before = ctrl.rtprop();
ctrl.on_ack(make_sample(1_000, 0.001, 0.005)); assert!(
ctrl.rtprop() < rtt_before,
"RTprop should update to smaller RTT"
);
}
#[test]
fn test_btlbw_tracks_maximum() {
let mut ctrl = BbrController::new(BbrConfig::default());
ctrl.on_ack(make_sample(100_000, 0.001, 0.01)); ctrl.on_ack(make_sample(50_000, 0.001, 0.01));
assert!(
ctrl.btlbw() >= 100_000.0 / 0.001 * 0.9,
"BtlBw should hold peak value, got {}",
ctrl.btlbw()
);
}
#[test]
fn test_app_limited_does_not_reduce_btlbw() {
let mut ctrl = BbrController::new(BbrConfig::default());
ctrl.on_ack(make_sample(100_000, 0.001, 0.01));
let bw_high = ctrl.btlbw();
let limited = AckSample {
delivered: 100,
elapsed_secs: 0.001,
rtt_secs: 0.01,
is_app_limited: true,
};
ctrl.on_ack(limited);
assert!(
ctrl.btlbw() >= bw_high * 0.99,
"app-limited sample should not reduce BtlBw: {} < {}",
ctrl.btlbw(),
bw_high
);
}
#[test]
fn test_inflight_target_at_least_min_cwnd() {
let ctrl = BbrController::new(BbrConfig::default());
assert!(
ctrl.inflight_target() >= MIN_CWND_BYTES,
"inflight_target should be >= MIN_CWND"
);
}
#[test]
fn test_inflight_target_proportional_to_bdp() {
let mut ctrl = BbrController::new(BbrConfig::default());
ctrl.btlbw = 1_000_000.0; ctrl.rtprop = 0.1; let target = ctrl.inflight_target();
assert!(
target > 100_000,
"inflight_target should exceed BDP, got {target}"
);
}
#[test]
fn test_state_display() {
assert_eq!(BbrState::Startup.to_string(), "startup");
assert_eq!(BbrState::Drain.to_string(), "drain");
assert_eq!(BbrState::ProbeBw.to_string(), "probe_bw");
assert_eq!(BbrState::ProbeRtt.to_string(), "probe_rtt");
}
#[test]
fn test_zero_elapsed_sample_ignored() {
let mut ctrl = BbrController::new(BbrConfig::default());
let initial_bw = ctrl.btlbw();
let bad = AckSample {
delivered: 1000,
elapsed_secs: 0.0,
rtt_secs: 0.01,
is_app_limited: false,
};
ctrl.on_ack(bad);
assert_eq!(ctrl.btlbw(), initial_bw);
assert_eq!(*ctrl.state(), BbrState::Startup);
}
#[test]
fn test_zero_rtt_sample_ignored() {
let mut ctrl = BbrController::new(BbrConfig::default());
let initial_rtprop = ctrl.rtprop();
let bad = AckSample {
delivered: 1000,
elapsed_secs: 0.001,
rtt_secs: 0.0,
is_app_limited: false,
};
ctrl.on_ack(bad);
assert_eq!(ctrl.rtprop(), initial_rtprop);
}
#[test]
fn test_many_acks_no_panic() {
let mut ctrl = BbrController::new(BbrConfig::default());
for i in 0..1_000u64 {
let rtt = 0.005 + (i % 20) as f64 * 0.001;
let delivered = 10_000 + (i % 5) * 1_000;
ctrl.on_ack(make_sample(delivered, 0.001, rtt));
}
assert!(ctrl.pacing_rate() > 0.0);
assert!(ctrl.cwnd() >= MIN_CWND_BYTES);
}
}