diff --git a/noq-proto/src/congestion/bbr3/mod.rs b/noq-proto/src/congestion/bbr3/mod.rs index f90753e8de..d3b3066a8f 100644 --- a/noq-proto/src/congestion/bbr3/mod.rs +++ b/noq-proto/src/congestion/bbr3/mod.rs @@ -458,6 +458,12 @@ pub struct Bbr3 { /// equivalent to BBR.full_bw_reached: A boolean that records whether BBR estimates that it has /// ever fully utilized its available bandwidth over the lifetime of the connection. full_bw_reached: bool, + /// Whether the pacing rate has been re-derived from a measured RTT. At construction no SRTT + /// exists, so the initial pacing rate is InitialCwnd / 1 ms (~266 Mbps for a 12 kB window) + /// and, until startup exits, BBRSetPacingRate only ever raises it. A path that stays + /// app-limited never exits startup and would therefore never be paced at all (Linux BBR: + /// `has_seen_rtt` / `bbr_init_pacing_rate_from_rtt`). + has_seen_rtt: bool, /// equivalent to BBR.full_bw_now: A boolean that records whether BBR estimates that it has /// fully utilized its available bandwidth since it most recetly started looking. full_bw_now: bool, @@ -659,6 +665,7 @@ impl Bbr3 { extra_acked_delivered: 0, extra_acked_filter: MaxFilter::new(EXTRA_ACKED_FILTER_LEN as u64), full_bw_reached: false, + has_seen_rtt: false, full_bw_now: false, full_bw: 0.0, full_bw_count: 0, @@ -1663,6 +1670,15 @@ impl Bbr3 { _app_limited: bool, rtt: &RttEstimator, ) { + if !self.has_seen_rtt && rtt.has_sample() { + // equivalent to BBRInitPacingRate with a real SRTT: the 1 ms placeholder used at + // construction is replaced by InitialCwnd / SRTT once the path has an RTT sample + self.has_seen_rtt = true; + let srtt = rtt.get().as_secs_f64().max(0.001); + let nominal_bandwidth = self.initial_cwnd as f64 / srtt; + self.pacing_rate = self.startup_pacing_gain * nominal_bandwidth; + self.set_send_quantum(); + } self.check_recovery_done(sent); self.delivered += bytes; self.delivered_time = Some(now); @@ -2232,6 +2248,50 @@ mod test { assert_eq!(bbr3.reno_rounds_bound, 63); } + /// The initial pacing rate is a placeholder computed with a 1 ms RTT (no SRTT exists at + /// construction), and `BBRSetPacingRate` only ever raises the rate before startup exits. A + /// flow that stays application-limited never exits startup, so without re-deriving the rate + /// from the first RTT sample it is never paced at all. Mirrors Linux BBR's + /// `bbr_init_pacing_rate_from_rtt` on `has_seen_rtt`. + #[test] + fn initial_pacing_rate_follows_first_rtt_sample() { + let config = Bbr3Config { + initial_window: 12_000, + probe_rng_seed: None, + startup_pacing_gain: None, + default_pacing_gain: None, + probe_bw_down_pacing_gain: None, + probe_bw_up_pacing_gain: None, + probe_bw_up_cwnd_gain: None, + probe_rtt_cwnd_gain: None, + drain_pacing_gain: None, + pacing_margin_percent: None, + default_cwnd_gain: None, + }; + let mut bbr3 = Bbr3::new(Arc::new(config), 1200); + // placeholder: InitialCwnd / 1 ms + let placeholder = STARTUP_PACING_GAIN * 12_000.0 / 0.001; + assert!((bbr3.pacing_rate - placeholder).abs() < 1.0); + + // one packet sent and acknowledged with a measured 40 ms RTT + let t0 = Instant::now(); + let mut rtt = RttEstimator::new(Duration::from_millis(333)); + rtt.update(Duration::ZERO, Duration::from_millis(40)); + bbr3.on_packet_sent(t0, 1200, 0, SpaceKind::Data); + let t1 = t0 + Duration::from_millis(40); + bbr3.on_ack(t1, t0, 1200, 0, SpaceKind::Data, false, &rtt); + + // re-derived from the sample: InitialCwnd / SRTT, far below the placeholder + let expected = STARTUP_PACING_GAIN * 12_000.0 / rtt.get().as_secs_f64(); + assert!( + (bbr3.pacing_rate - expected).abs() / expected < 0.01, + "pacing_rate {} B/s, expected {} B/s (placeholder was {})", + bbr3.pacing_rate, + expected, + placeholder + ); + } + /// A.1: Exiting STARTUP on a bandwidth plateau. /// equivalent to: /// Drives a flow through the real `on_packet_sent`/`on_ack`/`on_end_acks` diff --git a/noq-proto/src/connection/paths.rs b/noq-proto/src/connection/paths.rs index fca5361396..221e68bb96 100644 --- a/noq-proto/src/connection/paths.rs +++ b/noq-proto/src/connection/paths.rs @@ -797,6 +797,12 @@ impl RttEstimator { } } + /// Whether at least one RTT sample has been taken (before that, [`Self::get`] returns the + /// configured initial estimate). + pub fn has_sample(&self) -> bool { + self.smoothed.is_some() + } + /// The current best RTT estimation. pub fn get(&self) -> Duration { self.smoothed.unwrap_or(self.latest)