noq_proto/congestion/bbr3/mod.rs
1mod max_filter;
2
3use std::any::Any;
4use std::collections::VecDeque;
5use std::sync::Arc;
6
7use rand::{RngExt, SeedableRng};
8use rand_pcg::Pcg32;
9
10use crate::RttEstimator;
11use crate::congestion::bbr3::max_filter::MaxFilter;
12use crate::congestion::{BASE_DATAGRAM_SIZE, Controller, ControllerFactory, ControllerMetrics};
13use crate::connection::SpaceKind;
14use crate::{Duration, Instant};
15
16/// equivalent to BBR.MaxBwFilterLen <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-2.10>
17const MAX_BW_FILTER_LEN: usize = 2;
18
19/// equivalent to BBR.ExtraAckedFilterLen <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-2.11>
20const EXTRA_ACKED_FILTER_LEN: usize = 10;
21
22/// safety mechanism to flag packets as stale within our tracking VecDeque. rounds refer to <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.1>.
23/// The value of 10 rounds is picked because normally after max(kTimeThreshold * max(smoothed_rtt, latest_rtt), kGranularity) <https://datatracker.ietf.org/doc/html/rfc9002#section-6.1.2>
24/// the packet should have been declared lost already, this is just to guarantee that the VecDeque
25/// doesn't grow indefinitely.
26const ROUND_COUNT_WINDOW: u64 = 10;
27
28/// the minimum for the maximum datagram size <https://datatracker.ietf.org/doc/html/rfc9000#section-14>
29const MIN_MAX_DATAGRAM_SIZE: u16 = 1200;
30
31/// the maximum for the maximum datagram size <https://datatracker.ietf.org/doc/html/rfc9000#section-18.2>
32const MAX_DATAGRAM_SIZE: u64 = 65527;
33
34/// 64 KBytes in bytes
35/// one of the default high values for `set_send_quantum`
36/// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#section-5.6.3>
37const HIGH_PACE_MAX_QUANTUM: u64 = 64 * 1024;
38
39/// equivalent to BBR.StartupPacingGain: A constant specifying the minimum gain value for
40/// calculating the pacing rate that will allow the sending rate to double each round (4 * ln(2) ~=
41/// 2.77) BBRStartupPacingGain; used in Startup mode for BBR.pacing_gain. <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.1>
42const STARTUP_PACING_GAIN: f64 = 2.773;
43
44/// default pacing gain is 1, when cruising, probing for RTT or refilling <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.1>
45const DEFAULT_PACING_GAIN: f64 = 1.0;
46
47/// pacing gain when probing bandwidth down <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.1>
48const PROBE_BW_DOWN_PACING_GAIN: f64 = 0.9;
49
50/// pacing gain when probing bandwidth up <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.1>
51const PROBE_BW_UP_PACING_GAIN: f64 = 1.25;
52
53/// equivalent to BBR.PacingMarginPercent: The static discount factor of 1% used to scale BBR.bw to
54/// produce C.pacing_rate.
55const PACING_MARGIN_PERCENT: f64 = 1.0;
56
57/// equivalent to BBR.DefaultCwndGain: A constant specifying the minimum gain value that allows the
58/// sending rate to double each round (2) BBRStartupCwndGain. Used by default in most phases for
59/// BBR.cwnd_gain.
60const DEFAULT_CWND_GAIN: f64 = 2.0;
61
62/// equivalent to BBR.DrainPacingGain: A constant specifying the pacing gain value used in Drain
63/// mode, to attempt to drain the estimated queue at the bottleneck link in one round-trip or less.
64/// As noted in BBRDrainPacingGain, any value at or below 1 / BBRStartupCwndGain = 1 / 2 = 0.5 will
65/// theoretically achieve this. BBR uses the value 0.5, which has been shown to offer good
66/// performance when compared with other alternatives. <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-2.4>
67/// <https://github.com/google/bbr/blob/master/Documentation/startup/gain/analysis/bbr_drain_gain.pdf>
68const DRAIN_PACING_GAIN: f64 = 1.0 / DEFAULT_CWND_GAIN;
69
70/// cwnd gain used when probing up <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.1>
71const PROBE_BW_UP_CWND_GAIN: f64 = 2.25;
72
73/// cwnd gain used when probing RTT <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.1>
74const PROBE_RTT_CWND_GAIN: f64 = 0.5;
75
76/// equivalent to BBR.ProbeRTTDuration: A constant specifying the minimum duration for which
77/// ProbeRTT state holds C.inflight to BBR.MinPipeCwnd or fewer packets: 200 ms.
78const PROBE_RTT_DURATION_MS: u64 = 200;
79
80/// equivalent to BBR.ProbeRTTInterval: A constant specifying the minimum time interval between
81/// ProbeRTT states: 5 secs.
82const PROBE_RTT_INTERVAL_SEC: u64 = 5;
83
84/// equivalent to BBR.LossThresh: A constant specifying the maximum tolerated per-round-trip packet
85/// loss rate when probing for bandwidth (the default is 2%).
86const LOSS_THRESH: f64 = 0.02;
87
88/// equivalent to BBR.Beta: A constant specifying the default multiplicative decrease to make upon
89/// each round trip during which the connection detects packet loss (the value is 0.7).
90const BETA: f64 = 0.7;
91
92/// equivalent to BBR.Headroom: A constant specifying the multiplicative factor to apply to
93/// BBR.inflight_longterm when calculating a volume of free headroom to try to leave unused in the
94/// path (e.g. free space in the bottleneck buffer or free time slots in the bottleneck link) that
95/// can be used by cross traffic (the value is 0.15).
96const HEADROOM: f64 = 0.15;
97
98/// equivalent to BBR.MinRTTFilterLen: A constant specifying the length of the BBR.min_rtt min
99/// filter window, BBR.MinRTTFilterLen is 10 secs.
100const MIN_RTT_FILTER_LEN: u64 = 10;
101
102/// multiplier used to check growth when validating if the full bandwidth has been reached
103/// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.1.2-6>
104const FULL_BW_GROWTH: f64 = 1.25;
105
106/// maximum number of rounds needed before we consider that the pipe is full <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.1.2-6>
107const MAX_FULL_BW_COUNT: u64 = 3;
108
109/// equivalent to BBRStartupFullLossCnt: the minimum number of discontiguous loss
110/// events observed within a single round trip before the STARTUP high-loss
111/// estimator is allowed to exit STARTUP.
112/// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#section-5.3.1.3>
113const STARTUP_FULL_LOSS_CNT: u64 = 6;
114
115/// when setting `bw_probe_up_rounds` when raising our inflight long term slope we don't go above
116/// this <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.6-8>
117const MAX_LONG_TERM_PROBE_UP_ROUNDS: u32 = 30;
118
119/// equivalent to T_reno_bound: the two candidate values for the round-trip bound used when
120/// deciding to coexist with Reno / CUBIC. The spec picks randomly between them
121/// (`T_reno_bound = pick_randomly_either({62, 63})`) for better mixing and fairness
122/// convergence <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#section-5.3.3.8.2>
123const RENO_ROUNDS_BOUNDS: [u64; 2] = [62, 63];
124
125/// minimum amount of time to wait before probing again <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.5.3-5>
126const MIN_PROBE_WAIT_MS: u64 = 2000;
127
128/// when waiting before probing again we add up to one second of added wait time
129/// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.5.3-5>
130const MAX_ADDED_PROBE_WAIT_MS: u64 = 1000;
131
132/// Substates when probing bandwidth
133/// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3>
134#[derive(Debug, Clone, Copy, Eq, PartialEq)]
135enum ProbeBwSubstate {
136 /// Deceleration: sends slower than delivery rate to reduce queue
137 /// equivalent to ProbeBW_DOWN <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.1>
138 Down,
139
140 /// Cruising: sends at delivery rate to maintain high utilization
141 /// equivalent to ProbeBW_CRUISE <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.2>
142 Cruise,
143
144 /// Refill: sends at BBR.bw for one RTT to fill pipe before probing up
145 /// equivalent to ProbeBW_REFILL <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.3>
146 Refill,
147
148 /// Acceleration: sends faster than delivery rate to probe for more bandwidth
149 /// equivalent to ProbeBW_UP <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.4>
150 Up,
151}
152
153/// State Machine description from BBR3
154/// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3>
155#[derive(Debug, Clone, Copy, Eq, PartialEq)]
156enum BbrState {
157 /// Initial state: rapidly probes for bandwidth using high pacing_gain
158 /// equivalent to Startup <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.1>
159 Startup,
160
161 /// Drains queue created during Startup by using low pacing_gain (< 1.0)
162 /// equivalent to Drain <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.2>
163 Drain,
164
165 /// Steady-state phase that cycles through bandwidth probing tactics
166 /// equivalent to ProbeBW states <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3>
167 ProbeBw(ProbeBwSubstate),
168
169 /// Temporarily reduces inflight to measure true min_rtt
170 /// equivalent to ProbeRTT <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.4>
171 ProbeRtt,
172}
173
174/// Ack phases used during ProbeBW states
175/// equivalent to BBR.ack_phase states <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.6>
176#[derive(Debug, Clone, Copy, Eq, PartialEq)]
177enum AckPhase {
178 /// equivalent to ACKS_PROBE_STARTING
179 ProbeStarting,
180 /// equivalent to ACKS_PROBE_STOPPING
181 ProbeStopping,
182 /// equivalent to ACKS_REFILLING
183 Refilling,
184 /// equivalent to ACKS_PROBE_FEEDBACK
185 ProbeFeedback,
186}
187
188/// Description of a packet for the purposes of analysis through BBR3
189/// all volumes of data use bytes, all rates of data use bytes/sec
190/// equivalent to P <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-4.1.2.1.2>
191#[derive(Debug, Clone, Copy)]
192struct BbrPacket {
193 /// equivalent to P.delivered: C.delivered when the packet was sent from transport connection
194 /// C.
195 delivered: u64,
196 /// equivalent to P.delivered_time: C.delivered_time when the packet was sent.
197 delivered_time: Instant,
198 /// equivalent to P.first_send_time: C.first_send_time when the packet was sent.
199 first_send_time: Instant,
200 /// equivalent to P.send_time: The pacing departure time selected when the packet was scheduled
201 /// to be sent.
202 send_time: Instant,
203 /// equivalent to P.is_app_limited: true if C.app_limited was non-zero when the packet was
204 /// sent, else false.
205 is_app_limited: bool,
206 /// equivalent to P.tx_in_flight: C.inflight immediately after the transmission of packet P.
207 tx_in_flight: u64,
208 /// packet number from the connection, unique only within `space`
209 packet_number: u64,
210 /// packet number space the packet was sent in; each space numbers independently from zero, so
211 /// `packet_number` only identifies a packet together with this
212 space: SpaceKind,
213 /// packet size in bytes
214 size: u16,
215 /// equivalent to P.lost: C.lost when the packet was sent
216 lost: u64,
217 /// used to flag acknowledgement within our VecDeque, a packet can be flagged lost after having
218 /// been flagged acknowledged hence the necessity of this flag being set before we remove
219 /// it from packets.
220 acknowledged: bool,
221 /// once a packet has been acknowledged on a given round it is marked for removal on the next
222 /// round.
223 stale: bool,
224 /// used to mark packets stale if they're far from the current round <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.1>
225 round_count: u64,
226}
227
228/// Description of a per-ack rate sample state that will allow us to determine a short term
229/// evolution of the connection equivalent to RS <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-2.2>
230#[derive(Debug, Clone, Copy)]
231struct BbrRateSample {
232 /// equivalent to RS.delivery_rate: The delivery rate (aka bandwidth) sample obtained from the
233 /// packet that has just been ACKed.
234 delivery_rate: f64,
235 /// equivalent to RS.is_app_limited: The P.is_app_limited from the most recent packet
236 /// delivered; indicates whether the rate sample is application-limited.
237 is_app_limited: bool,
238 /// equivalent to RS.interval: The length of the sampling interval.
239 interval: Duration,
240 /// equivalent to RS.delivered: The volume of data delivered between the transmission of the
241 /// packet that has just been ACKed and the current time.
242 delivered: u64,
243 /// equivalent to RS.prior_delivered: The P.delivered count from the most recent packet
244 /// delivered.
245 prior_delivered: u64,
246 /// equivalent to RS.send_elapsed: Send time interval calculated from the most recent
247 /// packet delivered (see the "Send Rate" section above).
248 send_elapsed: Duration,
249 /// equivalent to RS.ack_elapsed: ACK time interval calculated from the most recent
250 /// packet delivered (see the "ACK Rate" section above).
251 ack_elapsed: Duration,
252 /// equivalent to RS.rtt: The RTT sample calculated based on the most recently-sent packet of
253 /// the packets that have just been ACKed.
254 rtt: Duration,
255 /// equivalent to RS.tx_in_flight: C.inflight at the time of the transmission of the packet
256 /// that has just been ACKed (the most recently sent packet among packets ACKed by the ACK
257 /// that was just received).
258 tx_in_flight: u64,
259 /// equivalent to RS.newly_acked: The volume of data in bytes cumulatively or selectively
260 /// acknowledged upon the ACK that was just received.
261 newly_acked: u64,
262 /// equivalent to RS.lost: The volume of data in bytes that was declared lost between the
263 /// transmission and acknowledgment of the packet that has just been ACKed (the most
264 /// recently sent packet among packets ACKed by the ACK that was just received).
265 lost: u64,
266 /// equivalent to RS.last_end_seq
267 last_end_seq: u64,
268 /// represents the last packet that was used in the generation of this rate sample
269 last_packet: BbrPacket,
270}
271
272/// Experimental! Use at your own risk.
273///
274/// Aims for reduced buffer bloat and improved performance over high bandwidth-delay product
275/// networks. Based on <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html>
276/// equivalent to a combination of BBR and C states
277/// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-2.4>
278/// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-2.1>
279#[derive(Debug, Clone)]
280pub struct Bbr3 {
281 /// equivalent to C.SMSS The Sender Maximum Send Size in bytes. <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-2.1>
282 /// <https://www.rfc-editor.org/rfc/rfc9000#name-datagram-size>
283 smss: u64,
284 /// equivalent to C.InitialCwnd: The initial congestion window set by the transport protocol
285 /// implementation for the connection at initialization time.
286 initial_cwnd: u64,
287 /// equivalent to C.delivered: The total amount of data delivered so far over the lifetime of
288 /// the transport connection C. This MUST NOT include pure ACK packets. It SHOULD include
289 /// spurious retransmissions that have been acknowledged as delivered.
290 delivered: u64,
291 /// equivalent to C.inflight: The connection's best estimate of the number of bytes outstanding
292 /// in the network. This includes the number of bytes that have been sent and have not been
293 /// acknowledged or marked as lost since their last transmission (e.g. "pipe" from RFC6675
294 /// or "bytes_in_flight" from RFC9002). This MUST NOT include pure ACK packets.
295 inflight: u64,
296 /// equivalent to C.is_cwnd_limited: True if the connection has fully utilized C.cwnd at any
297 /// point in the last packet-timed round trip. Transport-provided (via `on_cwnd_limited`);
298 /// snapshotted from `cwnd_limited_this_round` at each round boundary.
299 is_cwnd_limited: bool,
300 /// ORs every cwnd-blocked send in the current round; snapshotted into `is_cwnd_limited` and
301 /// cleared when the round advances.
302 cwnd_limited_this_round: bool,
303 /// equivalent to BBR.cycle_count: The virtual time used by the BBR.max_bw filter window.
304 /// since the BBR.max_bw_filter only needs to track samples from two time slots: the previous
305 /// ProbeBW cycle and the current ProbeBW cycle.
306 cycle_count: u64,
307 /// equivalent to C.cwnd: The transport sender's congestion window. When transmitting data, the
308 /// sending connection ensures that C.inflight does not exceed C.cwnd.
309 cwnd: u64,
310 /// equivalent to C.pacing_rate: The current pacing rate for a BBR flow, which controls
311 /// inter-packet spacing.
312 pacing_rate: f64,
313 /// equivalent to C.send_quantum: The maximum size of a data aggregate scheduled and
314 /// transmitted together as a unit, e.g., to amortize per-packet transmission overheads.
315 send_quantum: u64,
316 /// equivalent to BBR.pacing_gain: The dynamic gain factor used to scale BBR.bw to produce
317 /// C.pacing_rate.
318 pacing_gain: f64,
319 /// default pacing gain is 1, when cruising, probing for RTT or refilling <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.1>
320 default_pacing_gain: f64,
321 /// pacing gain when probing bandwidth down <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.1>
322 probe_bw_down_pacing_gain: f64,
323 /// pacing gain when probing bandwidth up <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.1>
324 probe_bw_up_pacing_gain: f64,
325 /// equivalent to BBR.StartupPacingGain: A constant specifying the minimum gain value for
326 /// calculating the pacing rate that will allow the sending rate to double each round (4 *
327 /// ln(2) ~= 2.77) BBRStartupPacingGain; used in Startup mode for BBR.pacing_gain. <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.1>
328 startup_pacing_gain: f64,
329 /// equivalent to BBR.DrainPacingGain: A constant specifying the pacing gain value used in
330 /// Drain mode, to attempt to drain the estimated queue at the bottleneck link in one
331 /// round-trip or less. As noted in BBRDrainPacingGain, any value at or below 1 /
332 /// BBRStartupCwndGain = 1 / 2 = 0.5 will theoretically achieve this. BBR uses the value
333 /// 0.5, which has been shown to offer good performance when compared with other alternatives. <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.1>
334 drain_pacing_gain: f64,
335 /// equivalent to BBR.PacingMarginPercent: The static discount factor of 1% used to scale
336 /// BBR.bw to produce C.pacing_rate.
337 pacing_margin_percent: f64,
338 /// equivalent to BBR.cwnd_gain: The dynamic gain factor used to scale the estimated BDP to
339 /// produce a congestion window (C.cwnd).
340 cwnd_gain: f64,
341 /// equivalent to BBR.DefaultCwndGain: A constant specifying the minimum gain value that allows
342 /// the sending rate to double each round (2) BBRStartupCwndGain. Used by default in most
343 /// phases for BBR.cwnd_gain.
344 default_cwnd_gain: f64,
345 /// used to generate random numbers when deciding how long to wait before probing again
346 /// using Pcg32 as it's a fast general purpose random number generator and fits our purpose
347 /// here these numbers will not be security critical as they're only used to decide when to
348 /// probe the connection next.
349 probe_rng: Pcg32,
350 /// cwnd gain used when probing up <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.1>
351 probe_bw_up_cwnd_gain: f64,
352 /// cwnd gain used when probing RTT <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.1>
353 probe_rtt_cwnd_gain: f64,
354 /// equivalent to BBR.state: The current state of a BBR flow in the BBR state machine. <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-3.3>
355 state: BbrState,
356 /// equivalent to BBR.undo_state: The state of a BBR flow in the BBR state machine saved in case a loss episode is later declared spurious. <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-3.3>
357 undo_state: BbrState,
358 /// equivalent to BBR.round_count: Count of packet-timed round trips elapsed so far.
359 round_count: u64,
360 /// equivalent to BBR.round_start: A boolean that BBR sets to true once per packet-timed round
361 /// trip, on ACKs that advance BBR.round_count.
362 round_start: bool,
363 /// equivalent to BBR.next_round_delivered: P.delivered value denoting the end of a
364 /// packet-timed round trip.
365 next_round_delivered: u64,
366 /// equivalent to BBR.idle_restart: A boolean that is true if and only if a connection is
367 /// restarting after being idle.
368 idle_restart: bool,
369 /// equivalent to BBR.MinPipeCwnd: The minimal C.cwnd value BBR targets, to allow pipelining
370 /// with endpoints that follow an "ACK every other packet" delayed-ACK policy: 4 * C.SMSS.
371 min_pipe_cwnd: u64,
372 /// equivalent to BBR.max_bw: The windowed maximum recent bandwidth sample, obtained using the
373 /// BBR delivery rate sampling algorithm in <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-4.1>,
374 /// measured during the current or previous bandwidth probing cycle (or during Startup, if the
375 /// flow is still in that state). (Part of the long-term model.)
376 max_bw: f64,
377 /// equivalent to BBR.bw_shortterm: The short-term maximum sending bandwidth that the algorithm
378 /// estimates is safe for matching the current network path delivery rate, based on any
379 /// loss signals in the current bandwidth probing cycle. This is generally lower than max_bw.
380 /// (Part of the short-term model.)
381 bw_shortterm: f64,
382 /// equivalent to BBR.undo_bw_shortterm: The short-term maximum sending bandwidth that the
383 /// algorithm estimates is safe for matching the current network path delivery rate,
384 /// based on any loss signals in the current bandwidth probing cycle. This is generally lower
385 /// than max_bw. (Part of the short-term model.) saved state in case a loss episode is
386 /// later declared spurious
387 undo_bw_shortterm: f64,
388 /// equivalent to BBR.bw: The maximum sending bandwidth that the algorithm estimates is
389 /// appropriate for matching the current network path delivery rate, given all available
390 /// signals in the model, at any time scale. It is the min() of max_bw and bw_shortterm.
391 bw: f64,
392 /// equivalent to BBR.min_rtt: The windowed minimum round-trip time sample measured over the
393 /// last BBR.MinRTTFilterLen = 10 seconds. This attempts to estimate the two-way
394 /// propagation delay of the network path when all connections sharing a bottleneck are using
395 /// BBR, but also allows BBR to estimate the value required for a BBR.bdp estimate that
396 /// allows full throughput if there are legacy loss-based Reno or CUBIC flows sharing the
397 /// bottleneck.
398 min_rtt: Duration,
399 /// equivalent to BBR.bdp: The estimate of the network path's BDP (Bandwidth-Delay Product),
400 /// computed as: BBR.bdp = BBR.bw * BBR.min_rtt.
401 bdp: u64,
402 /// equivalent to BBR.extra_acked: A volume of data that is the estimate of the recent degree
403 /// of aggregation in the network path.
404 extra_acked: u64,
405 /// equivalent to BBR.offload_budget: The estimate of the minimum volume of data necessary to
406 /// achieve full throughput when using sender (TSO/GSO) and receiver (LRO, GRO) host
407 /// offload mechanisms.
408 offload_budget: u64,
409 /// equivalent to BBR.max_inflight: The estimate of C.inflight required to fully utilize the
410 /// bottleneck bandwidth available to the flow, based on the BDP estimate (BBR.bdp), the
411 /// aggregation estimate (BBR.extra_acked), the offload budget (BBR.offload_budget), and
412 /// BBR.MinPipeCwnd.
413 max_inflight: u64,
414 /// equivalent to BBR.inflight_longterm: The long-term maximum inflight that the algorithm
415 /// estimates will produce acceptable queue pressure, based on signals in the current or
416 /// previous bandwidth probing cycle, as measured by loss. That is, if a flow is probing for
417 /// bandwidth, and observes that sending a particular inflight causes a loss rate higher
418 /// than the loss rate threshold, it sets inflight_longterm to that volume of data. (Part
419 /// of the long-term model.)
420 inflight_longterm: u64,
421 /// equivalent to BBR.inflight_longterm: The long-term maximum inflight that the algorithm
422 /// estimates will produce acceptable queue pressure, based on signals in the current or
423 /// previous bandwidth probing cycle, as measured by loss. That is, if a flow is probing for
424 /// bandwidth, and observes that sending a particular inflight causes a loss rate higher
425 /// than the loss rate threshold, it sets inflight_longterm to that volume of data. (Part
426 /// of the long-term model.) saved state in case a loss episode is later declared spurious
427 undo_inflight_longterm: u64,
428 /// equivalent to BBR.inflight_shortterm: Analogous to BBR.bw_shortterm,
429 /// the short-term maximum inflight that the algorithm estimates is safe for matching the
430 /// current network path delivery process, based on any loss signals in the current
431 /// bandwidth probing cycle. This is generally lower than max_inflight or inflight_longterm.
432 /// (Part of the short-term model.)
433 inflight_shortterm: u64,
434 /// equivalent to BBR.undo_inflight_shortterm: Analogous to BBR.bw_shortterm,
435 /// the short-term maximum inflight that the algorithm estimates is safe for matching the
436 /// current network path delivery process, based on any loss signals in the current
437 /// bandwidth probing cycle. This is generally lower than max_inflight or inflight_longterm.
438 /// (Part of the short-term model.) saved state in case a loss episode is later declared
439 /// spurious
440 undo_inflight_shortterm: u64,
441 /// equivalent to BBR.bw_latest: a 1-round-trip max of delivered bandwidth (RS.delivery_rate).
442 bw_latest: f64,
443 /// equivalent to BBR.inflight_latest: a 1-round-trip max of delivered volume of data
444 /// (RS.delivered).
445 inflight_latest: u64,
446 /// equivalent to BBR.max_bw_filter: A windowed max filter for RS.delivery_rate samples, for
447 /// estimating BBR.max_bw.
448 max_bw_filter: MaxFilter,
449 /// equivalent to BBR.extra_acked_interval_start: The start of the time interval for estimating
450 /// the excess amount of data acknowledged due to aggregation effects.
451 extra_acked_interval_start: Option<Instant>,
452 /// equivalent to BBR.extra_acked_delivered: The volume of data marked as delivered since
453 /// BBR.extra_acked_interval_start.
454 extra_acked_delivered: u64,
455 /// equivalent to BBR.extra_acked_filter: A windowed max filter for tracking the degree of
456 /// aggregation in the path.
457 extra_acked_filter: MaxFilter,
458 /// equivalent to BBR.full_bw_reached: A boolean that records whether BBR estimates that it has
459 /// ever fully utilized its available bandwidth over the lifetime of the connection.
460 full_bw_reached: bool,
461 /// equivalent to BBR.full_bw_now: A boolean that records whether BBR estimates that it has
462 /// fully utilized its available bandwidth since it most recetly started looking.
463 full_bw_now: bool,
464 /// equivalent to BBR.full_bw: A recent baseline BBR.max_bw to estimate if BBR has "filled the
465 /// pipe" in Startup.
466 full_bw: f64,
467 /// equivalent to BBR.full_bw_count: The number of non-app-limited round trips without large
468 /// increases in BBR.full_bw.
469 full_bw_count: u64,
470 /// equivalent to BBR.min_rtt_stamp: The wall clock time at which the current BBR.min_rtt
471 /// sample was obtained.
472 min_rtt_stamp: Option<Instant>,
473 /// equivalent to BBR.ProbeRTTDuration: A constant specifying the minimum duration for which
474 /// ProbeRTT state holds C.inflight to BBR.MinPipeCwnd or fewer packets: 200 ms.
475 probe_rtt_duration: Duration,
476 /// equivalent to BBR.ProbeRTTInterval: A constant specifying the minimum time interval between
477 /// ProbeRTT states: 5 secs.
478 probe_rtt_interval: Duration,
479 /// equivalent to BBR.probe_rtt_min_delay: The minimum RTT sample recorded in the last
480 /// ProbeRTTInterval.
481 probe_rtt_min_delay: Duration,
482 /// equivalent to BBR.probe_rtt_min_stamp: The wall clock time at which the current
483 /// BBR.probe_rtt_min_delay sample was obtained.
484 probe_rtt_min_stamp: Option<Instant>,
485 /// equivalent to BBR.probe_rtt_expired: A boolean recording whether the
486 /// BBR.probe_rtt_min_delay has expired and is due for a refresh with an application idle
487 /// period or a transition into ProbeRTT state.
488 probe_rtt_expired: bool,
489 /// equivalent to C.delivered_time: The wall clock time when C.delivered was last updated. <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-4.1.1.2.1>
490 delivered_time: Option<Instant>,
491 /// equivalent to C.first_send_time: If packets are in flight, then this holds the send time of
492 /// the packet that was most recently marked as delivered. Else, if the connection was
493 /// recently idle, then this holds the send time of most recently sent packet.
494 first_send_time: Option<Instant>,
495 /// equivalent to C.app_limited: marks the application-limited phase, or 0 if the connection is
496 /// not currently application-limited. A byte index into the delivery stream, so a packet sent
497 /// after the marker was taken is recognisable by its `P.delivered` exceeding it.
498 app_limited: u64,
499 /// equivalent to C.lost: the number of bytes that have been lost during the lifetime of this
500 /// connection
501 lost: u64,
502 /// collection of packets in flight or just acknowledged / lost, one queue per packet number
503 /// space indexed by `SpaceKind as usize`. Packet numbers are only unique and only monotonic
504 /// within a space, so the queues must be kept separate for the ordered lookups below to hold.
505 packets: [VecDeque<BbrPacket>; 3],
506 /// equivalent to RS: Per-ACK Rate Sample State <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-2.2>
507 rs: Option<BbrRateSample>,
508 /// equivalent to BBR.rounds_since_bw_probe: rounds since last bw probe state.
509 rounds_since_bw_probe: u64,
510 /// equivalent to BBR.bw_probe_wait: random wait time before entering probing state again
511 bw_probe_wait: Duration,
512 /// equivalent to BBR.bw_probe_up_rounds: number of rounds that have been executed in probe up
513 /// state
514 bw_probe_up_rounds: u32,
515 /// equivalent to BBR.bw_probe_up_acks: volume of data in bytes that has been acknowledged
516 /// during probe up state
517 bw_probe_up_acks: u64,
518 /// equivalent to BBR.probe_up_cnt: count of the number of times we've grown the cwnd during
519 /// probe up state
520 probe_up_cnt: u64,
521 /// equivalent to BBR.cycle_stamp: timestamp when we start probing down state
522 cycle_stamp: Option<Instant>,
523 /// equivalent to BBR.ack_phase: ACK phase during probing states
524 ack_phase: AckPhase,
525 /// equivalent to BBR.bw_probe_samples: <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.10.2>
526 bw_probe_samples: bool,
527 /// equivalent to BBR.loss_round_delivered: C.delivered during the first loss of the round
528 loss_round_delivered: u64,
529 /// equivalent to BBR.loss_in_round: flag set to true when loss occurs during the round
530 loss_in_round: bool,
531 /// equivalent to BBR.loss_events_in_round: count of discontiguous loss events
532 /// observed in the current round trip, used by the STARTUP high-loss exit
533 /// (BBRStartupFullLossCnt criterion). Reset at each loss-round boundary.
534 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#section-5.3.1.3>
535 loss_events_in_round: u64,
536 /// `(space, packet number)` of the most recent packet counted into
537 /// `loss_events_in_round`, used to collapse a contiguous run of lost packet numbers into
538 /// the single "discontiguous sequence range" the spec counts. Cleared at each loss-round
539 /// boundary so the first loss of a round always opens a new range.
540 last_lost_packet: Option<(SpaceKind, u64)>,
541 /// The time when loss was first detected, causing the connection to enter fast recovery. A
542 /// congestion event for a packet sent after this time starts a new recovery episode, while
543 /// losses of packets sent at or before it belong to the episode already underway.
544 /// <https://datatracker.ietf.org/doc/html/rfc9002#section-7.3.2>
545 recovery_start_time: Option<Instant>,
546 /// `round_count` when `recovery_start_time` was last set, used for the "in fast recovery for
547 /// at least one full packet-timed round trip" criterion of the STARTUP high-loss exit
548 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#section-5.3.1.3>
549 recovery_start_round: u64,
550 /// Whether the connection is currently in fast recovery, the first criterion of the STARTUP
551 /// high-loss exit. Cleared once a packet sent after `recovery_start_time` is acknowledged.
552 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#section-5.3.1.3>
553 in_recovery: bool,
554 /// equivalent to T_reno_bound: round-trip bound for the Reno-coexistence probe timer,
555 /// re-picked from [`RENO_ROUNDS_BOUNDS`] each time the probe wait is randomized
556 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#section-5.3.3.8.2>
557 reno_rounds_bound: u64,
558 /// equivalent to BBR.probe_rtt_done_stamp: timestamp when probe RTT state is finished
559 probe_rtt_done_stamp: Option<Instant>,
560 /// equivalent to BBR.probe_rtt_round_done: set once per round when BBR.probe_rtt_done_stamp to
561 /// check if we need to switch state
562 probe_rtt_round_done: bool,
563 /// equivalent to BBR.prior_cwnd: cwnd from last round
564 prior_cwnd: u64,
565 /// equivalent to BBR.loss_round_start: flag set to true at the very beginning of a round where
566 /// loss occurred
567 loss_round_start: bool,
568 /// equivalent to BBR.drain_start_round: The value of round_count when Drain state started.
569 drain_start_round: u64,
570 /// Number of ack-eliciting packets the peer may receive before sending an immediate ACK,
571 /// as requested via the QUIC ACK frequency extension. Used when computing `offload_budget`
572 /// per <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.8.2>.
573 ack_eliciting_threshold: u64,
574 /// `max_ack_delay` we requested the peer to use via the QUIC ACK frequency extension.
575 /// Used when computing `offload_budget` per
576 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.8.2>.
577 max_ack_delay: Duration,
578}
579
580impl Bbr3 {
581 fn new(config: Arc<Bbr3Config>, current_mtu: u16) -> Self {
582 let probe_rng: Pcg32;
583 if let Some(probe_seed) = config.probe_rng_seed {
584 probe_rng = Pcg32::from_seed(probe_seed);
585 } else {
586 probe_rng = Pcg32::from_rng(&mut rand::rng());
587 }
588 let smss = Ord::min(
589 Ord::max(MIN_MAX_DATAGRAM_SIZE, current_mtu) as u64,
590 MAX_DATAGRAM_SIZE,
591 );
592 let initial_cwnd = config.initial_window;
593 let startup_pacing_gain = config.startup_pacing_gain.unwrap_or(STARTUP_PACING_GAIN);
594 let default_pacing_gain = config.default_pacing_gain.unwrap_or(DEFAULT_PACING_GAIN);
595 let probe_bw_down_pacing_gain = config
596 .probe_bw_down_pacing_gain
597 .unwrap_or(PROBE_BW_DOWN_PACING_GAIN);
598 let probe_bw_up_pacing_gain = config
599 .probe_bw_up_pacing_gain
600 .unwrap_or(PROBE_BW_UP_PACING_GAIN);
601 let drain_pacing_gain = config.drain_pacing_gain.unwrap_or(DRAIN_PACING_GAIN);
602 let pacing_margin_percent = config
603 .pacing_margin_percent
604 .unwrap_or(PACING_MARGIN_PERCENT);
605 let default_cwnd_gain = config.default_cwnd_gain.unwrap_or(DEFAULT_CWND_GAIN);
606 let probe_bw_up_cwnd_gain = config
607 .probe_bw_up_cwnd_gain
608 .unwrap_or(PROBE_BW_UP_CWND_GAIN);
609 let probe_rtt_cwnd_gain = config.probe_rtt_cwnd_gain.unwrap_or(PROBE_RTT_CWND_GAIN);
610 // the calculation for initial pacing rate described here <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.2-5>
611 let nominal_bandwidth = initial_cwnd as f64 / 0.001;
612 let pacing_rate = startup_pacing_gain * nominal_bandwidth;
613 Self {
614 smss,
615 initial_cwnd,
616 delivered: 0,
617 inflight: 0,
618 is_cwnd_limited: false,
619 cwnd_limited_this_round: false,
620 cycle_count: 0,
621 cwnd: initial_cwnd,
622 pacing_rate,
623 send_quantum: 2 * smss, /* we start high, but it will be adjusted in set_send_quantum <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#section-5.6.3> */
624 pacing_gain: startup_pacing_gain,
625 startup_pacing_gain,
626 default_pacing_gain,
627 probe_bw_down_pacing_gain,
628 probe_bw_up_pacing_gain,
629 drain_pacing_gain,
630 pacing_margin_percent,
631 cwnd_gain: default_cwnd_gain,
632 default_cwnd_gain,
633 probe_rng,
634 probe_bw_up_cwnd_gain,
635 state: BbrState::Startup,
636 undo_state: BbrState::Startup,
637 round_count: 0,
638 round_start: true,
639 next_round_delivered: 0,
640 idle_restart: false,
641 min_pipe_cwnd: 4 * smss, /* 4 * C.SMSS as defined in <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-2.7-4> */
642 max_bw: 0.0,
643 bw_shortterm: f64::INFINITY,
644 undo_bw_shortterm: f64::INFINITY,
645 bw: 0.0,
646 min_rtt: Duration::from_secs(u64::MAX),
647 bdp: 0,
648 extra_acked: 0,
649 offload_budget: 0,
650 max_inflight: 0,
651 inflight_longterm: u64::MAX,
652 undo_inflight_longterm: u64::MAX,
653 inflight_shortterm: u64::MAX,
654 undo_inflight_shortterm: u64::MAX,
655 bw_latest: 0.0,
656 inflight_latest: 0,
657 max_bw_filter: MaxFilter::new(MAX_BW_FILTER_LEN as u64),
658 extra_acked_interval_start: None,
659 extra_acked_delivered: 0,
660 extra_acked_filter: MaxFilter::new(EXTRA_ACKED_FILTER_LEN as u64),
661 full_bw_reached: false,
662 full_bw_now: false,
663 full_bw: 0.0,
664 full_bw_count: 0,
665 min_rtt_stamp: None,
666 probe_rtt_cwnd_gain,
667 probe_rtt_duration: Duration::from_millis(PROBE_RTT_DURATION_MS),
668 probe_rtt_interval: Duration::from_secs(PROBE_RTT_INTERVAL_SEC),
669 // Infinity, as for `min_rtt`: a min filter zero-initialized is never lowered.
670 probe_rtt_min_delay: Duration::from_secs(u64::MAX),
671 probe_rtt_min_stamp: None,
672 probe_rtt_expired: false,
673 delivered_time: None,
674 first_send_time: None,
675 app_limited: 0,
676 lost: 0,
677 rs: None,
678 packets: Default::default(),
679 rounds_since_bw_probe: 0,
680 bw_probe_wait: Duration::ZERO,
681 bw_probe_up_rounds: 0,
682 bw_probe_up_acks: 0,
683 probe_up_cnt: 0,
684 cycle_stamp: None,
685 ack_phase: AckPhase::ProbeStarting,
686 bw_probe_samples: false,
687 loss_events_in_round: 0,
688 last_lost_packet: None,
689 recovery_start_time: None,
690 recovery_start_round: 0,
691 in_recovery: false,
692 reno_rounds_bound: RENO_ROUNDS_BOUNDS[0],
693 loss_round_delivered: 0,
694 loss_in_round: false,
695 probe_rtt_done_stamp: None,
696 probe_rtt_round_done: false,
697 prior_cwnd: 0,
698 loss_round_start: false,
699 drain_start_round: 0,
700 // Conservative defaults that match RFC 9000 §13.2.2 behavior (ACK every other
701 // ack-eliciting packet) and the default QUIC `max_ack_delay` of 25ms. Overridden
702 // when the connection supplies peer ACK-frequency parameters.
703 ack_eliciting_threshold: 1,
704 max_ack_delay: Duration::from_millis(25),
705 }
706 }
707
708 /// equivalent to BBRUpdateModelAndState <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.2.3>
709 fn update_model_and_state(&mut self, p: BbrPacket, now: Instant) {
710 self.update_latest_delivery_signals();
711 self.update_congestion_signals(p);
712 self.update_ack_aggregation(now);
713 self.check_full_bw_reached();
714 self.check_startup_done();
715 self.check_drain_done(now);
716 self.update_probe_bw_cycle_phase(now);
717 self.update_min_rtt(now);
718 self.check_probe_rtt(now);
719 self.advance_latest_delivery_signals();
720 self.bound_bw_for_model();
721 }
722
723 /// equivalent to BBRUpdateLatestDeliverySignals <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.10.3-8>
724 fn update_latest_delivery_signals(&mut self) {
725 self.loss_round_start = false;
726 if let Some(rate_sample) = self.rs {
727 self.bw_latest = [self.bw_latest, rate_sample.delivery_rate]
728 .iter()
729 .copied()
730 .fold(f64::NAN, f64::max);
731 self.inflight_latest = Ord::max(self.inflight_latest, rate_sample.delivered);
732
733 if rate_sample.prior_delivered >= self.loss_round_delivered {
734 self.loss_round_delivered = self.delivered;
735 self.loss_round_start = true;
736 }
737 }
738 }
739
740 /// equivalent to BBRUpdateCongestionSignals <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.10.3-8>
741 fn update_congestion_signals(&mut self, p: BbrPacket) {
742 self.update_max_bw(p);
743 if !self.loss_round_start {
744 return;
745 }
746 self.adapt_lower_bounds_from_congestion();
747 self.loss_in_round = false;
748 }
749
750 /// equivalent to BBRUpdateMaxBw <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.5>
751 fn update_max_bw(&mut self, p: BbrPacket) {
752 self.update_round(p);
753 if let Some(rate_sample) = self.rs
754 && rate_sample.delivery_rate > 0.0
755 && (rate_sample.delivery_rate >= self.max_bw || !rate_sample.is_app_limited)
756 {
757 self.max_bw_filter
758 .update_max(self.cycle_count, rate_sample.delivery_rate.round() as u64);
759
760 self.max_bw = self.max_bw_filter.get_max() as f64;
761 }
762 }
763
764 /// equivalent to BBRUpdateRound <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.1-9>
765 fn update_round(&mut self, packet: BbrPacket) {
766 if packet.delivered >= self.next_round_delivered {
767 self.start_round();
768 // Snapshot the just-ended round's cwnd-limited status for this round's decisions, then
769 // reset the accumulator for the new round.
770 self.is_cwnd_limited = self.cwnd_limited_this_round;
771 self.cwnd_limited_this_round = false;
772 self.round_count += 1;
773 self.rounds_since_bw_probe += 1;
774 self.round_start = true;
775 } else {
776 self.round_start = false;
777 }
778 }
779
780 /// equivalent to BBRAdaptLowerBoundsFromCongestion <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.10.3-8>
781 fn adapt_lower_bounds_from_congestion(&mut self) {
782 match self.state {
783 BbrState::ProbeBw(ProbeBwSubstate::Refill)
784 | BbrState::ProbeBw(ProbeBwSubstate::Up)
785 | BbrState::Startup => {}
786 _ => {
787 if self.loss_in_round {
788 self.init_lower_bounds();
789 self.loss_lower_bounds();
790 }
791 }
792 }
793 }
794
795 /// equivalent to BBRInitLowerBounds <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.10.3-8>
796 fn init_lower_bounds(&mut self) {
797 if self.bw_shortterm == f64::INFINITY {
798 self.bw_shortterm = self.max_bw;
799 }
800 if self.inflight_shortterm == u64::MAX {
801 self.inflight_shortterm = self.cwnd;
802 }
803 }
804
805 /// equivalent to BBRLossLowerBounds <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.10.3-8>
806 fn loss_lower_bounds(&mut self) {
807 // gives max of both f64
808 self.bw_shortterm = [self.bw_latest, BETA * self.bw_shortterm]
809 .iter()
810 .copied()
811 .fold(f64::NAN, f64::max);
812 self.inflight_shortterm = Ord::max(
813 self.inflight_latest,
814 (BETA * self.inflight_shortterm as f64) as u64,
815 );
816 }
817
818 /// equivalent to BBRUpdateACKAggregation <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.9>
819 fn update_ack_aggregation(&mut self, now: Instant) {
820 let interval;
821 if let Some(extra_acked_interval_start) = self.extra_acked_interval_start {
822 interval = now - extra_acked_interval_start;
823 } else {
824 interval = Duration::from_secs(0);
825 }
826 let mut expected_delivered = (self.bw * interval.as_secs_f64()) as u64;
827 if self.extra_acked_delivered <= expected_delivered {
828 self.extra_acked_delivered = 0;
829 self.extra_acked_interval_start = Some(now);
830 expected_delivered = 0;
831 }
832 if let Some(rate_sample) = self.rs {
833 self.extra_acked_delivered += rate_sample.newly_acked;
834 }
835
836 let mut extra = self
837 .extra_acked_delivered
838 .saturating_sub(expected_delivered);
839 extra = Ord::min(extra, self.cwnd);
840 if self.full_bw_reached {
841 self.extra_acked_filter.update_max(self.round_count, extra);
842 self.extra_acked = self.extra_acked_filter.get_max();
843 } else {
844 self.extra_acked = extra; // In startup, just remember 1 round
845 }
846 }
847
848 /// equivalent to BBRCheckFullBWReached <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.1.2-6>
849 fn check_full_bw_reached(&mut self) {
850 if self.full_bw_now || !self.round_start {
851 return;
852 }
853 if let Some(rate_sample) = self.rs {
854 if rate_sample.is_app_limited {
855 return;
856 }
857 if rate_sample.delivery_rate >= self.full_bw * FULL_BW_GROWTH {
858 self.reset_full_bw();
859 self.full_bw = rate_sample.delivery_rate;
860 return;
861 }
862 }
863 self.full_bw_count += 1;
864 self.full_bw_now = self.full_bw_count >= MAX_FULL_BW_COUNT;
865 if self.full_bw_now {
866 self.full_bw_reached = true;
867 }
868 }
869
870 /// equivalent to BBRCheckStartupDone <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.1.1-6>
871 fn check_startup_done(&mut self) {
872 self.check_startup_high_loss();
873 if self.state == BbrState::Startup && self.full_bw_reached {
874 self.enter_drain();
875 }
876 }
877
878 /// equivalent to BBRCheckStartupHighLoss <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.1.3>
879 fn check_startup_high_loss(&mut self) {
880 if self.full_bw_reached {
881 return;
882 }
883
884 // All three criteria of section 5.3.1.3 must hold: at least one full packet-timed round
885 // trip spent in fast recovery, a round-trip loss rate above `LOSS_THRESH`
886 // (`is_inflight_too_high`), and at least `STARTUP_FULL_LOSS_CNT` discontiguous lost
887 // sequence ranges within that round trip.
888 if self.loss_round_start
889 && self.in_recovery_for_a_full_round()
890 && self.loss_events_in_round >= STARTUP_FULL_LOSS_CNT
891 && self.is_inflight_too_high()
892 {
893 let mut new_inflight_hi = self.bdp.max(self.inflight_latest);
894 if let Some(rate_sample) = self.rs
895 && new_inflight_hi < rate_sample.delivered
896 {
897 new_inflight_hi = rate_sample.delivered;
898 }
899 self.inflight_longterm = new_inflight_hi;
900 self.full_bw_reached = true;
901 self.full_bw_now = true;
902 }
903
904 if self.loss_round_start {
905 self.loss_events_in_round = 0;
906 self.last_lost_packet = None;
907 }
908 }
909
910 /// First criterion of BBRCheckStartupHighLoss: "the connection has been in fast recovery
911 /// for at least one full packet-timed round trip"
912 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#section-5.3.1.3>
913 fn in_recovery_for_a_full_round(&self) -> bool {
914 self.in_recovery && self.round_count > self.recovery_start_round
915 }
916
917 /// A packet was declared lost: enter fast recovery, unless that packet was sent at or before
918 /// the start of the episode already underway and so belongs to it.
919 /// <https://datatracker.ietf.org/doc/html/rfc9002#section-7.3.2>
920 fn enter_recovery(&mut self, now: Instant, sent: Instant) {
921 if self.recovery_start_time.is_some_and(|start| sent <= start) {
922 return;
923 }
924 self.recovery_start_time = Some(now);
925 self.recovery_start_round = self.round_count;
926 self.in_recovery = true;
927 }
928
929 /// Fast recovery ends when a packet sent after it began is acknowledged.
930 /// <https://datatracker.ietf.org/doc/html/rfc9002#section-7.3.2>
931 fn check_recovery_done(&mut self, sent: Instant) {
932 if self.recovery_start_time.is_some_and(|start| sent > start) {
933 self.in_recovery = false;
934 }
935 }
936
937 /// equivalent to BBREnterDrain <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.2>
938 fn enter_drain(&mut self) {
939 self.state = BbrState::Drain;
940 self.pacing_gain = self.drain_pacing_gain;
941 self.cwnd_gain = self.default_cwnd_gain;
942 self.drain_start_round = self.round_count;
943 }
944
945 /// equivalent to BBRCheckDrainDone <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.2-3>
946 fn check_drain_done(&mut self, now: Instant) {
947 if self.state == BbrState::Drain
948 && (self.inflight <= self.get_inflight(1.0)
949 || self.round_count > self.drain_start_round + 3)
950 {
951 self.enter_probe_bw(now);
952 }
953 }
954
955 /// equivalent to BBREnterProbeBW <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.6>
956 fn enter_probe_bw(&mut self, now: Instant) {
957 self.cwnd_gain = self.default_cwnd_gain;
958 self.start_probe_bw_down(now);
959 }
960
961 /// equivalent to BBRUpdateProbeBWCyclePhase <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.6-6>
962 fn update_probe_bw_cycle_phase(&mut self, now: Instant) {
963 if !self.full_bw_reached {
964 return;
965 }
966 self.adapt_long_term_model();
967 let state = self.state;
968 match state {
969 BbrState::ProbeBw(ProbeBwSubstate::Down) => {
970 if self.maybe_enter_probe_bw_refill(now) {
971 return;
972 }
973 if self.maybe_update_budget_and_time_to_cruise() {
974 self.start_probe_bw_cruise();
975 }
976 }
977 BbrState::ProbeBw(ProbeBwSubstate::Cruise) if self.maybe_enter_probe_bw_refill(now) => {
978 }
979 BbrState::ProbeBw(ProbeBwSubstate::Refill) if self.round_start => {
980 self.bw_probe_samples = true;
981 self.start_probe_bw_up();
982 }
983 BbrState::ProbeBw(ProbeBwSubstate::Up) if self.maybe_go_down() => {
984 self.start_probe_bw_down(now);
985 }
986 _ => {}
987 }
988 }
989
990 /// equivalent to BBRAdaptLongTermModel <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.6-8>
991 fn adapt_long_term_model(&mut self) {
992 if self.ack_phase == AckPhase::ProbeStarting && self.round_start {
993 self.ack_phase = AckPhase::ProbeFeedback;
994 }
995 if self.ack_phase == AckPhase::ProbeStopping
996 && self.round_start
997 && let BbrState::ProbeBw(_) = self.state
998 && let Some(rate_sample) = self.rs
999 && !rate_sample.is_app_limited
1000 {
1001 self.advance_max_bw_filter();
1002 }
1003 if !self.is_inflight_too_high() {
1004 if self.inflight_longterm == u64::MAX {
1005 return;
1006 }
1007 if let Some(rate_sample) = self.rs
1008 && rate_sample.tx_in_flight > self.inflight_longterm
1009 {
1010 self.inflight_longterm = rate_sample.tx_in_flight;
1011 }
1012 if self.state == BbrState::ProbeBw(ProbeBwSubstate::Up) {
1013 self.probe_inflight_long_term_upward();
1014 }
1015 }
1016 }
1017
1018 /// equivalent to BBRAdvanceMaxBwFilter <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.6>
1019 fn advance_max_bw_filter(&mut self) {
1020 self.cycle_count = self.cycle_count.saturating_add(1);
1021 }
1022
1023 /// equivalent to BBRIsTimeToProbeBW <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.5.3-6>
1024 fn maybe_enter_probe_bw_refill(&mut self, now: Instant) -> bool {
1025 if self.has_elapsed_in_phase(self.bw_probe_wait, now)
1026 || self.is_reno_coexistence_probe_time()
1027 {
1028 self.start_probe_bw_refill();
1029 return true;
1030 }
1031 false
1032 }
1033
1034 /// equivalent to BBRHasElapsedInPhase <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.6-8>
1035 fn has_elapsed_in_phase(&mut self, interval: Duration, now: Instant) -> bool {
1036 if let Some(cycle_stamp) = self.cycle_stamp {
1037 now > cycle_stamp.checked_add(interval).unwrap_or(cycle_stamp)
1038 } else {
1039 true
1040 }
1041 }
1042
1043 /// equivalent to BBRIsRenoCoexistenceProbeTime <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.5.3-6>
1044 ///
1045 /// `reno_bdp = min(BBR.bdp, C.cwnd)` is a packet count in the spec, which quotes the BDPs it
1046 /// bounds against in packets ("25Mbps * 30 ms / (1514 bytes) ~= 62 packets"), so convert from
1047 /// bytes before comparing against a round count. Without a BDP estimate there is no
1048 /// Reno-equivalent round count to respect, and probing is left to `bw_probe_wait`.
1049 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#section-5.3.3.8.2>
1050 fn is_reno_coexistence_probe_time(&self) -> bool {
1051 let reno_rounds = self.target_inflight() / self.smss;
1052 if reno_rounds == 0 {
1053 return false;
1054 }
1055 let rounds = Ord::min(reno_rounds, self.reno_rounds_bound);
1056 self.rounds_since_bw_probe >= rounds
1057 }
1058
1059 /// equivalent to BBRStartProbeBW_REFILL <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.6-4>
1060 fn start_probe_bw_refill(&mut self) {
1061 self.reset_short_term_model();
1062 self.bw_probe_up_rounds = 0;
1063 self.bw_probe_up_acks = 0;
1064 self.ack_phase = AckPhase::Refilling;
1065 self.start_round();
1066 self.cwnd_gain = self.default_cwnd_gain;
1067 self.pacing_gain = self.default_pacing_gain;
1068 self.state = BbrState::ProbeBw(ProbeBwSubstate::Refill);
1069 }
1070
1071 /// equivalent to BBRIsTimeToCruise <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.6-8>
1072 fn maybe_update_budget_and_time_to_cruise(&mut self) -> bool {
1073 if self.inflight > self.inflight_with_headroom() {
1074 return false;
1075 }
1076 if self.inflight > self.get_inflight(1.0) {
1077 return false;
1078 }
1079 true
1080 }
1081
1082 /// equivalent to BBRInflight <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.4.2-2>
1083 fn get_inflight(&mut self, gain: f64) -> u64 {
1084 let inflight_cap = self.bdp_multiple(self.max_bw, gain);
1085 self.quantization_budget(inflight_cap)
1086 }
1087
1088 /// equivalent to BBRIsTimeToGoDown <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.6-6>
1089 fn maybe_go_down(&mut self) -> bool {
1090 if self.is_cwnd_limited && self.cwnd >= self.inflight_longterm {
1091 self.reset_full_bw();
1092 if let Some(rate_sample) = self.rs {
1093 self.full_bw = rate_sample.delivery_rate;
1094 }
1095 } else if self.full_bw_now {
1096 return true;
1097 }
1098 false
1099 }
1100
1101 /// equivalent to BBRProbeInflightLongtermUpward <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.6-8>
1102 fn probe_inflight_long_term_upward(&mut self) {
1103 if !self.is_cwnd_limited || self.cwnd < self.inflight_longterm {
1104 return;
1105 }
1106 if let Some(rate_sample) = self.rs {
1107 self.bw_probe_up_acks += rate_sample.newly_acked;
1108 }
1109 if self.bw_probe_up_acks >= self.probe_up_cnt && self.probe_up_cnt > 0 {
1110 let delta = self.bw_probe_up_acks / self.probe_up_cnt;
1111 self.bw_probe_up_acks -= delta * self.probe_up_cnt;
1112 self.inflight_longterm += delta;
1113 }
1114 if self.round_start {
1115 self.raise_inflight_long_term_slope();
1116 }
1117 }
1118
1119 /// equivalent to BBRUpdateMinRTT <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#section-5.3.4.3>
1120 ///
1121 /// The draft stamps `probe_rtt_min_stamp` at connection init, so the first ProbeRTT falls due
1122 /// one `probe_rtt_interval` later. Before the first ack there is no RTT sample to stamp, so an
1123 /// unset stamp counts as not yet expired and the first sample starts the interval. Counting it
1124 /// as expired instead would dip every connection into ProbeRTT on its very first ack, and so
1125 /// into `min_pipe_cwnd`, since `bw` is still 0.
1126 fn update_min_rtt(&mut self, now: Instant) {
1127 self.probe_rtt_expired = match self.probe_rtt_min_stamp {
1128 Some(probe_rtt_min_stamp) => {
1129 now > probe_rtt_min_stamp
1130 .checked_add(self.probe_rtt_interval)
1131 .unwrap_or(probe_rtt_min_stamp)
1132 }
1133 None => false,
1134 };
1135 if let Some(rate_sample) = self.rs
1136 && rate_sample.rtt >= Duration::from_secs(0)
1137 && (rate_sample.rtt < self.probe_rtt_min_delay || self.probe_rtt_expired)
1138 {
1139 self.probe_rtt_min_delay = rate_sample.rtt;
1140 self.probe_rtt_min_stamp = Some(now);
1141 }
1142
1143 let min_rtt_expired;
1144 if let Some(min_rtt_stamp) = self.min_rtt_stamp {
1145 min_rtt_expired = now
1146 > min_rtt_stamp
1147 .checked_add(Duration::from_secs(MIN_RTT_FILTER_LEN))
1148 .unwrap_or(min_rtt_stamp);
1149 } else {
1150 min_rtt_expired = true;
1151 }
1152 if self.probe_rtt_min_delay < self.min_rtt || min_rtt_expired {
1153 self.min_rtt = self.probe_rtt_min_delay;
1154 self.min_rtt_stamp = self.probe_rtt_min_stamp;
1155 }
1156 }
1157
1158 /// equivalent to BBRCheckProbeRTT <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.4.3-4>
1159 fn check_probe_rtt(&mut self, now: Instant) {
1160 match self.state {
1161 BbrState::ProbeRtt => {
1162 self.handle_probe_rtt(now);
1163 }
1164 _ => {
1165 if self.probe_rtt_expired && !self.idle_restart {
1166 self.enter_probe_rtt();
1167 self.save_cwnd();
1168 self.probe_rtt_done_stamp = None;
1169 self.ack_phase = AckPhase::ProbeStopping;
1170 self.start_round();
1171 }
1172 }
1173 }
1174 if let Some(rate_sample) = self.rs
1175 && rate_sample.delivered > 0
1176 {
1177 self.idle_restart = false;
1178 }
1179 }
1180
1181 /// equivalent to BBRHandleProbeRTT <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.4.3-4>
1182 fn handle_probe_rtt(&mut self, now: Instant) {
1183 if self.probe_rtt_done_stamp.is_none() && self.inflight <= self.probe_rtt_cwnd() {
1184 self.probe_rtt_done_stamp =
1185 Some(now.checked_add(self.probe_rtt_duration).unwrap_or(now));
1186 self.probe_rtt_round_done = false;
1187 self.start_round();
1188 } else if self.probe_rtt_done_stamp.is_some() {
1189 if self.round_start {
1190 self.probe_rtt_round_done = true;
1191 }
1192 if self.probe_rtt_round_done {
1193 self.check_probe_rtt_done(now);
1194 }
1195 }
1196 }
1197
1198 /// equivalent to BBREnterProbeRTT <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.4.3-4>
1199 fn enter_probe_rtt(&mut self) {
1200 self.state = BbrState::ProbeRtt;
1201 self.pacing_gain = self.default_pacing_gain;
1202 self.cwnd_gain = self.probe_rtt_cwnd_gain;
1203 }
1204
1205 /// equivalent to BBRSaveCwnd <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.4.4-13>
1206 fn save_cwnd(&mut self) {
1207 if !self.loss_in_round && self.state != BbrState::ProbeRtt {
1208 self.prior_cwnd = self.cwnd;
1209 } else {
1210 self.prior_cwnd = Ord::max(self.prior_cwnd, self.cwnd);
1211 }
1212 }
1213
1214 /// equivalent to BBRAdvanceLatestDeliverySignals <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.10.3-8>
1215 fn advance_latest_delivery_signals(&mut self) {
1216 if self.loss_round_start
1217 && let Some(rate_sample) = self.rs
1218 {
1219 self.bw_latest = rate_sample.delivery_rate;
1220 self.inflight_latest = rate_sample.delivered;
1221 }
1222 }
1223
1224 /// equivalent to BBRBoundBWForModel <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.10.3-8>
1225 fn bound_bw_for_model(&mut self) {
1226 // gives min of both f64
1227 self.bw = [self.max_bw, self.bw_shortterm]
1228 .iter()
1229 .copied()
1230 .fold(f64::NAN, f64::min);
1231 }
1232
1233 /// equivalent to BBRStartProbeBW_UP <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.6-4>
1234 fn start_probe_bw_up(&mut self) {
1235 self.ack_phase = AckPhase::ProbeStarting;
1236 self.start_round();
1237 self.reset_full_bw();
1238 if let Some(rate_sample) = self.rs {
1239 self.full_bw = rate_sample.delivery_rate;
1240 }
1241 self.state = BbrState::ProbeBw(ProbeBwSubstate::Up);
1242 self.pacing_gain = self.probe_bw_up_pacing_gain;
1243 self.cwnd_gain = self.probe_bw_up_cwnd_gain;
1244 self.raise_inflight_long_term_slope();
1245 }
1246
1247 /// equivalent to BBRResetFullBW <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.1.2-4>
1248 fn reset_full_bw(&mut self) {
1249 self.full_bw = 0.0;
1250 self.full_bw_count = 0;
1251 self.full_bw_now = false;
1252 }
1253
1254 /// equivalent to BBRRaiseInflightLongtermSlope <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.6-8>
1255 fn raise_inflight_long_term_slope(&mut self) {
1256 let growth_this_round = self
1257 .smss
1258 .checked_shl(self.bw_probe_up_rounds)
1259 .unwrap_or(u64::MAX);
1260 self.bw_probe_up_rounds =
1261 Ord::min(self.bw_probe_up_rounds + 1, MAX_LONG_TERM_PROBE_UP_ROUNDS);
1262 self.probe_up_cnt = Ord::max(self.cwnd / growth_this_round, 1);
1263 }
1264
1265 /// equivalent to BBRHandleRestartFromIdle <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.4.1>
1266 fn handle_restart_from_idle(&mut self, now: Instant) {
1267 if self.inflight == 0 && self.app_limited != 0 {
1268 self.idle_restart = true;
1269 self.extra_acked_interval_start = Some(now);
1270 match self.state {
1271 BbrState::ProbeBw(_) => {
1272 self.set_pacing_rate_with_gain(1.0);
1273 }
1274 BbrState::ProbeRtt => {
1275 self.check_probe_rtt_done(now);
1276 }
1277 _ => {}
1278 }
1279 }
1280 }
1281
1282 /// equivalent to BBRCheckProbeRTTDone <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.4.3-4>
1283 fn check_probe_rtt_done(&mut self, now: Instant) {
1284 if let Some(probe_rtt_done_stamp) = self.probe_rtt_done_stamp
1285 && now > probe_rtt_done_stamp
1286 {
1287 self.probe_rtt_min_stamp = Some(now);
1288 self.restore_cwnd();
1289 self.exit_probe_rtt(now);
1290 }
1291 }
1292
1293 /// equivalent to BBRRestoreCwnd <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.4.4-13>
1294 fn restore_cwnd(&mut self) {
1295 self.cwnd = Ord::max(self.cwnd, self.prior_cwnd);
1296 }
1297
1298 /// equivalent to BBRExitProbeRTT <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.4.4>
1299 fn exit_probe_rtt(&mut self, now: Instant) {
1300 self.reset_short_term_model();
1301 if self.full_bw_reached {
1302 self.start_probe_bw_down(now);
1303 self.start_probe_bw_cruise();
1304 } else {
1305 self.enter_startup();
1306 }
1307 }
1308
1309 /// equivalent to BBRStartProbeBW_CRUISE <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.4.4-4>
1310 fn start_probe_bw_cruise(&mut self) {
1311 self.state = BbrState::ProbeBw(ProbeBwSubstate::Cruise);
1312 self.pacing_gain = self.default_pacing_gain;
1313 self.cwnd_gain = self.default_cwnd_gain;
1314 }
1315
1316 /// equivalent to BBREnterStartup <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.1.1-3>
1317 fn enter_startup(&mut self) {
1318 self.state = BbrState::Startup;
1319 self.pacing_gain = self.startup_pacing_gain;
1320 self.cwnd_gain = self.default_cwnd_gain;
1321 }
1322
1323 /// equivalent to BBRUpdateControlParameters <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.2.3>
1324 fn update_control_parameters(&mut self) {
1325 self.set_pacing_rate();
1326 self.set_send_quantum();
1327 self.set_cwnd();
1328 }
1329
1330 /// equivalent to BBRSetCwnd <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.4.6>
1331 fn set_cwnd(&mut self) {
1332 self.update_max_inflight();
1333 if self.full_bw_reached {
1334 if let Some(rate_sample) = self.rs {
1335 self.cwnd = Ord::min(self.cwnd + rate_sample.newly_acked, self.max_inflight);
1336 } else {
1337 self.cwnd = Ord::min(self.cwnd, self.max_inflight);
1338 }
1339 } else if (self.cwnd < self.max_inflight || self.delivered < self.initial_cwnd)
1340 && let Some(rate_sample) = self.rs
1341 {
1342 self.cwnd += rate_sample.newly_acked;
1343 }
1344 self.cwnd = Ord::max(self.cwnd, self.min_pipe_cwnd);
1345 self.bound_cwnd_for_probe_rtt();
1346 self.bound_cwnd_for_model();
1347 }
1348
1349 /// equivalent to BBRUpdateMaxInflight <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.4.2-2>
1350 fn update_max_inflight(&mut self) {
1351 let mut inflight_cap = self.bdp_multiple(self.max_bw, self.cwnd_gain);
1352 inflight_cap += self.extra_acked;
1353 self.max_inflight = self.quantization_budget(inflight_cap);
1354 }
1355
1356 /// equivalent to BBRQuantizationBudget <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.4.2-2>
1357 fn quantization_budget(&mut self, inflight_cap: u64) -> u64 {
1358 self.update_offload_budget();
1359 let mut inflight_cap = Ord::max(inflight_cap, self.offload_budget);
1360 inflight_cap = Ord::max(inflight_cap, self.min_pipe_cwnd);
1361 if self.state == BbrState::ProbeBw(ProbeBwSubstate::Up) {
1362 inflight_cap += 2 * self.smss;
1363 }
1364 inflight_cap
1365 }
1366
1367 /// equivalent to BBRUpdateOffloadBudget for QUIC per
1368 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.8.2>.
1369 ///
1370 /// The delayed-ACK term accounts for the QUIC ACK frequency extension:
1371 /// `min(Ack-Eliciting Threshold, Requested Max Ack Delay * BBR.max_bw)`.
1372 fn update_offload_budget(&mut self) {
1373 let base = self.send_quantum;
1374
1375 // Ack-Eliciting Threshold is a packet count in the ACK_FREQUENCY frame; convert to
1376 // bytes using the current SMSS. A threshold of 0 requires an immediate ACK per packet,
1377 // so the delayed-ACK term contributes nothing in that case.
1378 let threshold_bytes = self.ack_eliciting_threshold.saturating_mul(self.smss);
1379 let delay_bytes = (self.max_ack_delay.as_secs_f64() * self.max_bw).round() as u64;
1380 let delayed_ack_term = Ord::min(threshold_bytes, delay_bytes);
1381
1382 self.offload_budget = base.saturating_add(delayed_ack_term);
1383 }
1384
1385 /// equivalent to BBRBoundCwndForProbeRTT <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.4.5-1>
1386 fn bound_cwnd_for_probe_rtt(&mut self) {
1387 if self.state == BbrState::ProbeRtt {
1388 self.cwnd = Ord::min(self.cwnd, self.probe_rtt_cwnd());
1389 }
1390 }
1391
1392 /// equivalent to BBRProbeRTTCwnd <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.4.5-1>
1393 fn probe_rtt_cwnd(&mut self) -> u64 {
1394 let mut probe_rtt_cwnd = self.bdp_multiple(self.bw, self.probe_rtt_cwnd_gain);
1395 probe_rtt_cwnd = Ord::max(probe_rtt_cwnd, self.min_pipe_cwnd);
1396 probe_rtt_cwnd
1397 }
1398
1399 /// equivalent to BBRBDPMultiple <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.4.2-2>
1400 fn bdp_multiple(&mut self, bw: f64, gain: f64) -> u64 {
1401 if self.min_rtt == Duration::from_secs(u64::MAX) {
1402 return self.initial_cwnd;
1403 }
1404 self.bdp = (bw * self.min_rtt.as_secs_f64()).round() as u64;
1405 (gain * self.bdp as f64) as u64
1406 }
1407
1408 /// equivalent to BBRBoundCwndForModel <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.4.7>
1409 fn bound_cwnd_for_model(&mut self) {
1410 let mut cap = u64::MAX;
1411 match self.state {
1412 BbrState::ProbeRtt => {
1413 cap = self.inflight_with_headroom();
1414 }
1415 BbrState::ProbeBw(ProbeBwSubstate::Cruise) => {
1416 cap = self.inflight_with_headroom();
1417 }
1418 BbrState::ProbeBw(_) => {
1419 cap = self.inflight_longterm;
1420 }
1421 _ => {}
1422 }
1423 cap = Ord::min(cap, self.inflight_shortterm);
1424 cap = Ord::max(cap, self.min_pipe_cwnd);
1425 self.cwnd = Ord::min(self.cwnd, cap);
1426 }
1427
1428 /// equivalent to BBRInflightWithHeadroom <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.6-8>
1429 fn inflight_with_headroom(&self) -> u64 {
1430 if self.inflight_longterm == u64::MAX {
1431 return u64::MAX;
1432 }
1433 let total_headroom = Ord::max(self.smss, (HEADROOM * self.inflight_longterm as f64) as u64);
1434 if let Some(inflight_with_headroom) = self.inflight_longterm.checked_sub(total_headroom) {
1435 Ord::max(inflight_with_headroom, self.min_pipe_cwnd)
1436 } else {
1437 self.min_pipe_cwnd
1438 }
1439 }
1440
1441 /// equivalent to BBRSetPacingRate <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.2-7>
1442 fn set_pacing_rate(&mut self) {
1443 self.set_pacing_rate_with_gain(self.pacing_gain);
1444 }
1445
1446 /// equivalent to BBRSetPacingRateWithGain <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.2-7>
1447 fn set_pacing_rate_with_gain(&mut self, gain: f64) {
1448 let rate = gain * self.bw * (100.0 - self.pacing_margin_percent) / 100.0;
1449 if self.full_bw_reached || rate > self.pacing_rate {
1450 self.pacing_rate = rate;
1451 }
1452 }
1453
1454 /// equivalent to BBRSetSendQuantum <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#section-5.6.3>
1455 fn set_send_quantum(&mut self) {
1456 // C.pacing_rate is in bytes/sec, so multiplying by 1ms is dividing by 1000.
1457 let mut quantum = (self.pacing_rate / 1000.0) as u64;
1458 quantum = Ord::min(quantum, HIGH_PACE_MAX_QUANTUM);
1459 quantum = Ord::max(quantum, 2 * self.smss);
1460 self.send_quantum = quantum;
1461 }
1462
1463 /// equivalent to IsNewestPacket <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-4.1.2.3-3>
1464 ///
1465 /// The `P.packet_id > RS.last_acked_packet_id` tie-break only orders two packets of the same
1466 /// space, since every space numbers independently from zero.
1467 fn is_newest_packet(&self, send_time: Instant, space: SpaceKind, end_seq: u64) -> bool {
1468 if let Some(first_send_time) = self.first_send_time {
1469 if send_time > first_send_time {
1470 return true;
1471 }
1472 if send_time == first_send_time
1473 && let Some(rate_sample) = self.rs
1474 && rate_sample.last_packet.space == space
1475 && end_seq > rate_sample.last_end_seq
1476 {
1477 return true;
1478 }
1479 }
1480 false
1481 }
1482
1483 /// equivalent to BBRHandleLostPacket <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.10.2-11>
1484 fn process_lost_packet(&mut self, packet_index: usize, space: SpaceKind, now: Instant) {
1485 let p = self.packets[space as usize][packet_index];
1486 self.enter_recovery(now, p.send_time);
1487 self.note_loss(space, p.packet_number);
1488 if !self.bw_probe_samples {
1489 self.packets[space as usize].remove(packet_index);
1490 return;
1491 }
1492 if let Some(mut rate_sample) = self.rs {
1493 rate_sample.tx_in_flight = p.tx_in_flight;
1494 rate_sample.lost = self.lost.saturating_sub(p.lost);
1495 rate_sample.is_app_limited = p.is_app_limited;
1496 self.rs = Some(rate_sample);
1497 if self.is_inflight_too_high() {
1498 rate_sample.tx_in_flight = self.inflight_at_loss(p.size as u64);
1499 self.rs = Some(rate_sample);
1500 self.handle_inflight_too_high(now);
1501 }
1502 }
1503 self.packets[space as usize].remove(packet_index);
1504 }
1505
1506 /// equivalent to BBRNoteLoss <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.10.2-11>
1507 ///
1508 /// `loss_events_in_round` counts discontiguous lost sequence ranges, not lost packets, so a
1509 /// contiguous burst of packet numbers is one event. Losses are reported in ascending packet
1510 /// number order within a packet number space, so a range ends wherever the next lost packet
1511 /// number is not the successor of the previous one.
1512 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#section-5.3.1.3>
1513 fn note_loss(&mut self, space: SpaceKind, packet_number: u64) {
1514 if !self.loss_in_round {
1515 self.loss_round_delivered = self.delivered;
1516 }
1517 self.save_state_upon_loss();
1518 self.loss_in_round = true;
1519 let continues_range = self
1520 .last_lost_packet
1521 .is_some_and(|(s, pn)| s == space && packet_number == pn.saturating_add(1));
1522 if !continues_range {
1523 self.loss_events_in_round = self.loss_events_in_round.saturating_add(1);
1524 }
1525 self.last_lost_packet = Some((space, packet_number));
1526 }
1527
1528 /// equivalent to BBRSaveStateUponLoss <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.11.1>
1529 /// Save state in case a loss episode is later declared spurious
1530 fn save_state_upon_loss(&mut self) {
1531 self.undo_state = self.state;
1532 self.undo_bw_shortterm = self.bw_shortterm;
1533 self.undo_inflight_shortterm = self.inflight_shortterm;
1534 self.undo_inflight_longterm = self.inflight_longterm;
1535 }
1536
1537 /// equivalent to IsInflightTooHigh <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.10.2-1>
1538 fn is_inflight_too_high(&self) -> bool {
1539 if let Some(rate_sample) = self.rs {
1540 return rate_sample.lost as f64 > rate_sample.tx_in_flight as f64 * LOSS_THRESH;
1541 }
1542 false
1543 }
1544
1545 /// equivalent to BBRInflightAtLoss <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.10.2-11>
1546 /// We check at what prefix of packet did losses exceed `loss_thresh`
1547 fn inflight_at_loss(&mut self, packet_size: u64) -> u64 {
1548 let Some(rate_sample) = self.rs else {
1549 return 0;
1550 };
1551 let inflight_prev = rate_sample.tx_in_flight.saturating_sub(packet_size) as f64;
1552 let lost_prev = rate_sample.lost.saturating_sub(packet_size) as f64;
1553 let lost_prefix = (LOSS_THRESH * inflight_prev - lost_prev) / (1.0 - LOSS_THRESH);
1554 (inflight_prev + lost_prefix) as u64
1555 }
1556
1557 /// equivalent to BBRHandleInflightTooHigh <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.10.2-1>
1558 fn handle_inflight_too_high(&mut self, now: Instant) {
1559 self.bw_probe_samples = false;
1560 if let Some(rate_sample) = self.rs
1561 && !rate_sample.is_app_limited
1562 {
1563 self.inflight_longterm = Ord::max(
1564 rate_sample.tx_in_flight,
1565 (self.target_inflight() as f64 * BETA) as u64,
1566 );
1567 }
1568
1569 if self.state == BbrState::ProbeBw(ProbeBwSubstate::Up) {
1570 self.start_probe_bw_down(now);
1571 }
1572 }
1573
1574 /// equivalent to BBRTargetInflight <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.5.3-6>
1575 fn target_inflight(&self) -> u64 {
1576 Ord::min(self.bdp, self.cwnd)
1577 }
1578
1579 /// equivalent to BBRResetShortTermModel <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.10.3-8>
1580 fn reset_short_term_model(&mut self) {
1581 self.bw_shortterm = f64::INFINITY;
1582 self.inflight_shortterm = u64::MAX;
1583 }
1584
1585 /// equivalent to BBRStartProbeBW_DOWN <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.6-4>
1586 fn start_probe_bw_down(&mut self, now: Instant) {
1587 self.reset_congestion_signals();
1588 self.probe_up_cnt = u64::MAX;
1589 self.pick_probe_wait();
1590 self.cycle_stamp = Some(now);
1591 self.ack_phase = AckPhase::ProbeStopping;
1592 self.start_round();
1593 self.pacing_gain = self.probe_bw_down_pacing_gain;
1594 self.cwnd_gain = self.default_cwnd_gain;
1595 self.state = BbrState::ProbeBw(ProbeBwSubstate::Down);
1596 }
1597
1598 /// equivalent to BBRResetCongestionSignals <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.10.3-8>
1599 fn reset_congestion_signals(&mut self) {
1600 self.loss_in_round = false;
1601 self.bw_latest = 0.0;
1602 self.inflight_latest = 0;
1603 }
1604
1605 /// equivalent to BBRPickProbeWait <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.5.3-6>
1606 fn pick_probe_wait(&mut self) {
1607 // 0 or 1
1608 self.rounds_since_bw_probe = self.probe_rng.random_bool(0.5) as u64;
1609 self.bw_probe_wait = Duration::from_millis(
1610 MIN_PROBE_WAIT_MS + self.probe_rng.random_range(0..=MAX_ADDED_PROBE_WAIT_MS),
1611 );
1612 // T_reno_bound = pick_randomly_either({62, 63})
1613 self.reno_rounds_bound = RENO_ROUNDS_BOUNDS[self.probe_rng.random_bool(0.5) as usize];
1614 }
1615
1616 /// equivalent to BBRStartRound <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.1-9>
1617 fn start_round(&mut self) {
1618 self.next_round_delivered = self.delivered;
1619 }
1620}
1621
1622impl Bbr3 {
1623 fn on_packet_sent(&mut self, now: Instant, bytes: u16, pn: u64, space: SpaceKind) {
1624 self.handle_restart_from_idle(now);
1625 if self.inflight == 0 {
1626 self.first_send_time = Some(now);
1627 self.delivered_time = Some(now);
1628 }
1629 let added_bytes = bytes as u64;
1630 self.inflight += added_bytes;
1631 self.packets[space as usize].push_back(BbrPacket {
1632 delivered: self.delivered,
1633 delivered_time: self.delivered_time.unwrap_or(now),
1634 first_send_time: self.first_send_time.unwrap_or(now),
1635 send_time: now,
1636 is_app_limited: self.app_limited != 0,
1637 tx_in_flight: self.inflight,
1638 packet_number: pn,
1639 space,
1640 size: bytes,
1641 lost: self.lost,
1642 acknowledged: false,
1643 stale: false,
1644 round_count: self.round_count,
1645 });
1646 }
1647
1648 fn on_cwnd_limited(&mut self) {
1649 self.cwnd_limited_this_round = true;
1650 }
1651
1652 /// UpdateRateSample accumulates `C.delivered` and `C.delivered_time` for every ACKed packet,
1653 /// independently of the newest-packet branch that folds the rate sample into the model, so
1654 /// neither may be conditional on a rate sample already existing.
1655 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#section-4.1.2.3>
1656 fn on_ack(
1657 &mut self,
1658 now: Instant,
1659 sent: Instant,
1660 bytes: u64,
1661 packet_number: u64,
1662 space: SpaceKind,
1663 _app_limited: bool,
1664 rtt: &RttEstimator,
1665 ) {
1666 self.check_recovery_done(sent);
1667 self.delivered += bytes;
1668 self.delivered_time = Some(now);
1669 if let Some(mut rate_sample) = self.rs {
1670 rate_sample.newly_acked += bytes;
1671 self.rs = Some(rate_sample);
1672 }
1673 let p_index_result =
1674 self.packets[space as usize].binary_search_by_key(&packet_number, |p| p.packet_number);
1675 let is_newest_packet = self.is_newest_packet(sent, space, packet_number);
1676 if let Ok(p_index) = p_index_result
1677 && let Some(p) = self.packets[space as usize].get_mut(p_index)
1678 {
1679 p.acknowledged = true;
1680 if let Some(mut rate_sample) = self.rs {
1681 rate_sample.rtt = now - p.send_time;
1682 if is_newest_packet {
1683 rate_sample.prior_delivered = p.delivered;
1684 rate_sample.is_app_limited = p.is_app_limited;
1685 rate_sample.tx_in_flight = p.tx_in_flight;
1686 rate_sample.lost = self.lost.saturating_sub(p.lost);
1687 rate_sample.send_elapsed = p.send_time - p.first_send_time;
1688 rate_sample.ack_elapsed = self.delivered_time.unwrap_or(now) - p.delivered_time;
1689 rate_sample.last_end_seq = packet_number;
1690 self.first_send_time = Some(p.send_time);
1691 rate_sample.last_packet = *p;
1692 self.rs = Some(rate_sample);
1693 self.update_model_and_state(rate_sample.last_packet, now);
1694 self.update_control_parameters();
1695 // Zero newly_acked after folding so each packet's bytes count once;
1696 // one ACK covers many packets and the model steps run per packet.
1697 if let Some(mut rate_sample) = self.rs {
1698 rate_sample.newly_acked = 0;
1699 self.rs = Some(rate_sample);
1700 }
1701 }
1702 } else {
1703 let rate_sample = BbrRateSample {
1704 rtt: rtt.get(),
1705 interval: Duration::ZERO,
1706 delivery_rate: 0.0,
1707 is_app_limited: p.is_app_limited,
1708 delivered: 0,
1709 prior_delivered: p.delivered,
1710 tx_in_flight: p.tx_in_flight,
1711 send_elapsed: p.send_time - p.first_send_time,
1712 ack_elapsed: self.delivered_time.unwrap_or(now) - p.delivered_time,
1713 newly_acked: bytes,
1714 lost: self.lost.saturating_sub(p.lost),
1715 last_end_seq: packet_number,
1716 last_packet: *p,
1717 };
1718 self.rs = Some(rate_sample);
1719 self.first_send_time = Some(p.send_time);
1720 self.update_model_and_state(rate_sample.last_packet, now);
1721 self.update_control_parameters();
1722 // Drain newly_acked after folding, as in the branch above.
1723 if let Some(mut rate_sample) = self.rs {
1724 rate_sample.newly_acked = 0;
1725 self.rs = Some(rate_sample);
1726 }
1727 }
1728 }
1729 }
1730
1731 /// equivalent to GenerateRateSample <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#section-4.1.2.4>
1732 fn on_end_acks(
1733 &mut self,
1734 _now: Instant,
1735 in_flight: u64,
1736 app_limited: bool,
1737 largest_packet_num_acked: Option<u64>,
1738 _space: SpaceKind,
1739 ) {
1740 self.inflight = in_flight;
1741 if largest_packet_num_acked.is_some() {
1742 if self.app_limited != 0 && self.delivered > self.app_limited {
1743 self.app_limited = 0;
1744 } else if app_limited {
1745 self.app_limited = Ord::max(self.delivered + self.inflight, 1);
1746 }
1747 let round_count = self.round_count;
1748 for packets in self.packets.iter_mut() {
1749 packets.retain(|&p| !p.stale);
1750 for p in packets.iter_mut() {
1751 if p.acknowledged || round_count - p.round_count > ROUND_COUNT_WINDOW {
1752 p.stale = true;
1753 }
1754 }
1755 }
1756 if let Some(mut rate_sample) = self.rs {
1757 rate_sample.interval = Ord::max(rate_sample.send_elapsed, rate_sample.ack_elapsed);
1758 rate_sample.delivered = self.delivered.saturating_sub(rate_sample.prior_delivered);
1759 // ignore this condition on an initially high min rtt as per <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-4.1.2.3-5>
1760 if rate_sample.interval < self.min_rtt
1761 && self.min_rtt != Duration::from_secs(u64::MAX)
1762 {
1763 return;
1764 }
1765 if rate_sample.interval != Duration::ZERO {
1766 rate_sample.delivery_rate =
1767 rate_sample.delivered as f64 / rate_sample.interval.as_secs_f64();
1768 }
1769 self.rs = Some(rate_sample);
1770 rate_sample.newly_acked = 0;
1771 rate_sample.lost = 0;
1772 self.rs = Some(rate_sample);
1773 }
1774 }
1775 }
1776
1777 fn on_congestion_event(
1778 &mut self,
1779 now: Instant,
1780 _sent: Instant,
1781 is_persistent_congestion: bool,
1782 is_ecn: bool,
1783 lost_bytes: u64,
1784 largest_lost_pn: u64,
1785 space: SpaceKind,
1786 ) {
1787 // only process ecn here, regular packet loss is detected per packet in on_packet_lost.
1788 if is_ecn {
1789 self.lost += lost_bytes;
1790 let p_index_result = self.packets[space as usize]
1791 .binary_search_by_key(&largest_lost_pn, |p| p.packet_number);
1792 if let Ok(p_index) = p_index_result {
1793 self.process_lost_packet(p_index, space, now);
1794 }
1795 }
1796 if is_persistent_congestion {
1797 self.cwnd = self.min_pipe_cwnd;
1798 }
1799 }
1800
1801 fn on_packet_lost(
1802 &mut self,
1803 lost_bytes: u16,
1804 packet_number: u64,
1805 space: SpaceKind,
1806 now: Instant,
1807 ) {
1808 let lost_bytes_64 = lost_bytes as u64;
1809 self.lost += lost_bytes_64;
1810 let p_index_result =
1811 self.packets[space as usize].binary_search_by_key(&packet_number, |p| p.packet_number);
1812 if let Ok(p_index) = p_index_result {
1813 self.process_lost_packet(p_index, space, now);
1814 }
1815 }
1816
1817 /// equivalent to BBRHandleSpuriousLossDetection: <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.11.2>
1818 fn on_spurious_congestion_event(&mut self) {
1819 self.loss_in_round = false;
1820 self.reset_full_bw();
1821 self.bw_shortterm = [self.bw_shortterm, self.undo_bw_shortterm]
1822 .iter()
1823 .copied()
1824 .fold(f64::NAN, f64::max);
1825 self.inflight_shortterm = Ord::max(self.inflight_shortterm, self.undo_inflight_shortterm);
1826 self.inflight_longterm = Ord::max(self.inflight_longterm, self.undo_inflight_longterm);
1827 if self.state != BbrState::ProbeRtt && self.state != self.undo_state {
1828 if self.undo_state == BbrState::Startup {
1829 self.enter_startup();
1830 } else if self.undo_state == BbrState::ProbeBw(ProbeBwSubstate::Up) {
1831 self.start_probe_bw_up();
1832 }
1833 }
1834 }
1835
1836 fn on_mtu_update(&mut self, new_mtu: u16) {
1837 self.smss = Ord::min(
1838 Ord::max(MIN_MAX_DATAGRAM_SIZE, new_mtu) as u64,
1839 MAX_DATAGRAM_SIZE,
1840 );
1841 self.min_pipe_cwnd = 4 * self.smss;
1842 self.set_send_quantum();
1843 self.set_cwnd();
1844 }
1845
1846 fn on_ack_frequency_update(
1847 &mut self,
1848 ack_eliciting_threshold: u64,
1849 requested_max_ack_delay: Duration,
1850 ) {
1851 self.ack_eliciting_threshold = ack_eliciting_threshold;
1852 self.max_ack_delay = requested_max_ack_delay;
1853 }
1854
1855 fn window(&self) -> u64 {
1856 self.cwnd
1857 }
1858
1859 fn metrics(&self) -> ControllerMetrics {
1860 ControllerMetrics {
1861 congestion_window: self.window(),
1862 ssthresh: None,
1863 pacing_rate: Some(self.pacing_rate.round() as u64),
1864 send_quantum: Some(self.send_quantum),
1865 }
1866 }
1867
1868 fn clone_box(&self) -> Box<dyn Controller> {
1869 Box::new(self.clone())
1870 }
1871
1872 fn initial_window(&self) -> u64 {
1873 self.initial_cwnd
1874 }
1875
1876 fn into_any(self: Box<Self>) -> Box<dyn Any> {
1877 self
1878 }
1879}
1880
1881// TODO(@divma): We need to expand the Controller trait to receive the SpaceKind. The current
1882// implementation simply uses SpaceKind::Data, which is wrong for PathId::Zero
1883impl Controller for Bbr3 {
1884 fn on_congestion_event(
1885 &mut self,
1886 now: Instant,
1887 sent: Instant,
1888 is_persistent_congestion: bool,
1889 is_ecn: bool,
1890 lost_bytes: u64,
1891 largest_lost_pn: u64,
1892 ) {
1893 Self::on_congestion_event(
1894 self,
1895 now,
1896 sent,
1897 is_persistent_congestion,
1898 is_ecn,
1899 lost_bytes,
1900 largest_lost_pn,
1901 SpaceKind::Data,
1902 );
1903 }
1904
1905 fn on_mtu_update(&mut self, new_mtu: u16) {
1906 Self::on_mtu_update(self, new_mtu);
1907 }
1908
1909 fn window(&self) -> u64 {
1910 Self::window(self)
1911 }
1912
1913 fn clone_box(&self) -> Box<dyn Controller> {
1914 Self::clone_box(self)
1915 }
1916
1917 fn initial_window(&self) -> u64 {
1918 Self::initial_window(self)
1919 }
1920
1921 fn into_any(self: Box<Self>) -> Box<dyn Any> {
1922 Self::into_any(self)
1923 }
1924
1925 fn on_packet_sent(&mut self, now: Instant, bytes: u16, pn: u64) {
1926 Self::on_packet_sent(self, now, bytes, pn, SpaceKind::Data);
1927 }
1928
1929 fn on_cwnd_limited(&mut self) {
1930 Self::on_cwnd_limited(self);
1931 }
1932
1933 fn on_ack(
1934 &mut self,
1935 now: Instant,
1936 sent: Instant,
1937 bytes: u64,
1938 pn: u64,
1939 app_limited: bool,
1940 rtt: &RttEstimator,
1941 ) {
1942 Self::on_ack(
1943 self,
1944 now,
1945 sent,
1946 bytes,
1947 pn,
1948 SpaceKind::Data,
1949 app_limited,
1950 rtt,
1951 );
1952 }
1953
1954 fn on_end_acks(
1955 &mut self,
1956 now: Instant,
1957 in_flight: u64,
1958 app_limited: bool,
1959 largest_packet_num_acked: Option<u64>,
1960 ) {
1961 Self::on_end_acks(
1962 self,
1963 now,
1964 in_flight,
1965 app_limited,
1966 largest_packet_num_acked,
1967 SpaceKind::Data,
1968 );
1969 }
1970
1971 fn on_packet_lost(&mut self, lost_bytes: u16, pn: u64, now: Instant) {
1972 Self::on_packet_lost(self, lost_bytes, pn, SpaceKind::Data, now);
1973 }
1974
1975 fn on_spurious_congestion_event(&mut self) {
1976 Self::on_spurious_congestion_event(self)
1977 }
1978
1979 fn on_ack_frequency_update(
1980 &mut self,
1981 ack_eliciting_threshold: u64,
1982 requested_max_ack_delay: Duration,
1983 ) {
1984 Self::on_ack_frequency_update(self, ack_eliciting_threshold, requested_max_ack_delay);
1985 }
1986
1987 fn metrics(&self) -> ControllerMetrics {
1988 Self::metrics(self)
1989 }
1990}
1991
1992/// Configuration for the `Bbr3` congestion controller
1993/// Different pacing_gains can be set to modify the multiplier used to
1994/// increase the sending rates.
1995/// Different cwnd_gains can be set to modify the multiplier used to increase
1996/// the congestion windows.
1997/// All of these parameters are specific to different states of the algorithm: see `BbrState`
1998/// `pacing_margin_percent` is used to set a margin when calculating the `pacing_rate` in order
1999/// to not send at 100% capacity when calculating pacing.
2000#[derive(Debug, Clone)]
2001pub struct Bbr3Config {
2002 initial_window: u64,
2003 probe_rng_seed: Option<[u8; 16]>,
2004 startup_pacing_gain: Option<f64>,
2005 default_pacing_gain: Option<f64>,
2006 probe_bw_down_pacing_gain: Option<f64>,
2007 probe_bw_up_pacing_gain: Option<f64>,
2008 probe_bw_up_cwnd_gain: Option<f64>,
2009 probe_rtt_cwnd_gain: Option<f64>,
2010 drain_pacing_gain: Option<f64>,
2011 pacing_margin_percent: Option<f64>,
2012 default_cwnd_gain: Option<f64>,
2013}
2014
2015impl Bbr3Config {
2016 /// Default limit on the amount of outstanding data in bytes.
2017 ///
2018 /// Recommended value: `min(10 * max_datagram_size, max(2 * max_datagram_size, 14720))`
2019 pub fn initial_window(&mut self, value: u64) -> &mut Self {
2020 self.initial_window = value;
2021 self
2022 }
2023}
2024
2025impl Default for Bbr3Config {
2026 fn default() -> Self {
2027 Self {
2028 // Bounded by the datagram size a path starts at, not [`MAX_DATAGRAM_SIZE`] (the
2029 // RFC 9000 ceiling), which would clamp 14720 up to 9x the intended window.
2030 initial_window: 14720.clamp(2 * BASE_DATAGRAM_SIZE, 10 * BASE_DATAGRAM_SIZE),
2031 probe_rng_seed: None,
2032 startup_pacing_gain: None,
2033 default_pacing_gain: None,
2034 probe_bw_down_pacing_gain: None,
2035 probe_bw_up_pacing_gain: None,
2036 probe_bw_up_cwnd_gain: None,
2037 probe_rtt_cwnd_gain: None,
2038 drain_pacing_gain: None,
2039 pacing_margin_percent: None,
2040 default_cwnd_gain: None,
2041 }
2042 }
2043}
2044
2045impl ControllerFactory for Bbr3Config {
2046 fn build(self: Arc<Self>, _now: Instant, current_mtu: u16) -> Box<dyn Controller> {
2047 Box::new(Bbr3::new(self, current_mtu))
2048 }
2049}
2050
2051#[cfg(test)]
2052mod test {
2053 use super::*;
2054 use std::cell::Cell;
2055 use std::ops::ControlFlow;
2056
2057 /// PROBE_UP undo snapshot taken before a (possibly spurious) loss:
2058 /// (state, bw_shortterm, inflight_shortterm, inflight_longterm).
2059 type UndoSnapshot = (BbrState, f64, u64, u64);
2060 /// A loss episode: (pre-loss undo snapshot, post-loss state, post-loss inflight_longterm).
2061 type LossEpisode = (UndoSnapshot, BbrState, u64);
2062
2063 /// A packet in flight in the link simulator: its packet number and the
2064 /// simulator-nanosecond timestamps at which it was sent and will be acked.
2065 struct SimPacket {
2066 pn: u64,
2067 send_ns: u64,
2068 ack_ns: u64,
2069 }
2070
2071 /// Single-bottleneck FIFO link simulator driving the real BBR
2072 /// `on_packet_sent`/`on_ack`/`on_end_acks` path against a constant bandwidth
2073 /// `bw`, constant propagation `rtt_ns`, and an infinite buffer (no loss).
2074 /// Packets queue at the bottleneck and are served at `bw`. The sender always
2075 /// has data, paced at BBR's chosen rate, so it is cwnd-limited (never
2076 /// application-limited). Shared harness for the constant-link tests
2077 /// (A.1/A.3/A.5/A.8/A.9/A.10); tests needing loss, app-limiting, a mid-flight
2078 /// bandwidth change, idle periods, or ACK aggregation use their own loops.
2079 struct Sim {
2080 bbr: Bbr3,
2081 base: Instant,
2082 rtt_est: RttEstimator,
2083 flight: VecDeque<SimPacket>,
2084 now_ns: u64,
2085 next_send_ns: u64,
2086 // time at which the bottleneck finishes serving everything queued so far
2087 btl_free_ns: u64,
2088 inflight: u64,
2089 pn: u64,
2090 mss: u64,
2091 fwd_ns: u64,
2092 ret_ns: u64,
2093 // bottleneck serialization time for one MSS-sized packet
2094 btl_service_ns: u64,
2095 }
2096
2097 impl Sim {
2098 fn new(config: Bbr3Config, mss: u64, bw: f64, rtt_ns: u64) -> Self {
2099 Self {
2100 bbr: Bbr3::new(Arc::new(config), mss as u16),
2101 base: Instant::now(),
2102 rtt_est: RttEstimator::new(Duration::from_nanos(rtt_ns)),
2103 flight: VecDeque::new(),
2104 now_ns: 0,
2105 next_send_ns: 0,
2106 btl_free_ns: 0,
2107 inflight: 0,
2108 pn: 0,
2109 mss,
2110 fwd_ns: rtt_ns / 2,
2111 ret_ns: rtt_ns / 2,
2112 btl_service_ns: (mss as f64 / bw * 1e9).round() as u64,
2113 }
2114 }
2115
2116 /// Convert a simulator-nanosecond offset into an `Instant`.
2117 fn at(&self, off_ns: u64) -> Instant {
2118 self.base + Duration::from_nanos(off_ns)
2119 }
2120
2121 /// Drive the send/ack loop for up to `max_iters` steps. On each step the
2122 /// sender sends if the window allows and a send is due no later than the
2123 /// next ack, otherwise it processes the next ack. `on_send` runs after each
2124 /// send, `on_ack` after each `on_end_acks`; either returning
2125 /// `ControlFlow::Break` stops the loop. Panics if the window fills with
2126 /// nothing in flight, or if `max_iters` elapses without a break.
2127 fn run(
2128 &mut self,
2129 max_iters: u64,
2130 mut on_send: impl FnMut(&mut Bbr3) -> ControlFlow<()>,
2131 mut on_ack: impl FnMut(&mut Bbr3, u64, u64, u64) -> ControlFlow<()>,
2132 ) {
2133 for _ in 0..max_iters {
2134 let can_send = self.inflight + self.mss <= self.bbr.window();
2135 // Report the cwnd-blocked signal as the connection layer does: always-backlogged,
2136 // so whenever the window (not pacing) is what stops the send, the flow is
2137 // cwnd-limited.
2138 if !can_send {
2139 self.bbr.on_cwnd_limited();
2140 }
2141 let next_ack = self.flight.front().map(|p| p.ack_ns);
2142 let do_send = can_send && next_ack.is_none_or(|ack| self.next_send_ns <= ack);
2143
2144 if do_send {
2145 self.now_ns = self.now_ns.max(self.next_send_ns);
2146 let send_ns = self.now_ns;
2147 // enqueue at the FIFO bottleneck, served at bw
2148 let arrival = send_ns + self.fwd_ns;
2149 let service_start = arrival.max(self.btl_free_ns);
2150 let finish = service_start + self.btl_service_ns;
2151 self.btl_free_ns = finish;
2152 let ack_ns = finish + self.ret_ns;
2153
2154 self.bbr.on_packet_sent(
2155 self.base + Duration::from_nanos(send_ns),
2156 self.mss as u16,
2157 self.pn,
2158 SpaceKind::Data,
2159 );
2160 self.inflight += self.mss;
2161 self.flight.push_back(SimPacket {
2162 pn: self.pn,
2163 send_ns,
2164 ack_ns,
2165 });
2166
2167 // pace the next send at BBR's chosen pacing rate
2168 let pacing = self.bbr.pacing_rate.max(1.0);
2169 self.next_send_ns = send_ns + (self.mss as f64 / pacing * 1e9).round() as u64;
2170 self.pn += 1;
2171
2172 if on_send(&mut self.bbr).is_break() {
2173 return;
2174 }
2175 } else if let Some(p) = self.flight.pop_front() {
2176 self.now_ns = self.now_ns.max(p.ack_ns);
2177 self.inflight -= self.mss;
2178 let now_at = self.base + Duration::from_nanos(self.now_ns);
2179 let send_at = self.base + Duration::from_nanos(p.send_ns);
2180 self.rtt_est.update(
2181 Duration::ZERO,
2182 Duration::from_nanos(self.now_ns - p.send_ns),
2183 );
2184 self.bbr.on_ack(
2185 now_at,
2186 send_at,
2187 self.mss,
2188 p.pn,
2189 SpaceKind::Data,
2190 false,
2191 &self.rtt_est,
2192 );
2193 self.bbr
2194 .on_end_acks(now_at, self.inflight, false, Some(p.pn), SpaceKind::Data);
2195
2196 if on_ack(&mut self.bbr, self.now_ns, self.inflight, p.pn).is_break() {
2197 return;
2198 }
2199 } else {
2200 panic!("simulation stalled: window full but nothing in flight");
2201 }
2202 }
2203 panic!("simulation exceeded {max_iters} iterations without reaching the target state");
2204 }
2205 }
2206
2207 #[test]
2208 fn test_probe_rng() {
2209 let seed: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
2210 let config = Bbr3Config {
2211 initial_window: 14720.clamp(2 * BASE_DATAGRAM_SIZE, 10 * BASE_DATAGRAM_SIZE),
2212 probe_rng_seed: Some(seed),
2213 startup_pacing_gain: None,
2214 default_pacing_gain: None,
2215 probe_bw_down_pacing_gain: None,
2216 probe_bw_up_pacing_gain: None,
2217 probe_bw_up_cwnd_gain: None,
2218 probe_rtt_cwnd_gain: None,
2219 drain_pacing_gain: None,
2220 pacing_margin_percent: None,
2221 default_cwnd_gain: None,
2222 };
2223 let mut bbr3 = Bbr3::new(Arc::new(config), 2500);
2224 bbr3.pick_probe_wait();
2225 assert_eq!(bbr3.rounds_since_bw_probe, 1);
2226 assert_eq!(bbr3.bw_probe_wait, Duration::from_millis(2652));
2227 // T_reno_bound is re-drawn on every pick, alongside the wall-clock bound
2228 assert_eq!(bbr3.reno_rounds_bound, 63);
2229 bbr3.pick_probe_wait();
2230 assert_eq!(bbr3.rounds_since_bw_probe, 0);
2231 assert_eq!(bbr3.bw_probe_wait, Duration::from_millis(2461));
2232 assert_eq!(bbr3.reno_rounds_bound, 63);
2233 }
2234
2235 /// A.1: Exiting STARTUP on a bandwidth plateau.
2236 /// equivalent to: <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#name-exiting-startup-on-bandwidt>
2237 /// Drives a flow through the real `on_packet_sent`/`on_ack`/`on_end_acks`
2238 /// path against a single-bottleneck simulator: constant bandwidth `BW`,
2239 /// constant propagation `RTT`, infinite buffer (no loss). Packets queue and
2240 /// are served at `BW`, so once the pipe fills the delivery-rate samples
2241 /// plateau at `BW`. The sender always has data, so it is never app-limited.
2242 ///
2243 /// Asserts that once the delivery rate stops growing by >=25% for 3
2244 /// consecutive rounds (`full_bw_count` == `MAX_FULL_BW_COUNT`),
2245 /// `full_bw_now`/`full_bw_reached` are set, `max_bw` sits within 2% of the
2246 /// simulated bandwidth, and the flow transitions STARTUP -> DRAIN.
2247 #[test]
2248 fn startup_exits_to_drain_on_bandwidth_plateau() {
2249 /// packet size in bytes
2250 const MSS: u64 = 1200;
2251 /// simulated bottleneck bandwidth: 100 Mbit/s in bytes/sec
2252 const BW: f64 = 12_500_000.0;
2253 /// Simulated propagation RTT. Kept large (100ms) so the transient
2254 /// ProbeRTT BBR enters on the first ack (its `probe_rtt_min_stamp`
2255 /// starts unset, initializing `min_rtt`) spans fewer than
2256 /// `MAX_FULL_BW_COUNT` rounds and cannot falsely complete the plateau
2257 /// there; the flow bounces back to STARTUP and ramps cleanly.
2258 const RTT_NS: u64 = 100_000_000;
2259
2260 // Drive the production default configuration.
2261 let mut sim = Sim::new(Bbr3Config::default(), MSS, BW, RTT_NS);
2262 assert_eq!(sim.bbr.state, BbrState::Startup);
2263
2264 // captured on the STARTUP -> DRAIN edge (DRAIN is only ever entered from
2265 // STARTUP, via check_startup_done). BBR dips through a transient ProbeRTT
2266 // right after the first ack, so we run until DRAIN rather than breaking on
2267 // the first non-STARTUP state.
2268 let mut transition: Option<(u64, bool, bool, f64)> = None;
2269 sim.run(
2270 1_000_000,
2271 |_| ControlFlow::Continue(()),
2272 |bbr, _now_ns, _inflight, _pn| {
2273 if bbr.state == BbrState::Drain {
2274 transition = Some((
2275 bbr.full_bw_count,
2276 bbr.full_bw_now,
2277 bbr.full_bw_reached,
2278 bbr.max_bw,
2279 ));
2280 return ControlFlow::Break(());
2281 }
2282 ControlFlow::Continue(())
2283 },
2284 );
2285
2286 // The break condition guarantees we landed on the STARTUP -> DRAIN edge.
2287 let (full_bw_count, full_bw_now, full_bw_reached, max_bw) =
2288 transition.expect("BBR never left STARTUP");
2289
2290 // Plateau detected: 3 consecutive rounds with <25% delivery-rate growth.
2291 assert_eq!(full_bw_count, MAX_FULL_BW_COUNT);
2292 assert!(full_bw_now, "full_bw_now should be set on plateau");
2293 assert!(full_bw_reached, "full_bw_reached should be set on plateau");
2294 // Bandwidth estimate within 2% of the simulated link bandwidth.
2295 let err = (max_bw - BW).abs() / BW;
2296 assert!(
2297 err < 0.02,
2298 "max_bw {max_bw} not within 2% of simulated {BW} (rel err {err})"
2299 );
2300 }
2301
2302 /// A.2: Exiting STARTUP on loss when application-limited.
2303 /// equivalent to: <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#name-exiting-startup-on-loss-whe>
2304 ///
2305 /// Drives a STARTUP flow whose delivery-rate samples are all app-limited (the
2306 /// app keeps only `APP_WINDOW` bytes outstanding, well below cwnd), so
2307 /// `check_full_bw_reached` bails every round (`rate_sample.is_app_limited`
2308 /// short-circuit) and the bandwidth-plateau path can never end STARTUP. Loss
2309 /// is then injected above `LOSS_THRESH` (2%), with at least
2310 /// `STARTUP_FULL_LOSS_CNT` discontiguous losses per round trip, so
2311 /// `check_startup_high_loss` observes the high loss rate and ends STARTUP:
2312 /// `full_bw_now`/`full_bw_reached` become true and the flow transitions
2313 /// STARTUP -> DRAIN, purely from loss.
2314 #[test]
2315 fn startup_exits_to_drain_on_loss_when_app_limited() {
2316 /// packet size in bytes
2317 const MSS: u64 = 1200;
2318 /// simulated bottleneck bandwidth: 100 Mbit/s in bytes/sec
2319 const BW: f64 = 12_500_000.0;
2320 /// simulated propagation round-trip time (100ms), matching A.1
2321 const RTT_NS: u64 = 100_000_000;
2322 const FWD_NS: u64 = RTT_NS / 2;
2323 const RET_NS: u64 = RTT_NS / 2;
2324 /// application window: bytes the app keeps outstanding. Fixed and well
2325 /// below the STARTUP cwnd so the sender is application-limited (never
2326 /// cwnd-limited), which blocks the bandwidth-plateau exit and isolates
2327 /// the loss path. Sized so a single round trip carries at least
2328 /// `STARTUP_FULL_LOSS_CNT` losses at the `LOSS_PERIOD` rate below.
2329 const APP_WINDOW: u64 = 200 * MSS;
2330 /// drop 1 in every `LOSS_PERIOD` packets -> 4% loss, above `LOSS_THRESH`
2331 /// (2%), spread evenly so each round trip carries loss over its full
2332 /// sequence range.
2333 const LOSS_PERIOD: u64 = 25;
2334
2335 // bottleneck serialization time for one MSS-sized packet
2336 let btl_service_ns: u64 = (MSS as f64 / BW * 1e9).round() as u64;
2337
2338 // Drive the production default configuration.
2339 let mut bbr = Bbr3::new(Arc::new(Bbr3Config::default()), MSS as u16);
2340 assert_eq!(bbr.state, BbrState::Startup);
2341
2342 let base = Instant::now();
2343 let at = |off_ns: u64| base + Duration::from_nanos(off_ns);
2344 let mut rtt_est = RttEstimator::new(Duration::from_nanos(RTT_NS));
2345
2346 struct InFlight {
2347 pn: u64,
2348 send_ns: u64,
2349 ack_ns: u64,
2350 lost: bool,
2351 }
2352 let mut flight: VecDeque<InFlight> = VecDeque::new();
2353
2354 let mut now_ns: u64 = 0;
2355 // time at which the bottleneck finishes serving everything queued so far
2356 let mut btl_free_ns: u64 = 0;
2357 let mut inflight: u64 = 0;
2358 let mut pn: u64 = 0;
2359
2360 // Whether BBRCheckStartupHighLoss ever observed a too-high loss rate
2361 // (the A.2 signal). Sampled right after each ack is processed.
2362 let mut observed_high_loss = false;
2363
2364 // Counts of STARTUP delivery-rate samples by app-limited flag. The
2365 // app-limited samples are the precondition A.2 relies on: they are what
2366 // check_full_bw_reached short-circuits on, blocking the bandwidth-plateau
2367 // exit so that loss is the only thing that can end STARTUP.
2368 let mut startup_app_limited_samples = 0u64;
2369 let mut startup_non_app_limited_samples = 0u64;
2370
2371 // captured on the STARTUP -> DRAIN edge (DRAIN is only ever entered from
2372 // STARTUP, via check_startup_done). A transient ProbeRTT dip right after
2373 // the first ack bounces back to STARTUP, so we run until DRAIN.
2374 let mut transition: Option<(u64, bool, bool)> = None;
2375
2376 for _ in 0..1_000_000 {
2377 // Application-limited: only send while the (small) app window has
2378 // room, independent of cwnd.
2379 let can_send = inflight + MSS <= APP_WINDOW.min(bbr.window());
2380 let next_ack = flight.front().map(|p| p.ack_ns);
2381
2382 if can_send && next_ack.is_none_or(|ack| now_ns <= ack) {
2383 let send_ns = now_ns;
2384 // enqueue at the FIFO bottleneck, served at BW
2385 let arrival = send_ns + FWD_NS;
2386 let service_start = arrival.max(btl_free_ns);
2387 let finish = service_start + btl_service_ns;
2388 btl_free_ns = finish;
2389 let ack_ns = finish + RET_NS;
2390 let lost = pn % LOSS_PERIOD == LOSS_PERIOD - 1;
2391
2392 bbr.on_packet_sent(at(send_ns), MSS as u16, pn, SpaceKind::Data);
2393 inflight += MSS;
2394 flight.push_back(InFlight {
2395 pn,
2396 send_ns,
2397 ack_ns,
2398 lost,
2399 });
2400 // Emulate MarkConnectionAppLimited. `on_end_acks` only stamps the marker
2401 // when the caller reports app-limited, so drive it here as a genuinely
2402 // app-limited quinn connection would.
2403 bbr.app_limited = Ord::max(bbr.delivered + bbr.inflight, 1);
2404 pn += 1;
2405 } else if let Some(p) = flight.pop_front() {
2406 now_ns = now_ns.max(p.ack_ns);
2407 inflight -= MSS;
2408 if p.lost {
2409 bbr.on_packet_lost(MSS as u16, p.pn, SpaceKind::Data, at(now_ns));
2410 } else {
2411 rtt_est.update(Duration::ZERO, Duration::from_nanos(now_ns - p.send_ns));
2412 bbr.on_ack(
2413 at(now_ns),
2414 at(p.send_ns),
2415 MSS,
2416 p.pn,
2417 SpaceKind::Data,
2418 true,
2419 &rtt_est,
2420 );
2421 if bbr.state == BbrState::Startup {
2422 match bbr.rs.map(|rs| rs.is_app_limited) {
2423 Some(true) => {
2424 startup_app_limited_samples =
2425 startup_app_limited_samples.saturating_add(1)
2426 }
2427 Some(false) => {
2428 startup_non_app_limited_samples =
2429 startup_non_app_limited_samples.saturating_add(1)
2430 }
2431 None => {}
2432 }
2433 }
2434 }
2435 // Sample before on_end_acks clears the rate sample's loss fields.
2436 observed_high_loss |= bbr.is_inflight_too_high();
2437 bbr.on_end_acks(at(now_ns), inflight, true, Some(p.pn), SpaceKind::Data);
2438 if bbr.state == BbrState::Drain {
2439 transition = Some((bbr.full_bw_count, bbr.full_bw_now, bbr.full_bw_reached));
2440 break;
2441 }
2442 } else {
2443 // app window drained and nothing left to ack: advance to next send
2444 now_ns += btl_service_ns;
2445 }
2446 }
2447
2448 // Landed on the STARTUP -> DRAIN edge.
2449 let (full_bw_count, full_bw_now, full_bw_reached) =
2450 transition.expect("BBR never left STARTUP on loss");
2451
2452 // Loss, not the plateau path, drove the exit: the plateau path is
2453 // blocked by app-limited samples, so full_bw_count stayed below the
2454 // 3-round plateau threshold.
2455 assert!(
2456 full_bw_count < MAX_FULL_BW_COUNT,
2457 "expected loss-driven exit, but plateau counter reached {full_bw_count}"
2458 );
2459 assert!(
2460 observed_high_loss,
2461 "BBRCheckStartupHighLoss never observed a too-high loss rate"
2462 );
2463 // The app-limited precondition held: STARTUP delivery-rate samples were
2464 // app-limited, so app-limiting (not a race with loss) kept the plateau
2465 // path from completing. The only packets sent non-app-limited are
2466 // the two warmup packets (pn 0 and 1, before app_limited was first set
2467 // nonzero); the bound tolerates them in case they are acked in STARTUP (in
2468 // practice they land in the transient first-ack ProbeRTT and are not counted).
2469 assert!(
2470 startup_app_limited_samples > 0,
2471 "no app-limited STARTUP samples observed"
2472 );
2473 assert!(
2474 startup_non_app_limited_samples <= 2,
2475 "expected app-limited samples throughout STARTUP, saw \
2476 {startup_non_app_limited_samples} non-app-limited"
2477 );
2478 assert!(
2479 full_bw_reached,
2480 "full_bw_reached should be set on high loss"
2481 );
2482 assert!(full_bw_now, "full_bw_now should be set on high loss");
2483 }
2484
2485 /// A.3: Exiting DRAIN based on inflight.
2486 /// equivalent to: <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#name-exiting-drain-based-on-infl>
2487 ///
2488 /// Drives a flow STARTUP -> DRAIN on the same infinite-buffer simulator as
2489 /// A.1, then keeps running through DRAIN. Entering DRAIN sets `pacing_gain`
2490 /// to `DrainPacingGain` (0.5) while `cwnd_gain` stays at the default, so the
2491 /// window stays wide but pacing sends slower than the link delivers. The
2492 /// queue built during STARTUP drains and `C.inflight` falls.
2493 ///
2494 /// Asserts that on the STARTUP -> DRAIN edge `pacing_gain == DRAIN_PACING_GAIN`
2495 /// (0.5), and that DRAIN ends via the inflight branch of `check_drain_done`
2496 /// (`C.inflight <= BBRInflight(1.0)`, i.e. `get_inflight(1.0)`, the estimated
2497 /// BDP at unit gain) rather than the `drain_start_round + 3` round fallback,
2498 /// transitioning to PROBE_BW (substate DOWN).
2499 #[test]
2500 fn drain_exits_to_probe_bw_on_inflight() {
2501 /// packet size in bytes
2502 const MSS: u64 = 1200;
2503 /// simulated bottleneck bandwidth: 100 Mbit/s in bytes/sec
2504 const BW: f64 = 12_500_000.0;
2505 /// simulated propagation round-trip time (100ms), matching A.1
2506 const RTT_NS: u64 = 100_000_000;
2507
2508 // Drive the production default configuration.
2509 let mut sim = Sim::new(Bbr3Config::default(), MSS, BW, RTT_NS);
2510 assert_eq!(sim.bbr.state, BbrState::Startup);
2511
2512 // pacing_gain observed on the STARTUP -> DRAIN edge (0.5), and the round
2513 // in which DRAIN started, captured once when DRAIN is first entered.
2514 let mut drain_pacing_gain: Option<f64> = None;
2515 let mut drain_start_round: u64 = 0;
2516 // captured on the DRAIN -> PROBE_BW edge: (state, inflight at exit,
2517 // BBRInflight(1.0) == get_inflight(1.0), round_count).
2518 let mut probe_bw_transition: Option<(BbrState, u64, u64, u64)> = None;
2519
2520 sim.run(
2521 1_000_000,
2522 |_| ControlFlow::Continue(()),
2523 |bbr, _now_ns, inflight, _pn| {
2524 // Capture the STARTUP -> DRAIN edge: entering DRAIN sets
2525 // pacing_gain to DrainPacingGain (0.5) and records the round.
2526 if bbr.state == BbrState::Drain && drain_pacing_gain.is_none() {
2527 drain_pacing_gain = Some(bbr.pacing_gain);
2528 drain_start_round = bbr.drain_start_round;
2529 }
2530
2531 // Capture the DRAIN -> PROBE_BW edge. get_inflight(1.0) is
2532 // BBRInflight(1.0), the estimated BDP at unit gain that
2533 // check_drain_done compares C.inflight against.
2534 if matches!(bbr.state, BbrState::ProbeBw(_)) {
2535 let bdp = bbr.get_inflight(1.0);
2536 probe_bw_transition = Some((bbr.state, inflight, bdp, bbr.round_count));
2537 return ControlFlow::Break(());
2538 }
2539 ControlFlow::Continue(())
2540 },
2541 );
2542
2543 // Entered DRAIN with the drain pacing gain (0.5).
2544 let drain_pacing_gain = drain_pacing_gain.expect("BBR never entered DRAIN");
2545 assert_eq!(
2546 drain_pacing_gain, DRAIN_PACING_GAIN,
2547 "DRAIN pacing_gain should be DrainPacingGain (0.5)"
2548 );
2549
2550 // Landed on the DRAIN -> PROBE_BW edge.
2551 let (state, inflight_at_exit, bdp, round_count) =
2552 probe_bw_transition.expect("BBR never left DRAIN");
2553
2554 // DRAIN enters PROBE_BW at DOWN, but the same ack may advance DOWN ->
2555 // CRUISE (the inflight condition that ends DRAIN also opens the
2556 // time-to-cruise gate). Refill can't fire on entry, so DOWN and CRUISE are
2557 // the only legitimate entry substates.
2558 assert!(
2559 matches!(
2560 state,
2561 BbrState::ProbeBw(ProbeBwSubstate::Down | ProbeBwSubstate::Cruise)
2562 ),
2563 "DRAIN should transition to PROBE_BW (DOWN or same-ack CRUISE), got {state:?}"
2564 );
2565
2566 // The inflight branch of check_drain_done drove the exit: C.inflight fell
2567 // to/below BBRInflight(1.0), and it happened within the 3-round window so
2568 // the `drain_start_round + 3` fallback did not fire.
2569 assert!(
2570 inflight_at_exit <= bdp,
2571 "expected inflight-driven DRAIN exit: inflight {inflight_at_exit} > BBRInflight(1.0) {bdp}"
2572 );
2573 assert!(
2574 round_count <= drain_start_round + 3,
2575 "expected inflight-driven exit, but the round fallback fired \
2576 (round_count {round_count} > drain_start_round {drain_start_round} + 3)"
2577 );
2578 }
2579
2580 /// A.4: Exiting DRAIN based on time.
2581 /// equivalent to: <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#name-exiting-drain-based-on-time>
2582 ///
2583 /// Same simulator as A.1/A.3, driven STARTUP -> DRAIN. On the DRAIN edge the
2584 /// link is cut, simulating a STARTUP bandwidth over-estimate: DRAIN keeps
2585 /// pacing off the stale (too-high) `max_bw`, so the queue never drains and
2586 /// `C.inflight` stays above `BBRInflight(1.0)` (`get_inflight(1.0)`) for
2587 /// several rounds. The inflight branch of `check_drain_done` never fires, so
2588 /// the time fallback exits DRAIN once `round_count > drain_start_round + 3`,
2589 /// even though `C.inflight` has not reached the target BDP.
2590 #[test]
2591 fn drain_exits_to_probe_bw_on_time() {
2592 /// packet size in bytes
2593 const MSS: u64 = 1200;
2594 /// STARTUP bottleneck bandwidth: 100 Mbit/s in bytes/sec
2595 const BW: f64 = 12_500_000.0;
2596 /// propagation round-trip time (100ms), matching A.1/A.3
2597 const RTT_NS: u64 = 100_000_000;
2598 const FWD_NS: u64 = RTT_NS / 2;
2599 const RET_NS: u64 = RTT_NS / 2;
2600 /// Fraction of STARTUP bandwidth surviving into DRAIN. `max_bw` holds its STARTUP
2601 /// peak, so DRAIN paces at `DRAIN_PACING_GAIN * BW`; the surviving link must stay
2602 /// below that for the queue to persist (the draft's 10% cut leaves the link
2603 /// outrunning drain pacing). Derived from the constant (0.8x → 0.4 today) so it
2604 /// tracks it.
2605 const DRAIN_BW_FACTOR: f64 = 0.8 * DRAIN_PACING_GAIN;
2606
2607 // Drive the production default configuration.
2608 let mut bbr = Bbr3::new(Arc::new(Bbr3Config::default()), MSS as u16);
2609 assert_eq!(bbr.state, BbrState::Startup);
2610
2611 let base = Instant::now();
2612 let at = |off_ns: u64| base + Duration::from_nanos(off_ns);
2613 let mut rtt_est = RttEstimator::new(Duration::from_nanos(RTT_NS));
2614
2615 struct InFlight {
2616 pn: u64,
2617 send_ns: u64,
2618 ack_ns: u64,
2619 }
2620 let mut flight: VecDeque<InFlight> = VecDeque::new();
2621
2622 let mut now_ns: u64 = 0;
2623 let mut next_send_ns: u64 = 0;
2624 // time at which the bottleneck finishes serving everything queued so far
2625 let mut btl_free_ns: u64 = 0;
2626 let mut inflight: u64 = 0;
2627 let mut pn: u64 = 0;
2628
2629 // bottleneck serialization time per MSS; cut on the DRAIN edge
2630 let mut btl_service_ns: u64 = (MSS as f64 / BW * 1e9).round() as u64;
2631
2632 let mut drain_start_round: Option<u64> = None;
2633 // per-round (round_count, inflight, bdp) while in DRAIN
2634 let mut drain_round_samples: Vec<(u64, u64, u64)> = Vec::new();
2635 let mut last_sampled_round: Option<u64> = None;
2636 // DRAIN -> PROBE_BW edge: (state, inflight, bdp, round)
2637 let mut probe_bw_transition: Option<(BbrState, u64, u64, u64)> = None;
2638
2639 for _ in 0..1_000_000 {
2640 let cwnd = bbr.window();
2641 let can_send = inflight + MSS <= cwnd;
2642 let next_ack = flight.front().map(|p| p.ack_ns);
2643
2644 // The sender always has data; send whenever the window allows and a
2645 // send is due no later than the next ack, otherwise process an ack.
2646 let do_send = can_send && next_ack.is_none_or(|ack| next_send_ns <= ack);
2647
2648 if do_send {
2649 now_ns = now_ns.max(next_send_ns);
2650 let send_ns = now_ns;
2651 // enqueue at the FIFO bottleneck, served at the current rate
2652 let arrival = send_ns + FWD_NS;
2653 let service_start = arrival.max(btl_free_ns);
2654 let finish = service_start + btl_service_ns;
2655 btl_free_ns = finish;
2656 let ack_ns = finish + RET_NS;
2657
2658 bbr.on_packet_sent(at(send_ns), MSS as u16, pn, SpaceKind::Data);
2659 inflight += MSS;
2660 flight.push_back(InFlight {
2661 pn,
2662 send_ns,
2663 ack_ns,
2664 });
2665
2666 let pacing = bbr.pacing_rate.max(1.0);
2667 next_send_ns = send_ns + (MSS as f64 / pacing * 1e9).round() as u64;
2668 pn += 1;
2669 } else if let Some(p) = flight.pop_front() {
2670 now_ns = now_ns.max(p.ack_ns);
2671 inflight -= MSS;
2672 rtt_est.update(Duration::ZERO, Duration::from_nanos(now_ns - p.send_ns));
2673 bbr.on_ack(
2674 at(now_ns),
2675 at(p.send_ns),
2676 MSS,
2677 p.pn,
2678 SpaceKind::Data,
2679 false,
2680 &rtt_est,
2681 );
2682 bbr.on_end_acks(at(now_ns), inflight, false, Some(p.pn), SpaceKind::Data);
2683
2684 // STARTUP -> DRAIN edge: cut the link (over-estimate), record round
2685 if bbr.state == BbrState::Drain && drain_start_round.is_none() {
2686 drain_start_round = Some(bbr.drain_start_round);
2687 btl_service_ns = (MSS as f64 / (BW * DRAIN_BW_FACTOR) * 1e9).round() as u64;
2688 // Re-drain the queued STARTUP backlog at the new (slower) link
2689 // rate. A FIFO bottleneck serves already-queued packets at the
2690 // rate in force when they are served, not the rate at enqueue,
2691 // so the pre-cut fast ack times must be recomputed. Without this
2692 // the backlog drains at the old STARTUP rate and inflight
2693 // collapses to BBRInflight(1.0) within a single round, firing the
2694 // inflight-branch exit and masking the time fallback under test.
2695 let mut serve = now_ns;
2696 for q in flight.iter_mut() {
2697 let service_start = (q.send_ns + FWD_NS).max(serve);
2698 let finish = service_start + btl_service_ns;
2699 serve = finish;
2700 q.ack_ns = finish + RET_NS;
2701 }
2702 btl_free_ns = serve;
2703 }
2704
2705 // sample inflight vs BBRInflight(1.0) once per DRAIN round
2706 if bbr.state == BbrState::Drain && last_sampled_round != Some(bbr.round_count) {
2707 let bdp = bbr.get_inflight(1.0);
2708 drain_round_samples.push((bbr.round_count, inflight, bdp));
2709 last_sampled_round = Some(bbr.round_count);
2710 }
2711
2712 if matches!(bbr.state, BbrState::ProbeBw(_)) {
2713 let bdp = bbr.get_inflight(1.0);
2714 probe_bw_transition = Some((bbr.state, inflight, bdp, bbr.round_count));
2715 break;
2716 }
2717 } else {
2718 panic!("simulation stalled: window full but nothing in flight");
2719 }
2720 }
2721
2722 let drain_start_round = drain_start_round.expect("BBR never entered DRAIN");
2723 let (state, inflight_at_exit, bdp_at_exit, round_count) =
2724 probe_bw_transition.expect("BBR never left DRAIN");
2725
2726 // DRAIN exits to PROBE_BW.
2727 assert!(
2728 matches!(
2729 state,
2730 BbrState::ProbeBw(ProbeBwSubstate::Down | ProbeBwSubstate::Cruise)
2731 ),
2732 "DRAIN should transition to PROBE_BW, got {state:?}"
2733 );
2734
2735 // Time fallback drove the exit: after 3 full DRAIN rounds...
2736 assert!(
2737 round_count > drain_start_round + 3,
2738 "expected time-driven DRAIN exit at round_count > drain_start_round + 3, \
2739 got round_count {round_count}, drain_start_round {drain_start_round}"
2740 );
2741
2742 // ...with C.inflight still above target (inflight branch never fired).
2743 assert!(
2744 inflight_at_exit > bdp_at_exit,
2745 "expected time-driven exit with inflight still above target: \
2746 inflight {inflight_at_exit} <= BBRInflight(1.0) {bdp_at_exit}"
2747 );
2748
2749 // inflight stayed above target every round in DRAIN
2750 assert!(
2751 drain_round_samples.iter().all(|&(_, ifl, bdp)| ifl > bdp),
2752 "C.inflight dropped to/below BBRInflight(1.0) during DRAIN: {drain_round_samples:?}"
2753 );
2754 assert!(
2755 drain_round_samples.len() >= 3,
2756 "expected several round trips observed in DRAIN, got {}",
2757 drain_round_samples.len()
2758 );
2759 }
2760
2761 /// A.5: Exiting PROBE_UP on a bandwidth plateau.
2762 /// equivalent to BBRIsTimeToGoDown:
2763 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.6-6>
2764 ///
2765 /// Same infinite-buffer simulator as A.1/A.3 (constant `BW`, constant `RTT`,
2766 /// no loss), driven STARTUP -> DRAIN -> PROBE_BW until PROBE_BW cycles into
2767 /// its PROBE_UP phase. In PROBE_UP `pacing_gain` is `ProbeBwUpPacingGain`
2768 /// (1.25), so the sender pushes above the link rate and a standing queue
2769 /// forms. `inflight_longterm`/`cwnd` grow to fully utilize that queue with no
2770 /// loss, but the measured delivery rate is pinned at `BW` and plateaus.
2771 ///
2772 /// Asserts that once the delivery rate grows by <25% for 3 consecutive rounds
2773 /// (`check_full_bw_reached` drives `full_bw_count` to `MAX_FULL_BW_COUNT` and
2774 /// sets `full_bw_now`), `BBRIsTimeToGoDown()` (`maybe_go_down`) fires and the
2775 /// flow transitions PROBE_UP -> PROBE_DOWN. On the deciding round-start ack
2776 /// `is_cwnd_limited` has just been cleared by `start_round`, so the "keep
2777 /// probing" branch of `maybe_go_down` is skipped and the plateau drives the
2778 /// exit.
2779 #[test]
2780 fn probe_bw_exits_probe_up_to_probe_down_on_bandwidth_plateau() {
2781 /// packet size in bytes
2782 const MSS: u64 = 1200;
2783 /// simulated bottleneck bandwidth: 100 Mbit/s in bytes/sec
2784 const BW: f64 = 12_500_000.0;
2785 /// simulated propagation round-trip time (100ms), matching A.1/A.3
2786 const RTT_NS: u64 = 100_000_000;
2787
2788 // Drive the production default configuration.
2789 let mut sim = Sim::new(Bbr3Config::default(), MSS, BW, RTT_NS);
2790 assert_eq!(sim.bbr.state, BbrState::Startup);
2791
2792 // Whether the flow has reached the PROBE_UP phase of PROBE_BW; the go-down
2793 // edge we care about is PROBE_UP -> PROBE_DOWN, distinct from the initial
2794 // DRAIN -> PROBE_BW(DOWN) entry.
2795 let mut reached_probe_up = false;
2796 // Captured on the PROBE_UP -> PROBE_DOWN edge: (state, full_bw_count,
2797 // full_bw_now). start_probe_bw_down leaves full_bw_count/full_bw_now
2798 // untouched, so they still read the plateau values right after the edge.
2799 let mut go_down_transition: Option<(BbrState, u64, bool)> = None;
2800
2801 sim.run(
2802 1_000_000,
2803 |_| ControlFlow::Continue(()),
2804 |bbr, _now_ns, _inflight, _pn| {
2805 if bbr.state == BbrState::ProbeBw(ProbeBwSubstate::Up) {
2806 reached_probe_up = true;
2807 }
2808
2809 // Capture the PROBE_UP -> PROBE_DOWN edge (only meaningful once
2810 // PROBE_UP has actually been entered).
2811 if reached_probe_up && bbr.state == BbrState::ProbeBw(ProbeBwSubstate::Down) {
2812 go_down_transition = Some((bbr.state, bbr.full_bw_count, bbr.full_bw_now));
2813 return ControlFlow::Break(());
2814 }
2815 ControlFlow::Continue(())
2816 },
2817 );
2818
2819 // Landed on the PROBE_UP -> PROBE_DOWN edge.
2820 assert!(reached_probe_up, "BBR never reached the PROBE_UP phase");
2821 let (state, full_bw_count, full_bw_now) =
2822 go_down_transition.expect("BBR never left PROBE_UP");
2823
2824 // Plateau drove the exit: 3 consecutive rounds with <25% delivery-rate
2825 // growth set full_bw_now, and BBRIsTimeToGoDown() moved to PROBE_DOWN.
2826 assert_eq!(
2827 full_bw_count, MAX_FULL_BW_COUNT,
2828 "full_bw_count should reach MAX_FULL_BW_COUNT on the plateau"
2829 );
2830 assert!(full_bw_now, "full_bw_now should be set on the plateau");
2831 assert_eq!(
2832 state,
2833 BbrState::ProbeBw(ProbeBwSubstate::Down),
2834 "PROBE_UP should transition to PROBE_DOWN on the plateau"
2835 );
2836 }
2837
2838 /// A.6: Exiting PROBE_UP on loss when application-limited.
2839 /// equivalent to BBRHandleInflightTooHigh:
2840 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.10.2-1>
2841 ///
2842 /// NOTE: the loss-driven PROBE_UP exit runs through `handle_inflight_too_high`
2843 /// (BBRHandleInflightTooHigh), reached per-lost-packet from
2844 /// `process_lost_packet`, NOT through `maybe_go_down` (BBRIsTimeToGoDown).
2845 /// BBRIsTimeToGoDown only inspects the cwnd-limited/plateau signals
2846 /// (`full_bw_now`) and never the loss rate, so high loss cannot trigger it;
2847 /// the plateau path (A.5) is what BBRIsTimeToGoDown covers. This test drives
2848 /// the code path that ends PROBE_UP on excess loss.
2849 ///
2850 /// Same simulator as A.1/A.3/A.5, in two phases:
2851 /// 1. Not app-limited, no loss, full-cwnd (identical to A.5) until the flow cycles STARTUP ->
2852 /// DRAIN -> PROBE_BW -> PROBE_UP.
2853 /// 2. Once PROBE_UP is entered, the app is throttled to a small fixed window (`APP_WINDOW`,
2854 /// well below cwnd) so every fresh sample is app-limited, and 1-in-`LOSS_PERIOD` packets
2855 /// are dropped -> a per-round loss rate (4%) above `BBR.LossThresh` (2%).
2856 ///
2857 /// In PROBE_UP `bw_probe_samples` is true, so each lost packet is fed through
2858 /// `process_lost_packet`; `is_inflight_too_high()` sees the loss exceed
2859 /// `LOSS_THRESH * tx_in_flight` and calls `handle_inflight_too_high`. Because
2860 /// the deciding sample is app-limited, the `!is_app_limited` guard in
2861 /// `handle_inflight_too_high` skips the `inflight_longterm` reduction (an
2862 /// app-limited loss sample is not trusted to lower the long-term model), yet
2863 /// the `state == PROBE_UP` branch still runs `start_probe_bw_down`
2864 /// unconditionally.
2865 ///
2866 /// Asserts that, purely from loss, the flow transitions PROBE_UP ->
2867 /// PROBE_DOWN with the deciding sample flagged app-limited, that the plateau
2868 /// path did NOT drive it (`full_bw_now` stays false, blocked by the
2869 /// app-limited short-circuit in `check_full_bw_reached`), and that
2870 /// `inflight_longterm` is updated appropriately for an app-limited sample,
2871 /// i.e. left unchanged across the transition rather than lowered.
2872 #[test]
2873 fn probe_bw_exits_probe_up_to_probe_down_on_loss_when_app_limited() {
2874 /// packet size in bytes
2875 const MSS: u64 = 1200;
2876 /// simulated bottleneck bandwidth: 100 Mbit/s in bytes/sec
2877 const BW: f64 = 12_500_000.0;
2878 /// simulated propagation round-trip time (100ms), matching A.1/A.3/A.5
2879 const RTT_NS: u64 = 100_000_000;
2880 const FWD_NS: u64 = RTT_NS / 2;
2881 const RET_NS: u64 = RTT_NS / 2;
2882 /// application window once PROBE_UP is reached: bytes the app keeps
2883 /// outstanding. Well below the PROBE_UP cwnd (~2*BDP, ~1000 packets here)
2884 /// so the sender is app-limited (never cwnd-limited), keeping every sample
2885 /// app-limited and isolating the loss path. Matches A.2's window.
2886 const APP_WINDOW: u64 = 200 * MSS;
2887 /// drop 1 in every `LOSS_PERIOD` packets -> 4% loss, above `LOSS_THRESH`
2888 /// (2%), spread evenly so each round trip carries loss over its full
2889 /// sequence range.
2890 const LOSS_PERIOD: u64 = 25;
2891
2892 // bottleneck serialization time for one MSS-sized packet
2893 let btl_service_ns: u64 = (MSS as f64 / BW * 1e9).round() as u64;
2894
2895 // Drive the production default configuration.
2896 let mut bbr = Bbr3::new(Arc::new(Bbr3Config::default()), MSS as u16);
2897 assert_eq!(bbr.state, BbrState::Startup);
2898
2899 let base = Instant::now();
2900 let at = |off_ns: u64| base + Duration::from_nanos(off_ns);
2901 let mut rtt_est = RttEstimator::new(Duration::from_nanos(RTT_NS));
2902
2903 struct InFlight {
2904 pn: u64,
2905 send_ns: u64,
2906 ack_ns: u64,
2907 lost: bool,
2908 }
2909 let mut flight: VecDeque<InFlight> = VecDeque::new();
2910
2911 let mut now_ns: u64 = 0;
2912 let mut next_send_ns: u64 = 0;
2913 // time at which the bottleneck finishes serving everything queued so far
2914 let mut btl_free_ns: u64 = 0;
2915 let mut inflight: u64 = 0;
2916 let mut pn: u64 = 0;
2917
2918 // Phase 2 begins once PROBE_UP is reached: from then the app is limited
2919 // to APP_WINDOW and packets are dropped at the LOSS_PERIOD rate.
2920 let mut app_limited_phase = false;
2921 let mut reached_probe_up = false;
2922 // Captured on the loss-driven PROBE_UP -> PROBE_DOWN edge:
2923 // (inflight_longterm before/after the deciding loss, whether the deciding
2924 // sample was app-limited, full_bw_now at the edge).
2925 let mut go_down: Option<(u64, u64, bool, bool)> = None;
2926
2927 for _ in 0..1_000_000 {
2928 let cwnd = bbr.window();
2929 let window_cap = if app_limited_phase {
2930 APP_WINDOW.min(cwnd)
2931 } else {
2932 cwnd
2933 };
2934 let can_send = inflight + MSS <= window_cap;
2935 let next_ack = flight.front().map(|p| p.ack_ns);
2936
2937 // Send whenever the window allows and a paced send is due no later
2938 // than the next ack; otherwise process an ack. In the app-limited
2939 // phase the small window is the binding limit, not cwnd.
2940 let do_send = can_send && next_ack.is_none_or(|ack| next_send_ns <= ack);
2941
2942 if do_send {
2943 now_ns = now_ns.max(next_send_ns);
2944 let send_ns = now_ns;
2945 // enqueue at the FIFO bottleneck, served at BW
2946 let arrival = send_ns + FWD_NS;
2947 let service_start = arrival.max(btl_free_ns);
2948 let finish = service_start + btl_service_ns;
2949 btl_free_ns = finish;
2950 let ack_ns = finish + RET_NS;
2951 // Only drop packets once app-limited (phase 2); phase 1 is loss
2952 // free so the flow reaches PROBE_UP exactly as in A.5.
2953 let lost = app_limited_phase && pn % LOSS_PERIOD == LOSS_PERIOD - 1;
2954
2955 bbr.on_packet_sent(at(send_ns), MSS as u16, pn, SpaceKind::Data);
2956 inflight += MSS;
2957 flight.push_back(InFlight {
2958 pn,
2959 send_ns,
2960 ack_ns,
2961 lost,
2962 });
2963
2964 if app_limited_phase {
2965 // Emulate MarkConnectionAppLimited so the next packet is stamped
2966 // app-limited at send time. Same shape as A.2.
2967 bbr.app_limited = Ord::max(bbr.delivered + bbr.inflight, 1);
2968 }
2969
2970 // pace the next send at BBR's chosen pacing rate
2971 let pacing = bbr.pacing_rate.max(1.0);
2972 next_send_ns = send_ns + (MSS as f64 / pacing * 1e9).round() as u64;
2973 pn += 1;
2974 } else if let Some(p) = flight.pop_front() {
2975 now_ns = now_ns.max(p.ack_ns);
2976 inflight -= MSS;
2977 if p.lost {
2978 // Capture the loss-driven PROBE_UP -> PROBE_DOWN edge. The
2979 // transition happens inside on_packet_lost (via
2980 // handle_inflight_too_high), never on an ack, so any Up->Down
2981 // move seen here is attributable to this loss.
2982 let before_ilt = bbr.inflight_longterm;
2983 let was_up = bbr.state == BbrState::ProbeBw(ProbeBwSubstate::Up);
2984 bbr.on_packet_lost(MSS as u16, p.pn, SpaceKind::Data, at(now_ns));
2985 if was_up && bbr.state == BbrState::ProbeBw(ProbeBwSubstate::Down) {
2986 let app_lim = bbr.rs.is_some_and(|rs| rs.is_app_limited);
2987 go_down =
2988 Some((before_ilt, bbr.inflight_longterm, app_lim, bbr.full_bw_now));
2989 break;
2990 }
2991 } else {
2992 rtt_est.update(Duration::ZERO, Duration::from_nanos(now_ns - p.send_ns));
2993 bbr.on_ack(
2994 at(now_ns),
2995 at(p.send_ns),
2996 MSS,
2997 p.pn,
2998 SpaceKind::Data,
2999 app_limited_phase,
3000 &rtt_est,
3001 );
3002 bbr.on_end_acks(
3003 at(now_ns),
3004 inflight,
3005 app_limited_phase,
3006 Some(p.pn),
3007 SpaceKind::Data,
3008 );
3009
3010 // Flip to the application-limited, lossy phase the moment
3011 // PROBE_BW is entered, so that by the time the cycle reaches
3012 // PROBE_UP the pipe has already drained to APP_WINDOW and
3013 // every in-flight sample is app-limited (the plateau path
3014 // cannot fire on stale non-app-limited samples).
3015 if !app_limited_phase && matches!(bbr.state, BbrState::ProbeBw(_)) {
3016 app_limited_phase = true;
3017 }
3018 if bbr.state == BbrState::ProbeBw(ProbeBwSubstate::Up) {
3019 reached_probe_up = true;
3020 }
3021 }
3022 } else {
3023 panic!("simulation stalled: window full but nothing in flight");
3024 }
3025 }
3026
3027 // Landed on the loss-driven PROBE_UP -> PROBE_DOWN edge.
3028 assert!(reached_probe_up, "BBR never reached the PROBE_UP phase");
3029 let (before_ilt, after_ilt, app_lim, full_bw_now) =
3030 go_down.expect("BBR never left PROBE_UP on loss");
3031
3032 // The deciding loss sample was application-limited.
3033 assert!(
3034 app_lim,
3035 "deciding loss sample should be application-limited"
3036 );
3037 // Loss, not the plateau path, drove the exit: check_full_bw_reached bails
3038 // on app-limited samples, so full_bw_now (BBRIsTimeToGoDown's plateau
3039 // signal) never got set.
3040 assert!(
3041 !full_bw_now,
3042 "expected loss-driven exit, but the plateau signal full_bw_now was set"
3043 );
3044 // Updated appropriately for an app-limited sample: handle_inflight_too_high
3045 // skips the reduction (the !is_app_limited guard), so inflight_longterm is
3046 // left unchanged across the transition rather than lowered toward
3047 // max(tx_in_flight, target_inflight * BETA). A non-app-limited loss would
3048 // instead set it here.
3049 assert_eq!(
3050 before_ilt, after_ilt,
3051 "inflight_longterm should be unchanged on an app-limited loss exit"
3052 );
3053 }
3054
3055 /// A.7: Never exiting STARTUP when application-limited with no loss.
3056 ///
3057 /// The negative counterpart to A.1 (plateau exit) and A.2 (loss exit): with
3058 /// neither signal present, STARTUP must persist. STARTUP leaves for DRAIN only
3059 /// via `check_startup_done`, which requires `full_bw_reached`
3060 /// (`self.state == Startup && self.full_bw_reached` -> `enter_drain`), plus the
3061 /// high-loss escape in `check_startup_high_loss`. When every round is
3062 /// app-limited, `check_full_bw_reached` bails on the `is_app_limited` guard, so
3063 /// `full_bw_now`/`full_bw_reached` are never set; with zero loss the high-loss
3064 /// escape never fires either. Both STARTUP -> DRAIN triggers are closed.
3065 ///
3066 /// The one state change that still occurs is the scheduled min-RTT refresh:
3067 /// with a constant RTT the min-RTT filter expires every `probe_rtt_interval`
3068 /// (5s) and `check_probe_rtt` moves STARTUP -> PROBE_RTT. This is orthogonal to
3069 /// the app-limited/loss exits A.7 concerns, and because `full_bw_reached` is
3070 /// still false, `exit_probe_rtt` routes back to STARTUP (`enter_startup`)
3071 /// rather than on to PROBE_BW. So the flow oscillates STARTUP <-> PROBE_RTT and
3072 /// never advances past STARTUP, i.e. it stays in STARTUP indefinitely.
3073 ///
3074 /// Same infinite-buffer simulator as A.1/A.2, but the app is limited to a
3075 /// small fixed window (`APP_WINDOW`, well below cwnd) from the first packet so
3076 /// every sample is app-limited, and no packet is ever dropped.
3077 ///
3078 /// Runs long enough (`ROUNDS_TO_OBSERVE`, several `probe_rtt_interval`s) to
3079 /// cover multiple PROBE_RTT interludes, and asserts that: the flow only ever
3080 /// occupies STARTUP or PROBE_RTT (never DRAIN/PROBE_BW), at least one
3081 /// PROBE_RTT interlude was exercised and returned to STARTUP, every observed
3082 /// sample was application-limited, `full_bw_reached`/`full_bw_now` were never
3083 /// set, and `full_bw_count` never reached `MAX_FULL_BW_COUNT`.
3084 #[test]
3085 fn startup_never_exits_when_app_limited_without_loss() {
3086 /// packet size in bytes
3087 const MSS: u64 = 1200;
3088 /// simulated bottleneck bandwidth: 100 Mbit/s in bytes/sec
3089 const BW: f64 = 12_500_000.0;
3090 /// simulated propagation round-trip time (100ms), matching A.1/A.2
3091 const RTT_NS: u64 = 100_000_000;
3092 const FWD_NS: u64 = RTT_NS / 2;
3093 const RET_NS: u64 = RTT_NS / 2;
3094 /// bytes the app keeps outstanding, from the first packet on. Well below
3095 /// cwnd (initial cwnd ~109*MSS, and the app-limited delivery rate keeps
3096 /// cwnd = cwnd_gain*bdp ~= 2.77*APP_WINDOW thereafter) so the sender is
3097 /// app-limited, never cwnd-limited. Comfortably above `min_pipe_cwnd`
3098 /// (4*MSS).
3099 const APP_WINDOW: u64 = 20 * MSS;
3100 /// rounds to observe before declaring "indefinitely". Each round is ~1 RTT
3101 /// (100ms), so this spans ~16s (several `probe_rtt_interval`s of 5s) and
3102 /// covers multiple STARTUP <-> PROBE_RTT oscillations.
3103 const ROUNDS_TO_OBSERVE: u64 = 160;
3104
3105 // bottleneck serialization time for one MSS-sized packet
3106 let btl_service_ns: u64 = (MSS as f64 / BW * 1e9).round() as u64;
3107
3108 // Drive the production default configuration.
3109 let mut bbr = Bbr3::new(Arc::new(Bbr3Config::default()), MSS as u16);
3110 assert_eq!(bbr.state, BbrState::Startup);
3111
3112 let base = Instant::now();
3113 let at = |off_ns: u64| base + Duration::from_nanos(off_ns);
3114 let mut rtt_est = RttEstimator::new(Duration::from_nanos(RTT_NS));
3115
3116 struct InFlight {
3117 pn: u64,
3118 send_ns: u64,
3119 ack_ns: u64,
3120 }
3121 let mut flight: VecDeque<InFlight> = VecDeque::new();
3122
3123 let mut now_ns: u64 = 0;
3124 let mut next_send_ns: u64 = 0;
3125 // time at which the bottleneck finishes serving everything queued so far
3126 let mut btl_free_ns: u64 = 0;
3127 let mut inflight: u64 = 0;
3128 // From 1: `C.app_limited` uses 0 to mean "not app-limited", so packet number 0 could
3129 // never be stamped app-limited and would break the premise below.
3130 let mut pn: u64 = 1;
3131
3132 // Signals gathered over the run; every assertion is checked after the loop.
3133 // The set of states ever visited (must stay within {Startup, ProbeRtt}).
3134 let mut saw_probe_rtt = false;
3135 // A PROBE_RTT interlude was seen and the flow subsequently returned to
3136 // STARTUP: proof exit_probe_rtt routed back to STARTUP, not on to
3137 // PROBE_BW.
3138 let mut returned_to_startup = false;
3139 // Set true the moment any forbidden (past-STARTUP) state is entered.
3140 let mut advanced_past_startup: Option<BbrState> = None;
3141 // Whether every ack we processed carried an application-limited sample.
3142 let mut all_samples_app_limited = true;
3143 let mut samples_seen: u64 = 0;
3144 // Highest full_bw_count / whether full_bw_now/full_bw_reached ever set.
3145 let mut max_full_bw_count: u64 = 0;
3146 let mut full_bw_now_ever = false;
3147 let mut full_bw_reached_ever = false;
3148
3149 for _ in 0..1_000_000 {
3150 let cwnd = bbr.window();
3151 // The app never wants more than APP_WINDOW outstanding.
3152 let window_cap = APP_WINDOW.min(cwnd);
3153 let can_send = inflight + MSS <= window_cap;
3154 let next_ack = flight.front().map(|p| p.ack_ns);
3155
3156 // Send whenever the small app window allows and a paced send is due no
3157 // later than the next ack; otherwise process an ack. The app window is
3158 // always the binding limit, not cwnd.
3159 let do_send = can_send && next_ack.is_none_or(|ack| next_send_ns <= ack);
3160
3161 if do_send {
3162 now_ns = now_ns.max(next_send_ns);
3163 let send_ns = now_ns;
3164 // enqueue at the FIFO bottleneck, served at BW (infinite buffer, no
3165 // loss)
3166 let arrival = send_ns + FWD_NS;
3167 let service_start = arrival.max(btl_free_ns);
3168 let finish = service_start + btl_service_ns;
3169 btl_free_ns = finish;
3170 let ack_ns = finish + RET_NS;
3171
3172 // Emulate MarkConnectionAppLimited before the send, so this packet is
3173 // stamped app-limited at send time: the app is limited from the very
3174 // first packet on. Same shape as A.2/A.6.
3175 bbr.app_limited = Ord::max(bbr.delivered + bbr.inflight, 1);
3176
3177 bbr.on_packet_sent(at(send_ns), MSS as u16, pn, SpaceKind::Data);
3178 inflight += MSS;
3179 flight.push_back(InFlight {
3180 pn,
3181 send_ns,
3182 ack_ns,
3183 });
3184
3185 // pace the next send at BBR's chosen pacing rate
3186 let pacing = bbr.pacing_rate.max(1.0);
3187 next_send_ns = send_ns + (MSS as f64 / pacing * 1e9).round() as u64;
3188 pn += 1;
3189 } else if let Some(p) = flight.pop_front() {
3190 now_ns = now_ns.max(p.ack_ns);
3191 inflight -= MSS;
3192 rtt_est.update(Duration::ZERO, Duration::from_nanos(now_ns - p.send_ns));
3193 bbr.on_ack(
3194 at(now_ns),
3195 at(p.send_ns),
3196 MSS,
3197 p.pn,
3198 SpaceKind::Data,
3199 true,
3200 &rtt_est,
3201 );
3202 bbr.on_end_acks(at(now_ns), inflight, true, Some(p.pn), SpaceKind::Data);
3203
3204 // Record the sample's app-limited flag, but only for STARTUP
3205 // rounds: PROBE_RTT deliberately clamps cwnd to min_pipe_cwnd
3206 // (below APP_WINDOW), so its samples are cwnd-limited by design and
3207 // are not part of the app-limited premise.
3208 if let Some(rs) = bbr.rs
3209 && bbr.state == BbrState::Startup
3210 {
3211 samples_seen += 1;
3212 all_samples_app_limited &= rs.is_app_limited;
3213 }
3214 max_full_bw_count = max_full_bw_count.max(bbr.full_bw_count);
3215 full_bw_now_ever |= bbr.full_bw_now;
3216 full_bw_reached_ever |= bbr.full_bw_reached;
3217
3218 match bbr.state {
3219 BbrState::Startup => {
3220 // Returning to STARTUP after a PROBE_RTT interlude confirms
3221 // exit_probe_rtt routed back here (full_bw_reached false).
3222 if saw_probe_rtt {
3223 returned_to_startup = true;
3224 }
3225 }
3226 BbrState::ProbeRtt => {
3227 saw_probe_rtt = true;
3228 }
3229 // Any of these means STARTUP was actually left for the next
3230 // phase: the failure A.7 guards against.
3231 other => {
3232 advanced_past_startup.get_or_insert(other);
3233 }
3234 }
3235
3236 if advanced_past_startup.is_some() || bbr.round_count >= ROUNDS_TO_OBSERVE {
3237 break;
3238 }
3239 } else {
3240 panic!("simulation stalled: window full but nothing in flight");
3241 }
3242 }
3243
3244 // Never advanced past STARTUP: only STARTUP and the scheduled PROBE_RTT
3245 // min-RTT refresh were ever entered.
3246 assert!(
3247 advanced_past_startup.is_none(),
3248 "BBR left STARTUP for {:?} while application-limited with no loss",
3249 advanced_past_startup,
3250 );
3251 // The run was long enough to actually exercise the oscillation.
3252 assert!(
3253 bbr.round_count >= ROUNDS_TO_OBSERVE,
3254 "simulation ended early ({} rounds) before observing enough rounds",
3255 bbr.round_count,
3256 );
3257 // The min-RTT refresh fired and returned to STARTUP (not on to PROBE_BW),
3258 // proving STARTUP is genuinely re-entered rather than merely never left
3259 // because time stood still.
3260 assert!(saw_probe_rtt, "expected a scheduled PROBE_RTT interlude");
3261 assert!(
3262 returned_to_startup,
3263 "PROBE_RTT should route back to STARTUP while full_bw_reached is false"
3264 );
3265 assert_eq!(
3266 bbr.state,
3267 BbrState::Startup,
3268 "BBR should still be in STARTUP at the end of the run"
3269 );
3270 // The premise held: every sample really was application-limited.
3271 assert!(samples_seen > 0, "no samples were observed");
3272 assert!(
3273 all_samples_app_limited,
3274 "every sample should be application-limited"
3275 );
3276 // The plateau path never armed: check_full_bw_reached short-circuits on
3277 // app-limited samples, so full_bw_reached/full_bw_now stayed false and
3278 // full_bw_count never reached MAX_FULL_BW_COUNT.
3279 assert!(
3280 !full_bw_reached_ever,
3281 "full_bw_reached must never be set on application-limited samples"
3282 );
3283 assert!(
3284 !full_bw_now_ever,
3285 "full_bw_now must never be set on application-limited samples"
3286 );
3287 assert!(
3288 max_full_bw_count < MAX_FULL_BW_COUNT,
3289 "full_bw_count must never reach MAX_FULL_BW_COUNT on application-limited samples (was {max_full_bw_count})"
3290 );
3291 }
3292
3293 /// A.8: Exiting PROBE_DOWN on inflight.
3294 /// equivalent to BBRIsTimeToCruise:
3295 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.6-8>
3296 ///
3297 /// Same infinite-buffer simulator as A.5 (constant `BW`, constant `RTT`, no
3298 /// loss, sender always has data), driven STARTUP -> DRAIN -> PROBE_BW until
3299 /// PROBE_BW has cycled through PROBE_UP and back into a PROBE_DOWN phase.
3300 /// PROBE_UP paces at `ProbeBwUpPacingGain` (1.25) and builds a standing queue,
3301 /// so on the PROBE_UP -> PROBE_DOWN edge `C.inflight` sits well above both
3302 /// cruise thresholds: a genuine queue to drain (distinct from the first
3303 /// DRAIN -> PROBE_DOWN entry, where DRAIN has already emptied the pipe).
3304 ///
3305 /// In PROBE_DOWN `pacing_gain` is `ProbeDownPacingGain` (0.90), so the sender
3306 /// paces below the link rate and the standing queue drains at ~0.1*`BW`. Each
3307 /// ack runs `update_probe_bw_cycle_phase`, whose PROBE_DOWN arm first checks
3308 /// `maybe_enter_probe_bw_refill` (still false: `bw_probe_wait` is 2-3s and
3309 /// `rounds_since_bw_probe` was reset at down entry, so neither the elapsed-time
3310 /// nor the Reno-coexistence trigger fires within the short drain) and then
3311 /// `maybe_update_budget_and_time_to_cruise` (`BBRIsTimeToCruise`). The latter
3312 /// returns true only once `C.inflight` has fallen to <= both
3313 /// `BBRInflightWithHeadroom()` and `BBRInflight(1.0)`, at which point
3314 /// `start_probe_bw_cruise` moves PROBE_DOWN -> PROBE_CRUISE.
3315 ///
3316 /// `update_probe_bw_cycle_phase` reads `self.inflight`, which the previous
3317 /// `on_end_acks` set from the simulator's `inflight` one tick earlier, so the
3318 /// deciding value lags the loop's `inflight` by a single MSS: the same lag
3319 /// A.3's `check_drain_done` relies on. Because the queue only shrinks, the
3320 /// post-transition `C.inflight` (slightly smaller still) is likewise <= both
3321 /// thresholds, so the thresholds recomputed right after the edge witness the
3322 /// same condition that fired it (`start_probe_bw_cruise` touches neither
3323 /// `max_bw`, `min_rtt`, `inflight_longterm`, nor `C.inflight`).
3324 ///
3325 /// Asserts that: the flow entered PROBE_DOWN via PROBE_UP with
3326 /// `pacing_gain == ProbeDownPacingGain` (0.90) and `C.inflight` above at least
3327 /// one cruise threshold (a real queue to drain); the flow then transitioned to
3328 /// PROBE_CRUISE with `pacing_gain` back at `DefaultPacingGain`; and at that
3329 /// edge `C.inflight` was <= both `BBRInflightWithHeadroom()` and
3330 /// `BBRInflight(1.0)`.
3331 #[test]
3332 fn probe_bw_exits_probe_down_to_probe_cruise_on_inflight() {
3333 /// packet size in bytes
3334 const MSS: u64 = 1200;
3335 /// simulated bottleneck bandwidth: 100 Mbit/s in bytes/sec
3336 const BW: f64 = 12_500_000.0;
3337 /// simulated propagation round-trip time (100ms), matching A.1/A.3/A.5
3338 const RTT_NS: u64 = 100_000_000;
3339
3340 // Drive the production default configuration.
3341 let mut sim = Sim::new(Bbr3Config::default(), MSS, BW, RTT_NS);
3342 assert_eq!(sim.bbr.state, BbrState::Startup);
3343
3344 // Whether the flow has reached the PROBE_UP phase; the PROBE_DOWN we care
3345 // about is the one PROBE_UP cycles back into (it carries the standing queue
3346 // PROBE_UP built), not the initial DRAIN -> PROBE_DOWN entry.
3347 let mut reached_probe_up = false;
3348 // Captured on the PROBE_UP -> PROBE_DOWN edge: (pacing_gain, C.inflight,
3349 // BBRInflightWithHeadroom(), BBRInflight(1.0)) at entry, before any drain.
3350 let mut down_entry: Option<(f64, u64, u64, u64)> = None;
3351 // Captured on the PROBE_DOWN -> PROBE_CRUISE edge: (pacing_gain,
3352 // C.inflight, BBRInflightWithHeadroom(), BBRInflight(1.0)).
3353 let mut cruise_edge: Option<(f64, u64, u64, u64)> = None;
3354
3355 sim.run(
3356 1_000_000,
3357 |_| ControlFlow::Continue(()),
3358 |bbr, _now_ns, _inflight, _pn| {
3359 if bbr.state == BbrState::ProbeBw(ProbeBwSubstate::Up) {
3360 reached_probe_up = true;
3361 }
3362
3363 // Capture the PROBE_UP -> PROBE_DOWN entry (only meaningful once
3364 // PROBE_UP has actually been entered, and only the first time).
3365 if reached_probe_up
3366 && down_entry.is_none()
3367 && bbr.state == BbrState::ProbeBw(ProbeBwSubstate::Down)
3368 {
3369 down_entry = Some((
3370 bbr.pacing_gain,
3371 bbr.inflight,
3372 bbr.inflight_with_headroom(),
3373 bbr.get_inflight(1.0),
3374 ));
3375 }
3376
3377 // Capture the PROBE_DOWN -> PROBE_CRUISE edge and stop. Reachable
3378 // only after the down entry has been recorded.
3379 if down_entry.is_some() && bbr.state == BbrState::ProbeBw(ProbeBwSubstate::Cruise) {
3380 cruise_edge = Some((
3381 bbr.pacing_gain,
3382 bbr.inflight,
3383 bbr.inflight_with_headroom(),
3384 bbr.get_inflight(1.0),
3385 ));
3386 return ControlFlow::Break(());
3387 }
3388 ControlFlow::Continue(())
3389 },
3390 );
3391 let bbr = &mut sim.bbr;
3392
3393 // Entered PROBE_DOWN via PROBE_UP, pacing at ProbeDownPacingGain (0.90),
3394 // with a genuine queue still to drain.
3395 assert!(reached_probe_up, "BBR never reached the PROBE_UP phase");
3396 let (down_gain, down_inflight, down_headroom, down_inflight_1) =
3397 down_entry.expect("BBR never entered PROBE_DOWN after PROBE_UP");
3398 assert_eq!(
3399 down_gain, bbr.probe_bw_down_pacing_gain,
3400 "PROBE_DOWN pacing_gain should be ProbeDownPacingGain"
3401 );
3402 assert_eq!(
3403 down_gain, PROBE_BW_DOWN_PACING_GAIN,
3404 "ProbeDownPacingGain should be 0.90"
3405 );
3406 // Standing queue at entry: C.inflight exceeded the binding cruise threshold
3407 // BBRInflight(1.0), so cruise couldn't fire immediately. (Loss-free here, so
3408 // inflight_longterm stays u64::MAX and BBRInflightWithHeadroom() never binds; cf. A.9.)
3409 assert!(
3410 down_inflight > down_inflight_1,
3411 "expected a standing queue at PROBE_DOWN entry (inflight {down_inflight} vs \
3412 inflight(1.0) {down_inflight_1}; headroom {down_headroom} unbounded)"
3413 );
3414
3415 // Drained into PROBE_CRUISE.
3416 let (cruise_gain, cruise_inflight, cruise_headroom, cruise_inflight_1) =
3417 cruise_edge.expect("PROBE_DOWN never transitioned to PROBE_CRUISE");
3418 assert_eq!(
3419 bbr.state,
3420 BbrState::ProbeBw(ProbeBwSubstate::Cruise),
3421 "flow should have transitioned to PROBE_CRUISE"
3422 );
3423 // Cruise resets pacing_gain to DefaultPacingGain.
3424 assert_eq!(
3425 cruise_gain, bbr.default_pacing_gain,
3426 "PROBE_CRUISE pacing_gain should be DefaultPacingGain"
3427 );
3428 // BBRIsTimeToCruise held: C.inflight fell to <= BBRInflight(1.0), the binding
3429 // threshold. The queue only shrinks, so the post-edge recompute still holds.
3430 // (Headroom is unbounded here, inflight_longterm == u64::MAX, so it never binds; cf. A.9.)
3431 assert!(
3432 cruise_inflight <= cruise_inflight_1,
3433 "at PROBE_CRUISE, inflight ({cruise_inflight}) should be <= BBRInflight(1.0) \
3434 ({cruise_inflight_1}); headroom {cruise_headroom} unbounded"
3435 );
3436 }
3437
3438 /// A.9: Exiting PROBE_DOWN after max time, direct to PROBE_REFILL, bypassing
3439 /// PROBE_CRUISE.
3440 /// equivalent to BBRIsTimeToProbeBW:
3441 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.5.3-6>
3442 ///
3443 /// Drives the same single-bottleneck simulator as A.8 through
3444 /// STARTUP -> DRAIN -> PROBE_BW and on into the PROBE_DOWN that PROBE_UP cycles
3445 /// back into (carrying the standing queue PROBE_UP built), breaking on that
3446 /// PROBE_UP -> PROBE_DOWN edge while `C.inflight` is still well above the cruise
3447 /// threshold `BBRInflight(1.0)` (~BDP). That standing queue is exactly the state
3448 /// A.9 constructs by "decreasing available bandwidth 10% on entering PROBE_DOWN so
3449 /// C.inflight never drops below the cruise threshold". (In this loss-free single
3450 /// flow `inflight_longterm` stays at its `u64::MAX` init, so
3451 /// `BBRInflightWithHeadroom()` is unbounded and `BBRInflight(1.0)` is the only
3452 /// binding cruise threshold.)
3453 ///
3454 /// From that state it isolates the elapsed-time exit: it refreshes
3455 /// `probe_rtt_min_stamp` so the periodic min-RTT re-probe (PROBE_RTT, cf. A.10)
3456 /// cannot preempt, advances `now` just past `cycle_stamp + bw_probe_wait`, and
3457 /// drives `update_probe_bw_cycle_phase`. Because that function checks
3458 /// `BBRIsTimeToProbeBW` (the timer) *before* `BBRIsTimeToCruise`, and `C.inflight`
3459 /// is above the cruise threshold, the flow moves in a single cycle step DIRECTLY
3460 /// from PROBE_DOWN to PROBE_REFILL, bypassing PROBE_CRUISE. The Reno-coexistence
3461 /// disjunct is not the trigger: `rounds_since_bw_probe` was reset at down entry and
3462 /// stays below the `min(target_inflight() / SMSS, reno_rounds_bound)` threshold.
3463 ///
3464 /// Asserts that: the flow entered PROBE_DOWN via PROBE_UP with
3465 /// `pacing_gain == ProbeDownPacingGain` (0.90); at the exit `C.inflight` was above
3466 /// `BBRInflight(1.0)` so cruise was not an available exit; the single cycle step
3467 /// took PROBE_DOWN straight to PROBE_REFILL (no intervening PROBE_CRUISE); and
3468 /// PROBE_REFILL reset `pacing_gain` to `DefaultPacingGain`.
3469 #[test]
3470 fn probe_bw_exits_probe_down_to_probe_refill_on_max_time() {
3471 /// packet size in bytes
3472 const MSS: u64 = 1200;
3473 /// simulated bottleneck bandwidth: 100 Mbit/s in bytes/sec
3474 const BW: f64 = 12_500_000.0;
3475 /// simulated propagation round-trip time (100ms), matching A.1/A.3/A.5/A.8
3476 const RTT_NS: u64 = 100_000_000;
3477
3478 // Drive the production default configuration with a fixed probe RNG seed so
3479 // bw_probe_wait is deterministic; its exact value does not matter here since
3480 // the deadline is computed from it below.
3481 let config = Bbr3Config {
3482 probe_rng_seed: Some([6; 16]),
3483 ..Bbr3Config::default()
3484 };
3485 let mut sim = Sim::new(config, MSS, BW, RTT_NS);
3486 assert_eq!(sim.bbr.state, BbrState::Startup);
3487
3488 // Whether the flow has reached the PROBE_UP phase; the PROBE_DOWN we care
3489 // about is the one PROBE_UP cycles back into (it carries the standing queue),
3490 // not the initial DRAIN -> PROBE_DOWN entry.
3491 let mut reached_probe_up = false;
3492 // pacing_gain captured on the PROBE_UP -> PROBE_DOWN edge; also the break
3493 // signal (Some once we have landed in the PROBE_DOWN we care about).
3494 let mut down_gain: Option<f64> = None;
3495 // Deadline after which BBRIsTimeToProbeBW's elapsed-time trigger fires:
3496 // cycle_stamp + bw_probe_wait, in simulator nanoseconds.
3497 let mut probe_deadline_ns: u64 = 0;
3498
3499 sim.run(
3500 1_000_000,
3501 |_| ControlFlow::Continue(()),
3502 |bbr, now_ns, _inflight, _pn| {
3503 if bbr.state == BbrState::ProbeBw(ProbeBwSubstate::Up) {
3504 reached_probe_up = true;
3505 }
3506
3507 // Break on the PROBE_UP -> PROBE_DOWN edge (only once PROBE_UP has been
3508 // entered, so not the initial DRAIN -> PROBE_DOWN entry). At this point
3509 // the queue PROBE_UP built is still standing, so C.inflight is above the
3510 // cruise threshold. Capture the pacing gain and the bw_probe_wait
3511 // deadline stamped by start_probe_bw_down (cycle_stamp == at(now_ns)).
3512 if reached_probe_up && bbr.state == BbrState::ProbeBw(ProbeBwSubstate::Down) {
3513 down_gain = Some(bbr.pacing_gain);
3514 probe_deadline_ns = now_ns + bbr.bw_probe_wait.as_nanos() as u64;
3515 return ControlFlow::Break(());
3516 }
3517 ControlFlow::Continue(())
3518 },
3519 );
3520
3521 // Entered PROBE_DOWN via PROBE_UP, pacing at ProbeDownPacingGain (0.90).
3522 assert!(reached_probe_up, "BBR never reached the PROBE_UP phase");
3523 let down_gain = down_gain.expect("BBR never entered PROBE_DOWN after PROBE_UP");
3524 // Instant for the elapsed-time fire, computed before borrowing bbr below.
3525 let fire_at = sim.at(probe_deadline_ns + 1);
3526 let bbr = &mut sim.bbr;
3527 assert_eq!(
3528 down_gain, bbr.probe_bw_down_pacing_gain,
3529 "PROBE_DOWN pacing_gain should be ProbeDownPacingGain"
3530 );
3531 assert_eq!(
3532 down_gain, PROBE_BW_DOWN_PACING_GAIN,
3533 "ProbeDownPacingGain should be 0.90"
3534 );
3535
3536 // The standing PROBE_UP queue keeps C.inflight above the cruise threshold
3537 // BBRInflight(1.0), so BBRIsTimeToCruise is false and PROBE_CRUISE is not an
3538 // available exit: the state A.9 sets up by decreasing bandwidth 10% on
3539 // PROBE_DOWN entry so inflight never drops below the threshold.
3540 let cruise_threshold = bbr.get_inflight(1.0);
3541 assert!(
3542 bbr.inflight > cruise_threshold,
3543 "C.inflight ({}) should be above the cruise threshold BBRInflight(1.0) ({cruise_threshold})",
3544 bbr.inflight
3545 );
3546 assert_eq!(bbr.state, BbrState::ProbeBw(ProbeBwSubstate::Down));
3547
3548 // The Reno-coexistence disjunct is not the trigger either: this link's TargetInflight()
3549 // is ~1000 packets, so the round bound is T_reno_bound and rounds_since_bw_probe, reset
3550 // at PROBE_DOWN entry, is far below it. The elapsed-time exit is left as the only one.
3551 assert!(
3552 !bbr.is_reno_coexistence_probe_time(),
3553 "rounds_since_bw_probe ({}) should be below the Reno-coexistence bound ({})",
3554 bbr.rounds_since_bw_probe,
3555 bbr.reno_rounds_bound
3556 );
3557
3558 // Isolate the elapsed-time exit. Refresh probe_rtt_min_stamp so the periodic
3559 // min-RTT re-probe (PROBE_RTT, cf. A.10) cannot preempt, then advance now just
3560 // past cycle_stamp + bw_probe_wait (has_elapsed_in_phase is a strict `>`) and
3561 // drive one cycle-phase step.
3562 bbr.probe_rtt_min_stamp = Some(fire_at);
3563 bbr.update_probe_bw_cycle_phase(fire_at);
3564
3565 // A single cycle step took PROBE_DOWN straight to PROBE_REFILL, bypassing
3566 // PROBE_CRUISE, because update_probe_bw_cycle_phase checks BBRIsTimeToProbeBW
3567 // (the timer) before BBRIsTimeToCruise. Refill resets pacing_gain to
3568 // DefaultPacingGain.
3569 assert_eq!(
3570 bbr.state,
3571 BbrState::ProbeBw(ProbeBwSubstate::Refill),
3572 "PROBE_DOWN should transition directly to PROBE_REFILL"
3573 );
3574 assert_eq!(
3575 bbr.pacing_gain, bbr.default_pacing_gain,
3576 "PROBE_REFILL pacing_gain should be DefaultPacingGain"
3577 );
3578 }
3579
3580 /// A.10: Entering and exiting PROBE_RTT.
3581 /// equivalent to BBRCheckProbeRTT / BBRHandleProbeRTT / BBRExitProbeRTT:
3582 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.4.3>
3583 ///
3584 /// Same infinite-buffer FIFO simulator as A.1/A.8 (constant link rate,
3585 /// constant propagation delay), driven STARTUP -> DRAIN -> PROBE_BW and then
3586 /// left to run for more than `BBR.ProbeRTTInterval` (5 s). This is the
3587 /// periodic min-RTT re-probe: once `BBR.probe_rtt_min_delay` has not been
3588 /// lowered for a full ProbeRTTInterval, `update_min_rtt` flips
3589 /// `BBR.probe_rtt_expired` true and `check_probe_rtt` enters PROBE_RTT.
3590 ///
3591 /// Why `probe_rtt_min_delay` stays put for the whole 5 s: with a constant link
3592 /// rate and propagation delay, the smallest achievable RTT is the fixed floor
3593 /// `FWD + service + RET`, hit whenever the bottleneck queue is empty (every
3594 /// PROBE_DOWN drains to it). `update_min_rtt` only refreshes
3595 /// `probe_rtt_min_stamp` on a *strictly* lower sample (`rtt < probe_rtt_min_delay`),
3596 /// so repeated floor-equal samples never move the stamp. The first ack stamps the
3597 /// floor (~t=100 ms via the `probe_rtt_expired`-on-None branch); nothing beats it
3598 /// afterward, so the stamp is frozen and expiry fires ~5 s later, comfortably
3599 /// after STARTUP/DRAIN have handed off to PROBE_BW with `full_bw_reached` true.
3600 ///
3601 /// On entry `check_probe_rtt` calls `enter_probe_rtt` (state -> ProbeRtt,
3602 /// `cwnd_gain` -> `ProbeRTTCwndGain` = 0.5), saves the cwnd, clears
3603 /// `probe_rtt_done_stamp`, and starts a round. `bound_cwnd_for_probe_rtt` then
3604 /// caps `C.cwnd` at `BBRProbeRTTCwnd` (~0.5·BDP), so the sender stalls until
3605 /// `C.inflight` drains below that cap. When it does, `handle_probe_rtt` stamps
3606 /// `probe_rtt_done_stamp = now + ProbeRTTDuration` (200 ms) and starts a fresh
3607 /// round; PROBE_RTT then holds until *both* one packet-timed round has elapsed
3608 /// (`probe_rtt_round_done`) *and* `now > probe_rtt_done_stamp`. `check_probe_rtt_done`
3609 /// then restores the cwnd and calls `exit_probe_rtt`, which (because
3610 /// `full_bw_reached` is true) runs `start_probe_bw_down` then
3611 /// `start_probe_bw_cruise`, landing back in PROBE_BW (Cruise) and lifting the
3612 /// cwnd cap so the sender resumes.
3613 ///
3614 /// Asserts that: the flow reached PROBE_BW with `full_bw_reached` true before any
3615 /// PROBE_RTT entry; PROBE_RTT was entered only after ProbeRTTInterval (5 s) had
3616 /// elapsed, with `cwnd_gain == ProbeRTTCwndGain` (0.5); the exit came at least
3617 /// ProbeRTTDuration (200 ms) *and* one round after `probe_rtt_done_stamp` was
3618 /// armed; and the flow transitioned back to PROBE_BW (Cruise) with
3619 /// `full_bw_reached` still true and resumed sending.
3620 #[test]
3621 fn probe_bw_enters_and_exits_probe_rtt() {
3622 /// packet size in bytes
3623 const MSS: u64 = 1200;
3624 /// simulated bottleneck bandwidth: 100 Mbit/s in bytes/sec
3625 const BW: f64 = 12_500_000.0;
3626 /// simulated propagation round-trip time (100ms), matching A.1/A.8
3627 const RTT_NS: u64 = 100_000_000;
3628
3629 // Held constant (no queue-shaping hacks) so the min RTT floor is deterministic.
3630 let mut sim = Sim::new(Bbr3Config::default(), MSS, BW, RTT_NS);
3631 assert_eq!(sim.bbr.state, BbrState::Startup);
3632
3633 // Whether the flow reached PROBE_BW with full_bw_reached before PROBE_RTT.
3634 let mut reached_probe_bw = false;
3635 // Captured on the first transition into PROBE_RTT: (now_ns, cwnd_gain, round).
3636 let mut probe_rtt_entry: Option<(u64, f64, u64)> = None;
3637 // Captured when probe_rtt_done_stamp is first armed inside PROBE_RTT (i.e.
3638 // once C.inflight has drained below the ProbeRTT cwnd cap): (now_ns, round).
3639 let mut done_armed: Option<(u64, u64)> = None;
3640 // Captured on the PROBE_RTT -> PROBE_BW exit edge:
3641 // (now_ns, state, round, full_bw_reached).
3642 let mut probe_rtt_exit: Option<(u64, BbrState, u64, bool)> = None;
3643 // Whether the exit has happened (shared with the send hook via a Cell so
3644 // both closures can read it without aliasing probe_rtt_exit).
3645 let exited = Cell::new(false);
3646 // Sends observed after the exit, proving the flow resumed transmitting.
3647 let mut sends_after_exit: u64 = 0;
3648
3649 sim.run(
3650 2_000_000,
3651 |_bbr| {
3652 if exited.get() {
3653 sends_after_exit += 1;
3654 }
3655 // Stop a few sends after the exit, enough to prove sending resumed.
3656 if sends_after_exit >= 3 {
3657 return ControlFlow::Break(());
3658 }
3659 ControlFlow::Continue(())
3660 },
3661 |bbr, now_ns, _inflight, _pn| {
3662 // Note the first time PROBE_BW is reached with the bandwidth model
3663 // considered full: the precondition for PROBE_RTT's full_bw exit.
3664 if !reached_probe_bw
3665 && bbr.full_bw_reached
3666 && matches!(bbr.state, BbrState::ProbeBw(_))
3667 {
3668 reached_probe_bw = true;
3669 }
3670
3671 // Entry edge into PROBE_RTT: the periodic min-RTT re-probe we care
3672 // about, i.e. the one after PROBE_BW. (A transient PROBE_RTT also
3673 // fires on the very first ack, because probe_rtt_min_stamp starts
3674 // unset so probe_rtt_expired is true at t=0; that one happens during
3675 // STARTUP, exits straight back to STARTUP via !full_bw_reached, and
3676 // is filtered out by the reached_probe_bw guard.)
3677 if reached_probe_bw && probe_rtt_entry.is_none() && bbr.state == BbrState::ProbeRtt
3678 {
3679 probe_rtt_entry = Some((now_ns, bbr.cwnd_gain, bbr.round_count));
3680 }
3681
3682 // The moment probe_rtt_done_stamp is armed: C.inflight has drained
3683 // below the ProbeRTT cwnd cap and the ProbeRTTDuration clock starts.
3684 if probe_rtt_entry.is_some()
3685 && done_armed.is_none()
3686 && bbr.state == BbrState::ProbeRtt
3687 && bbr.probe_rtt_done_stamp.is_some()
3688 {
3689 done_armed = Some((now_ns, bbr.round_count));
3690 }
3691
3692 // Exit edge: PROBE_RTT -> PROBE_BW.
3693 if probe_rtt_entry.is_some()
3694 && probe_rtt_exit.is_none()
3695 && matches!(bbr.state, BbrState::ProbeBw(_))
3696 {
3697 probe_rtt_exit =
3698 Some((now_ns, bbr.state, bbr.round_count, bbr.full_bw_reached));
3699 exited.set(true);
3700 }
3701 ControlFlow::Continue(())
3702 },
3703 );
3704 let bbr = &sim.bbr;
3705
3706 // Reached PROBE_BW with a full bandwidth model before probing RTT.
3707 assert!(
3708 reached_probe_bw,
3709 "BBR never reached PROBE_BW with full_bw_reached before PROBE_RTT"
3710 );
3711
3712 // Entered PROBE_RTT, and only after ProbeRTTInterval (5 s) elapsed with
3713 // probe_rtt_min_delay never lowered.
3714 let (entry_ns, entry_cwnd_gain, entry_round) =
3715 probe_rtt_entry.expect("BBR never entered PROBE_RTT");
3716 assert!(
3717 entry_ns >= PROBE_RTT_INTERVAL_SEC * 1_000_000_000,
3718 "PROBE_RTT entered before ProbeRTTInterval elapsed \
3719 (entry {entry_ns} ns vs interval {}s)",
3720 PROBE_RTT_INTERVAL_SEC
3721 );
3722 // cwnd_gain was set to ProbeRTTCwndGain (0.5) on entry.
3723 assert_eq!(
3724 entry_cwnd_gain, bbr.probe_rtt_cwnd_gain,
3725 "PROBE_RTT cwnd_gain should be ProbeRTTCwndGain"
3726 );
3727 assert_eq!(
3728 entry_cwnd_gain, PROBE_RTT_CWND_GAIN,
3729 "ProbeRTTCwndGain should be 0.5"
3730 );
3731
3732 // The ProbeRTTDuration clock was armed once inflight drained below the cap.
3733 let (done_ns, done_round) = done_armed.expect("PROBE_RTT never armed probe_rtt_done_stamp");
3734
3735 // Exited PROBE_RTT back to PROBE_BW (Cruise), full_bw_reached still true.
3736 let (exit_ns, exit_state, exit_round, exit_full_bw) =
3737 probe_rtt_exit.expect("BBR never exited PROBE_RTT");
3738 assert_eq!(
3739 exit_state,
3740 BbrState::ProbeBw(ProbeBwSubstate::Cruise),
3741 "PROBE_RTT should exit to PROBE_BW (Cruise) when full_bw_reached"
3742 );
3743 assert!(
3744 exit_full_bw,
3745 "full_bw_reached should remain true across the PROBE_RTT exit"
3746 );
3747
3748 // Held for at least ProbeRTTDuration (200 ms) after the clock was armed
3749 // (check_probe_rtt_done uses a strict `now > probe_rtt_done_stamp`).
3750 assert!(
3751 exit_ns - done_ns >= PROBE_RTT_DURATION_MS * 1_000_000,
3752 "PROBE_RTT exited before ProbeRTTDuration elapsed \
3753 (held {} ns vs duration {} ms)",
3754 exit_ns - done_ns,
3755 PROBE_RTT_DURATION_MS
3756 );
3757 // ...and for at least one packet-timed round after arming.
3758 assert!(
3759 exit_round > done_round,
3760 "PROBE_RTT should hold at least one round after arming \
3761 (arm round {done_round} vs exit round {exit_round})"
3762 );
3763 // Sanity: entry preceded the exit.
3764 assert!(exit_round >= entry_round && exit_ns > entry_ns);
3765
3766 // The flow resumed sending after exiting PROBE_RTT (cwnd cap lifted).
3767 assert!(
3768 sends_after_exit > 0,
3769 "flow did not resume sending after exiting PROBE_RTT"
3770 );
3771 }
3772
3773 /// A.11: Skipping PROBE_RTT due to application-limited (restart-from-idle) sending.
3774 /// equivalent to BBRHandleRestartFromIdle / BBRCheckProbeRTT:
3775 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.4.1>
3776 ///
3777 /// Same infinite-buffer FIFO simulator as A.10 (constant link rate, constant
3778 /// propagation delay), driven STARTUP -> DRAIN -> PROBE_BW. Then the
3779 /// application stops offering data: every in-flight packet is acked with no new
3780 /// sends, so `C.inflight` drains to 0 and the connection goes idle. Virtual time is
3781 /// then advanced past `BBR.ProbeRTTInterval` (5 s) with nothing in flight, long
3782 /// enough that the periodic min-RTT re-probe is due (`probe_rtt_expired` becomes
3783 /// true on the next `update_min_rtt`), exactly the condition that drove the PROBE_RTT
3784 /// entry in A.10.
3785 ///
3786 /// The difference here is the idle gap. When the application resumes and sends the
3787 /// first packet, `on_packet_sent` calls `handle_restart_from_idle`: because
3788 /// `C.inflight` was 0 and the connection is application-limited (`C.app_limited != 0`),
3789 /// it sets `BBR.idle_restart = true`. On the resulting ack, `check_probe_rtt` sees
3790 /// `probe_rtt_expired` true but refuses to `enter_probe_rtt` because of the
3791 /// `!idle_restart` guard: an idle period is itself deemed a sufficient drain of the
3792 /// bottleneck queue, so a formal PROBE_RTT is unnecessary. `idle_restart` is then
3793 /// cleared once a delivering ack arrives, and the refreshed `probe_rtt_min_stamp`
3794 /// keeps expiry from re-firing on the following acks.
3795 ///
3796 /// Asserts that: the flow reached PROBE_BW with `full_bw_reached` and then drained to
3797 /// idle (`C.inflight == 0`) while still in PROBE_BW; more than ProbeRTTInterval (5 s)
3798 /// elapsed during the idle gap; the first send after idle set `BBR.idle_restart`; and
3799 /// although the min-RTT re-probe was due at that point (`probe_rtt_expired` true), the
3800 /// connection never entered PROBE_RTT over the subsequent rounds.
3801 #[test]
3802 fn probe_bw_skips_probe_rtt_on_restart_from_idle() {
3803 /// packet size in bytes
3804 const MSS: u64 = 1200;
3805 /// simulated bottleneck bandwidth: 100 Mbit/s in bytes/sec
3806 const BW: f64 = 12_500_000.0;
3807 /// simulated propagation round-trip time (100ms), matching A.10
3808 const RTT_NS: u64 = 100_000_000;
3809 const FWD_NS: u64 = RTT_NS / 2;
3810 const RET_NS: u64 = RTT_NS / 2;
3811
3812 let btl_service_ns: u64 = (MSS as f64 / BW * 1e9).round() as u64;
3813
3814 let mut bbr = Bbr3::new(Arc::new(Bbr3Config::default()), MSS as u16);
3815 assert_eq!(bbr.state, BbrState::Startup);
3816
3817 let base = Instant::now();
3818 let at = |off_ns: u64| base + Duration::from_nanos(off_ns);
3819 let mut rtt_est = RttEstimator::new(Duration::from_nanos(RTT_NS));
3820
3821 struct InFlight {
3822 pn: u64,
3823 send_ns: u64,
3824 ack_ns: u64,
3825 }
3826 let mut flight: VecDeque<InFlight> = VecDeque::new();
3827
3828 let mut now_ns: u64 = 0;
3829 let mut next_send_ns: u64 = 0;
3830 let mut btl_free_ns: u64 = 0;
3831 let mut inflight: u64 = 0;
3832 let mut pn: u64 = 0;
3833
3834 // Phase 1: drive STARTUP -> DRAIN -> PROBE_BW, stopping as soon as the flow is in
3835 // PROBE_BW with the bandwidth model considered full. The application then stops.
3836 let mut reached_probe_bw = false;
3837 for _ in 0..2_000_000 {
3838 let cwnd = bbr.window();
3839 let can_send = inflight + MSS <= cwnd;
3840 let next_ack = flight.front().map(|p| p.ack_ns);
3841 let do_send = can_send && next_ack.is_none_or(|ack| next_send_ns <= ack);
3842
3843 if do_send {
3844 now_ns = now_ns.max(next_send_ns);
3845 let send_ns = now_ns;
3846 let arrival = send_ns + FWD_NS;
3847 let service_start = arrival.max(btl_free_ns);
3848 let finish = service_start + btl_service_ns;
3849 btl_free_ns = finish;
3850 let ack_ns = finish + RET_NS;
3851
3852 bbr.on_packet_sent(at(send_ns), MSS as u16, pn, SpaceKind::Data);
3853 inflight += MSS;
3854 flight.push_back(InFlight {
3855 pn,
3856 send_ns,
3857 ack_ns,
3858 });
3859
3860 let pacing = bbr.pacing_rate.max(1.0);
3861 next_send_ns = send_ns + (MSS as f64 / pacing * 1e9).round() as u64;
3862 pn += 1;
3863 } else if let Some(p) = flight.pop_front() {
3864 now_ns = now_ns.max(p.ack_ns);
3865 inflight -= MSS;
3866 rtt_est.update(Duration::ZERO, Duration::from_nanos(now_ns - p.send_ns));
3867 bbr.on_ack(
3868 at(now_ns),
3869 at(p.send_ns),
3870 MSS,
3871 p.pn,
3872 SpaceKind::Data,
3873 false,
3874 &rtt_est,
3875 );
3876 bbr.on_end_acks(at(now_ns), inflight, false, Some(p.pn), SpaceKind::Data);
3877
3878 if bbr.full_bw_reached && matches!(bbr.state, BbrState::ProbeBw(_)) {
3879 reached_probe_bw = true;
3880 break;
3881 }
3882 } else {
3883 panic!("simulation stalled: window full but nothing in flight");
3884 }
3885 }
3886 assert!(
3887 reached_probe_bw,
3888 "BBR never reached PROBE_BW with full_bw_reached"
3889 );
3890
3891 // Phase 2: the application pauses. Ack every remaining in-flight packet without
3892 // sending anything new, so the connection goes fully idle (C.inflight == 0).
3893 while let Some(p) = flight.pop_front() {
3894 now_ns = now_ns.max(p.ack_ns);
3895 inflight -= MSS;
3896 rtt_est.update(Duration::ZERO, Duration::from_nanos(now_ns - p.send_ns));
3897 bbr.on_ack(
3898 at(now_ns),
3899 at(p.send_ns),
3900 MSS,
3901 p.pn,
3902 SpaceKind::Data,
3903 false,
3904 &rtt_est,
3905 );
3906 bbr.on_end_acks(at(now_ns), inflight, false, Some(p.pn), SpaceKind::Data);
3907 }
3908 assert_eq!(
3909 inflight, 0,
3910 "harness should have drained all in-flight data"
3911 );
3912 assert_eq!(
3913 bbr.inflight, 0,
3914 "C.inflight should be 0 once the app goes idle"
3915 );
3916 assert!(
3917 matches!(bbr.state, BbrState::ProbeBw(_)),
3918 "flow should still be in PROBE_BW when it goes idle, got {:?}",
3919 bbr.state
3920 );
3921 let idle_start_ns = now_ns;
3922
3923 // Phase 3: stay idle past BBR.ProbeRTTInterval (5 s), so the periodic min-RTT
3924 // re-probe becomes due. Nothing is in flight, so no BBR callbacks fire; time just
3925 // advances. With no data to send during the gap, the connection is app-limited.
3926 now_ns = idle_start_ns + PROBE_RTT_INTERVAL_SEC * 1_000_000_000 + 2 * RTT_NS;
3927 bbr.app_limited = Ord::max(bbr.delivered + bbr.inflight, 1);
3928 assert!(
3929 now_ns - idle_start_ns >= PROBE_RTT_INTERVAL_SEC * 1_000_000_000,
3930 "idle gap must exceed ProbeRTTInterval (5 s)"
3931 );
3932
3933 // Phase 4: the application resumes and sends one packet. handle_restart_from_idle
3934 // runs on this transmit and must set BBR.idle_restart because C.inflight was 0 and
3935 // the connection is app-limited.
3936 let resume_pn = pn;
3937 {
3938 let send_ns = now_ns;
3939 let arrival = send_ns + FWD_NS;
3940 let service_start = arrival.max(btl_free_ns);
3941 let finish = service_start + btl_service_ns;
3942 btl_free_ns = finish;
3943 let ack_ns = finish + RET_NS;
3944
3945 bbr.on_packet_sent(at(send_ns), MSS as u16, pn, SpaceKind::Data);
3946 inflight += MSS;
3947 flight.push_back(InFlight {
3948 pn,
3949 send_ns,
3950 ack_ns,
3951 });
3952 next_send_ns = now_ns;
3953 pn += 1;
3954 }
3955 assert!(
3956 bbr.idle_restart,
3957 "handle_restart_from_idle should set BBR.idle_restart on the first send \
3958 after an idle, app-limited period"
3959 );
3960
3961 // Phase 5: keep sending/acking. On the resume packet's ack the min-RTT re-probe is
3962 // due (probe_rtt_expired true), yet PROBE_RTT must be skipped because idle_restart
3963 // is set. Run enough rounds to prove it stays skipped.
3964 let mut probe_rtt_expired_at_resume: Option<bool> = None;
3965 let mut entered_probe_rtt = false;
3966 let mut acks_after_resume: u64 = 0;
3967 for _ in 0..2_000_000 {
3968 let cwnd = bbr.window();
3969 let can_send = inflight + MSS <= cwnd;
3970 let next_ack = flight.front().map(|p| p.ack_ns);
3971 let do_send = can_send && next_ack.is_none_or(|ack| next_send_ns <= ack);
3972
3973 if do_send {
3974 now_ns = now_ns.max(next_send_ns);
3975 let send_ns = now_ns;
3976 let arrival = send_ns + FWD_NS;
3977 let service_start = arrival.max(btl_free_ns);
3978 let finish = service_start + btl_service_ns;
3979 btl_free_ns = finish;
3980 let ack_ns = finish + RET_NS;
3981
3982 bbr.on_packet_sent(at(send_ns), MSS as u16, pn, SpaceKind::Data);
3983 inflight += MSS;
3984 flight.push_back(InFlight {
3985 pn,
3986 send_ns,
3987 ack_ns,
3988 });
3989
3990 let pacing = bbr.pacing_rate.max(1.0);
3991 next_send_ns = send_ns + (MSS as f64 / pacing * 1e9).round() as u64;
3992 pn += 1;
3993 } else if let Some(p) = flight.pop_front() {
3994 now_ns = now_ns.max(p.ack_ns);
3995 inflight -= MSS;
3996 rtt_est.update(Duration::ZERO, Duration::from_nanos(now_ns - p.send_ns));
3997 bbr.on_ack(
3998 at(now_ns),
3999 at(p.send_ns),
4000 MSS,
4001 p.pn,
4002 SpaceKind::Data,
4003 false,
4004 &rtt_est,
4005 );
4006 bbr.on_end_acks(at(now_ns), inflight, false, Some(p.pn), SpaceKind::Data);
4007
4008 if p.pn == resume_pn {
4009 // Snapshot right after the restarting packet's ack: the re-probe was
4010 // due (probe_rtt_expired) so only idle_restart could suppress entry.
4011 probe_rtt_expired_at_resume = Some(bbr.probe_rtt_expired);
4012 }
4013 acks_after_resume += 1;
4014
4015 if bbr.state == BbrState::ProbeRtt {
4016 entered_probe_rtt = true;
4017 break;
4018 }
4019 if acks_after_resume >= 40 {
4020 break;
4021 }
4022 } else {
4023 panic!("simulation stalled: window full but nothing in flight");
4024 }
4025 }
4026
4027 // The min-RTT re-probe was due at resume (the same trigger that entered
4028 // PROBE_RTT in A.10), so idle_restart is the only thing that could suppress entry.
4029 assert_eq!(
4030 probe_rtt_expired_at_resume,
4031 Some(true),
4032 "probe_rtt_expired should be true at resume (5 s elapsed), making the \
4033 PROBE_RTT skip attributable to idle_restart"
4034 );
4035 // The connection skipped PROBE_RTT: idleness was a sufficient queue drain.
4036 assert!(
4037 !entered_probe_rtt,
4038 "connection must skip PROBE_RTT after restarting from idle (idle_restart set)"
4039 );
4040 }
4041
4042 /// A.12: Achieving expected STARTUP bandwidth on a link with ACK aggregation.
4043 /// equivalent to BBRUpdateACKAggregation / BBRUpdateMaxInflight:
4044 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.9>
4045 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.4.2>
4046 ///
4047 /// Same constant-rate FIFO simulator as A.1, with one change: the path
4048 /// aggregates ACKs. Instead of each served packet being acked as soon as it clears
4049 /// the bottleneck (+propagation), completed packets are held and released in bursts on a
4050 /// fixed `AGG_NS` epoch grid, modelling the L2 batching / radio-DRX behaviour of a
4051 /// cellular link, where the receiver's ACKs for many packets arrive bunched at one
4052 /// instant. Every packet whose service finishes inside the same `AGG_NS` window shares a
4053 /// single delivery instant, so the sender sees one ACK "event" cover many packets. The
4054 /// bottleneck still drains at exactly `BW`, so the *average* delivery rate is unchanged;
4055 /// only its arrival timing is bursty.
4056 ///
4057 /// Each aggregation burst is fed to BBR exactly as the connection layer would (cf.
4058 /// `Connection::on_packet_acked` looping over the newly-acked packets, then a single
4059 /// `on_end_acks`): one `on_ack` per packet in the burst, all stamped with the same ack
4060 /// instant `now`, followed by one `on_end_acks` carrying the burst's largest packet
4061 /// number.
4062 ///
4063 /// This exercises two mechanisms the draft calls for on aggregating paths:
4064 /// 1. The delivery-rate sampler must not be fooled by the burst. A burst delivers `K*MSS` over
4065 /// a near-zero ACK-arrival span, but the underlying packets were *sent* over a much longer
4066 /// span; because `RS.interval = max(send_elapsed, ack_elapsed)` uses the (longer) send
4067 /// span, the sampled rate is capped at the send rate and `BBR.max_bw` tracks the true
4068 /// bottleneck `BW` rather than the instantaneous burst rate. Asserted via `max_bw` staying
4069 /// within a few percent of `BW`.
4070 /// 2. `BBRUpdateACKAggregation` must estimate the excess data delivered by aggregation
4071 /// (`BBR.extra_acked`) and `BBRUpdateMaxInflight` must add it to the cwnd budget, so that
4072 /// inflight does not throttle throughput on the bursty path. With STARTUP's `cwnd_gain` of
4073 /// 2, `max_inflight = 2*BDP + extra_acked`, so a positive `extra_acked` drives `C.cwnd`
4074 /// above `2 * BDP`. Asserted directly.
4075 ///
4076 /// Despite the aggregation, STARTUP must still ramp (pacing_gain 2.773 doubles the send
4077 /// rate each round) and discover the full bottleneck bandwidth, exiting to DRAIN on the
4078 /// delivery-rate plateau with `full_bw_reached` and `max_bw` ~= `BW`, exactly as A.1.
4079 #[test]
4080 fn startup_reaches_full_bw_with_ack_aggregation() {
4081 /// packet size in bytes
4082 const MSS: u64 = 1200;
4083 /// simulated bottleneck bandwidth: 100 Mbit/s in bytes/sec
4084 const BW: f64 = 12_500_000.0;
4085 /// simulated propagation round-trip time (100ms), matching A.1
4086 const RTT_NS: u64 = 100_000_000;
4087 const FWD_NS: u64 = RTT_NS / 2;
4088 const RET_NS: u64 = RTT_NS / 2;
4089 /// ACK-aggregation epoch. All packets whose bottleneck service finishes within the
4090 /// same `AGG_NS` window have their ACKs released together. 1ms is ~10 MSS-times at
4091 /// BW (bursty, cellular-like) yet well under the 100ms RTT, so bursts stay within a
4092 /// round trip.
4093 const AGG_NS: u64 = 1_000_000;
4094
4095 // bottleneck serialization time for one MSS-sized packet
4096 let btl_service_ns: u64 = (MSS as f64 / BW * 1e9).round() as u64;
4097
4098 // Drive the production default configuration.
4099 let mut bbr = Bbr3::new(Arc::new(Bbr3Config::default()), MSS as u16);
4100 assert_eq!(bbr.state, BbrState::Startup);
4101
4102 let base = Instant::now();
4103 let at = |off_ns: u64| base + Duration::from_nanos(off_ns);
4104 let mut rtt_est = RttEstimator::new(Duration::from_nanos(RTT_NS));
4105
4106 struct InFlight {
4107 pn: u64,
4108 send_ns: u64,
4109 ack_ns: u64,
4110 }
4111 let mut flight: VecDeque<InFlight> = VecDeque::new();
4112
4113 let mut now_ns: u64 = 0;
4114 let mut next_send_ns: u64 = 0;
4115 // time at which the bottleneck finishes serving everything queued so far
4116 let mut btl_free_ns: u64 = 0;
4117 let mut inflight: u64 = 0;
4118 let mut pn: u64 = 0;
4119
4120 // Signals gathered while still in STARTUP; asserted after the loop.
4121 // Largest ACK burst (packets acked at one instant) actually produced; proves the
4122 // path aggregated rather than degenerating to one-packet acks.
4123 let mut max_burst: usize = 0;
4124 // Highest BBR.extra_acked observed in STARTUP (aggregation estimate).
4125 let mut max_extra_acked: u64 = 0;
4126 // A STARTUP burst where extra_acked>0 pushed C.cwnd strictly above 2*BDP.
4127 let mut cwnd_exceeded_2bdp = false;
4128 // Peak max_bw seen in STARTUP; must stay ~BW, proving the burst didn't inflate the
4129 // delivery-rate estimate above the send rate.
4130 let mut peak_startup_max_bw: f64 = 0.0;
4131 // Captured on the STARTUP -> DRAIN edge, as in A.1.
4132 let mut transition: Option<(bool, f64)> = None;
4133
4134 for _ in 0..2_000_000 {
4135 let cwnd = bbr.window();
4136 let can_send = inflight + MSS <= cwnd;
4137 let next_ack = flight.front().map(|p| p.ack_ns);
4138 let do_send = can_send && next_ack.is_none_or(|ack| next_send_ns <= ack);
4139
4140 if do_send {
4141 now_ns = now_ns.max(next_send_ns);
4142 let send_ns = now_ns;
4143 // enqueue at the FIFO bottleneck, served at BW
4144 let arrival = send_ns + FWD_NS;
4145 let service_start = arrival.max(btl_free_ns);
4146 let finish = service_start + btl_service_ns;
4147 btl_free_ns = finish;
4148 // ACK aggregation: hold the completed packet until the next AGG_NS epoch
4149 // boundary at/after `finish`, so packets finishing in the same window are
4150 // released to the sender together (identical ack_ns == one ACK event).
4151 let ack_ns = finish.div_ceil(AGG_NS) * AGG_NS + RET_NS;
4152
4153 bbr.on_packet_sent(at(send_ns), MSS as u16, pn, SpaceKind::Data);
4154 inflight += MSS;
4155 flight.push_back(InFlight {
4156 pn,
4157 send_ns,
4158 ack_ns,
4159 });
4160
4161 // pace the next send at BBR's chosen pacing rate
4162 let pacing = bbr.pacing_rate.max(1.0);
4163 next_send_ns = send_ns + (MSS as f64 / pacing * 1e9).round() as u64;
4164 pn += 1;
4165 } else if let Some(first) = flight.pop_front() {
4166 // Gather the whole aggregation burst: every in-flight packet sharing this
4167 // release instant is acknowledged by the same ACK event.
4168 let burst_ack_ns = first.ack_ns;
4169 let mut burst = vec![first];
4170 while flight.front().is_some_and(|p| p.ack_ns == burst_ack_ns) {
4171 burst.push(flight.pop_front().unwrap());
4172 }
4173 now_ns = now_ns.max(burst_ack_ns);
4174 max_burst = max_burst.max(burst.len());
4175
4176 // Feed the burst as the connection layer does: one on_ack per packet (same
4177 // ack instant), then a single on_end_acks with the largest pn in the burst.
4178 let largest_pn = burst.last().map(|p| p.pn).unwrap();
4179 for p in &burst {
4180 inflight -= MSS;
4181 rtt_est.update(Duration::ZERO, Duration::from_nanos(now_ns - p.send_ns));
4182 bbr.on_ack(
4183 at(now_ns),
4184 at(p.send_ns),
4185 MSS,
4186 p.pn,
4187 SpaceKind::Data,
4188 false,
4189 &rtt_est,
4190 );
4191 }
4192 bbr.on_end_acks(
4193 at(now_ns),
4194 inflight,
4195 false,
4196 Some(largest_pn),
4197 SpaceKind::Data,
4198 );
4199
4200 if bbr.state == BbrState::Startup {
4201 max_extra_acked = max_extra_acked.max(bbr.extra_acked);
4202 peak_startup_max_bw = peak_startup_max_bw.max(bbr.max_bw);
4203 // bbr.bdp is refreshed to max_bw*min_rtt on every set_cwnd; once the
4204 // aggregation estimate is positive it should lift cwnd past 2*BDP.
4205 if bbr.extra_acked > 0 && bbr.bdp > 0 && bbr.cwnd > 2 * bbr.bdp {
4206 cwnd_exceeded_2bdp = true;
4207 }
4208 }
4209 if bbr.state == BbrState::Drain {
4210 transition = Some((bbr.full_bw_reached, bbr.max_bw));
4211 break;
4212 }
4213 } else {
4214 panic!("simulation stalled: window full but nothing in flight");
4215 }
4216 }
4217
4218 // The path genuinely aggregated: at least one ACK event covered many packets.
4219 assert!(
4220 max_burst > 1,
4221 "harness should have produced aggregated ACK bursts, max burst was {max_burst}"
4222 );
4223 // BBRUpdateACKAggregation estimated a positive excess from the bursts.
4224 assert!(
4225 max_extra_acked > 0,
4226 "extra_acked should be positive on an aggregating path"
4227 );
4228 // The extra_acked budget lifted the cwnd above 2*BDP (BBRUpdateMaxInflight adds
4229 // extra_acked on top of cwnd_gain*BDP, cwnd_gain being 2 in STARTUP).
4230 assert!(
4231 cwnd_exceeded_2bdp,
4232 "C.cwnd should exceed 2*BDP while extra_acked>0 in STARTUP (max_extra_acked {max_extra_acked})"
4233 );
4234 // The delivery-rate sampler was not fooled by the bursts: interval =
4235 // max(send_elapsed, ack_elapsed) caps the sample at the send rate, so max_bw never
4236 // ran far above the true bottleneck BW during STARTUP.
4237 let startup_bw_err = (peak_startup_max_bw - BW).abs() / BW;
4238 assert!(
4239 peak_startup_max_bw <= BW * 1.05,
4240 "max_bw {peak_startup_max_bw} inflated above send rate {BW} by bursts (rel {startup_bw_err})"
4241 );
4242
4243 // STARTUP still ramped and discovered the full bottleneck bandwidth: it exited to
4244 // DRAIN on the plateau with full_bw_reached and max_bw ~= BW, as in A.1.
4245 let (full_bw_reached, max_bw) = transition.expect("BBR never left STARTUP");
4246 assert!(
4247 full_bw_reached,
4248 "full_bw_reached should be set on the bandwidth plateau"
4249 );
4250 let err = (max_bw - BW).abs() / BW;
4251 assert!(
4252 err < 0.05,
4253 "max_bw {max_bw} not within 5% of simulated {BW} (rel err {err})"
4254 );
4255 }
4256
4257 /// A.13: Achieving expected cruise bandwidth on a link with ACK aggregation.
4258 /// equivalent to BBRUpdateACKAggregation / BBRUpdateMaxInflight:
4259 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.9>
4260 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.4.2>
4261 ///
4262 /// The aggregation-in-STARTUP counterpart is A.12; this covers the same excess-data
4263 /// mechanism in the steady-state PROBE_BW cruise phase, where the aggregation estimate
4264 /// is drawn from the windowed max filter rather than a single round.
4265 ///
4266 /// Two phases over the same constant-rate FIFO simulator as A.1:
4267 /// 1. Reach steady state with plain (unaggregated) delivery, one ACK per packet, as in
4268 /// `probe_bw_exits_probe_down_to_probe_cruise_on_inflight`, driving STARTUP -> DRAIN ->
4269 /// PROBE_BW and on into PROBE_CRUISE.
4270 /// 2. On entering PROBE_CRUISE, switch the path to aggregate ACKs: completed packets are held
4271 /// and released in bursts on a fixed `AGG_NS` epoch grid (the L2 batching / radio-DRX
4272 /// behaviour A.12 models), so many packets share one delivery instant and the sender sees
4273 /// one ACK "event" cover a burst. The bottleneck still drains at exactly `BW`, so only ACK
4274 /// arrival *timing* is bursty; the average rate is unchanged.
4275 ///
4276 /// Each burst is fed exactly as the connection layer would (cf. `Connection::on_packet_acked`
4277 /// looping over the newly-acked packets, then a single `on_end_acks`): one `on_ack` per
4278 /// packet, all stamped with the same ack instant, followed by one `on_end_acks` carrying
4279 /// the burst's largest packet number.
4280 ///
4281 /// Once `full_bw_reached` (true throughout PROBE_BW), `BBRUpdateACKAggregation` tracks the
4282 /// per-round excess in a windowed max filter over the last `BBR.ExtraAckedFilterLen`
4283 /// (`EXTRA_ACKED_FILTER_LEN`, 10) rounds and sets `BBR.extra_acked` to that max, unlike
4284 /// STARTUP, which just remembers one round (A.12). `BBRUpdateMaxInflight` then adds
4285 /// `extra_acked` on top of `cwnd_gain*BDP` (cruise `cwnd_gain` is `DefaultCwndGain` = 2),
4286 /// so `C.cwnd` is lifted above `2*BDP`.
4287 ///
4288 /// Asserts that, in PROBE_CRUISE on the aggregating path:
4289 /// - the path genuinely aggregated (some ACK event covered many packets);
4290 /// - `extra_acked` became positive and equalled `extra_acked_filter.get_max()` on every ack,
4291 /// i.e. it is sourced from the windowed max filter, not the instantaneous round;
4292 /// - the windowed max held: within `EXTRA_ACKED_FILTER_LEN` rounds of the peak, a lower-excess
4293 /// round (an inter-ACK silence) never knocked `extra_acked` below that peak (the filter
4294 /// retained it), so the cwnd budget did not collapse between bursts;
4295 /// - `C.cwnd` exceeded `2*BDP` while `extra_acked>0` (the augmentation), and actual inflight
4296 /// rose above `2*BDP` too: the sender kept the pipe full across the silences rather than
4297 /// stalling at the un-augmented budget.
4298 #[test]
4299 fn probe_cruise_reaches_full_bw_with_ack_aggregation() {
4300 /// packet size in bytes
4301 const MSS: u64 = 1200;
4302 /// simulated bottleneck bandwidth: 100 Mbit/s in bytes/sec
4303 const BW: f64 = 12_500_000.0;
4304 /// simulated propagation round-trip time (100ms), matching A.1/A.12
4305 const RTT_NS: u64 = 100_000_000;
4306 const FWD_NS: u64 = RTT_NS / 2;
4307 const RET_NS: u64 = RTT_NS / 2;
4308 /// ACK-aggregation epoch (enabled only once in PROBE_CRUISE). All packets whose
4309 /// bottleneck service finishes within the same `AGG_NS` window have their ACKs
4310 /// released together. 1ms is ~10 MSS-times at BW (bursty) yet well under the 100ms
4311 /// RTT, so bursts stay within a round trip. Matches A.12.
4312 const AGG_NS: u64 = 1_000_000;
4313
4314 // bottleneck serialization time for one MSS-sized packet
4315 let btl_service_ns: u64 = (MSS as f64 / BW * 1e9).round() as u64;
4316
4317 // Drive the production default configuration.
4318 let mut bbr = Bbr3::new(Arc::new(Bbr3Config::default()), MSS as u16);
4319 assert_eq!(bbr.state, BbrState::Startup);
4320
4321 let base = Instant::now();
4322 let at = |off_ns: u64| base + Duration::from_nanos(off_ns);
4323 let mut rtt_est = RttEstimator::new(Duration::from_nanos(RTT_NS));
4324
4325 struct InFlight {
4326 pn: u64,
4327 send_ns: u64,
4328 ack_ns: u64,
4329 }
4330 let mut flight: VecDeque<InFlight> = VecDeque::new();
4331
4332 let mut now_ns: u64 = 0;
4333 let mut next_send_ns: u64 = 0;
4334 // time at which the bottleneck finishes serving everything queued so far
4335 let mut btl_free_ns: u64 = 0;
4336 let mut inflight: u64 = 0;
4337 let mut pn: u64 = 0;
4338
4339 // Aggregation is off until the flow reaches PROBE_CRUISE; before that the path acks
4340 // one packet at a time (distinct ack_ns), exactly like the cruise-reaching harness in
4341 // probe_bw_exits_probe_down_to_probe_cruise_on_inflight.
4342 let mut agg_on = false;
4343 let mut cruise_start_round: Option<u64> = None;
4344
4345 // Signals gathered while in PROBE_CRUISE; asserted after the loop.
4346 // Largest ACK burst (packets acked at one instant) actually produced.
4347 let mut max_burst: usize = 0;
4348 // Highest BBR.extra_acked observed in cruise, and the round it was first seen.
4349 let mut max_extra_acked: u64 = 0;
4350 let mut peak_round: Option<u64> = None;
4351 // extra_acked must be sourced from the windowed max filter on every cruise ack.
4352 let mut extra_acked_is_filter_max = true;
4353 // Largest amount by which C.cwnd sat above the un-augmented cruise budget (2*BDP),
4354 // i.e. the headroom BBRUpdateMaxInflight added from extra_acked.
4355 let mut max_cwnd_augmentation: u64 = 0;
4356 // The sender was never cwnd-blocked in cruise (cwnd stayed strictly above inflight on
4357 // every ack); a stall would show up as inflight catching the cwnd cap.
4358 let mut never_cwnd_blocked = true;
4359 // Peak inflight seen in cruise; should stay near a full BDP (pipe kept full).
4360 let mut max_inflight: u64 = 0;
4361 // Peak max_bw in cruise; must stay ~BW, proving the bursts didn't inflate the
4362 // delivery-rate estimate above the send rate (same sampler guard as A.12).
4363 let mut peak_cruise_max_bw: f64 = 0.0;
4364 // (round_count, extra_acked) at every cruise ack, for the windowed-retention check.
4365 let mut cruise_samples: Vec<(u64, u64)> = Vec::new();
4366
4367 for _ in 0..3_000_000 {
4368 let cwnd = bbr.window();
4369 let can_send = inflight + MSS <= cwnd;
4370 let next_ack = flight.front().map(|p| p.ack_ns);
4371 let do_send = can_send && next_ack.is_none_or(|ack| next_send_ns <= ack);
4372
4373 if do_send {
4374 now_ns = now_ns.max(next_send_ns);
4375 let send_ns = now_ns;
4376 // enqueue at the FIFO bottleneck, served at BW
4377 let arrival = send_ns + FWD_NS;
4378 let service_start = arrival.max(btl_free_ns);
4379 let finish = service_start + btl_service_ns;
4380 btl_free_ns = finish;
4381 // Once aggregating, hold the completed packet until the next AGG_NS epoch
4382 // boundary at/after `finish` so packets finishing in the same window release
4383 // together (identical ack_ns == one ACK event); otherwise ack as soon as it
4384 // clears the bottleneck (+propagation), one ack per packet.
4385 let ack_ns = if agg_on {
4386 finish.div_ceil(AGG_NS) * AGG_NS + RET_NS
4387 } else {
4388 finish + RET_NS
4389 };
4390
4391 bbr.on_packet_sent(at(send_ns), MSS as u16, pn, SpaceKind::Data);
4392 inflight += MSS;
4393 flight.push_back(InFlight {
4394 pn,
4395 send_ns,
4396 ack_ns,
4397 });
4398
4399 // pace the next send at BBR's chosen pacing rate
4400 let pacing = bbr.pacing_rate.max(1.0);
4401 next_send_ns = send_ns + (MSS as f64 / pacing * 1e9).round() as u64;
4402 pn += 1;
4403 } else if let Some(first) = flight.pop_front() {
4404 // Gather the whole aggregation burst: every in-flight packet sharing this
4405 // release instant is acknowledged by the same ACK event. Off the aggregating
4406 // path each ack_ns is unique, so bursts degenerate to a single packet.
4407 let burst_ack_ns = first.ack_ns;
4408 let mut burst = vec![first];
4409 while flight.front().is_some_and(|p| p.ack_ns == burst_ack_ns) {
4410 burst.push(flight.pop_front().unwrap());
4411 }
4412 now_ns = now_ns.max(burst_ack_ns);
4413
4414 // Feed the burst as the connection layer does: one on_ack per packet (same
4415 // ack instant), then a single on_end_acks with the largest pn in the burst.
4416 let largest_pn = burst.last().map(|p| p.pn).unwrap();
4417 for p in &burst {
4418 inflight -= MSS;
4419 rtt_est.update(Duration::ZERO, Duration::from_nanos(now_ns - p.send_ns));
4420 bbr.on_ack(
4421 at(now_ns),
4422 at(p.send_ns),
4423 MSS,
4424 p.pn,
4425 SpaceKind::Data,
4426 false,
4427 &rtt_est,
4428 );
4429 }
4430 bbr.on_end_acks(
4431 at(now_ns),
4432 inflight,
4433 false,
4434 Some(largest_pn),
4435 SpaceKind::Data,
4436 );
4437
4438 let in_cruise = bbr.state == BbrState::ProbeBw(ProbeBwSubstate::Cruise);
4439
4440 // On the first cruise ack, turn the path aggregating for all subsequent sends.
4441 if in_cruise && cruise_start_round.is_none() {
4442 agg_on = true;
4443 cruise_start_round = Some(bbr.round_count);
4444 }
4445
4446 if in_cruise && agg_on {
4447 max_burst = max_burst.max(burst.len());
4448
4449 let ea = bbr.extra_acked;
4450 // extra_acked is fed from the windowed max filter (full_bw_reached arm of
4451 // BBRUpdateACKAggregation), not the raw per-round excess.
4452 if ea != bbr.extra_acked_filter.get_max() {
4453 extra_acked_is_filter_max = false;
4454 }
4455 if ea > max_extra_acked {
4456 max_extra_acked = ea;
4457 peak_round = Some(bbr.round_count);
4458 }
4459 cruise_samples.push((bbr.round_count, ea));
4460
4461 peak_cruise_max_bw = peak_cruise_max_bw.max(bbr.max_bw);
4462 max_inflight = max_inflight.max(inflight);
4463 if bbr.cwnd <= inflight {
4464 never_cwnd_blocked = false;
4465 }
4466 // bbr.bdp is refreshed to max_bw*min_rtt inside update_max_inflight on
4467 // every set_cwnd; 2*bdp is the un-augmented cruise budget (cwnd_gain 2), so
4468 // any excess of cwnd over 2*bdp is exactly the extra_acked headroom
4469 // BBRUpdateMaxInflight added.
4470 if bbr.bdp > 0 {
4471 max_cwnd_augmentation =
4472 max_cwnd_augmentation.max(bbr.cwnd.saturating_sub(2 * bbr.bdp));
4473 }
4474
4475 // Gathered a couple of filter windows' worth of cruise rounds.
4476 if bbr.round_count - cruise_start_round.unwrap()
4477 >= 2 * EXTRA_ACKED_FILTER_LEN as u64
4478 {
4479 break;
4480 }
4481 } else if cruise_start_round.is_some() && !in_cruise {
4482 // Left PROBE_CRUISE (on to PROBE_REFILL/UP); stop gathering.
4483 break;
4484 }
4485 } else {
4486 panic!("simulation stalled: window full but nothing in flight");
4487 }
4488 }
4489
4490 // The flow reached PROBE_CRUISE and we gathered acks there.
4491 let cruise_start_round = cruise_start_round.expect("flow never reached PROBE_CRUISE");
4492 assert!(
4493 !cruise_samples.is_empty(),
4494 "no acks gathered in PROBE_CRUISE"
4495 );
4496 // The cruise sojourn spanned at least one full filter window, so the windowed-max
4497 // behaviour was actually exercised.
4498 let last_round = cruise_samples.last().unwrap().0;
4499 assert!(
4500 last_round - cruise_start_round >= EXTRA_ACKED_FILTER_LEN as u64,
4501 "cruise spanned only {} rounds, need >= {EXTRA_ACKED_FILTER_LEN} to exercise the filter",
4502 last_round - cruise_start_round
4503 );
4504
4505 // The path genuinely aggregated: at least one ACK event covered many packets.
4506 assert!(
4507 max_burst > 1,
4508 "harness should have produced aggregated ACK bursts, max burst was {max_burst}"
4509 );
4510 // BBRUpdateACKAggregation estimated a positive excess from the bursts.
4511 assert!(
4512 max_extra_acked > 0,
4513 "extra_acked should be positive on an aggregating path in cruise"
4514 );
4515 // extra_acked was sourced from the windowed max filter on every cruise ack.
4516 assert!(
4517 extra_acked_is_filter_max,
4518 "extra_acked should equal extra_acked_filter.get_max() throughout cruise"
4519 );
4520
4521 // Windowed max held: within EXTRA_ACKED_FILTER_LEN rounds of the peak, no lower-excess
4522 // round (inter-ACK silence) drove extra_acked below the peak: the max filter retained
4523 // it over its window, keeping the cwnd budget from collapsing between bursts.
4524 let peak_round = peak_round.expect("no positive extra_acked observed in cruise");
4525 for &(round, ea) in &cruise_samples {
4526 if round > peak_round && round <= peak_round + EXTRA_ACKED_FILTER_LEN as u64 {
4527 assert!(
4528 ea >= max_extra_acked,
4529 "extra_acked {ea} at round {round} fell below the peak {max_extra_acked} \
4530 (peak round {peak_round}) still inside the {EXTRA_ACKED_FILTER_LEN}-round filter window"
4531 );
4532 }
4533 }
4534
4535 // C.cwnd carried the extra_acked headroom: BBRUpdateMaxInflight adds extra_acked on top
4536 // of cwnd_gain*BDP (=2*BDP in cruise). Both sides are peak maxima reduced independently
4537 // over the sojourn (not necessarily the same round), so this asserts peak augmentation
4538 // >= peak extra_acked, enough to catch dropping the `+ extra_acked` term.
4539 assert!(
4540 max_cwnd_augmentation >= max_extra_acked,
4541 "C.cwnd should sit >= max_extra_acked ({max_extra_acked}) above 2*BDP in cruise, \
4542 observed augmentation {max_cwnd_augmentation}"
4543 );
4544 // That augmentation is what prevents an inter-ACK stall: cwnd stayed strictly above
4545 // inflight on every cruise ack, so the sender was never cwnd-blocked despite the bursty,
4546 // silence-punctuated acks; without the extra_acked headroom a burst could push inflight
4547 // into the cwnd cap and stall the flow.
4548 assert!(
4549 never_cwnd_blocked,
4550 "cwnd should stay above inflight throughout cruise (no cwnd-induced stall)"
4551 );
4552 // Full utilization was maintained: inflight stayed near a full BDP (the pipe never
4553 // drained empty between bursts).
4554 assert!(
4555 max_inflight * 10 >= bbr.bdp * 9,
4556 "inflight ({max_inflight}) should stay near a full BDP ({}) in cruise",
4557 bbr.bdp
4558 );
4559 // The delivery-rate sampler was not fooled by the bursts: max_bw tracked the true
4560 // bottleneck BW rather than the instantaneous burst rate (interval =
4561 // max(send_elapsed, ack_elapsed) caps the sample at the send rate).
4562 let cruise_bw_err = (peak_cruise_max_bw - BW).abs() / BW;
4563 assert!(
4564 cruise_bw_err < 0.05,
4565 "max_bw {peak_cruise_max_bw} not within 5% of simulated {BW} (rel err {cruise_bw_err})"
4566 );
4567 }
4568
4569 /// A.14: Correctly managing sub-packet BDPs.
4570 /// equivalent to BBRInflight / BBRQuantizationBudget (the BBR.MinPipeCwnd floor):
4571 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.6.4.2>
4572 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-2.7-4>
4573 ///
4574 /// On a very slow bottleneck the model-derived congestion window collapses below the
4575 /// pipelining minimum. `BBR.MinPipeCwnd` (4 * SMSS) is the floor that keeps enough packets
4576 /// outstanding to tolerate an "ACK every other packet" delayed-ACK receiver without
4577 /// stalling. This test drives that regime and asserts the floor governs `C.cwnd` while the
4578 /// pacing rate still tracks the low link bandwidth.
4579 ///
4580 /// A single-bottleneck FIFO link at `BW` = 1 MB/s with a small propagation delay. Because
4581 /// the bottleneck serialization of one MSS (`MSS/BW` = 1.2ms) dominates the measured
4582 /// min-RTT, the model's BDP estimate (`bw*min_rtt`) sits near a single packet (the
4583 /// propagation BDP `bw*prop` is well under one packet). Either way the cruise inflight
4584 /// budget `cwnd_gain*BDP` (cwnd_gain = DefaultCwndGain = 2) falls below `MinPipeCwnd`
4585 /// (4 packets), so `MinPipeCwnd` is the binding floor on `C.cwnd`.
4586 ///
4587 /// Two phases over one bespoke loop (the shared `Sim` acks one packet per ack; this needs a
4588 /// delayed-ACK receiver, so it drives the send/ack path directly like A.12/A.13):
4589 /// 1. Reach steady-state PROBE_BW/PROBE_CRUISE with a plain receiver (one ACK per packet),
4590 /// driving STARTUP -> DRAIN -> PROBE_BW -> PROBE_CRUISE. Capture BDP, `C.cwnd`, the pacing
4591 /// rate and `MinPipeCwnd` at cruise entry.
4592 /// 2. Switch to an "ACK every other packet" receiver: completed packets are released in pairs
4593 /// (the second packet's arrival triggers one ACK covering both), the delayed-ACK policy
4594 /// `MinPipeCwnd` exists to serve. Because the 4-packet floor keeps ~4 packets outstanding,
4595 /// a pair is always forming, so the bottleneck never idles waiting on a held ACK, so the
4596 /// pipeline does not stall and throughput stays at `BW`. With only the sub-packet model
4597 /// budget (~1 packet) the receiver would hold its lone packet's ACK forever and the flow
4598 /// would deadlock; the floor is what prevents that. The measurement runs across whole
4599 /// PROBE_BW cycles rather than one cruise sojourn: `TargetInflight()` is a single packet
4600 /// here, so the Reno-coexistence time scale makes every round a probe round. `MinPipeCwnd`
4601 /// is a lower bound on `C.cwnd` in every state, so this does not weaken the check.
4602 ///
4603 /// Asserts:
4604 /// - at cruise entry the model budget was genuinely sub-floor (`cwnd_gain*BDP < MinPipeCwnd`,
4605 /// BDP no more than ~2 packets) and `MinPipeCwnd == 4*MSS`;
4606 /// - `C.cwnd` sat exactly at `MinPipeCwnd` (the floor, not the tiny model budget, governs);
4607 /// - the pacing rate matched the low link bandwidth (within 5% of `BW`);
4608 /// - under the delayed-ACK receiver the pipeline never stalled: pairs genuinely formed,
4609 /// `C.cwnd` never fell below the 4-packet floor, and achieved throughput stayed at `BW`
4610 /// (within 10%).
4611 #[test]
4612 fn probe_bw_floors_sub_packet_bdp_at_min_pipe_cwnd() {
4613 /// packet size in bytes
4614 const MSS: u64 = 1200;
4615 /// bottleneck bandwidth: 8 Mbit/s (1 MB/s) in bytes/sec. Two constraints pin this:
4616 /// - cruise pacing (~0.99*BW) times 1ms (~990 B) must stay under the `2*SMSS`
4617 /// `set_send_quantum` floor, so `send_quantum` sits at that floor and `offload_budget`
4618 /// (send_quantum + delayed-ACK term) stays below `MinPipeCwnd` (4*SMSS); otherwise the
4619 /// floor can't bind.
4620 /// - it must still be fast enough to reach PROBE_CRUISE well within the 10s min-RTT filter
4621 /// window, so the clean (drained) min-RTT sample from DRAIN survives to cruise rather
4622 /// than aging out and re-latching to a queued value.
4623 const BW: f64 = 1_000_000.0;
4624 /// small propagation round-trip time (0.4ms): the propagation BDP `bw*prop` = 400 bytes is
4625 /// under one packet ("sub-packet BDP"). The measured clean min-RTT is `prop + MSS/BW`
4626 /// (one packet serializes through the bottleneck), so the model BDP lands near a single
4627 /// packet and the cruise budget `2*BDP` stays below the 4-packet `MinPipeCwnd`.
4628 const PROP_NS: u64 = 400_000;
4629 const FWD_NS: u64 = PROP_NS / 2;
4630 const RET_NS: u64 = PROP_NS / 2;
4631
4632 // bottleneck serialization time for one MSS-sized packet
4633 let btl_service_ns: u64 = (MSS as f64 / BW * 1e9).round() as u64;
4634
4635 // Seed the probe RNG so the PROBE_BW bw-probe wait (hence the cruise sojourn length) is
4636 // deterministic and the pair-count target below is not flaky.
4637 let seed: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
4638 let config = Bbr3Config {
4639 probe_rng_seed: Some(seed),
4640 ..Bbr3Config::default()
4641 };
4642 let mut bbr = Bbr3::new(Arc::new(config), MSS as u16);
4643 assert_eq!(bbr.state, BbrState::Startup);
4644
4645 let base = Instant::now();
4646 let at = |off_ns: u64| base + Duration::from_nanos(off_ns);
4647 let mut rtt_est = RttEstimator::new(Duration::from_nanos(PROP_NS));
4648
4649 struct InFlight {
4650 pn: u64,
4651 send_ns: u64,
4652 ack_ns: u64,
4653 }
4654 let mut flight: VecDeque<InFlight> = VecDeque::new();
4655
4656 let mut now_ns: u64 = 0;
4657 let mut next_send_ns: u64 = 0;
4658 // time at which the bottleneck finishes serving everything queued so far
4659 let mut btl_free_ns: u64 = 0;
4660 let mut inflight: u64 = 0;
4661 let mut pn: u64 = 0;
4662
4663 // Delayed-ACK receiver is off until PROBE_CRUISE; before that one ACK per packet, as in
4664 // probe_bw_exits_probe_down_to_probe_cruise_on_inflight.
4665 let mut delayed_on = false;
4666 // Packet number of the first packet of a pair still waiting for its partner (the
4667 // "every other packet" hold). When the partner is sent, both are stamped with the
4668 // partner's (later) arrival so they share one ACK instant.
4669 let mut pending_first: Option<u64> = None;
4670
4671 // Captured at the first PROBE_CRUISE ack (plain receiver).
4672 let mut cruise_capture: Option<(u64, u64, u64, f64, f64)> = None;
4673
4674 // Delayed-phase signals, asserted after the loop.
4675 // Largest ACK event size (packets acked at one instant) on the delayed path.
4676 let mut max_burst: usize = 0;
4677 // Number of ACK events that covered exactly a pair (the every-other-packet policy).
4678 let mut pair_events: usize = 0;
4679 // C.cwnd never dropped below the 4-packet floor on any delayed ack.
4680 let mut cwnd_floor_held = true;
4681 // Bytes delivered, and the first/last delivery instant, while in the delayed phase, for
4682 // the achieved-throughput (no-stall) check.
4683 let mut delayed_delivered: u64 = 0;
4684 let mut delayed_first_ns: Option<u64> = None;
4685 let mut delayed_last_ns: u64 = 0;
4686
4687 for _ in 0..3_000_000 {
4688 let cwnd = bbr.window();
4689 let can_send = inflight + MSS <= cwnd;
4690 let next_ack = flight.front().map(|p| p.ack_ns);
4691 let do_send = can_send && next_ack.is_none_or(|ack| next_send_ns <= ack);
4692
4693 if do_send {
4694 now_ns = now_ns.max(next_send_ns);
4695 let send_ns = now_ns;
4696 // enqueue at the FIFO bottleneck, served at BW
4697 let arrival = send_ns + FWD_NS;
4698 let service_start = arrival.max(btl_free_ns);
4699 let finish = service_start + btl_service_ns;
4700 btl_free_ns = finish;
4701 let arrival_ns = finish + RET_NS;
4702
4703 // Delayed-ACK ("every other packet") pairing, assigned at send time so a pair
4704 // shares one ACK instant without stalling paced sends in between: the first of a
4705 // pair is provisionally stamped with its own arrival, then lifted to the second's
4706 // (later) arrival when the partner is sent; both then release together. Off the
4707 // delayed path each packet acks on its own arrival.
4708 let ack_ns = if delayed_on {
4709 if let Some(first_pn) = pending_first.take() {
4710 // second of the pair: lift the held first to this (later) arrival
4711 for p in flight.iter_mut() {
4712 if p.pn == first_pn {
4713 p.ack_ns = arrival_ns;
4714 }
4715 }
4716 arrival_ns
4717 } else {
4718 pending_first = Some(pn);
4719 arrival_ns
4720 }
4721 } else {
4722 arrival_ns
4723 };
4724
4725 bbr.on_packet_sent(at(send_ns), MSS as u16, pn, SpaceKind::Data);
4726 inflight += MSS;
4727 flight.push_back(InFlight {
4728 pn,
4729 send_ns,
4730 ack_ns,
4731 });
4732
4733 // pace the next send at BBR's chosen pacing rate
4734 let pacing = bbr.pacing_rate.max(1.0);
4735 next_send_ns = send_ns + (MSS as f64 / pacing * 1e9).round() as u64;
4736 pn += 1;
4737 } else if let Some(first) = flight.pop_front() {
4738 // Gather every packet sharing this release instant: a delayed-ACK pair carries
4739 // one ACK instant (equal ack_ns), so both release together; off the delayed path
4740 // each ack_ns is unique and the burst degenerates to a single packet.
4741 let burst_ack_ns = first.ack_ns;
4742 let mut burst = vec![first];
4743 while flight.front().is_some_and(|p| p.ack_ns == burst_ack_ns) {
4744 burst.push(flight.pop_front().unwrap());
4745 }
4746 now_ns = now_ns.max(burst_ack_ns);
4747
4748 let largest_pn = burst.iter().map(|p| p.pn).max().unwrap();
4749 for p in &burst {
4750 inflight -= MSS;
4751 rtt_est.update(Duration::ZERO, Duration::from_nanos(now_ns - p.send_ns));
4752 bbr.on_ack(
4753 at(now_ns),
4754 at(p.send_ns),
4755 MSS,
4756 p.pn,
4757 SpaceKind::Data,
4758 false,
4759 &rtt_est,
4760 );
4761 }
4762 bbr.on_end_acks(
4763 at(now_ns),
4764 inflight,
4765 false,
4766 Some(largest_pn),
4767 SpaceKind::Data,
4768 );
4769
4770 let in_cruise = bbr.state == BbrState::ProbeBw(ProbeBwSubstate::Cruise);
4771
4772 // First cruise ack (plain receiver): capture the model state, then turn the
4773 // receiver delayed for the rest of the run.
4774 if in_cruise && cruise_capture.is_none() {
4775 cruise_capture = Some((
4776 bbr.bdp,
4777 bbr.cwnd,
4778 bbr.min_pipe_cwnd,
4779 bbr.pacing_rate,
4780 bbr.default_cwnd_gain,
4781 ));
4782 delayed_on = true;
4783 continue;
4784 }
4785
4786 if delayed_on {
4787 // The measured window spans whole PROBE_BW cycles rather than one cruise
4788 // sojourn: `target_inflight` here is a single packet, so the Reno-coexistence
4789 // timer makes every round a probe round and cruise never lasts. The floor is a
4790 // lower bound on `C.cwnd` in every state, so the checks below still hold.
4791 max_burst = max_burst.max(burst.len());
4792 if burst.len() == 2 {
4793 pair_events += 1;
4794 }
4795 // The floor is a lower bound; delayed (paired) ACKs read as ACK aggregation,
4796 // so extra_acked may lift cwnd above it (as in A.13), but never below.
4797 if bbr.cwnd < bbr.min_pipe_cwnd {
4798 cwnd_floor_held = false;
4799 }
4800 delayed_delivered += burst.len() as u64 * MSS;
4801 delayed_first_ns.get_or_insert(now_ns);
4802 delayed_last_ns = now_ns;
4803
4804 // Cap well above one cruise sojourn; in practice the loop exits earlier when
4805 // the flow leaves PROBE_CRUISE (the `!in_cruise` break above).
4806 if pair_events >= 40 {
4807 break;
4808 }
4809 }
4810 } else {
4811 panic!("simulation stalled: window full but nothing in flight");
4812 }
4813 }
4814
4815 let (bdp, cwnd, min_pipe_cwnd, pacing_rate, cwnd_gain) =
4816 cruise_capture.expect("flow never reached PROBE_CRUISE");
4817
4818 // The model budget was genuinely sub-floor: a very low, near-single-packet BDP whose
4819 // cruise inflight target (cwnd_gain*BDP) sits below the 4-packet MinPipeCwnd.
4820 assert!(bdp > 0, "BDP should be defined once min-RTT is known");
4821 assert!(
4822 bdp <= 2 * MSS,
4823 "expected a sub-two-packet BDP on this slow link, got {bdp} ({} packets)",
4824 bdp as f64 / MSS as f64
4825 );
4826 assert!(
4827 (cwnd_gain * bdp as f64) < min_pipe_cwnd as f64,
4828 "model cruise budget cwnd_gain*BDP ({}) should be below MinPipeCwnd ({min_pipe_cwnd}) \
4829 for the floor to bind",
4830 cwnd_gain * bdp as f64
4831 );
4832 // MinPipeCwnd is 4 * SMSS.
4833 assert_eq!(min_pipe_cwnd, 4 * MSS, "MinPipeCwnd should be 4 packets");
4834 // The floor, not the tiny model budget, governs C.cwnd.
4835 assert_eq!(
4836 cwnd, min_pipe_cwnd,
4837 "C.cwnd should sit at the MinPipeCwnd floor on a sub-packet BDP"
4838 );
4839 // Pacing still tracks the low link bandwidth (cruise pacing_gain = 1, 1% margin).
4840 let pacing_err = (pacing_rate - BW).abs() / BW;
4841 assert!(
4842 pacing_err < 0.05,
4843 "pacing_rate {pacing_rate} should match low link BW {BW} (rel err {pacing_err})"
4844 );
4845
4846 // The delayed-ACK receiver was genuinely exercised: ACK events covered pairs.
4847 assert!(
4848 max_burst == 2 && pair_events >= 10,
4849 "expected the every-other-packet receiver to produce ACK pairs \
4850 (max_burst {max_burst}, pair_events {pair_events})"
4851 );
4852 // C.cwnd never dropped below the 4-packet floor throughout the delayed phase.
4853 assert!(
4854 cwnd_floor_held,
4855 "C.cwnd should never drop below the MinPipeCwnd floor during the delayed-ACK phase"
4856 );
4857 // No stall: with 4 packets outstanding a pair is always forming, so the bottleneck never
4858 // idled waiting on a held ACK: achieved throughput stayed at the link rate. A stall
4859 // (as a sub-floor cwnd would cause) would collapse this far below BW.
4860 let first_ns = delayed_first_ns.expect("no delayed-phase acks gathered");
4861 let elapsed_s = (delayed_last_ns - first_ns) as f64 / 1e9;
4862 assert!(elapsed_s > 0.0, "delayed phase had no elapsed time");
4863 let throughput = delayed_delivered as f64 / elapsed_s;
4864 let throughput_err = (throughput - BW).abs() / BW;
4865 assert!(
4866 throughput_err < 0.10,
4867 "delayed-ACK throughput {throughput} should hold at link BW {BW} \
4868 (rel err {throughput_err}); a stalled pipeline would fall well below"
4869 );
4870 }
4871
4872 /// A.15: Increasing bandwidth 10x and ensuring full bandwidth is reached.
4873 /// equivalent to BBRRaiseInflightLongtermSlope / BBRProbeInflightLongtermUpward:
4874 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.3.6-8>
4875 ///
4876 /// After PROBE_BW is reached at a low link rate, the bottleneck bandwidth jumps 10x. In
4877 /// PROBE_UP BBR grows `inflight_longterm` with an exponentially increasing per-round step so
4878 /// it rediscovers a much larger BDP in O(log(BDP)) round trips rather than linearly.
4879 ///
4880 /// The step doubling comes from `raise_inflight_long_term_slope`, called once per round-start
4881 /// while probing up: `growth_this_round = SMSS << bw_probe_up_rounds` and `bw_probe_up_rounds`
4882 /// increments each round, so the unit of growth doubles every round. `probe_up_cnt` (bytes to
4883 /// ack per +1 byte of `inflight_longterm`) is set to `cwnd / growth_this_round`, so over one
4884 /// round (~cwnd bytes acked) `inflight_longterm` climbs by ~`growth_this_round`, a per-round
4885 /// increment that doubles each round.
4886 ///
4887 /// The growth path only engages when the flow is genuinely cwnd-limited. That signal is
4888 /// spec-defined as connection-provided (`C.is_cwnd_limited`), so the harness reports it exactly
4889 /// as the connection layer does: calling `on_cwnd_limited` whenever a send is blocked by the
4890 /// window rather than by pacing.
4891 ///
4892 /// A bespoke single-bottleneck FIFO loop (bandwidth changes mid-flight, which the shared `Sim`
4893 /// can't express; cf. A.12/A.13/A.14, which also drive the path directly) with an always-
4894 /// backlogged, paced sender. In PROBE_UP the pacing gain (1.25) drives sends above the delivery
4895 /// rate, so the flow rides at cwnd (cwnd-limited) and the 25% surplus probes for more
4896 /// bandwidth.
4897 /// 1. `BW_LO` = 10 Mbit/s. Ramp cleanly to `BW_LO` in PROBE_BW, then a brief
4898 /// 1-in-`LOSS_PERIOD` loss seeds a finite `inflight_longterm` (a PROBE_UP loss runs
4899 /// `handle_inflight_too_high`); the loss is switched off the instant it fires. Only a
4900 /// finite `inflight_longterm` gives the exponential slope a base to grow from.
4901 /// 2. Jump the bottleneck rate 10x (`BW_HI` = 100 Mbit/s). PROBE_BW cycles into PROBE_UP,
4902 /// where `inflight_longterm` is grown back up. Record it at each PROBE_UP round-start; run
4903 /// until `max_bw` reaches `BW_HI`.
4904 ///
4905 /// Asserts:
4906 /// - at the bump the flow was in the low-rate regime (`max_bw` well below `BW_HI`);
4907 /// - the additive step added to `inflight_longterm` doubles each round trip:
4908 /// `bw_probe_up_rounds` (the `SMSS << bw_probe_up_rounds` slope) advances once per
4909 /// cwnd-limited round, and the per-round `inflight_longterm` increment grows geometrically
4910 /// (a sustained ~2x run);
4911 /// - the full 100 Mbit/s is rediscovered (`max_bw` >= 97% of `BW_HI`) within a small,
4912 /// O(log(BDP)) number of PROBE_UP round trips.
4913 #[test]
4914 fn probe_up_rediscovers_full_bw_after_10x_increase() {
4915 /// packet size in bytes
4916 const MSS: u64 = 1200;
4917 /// simulated propagation round-trip time (100ms), matching A.1
4918 const RTT_NS: u64 = 100_000_000;
4919 /// low link rate before the jump: 10 Mbit/s in bytes/sec
4920 const BW_LO: f64 = 1_250_000.0;
4921 /// high link rate after the jump: 100 Mbit/s in bytes/sec (10x)
4922 const BW_HI: f64 = 12_500_000.0;
4923 const FWD_NS: u64 = RTT_NS / 2;
4924 const RET_NS: u64 = RTT_NS / 2;
4925 /// drop 1 in every `LOSS_PERIOD` packets during the low-rate PROBE_BW phase, until the
4926 /// first loss taken in PROBE_UP drives `handle_inflight_too_high`. That is what pulls
4927 /// `inflight_longterm` down from its `u64::MAX` init to a finite value; only once it is
4928 /// finite does the exponential-slope machinery (`raise_inflight_long_term_slope`) have a
4929 /// base to grow. The loss is switched off the instant it fires (see `loss_active`), so the
4930 /// post-jump probing sees a clean, loss-free 100 Mbit/s link.
4931 const LOSS_PERIOD: u64 = 25;
4932
4933 // Seed the probe RNG so the PROBE_BW cycle timing (hence when PROBE_UP is entered) is
4934 // deterministic.
4935 let seed: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
4936 let config = Bbr3Config {
4937 probe_rng_seed: Some(seed),
4938 ..Bbr3Config::default()
4939 };
4940 let mut bbr = Bbr3::new(Arc::new(config), MSS as u16);
4941 assert_eq!(bbr.state, BbrState::Startup);
4942
4943 let base = Instant::now();
4944 let at = |off_ns: u64| base + Duration::from_nanos(off_ns);
4945 let mut rtt_est = RttEstimator::new(Duration::from_nanos(RTT_NS));
4946
4947 struct InFlight {
4948 pn: u64,
4949 send_ns: u64,
4950 // time this packet resolves: its ACK arrival, or (for a seeded drop) the instant its
4951 // loss is detected.
4952 event_ns: u64,
4953 lost: bool,
4954 }
4955 let mut flight: VecDeque<InFlight> = VecDeque::new();
4956
4957 let mut now_ns: u64 = 0;
4958 let mut next_send_ns: u64 = 0;
4959 // time at which the bottleneck finishes serving everything queued so far
4960 let mut btl_free_ns: u64 = 0;
4961 let mut inflight: u64 = 0;
4962 let mut pn: u64 = 0;
4963
4964 // bottleneck serialization time for one MSS-sized packet; lowered 10x at the bump.
4965 let mut btl_service_ns: u64 = (MSS as f64 / BW_LO * 1e9).round() as u64;
4966 // The 10x jump fires once PROBE_BW has been reached AND inflight_longterm is finite (the
4967 // seeding loss has set it). Only then does the exponential-slope machinery have a base.
4968 let mut bumped = false;
4969 // Low-rate seeding loss: off until the flow has ramped cleanly to BW_LO in PROBE_BW, then
4970 // on until inflight_longterm is first set finite (so it is seeded from a healthy operating
4971 // point ~BDP_LO rather than a loss-depressed one).
4972 let mut loss_active = false;
4973 // max_bw captured at the bump (the low-rate operating point) and the round it happened.
4974 let mut bump_max_bw = 0.0f64;
4975 let mut bump_round: u64 = 0;
4976 // Round the first post-bump PROBE_UP began, for the O(log) discovery bound.
4977 let mut first_up_round: Option<u64> = None;
4978 // (inflight_longterm, bw_probe_up_rounds) at each post-bump PROBE_UP round-start.
4979 let mut up_rounds: Vec<(u64, u32)> = Vec::new();
4980 // PROBE_UP rounds from first probe to rediscovering the full BW_HI.
4981 let mut discover_rounds: Option<u64> = None;
4982
4983 for _ in 0..5_000_000 {
4984 let cwnd = bbr.window();
4985 // Always-backlogged, paced sender: it offers data continuously and is paced at BBR's
4986 // chosen rate (in PROBE_UP that is 1.25x the delivery rate, the probe that drives the
4987 // bandwidth search). Whenever the congestion window (not pacing) is what stops the
4988 // next send, report the cwnd-blocked signal exactly as the connection layer does.
4989 let can_send = inflight + MSS <= cwnd;
4990 if !can_send {
4991 bbr.on_cwnd_limited();
4992 }
4993 let next_ack = flight.front().map(|p| p.event_ns);
4994 let do_send = can_send && next_ack.is_none_or(|ev| next_send_ns <= ev);
4995
4996 if do_send {
4997 let send_ns = now_ns.max(next_send_ns);
4998 now_ns = send_ns;
4999 let arrival = send_ns + FWD_NS;
5000 let service_start = arrival.max(btl_free_ns);
5001 let finish = service_start + btl_service_ns;
5002 btl_free_ns = finish;
5003 let event_ns = finish + RET_NS;
5004
5005 // Low-rate loss to seed a finite inflight_longterm: drop 1-in-LOSS_PERIOD only
5006 // while in PROBE_BW and inflight_longterm is still unset. A drop taken in PROBE_UP
5007 // runs handle_inflight_too_high, which sets inflight_longterm and stops the loss.
5008 let lost = loss_active
5009 && matches!(bbr.state, BbrState::ProbeBw(_))
5010 && pn % LOSS_PERIOD == LOSS_PERIOD - 1;
5011
5012 bbr.on_packet_sent(at(send_ns), MSS as u16, pn, SpaceKind::Data);
5013 inflight += MSS;
5014 flight.push_back(InFlight {
5015 pn,
5016 send_ns,
5017 event_ns,
5018 lost,
5019 });
5020 // pace the next send at BBR's chosen pacing rate
5021 let pacing = bbr.pacing_rate.max(1.0);
5022 next_send_ns = send_ns + (MSS as f64 / pacing * 1e9).round() as u64;
5023 pn += 1;
5024 } else if let Some(p) = flight.pop_front() {
5025 now_ns = now_ns.max(p.event_ns);
5026 inflight -= MSS;
5027 if p.lost {
5028 bbr.on_packet_lost(MSS as u16, p.pn, SpaceKind::Data, at(now_ns));
5029 } else {
5030 rtt_est.update(Duration::ZERO, Duration::from_nanos(now_ns - p.send_ns));
5031 bbr.on_ack(
5032 at(now_ns),
5033 at(p.send_ns),
5034 MSS,
5035 p.pn,
5036 SpaceKind::Data,
5037 false,
5038 &rtt_est,
5039 );
5040 bbr.on_end_acks(at(now_ns), inflight, false, Some(p.pn), SpaceKind::Data);
5041 }
5042
5043 // Turn the seeding loss on once cleanly ramped to BW_LO, off the instant
5044 // inflight_longterm is set finite.
5045 if !bumped && matches!(bbr.state, BbrState::ProbeBw(_)) && bbr.max_bw >= 0.9 * BW_LO
5046 {
5047 loss_active = true;
5048 }
5049 if bbr.inflight_longterm != u64::MAX {
5050 loss_active = false;
5051 }
5052
5053 // Phase 1 -> 2: once settled in PROBE_BW at the low rate (max_bw near BW_LO) with a
5054 // finite inflight_longterm (a low-rate PROBE_UP overshoot has hit the buffer), jump
5055 // the link 10x. Only a finite inflight_longterm gives the exponential slope a base.
5056 if !bumped
5057 && matches!(bbr.state, BbrState::ProbeBw(_))
5058 && bbr.inflight_longterm != u64::MAX
5059 {
5060 bumped = true;
5061 bump_max_bw = bbr.max_bw;
5062 bump_round = bbr.round_count;
5063 up_rounds.push((bbr.inflight_longterm, bbr.bw_probe_up_rounds));
5064 btl_service_ns = (MSS as f64 / BW_HI * 1e9).round() as u64;
5065 }
5066
5067 // Record (inflight_longterm, bw_probe_up_rounds) once per PROBE_UP round-start
5068 // after the bump, and the round the first post-bump PROBE_UP began.
5069 if bumped && bbr.state == BbrState::ProbeBw(ProbeBwSubstate::Up) && bbr.round_start
5070 {
5071 first_up_round.get_or_insert(bbr.round_count);
5072 up_rounds.push((bbr.inflight_longterm, bbr.bw_probe_up_rounds));
5073 }
5074
5075 // Full BW_HI rediscovered.
5076 if bumped && bbr.max_bw >= 0.97 * BW_HI {
5077 discover_rounds = Some(bbr.round_count - first_up_round.unwrap_or(bump_round));
5078 break;
5079 }
5080 } else {
5081 panic!("simulation stalled: window full but nothing in flight");
5082 }
5083 }
5084
5085 // The flow was jumped 10x while genuinely in the low-rate regime (well below BW_HI).
5086 assert!(
5087 bumped,
5088 "flow never reached PROBE_BW with a finite inflight_longterm to bump"
5089 );
5090 assert!(
5091 bump_max_bw > 0.0 && bump_max_bw < 0.3 * BW_HI,
5092 "at the bump the flow should be in the low-rate regime, got max_bw {bump_max_bw}"
5093 );
5094
5095 // The full 100 Mbit/s was discovered, and within a small, O(log(BDP)) number of PROBE_UP
5096 // round trips (BDP_HI is ~1041 packets, log2 ~= 10; the bound leaves generous headroom for
5097 // constant factors and the rounds where the paced sender briefly wasn't cwnd-limited).
5098 let discover_rounds = discover_rounds.expect("BBR never rediscovered the full 100 Mbit/s");
5099 assert!(
5100 discover_rounds <= 25,
5101 "expected O(log(BDP)) PROBE_UP rounds to rediscover BW_HI, took {discover_rounds}"
5102 );
5103
5104 // The additive step added to inflight_longterm doubles each round trip: bw_probe_up_rounds
5105 // is raised once per cwnd-limited round (the `SMSS << bw_probe_up_rounds` slope), and the
5106 // per-round inflight_longterm increment grows geometrically as a result.
5107 let max_probe_up_rounds = up_rounds.iter().map(|&(_, r)| r).max().unwrap_or(0);
5108 assert!(
5109 max_probe_up_rounds >= 6,
5110 "the slope should be raised each cwnd-limited round; bw_probe_up_rounds only reached {max_probe_up_rounds}"
5111 );
5112
5113 // Per-round inflight_longterm increments (over the rounds where growth actually occurred).
5114 let steps: Vec<u64> = up_rounds
5115 .windows(2)
5116 .map(|w| w[1].0.saturating_sub(w[0].0))
5117 .filter(|&d| d > 0)
5118 .collect();
5119 // Find the longest run of consecutive increments that each at least ~1.6x the previous:
5120 // the exponential doubling (a linear ramp would hold the step constant, ratio ~1).
5121 let mut best_run = 1usize;
5122 let mut run = 1usize;
5123 for w in steps.windows(2) {
5124 if w[1] as f64 >= 1.6 * w[0] as f64 {
5125 run += 1;
5126 best_run = best_run.max(run);
5127 } else {
5128 run = 1;
5129 }
5130 }
5131 assert!(
5132 best_run >= 4,
5133 "expected a sustained per-round doubling of the inflight_longterm step, \
5134 longest ~2x run was {best_run} over steps {steps:?}"
5135 );
5136 }
5137
5138 /// A.16: Decreasing bandwidth 10x and ensuring max bandwidth adapts down.
5139 /// Exercises the short-term loss response (`loss_lower_bounds`) and the windowed `max_bw`
5140 /// filter expiry (`advance_max_bw_filter`, `BBR.MaxBwFilterLen`):
5141 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-2.10>
5142 ///
5143 /// After PROBE_BW is reached at a high link rate, the bottleneck bandwidth drops 10x. The
5144 /// flow is still carrying ~BDP of the *old* (high) rate, so once the link slows the standing
5145 /// queue overflows the bottleneck buffer and packets are lost. Two things must happen:
5146 ///
5147 /// 1. The short-term model reacts within the current probe cycle. A loss round runs
5148 /// `loss_lower_bounds`, decaying `BBR.bw_shortterm` by `BETA` (0.7) toward the freshly
5149 /// measured `bw_latest`. Because `bw = min(max_bw, bw_shortterm)`, this throttles
5150 /// pacing/cwnd immediately, long before the long-term `max_bw` moves. (Each PROBE_BW refill
5151 /// resets `bw_shortterm` and re-seeds it from the still-stale-high `max_bw`, so per cycle
5152 /// it only steps down ~one `BETA`; the full collapse to the new rate is the filter's job,
5153 /// below.)
5154 /// 2. The long-term `max_bw` is a max filter over `RS.delivery_rate` keyed on `cycle_count`
5155 /// with a window of `MAX_BW_FILTER_LEN` (2). The stale high sample only expires once
5156 /// `cycle_count` has advanced past the window, i.e. after ~2 PROBE_BW cycles
5157 /// (`advance_max_bw_filter` ticks once per cycle at `ProbeStopping`). Then `get_max()`
5158 /// returns the recent ~low-rate samples and `max_bw` drops to the new 10 Mbit/s limit.
5159 ///
5160 /// Same bespoke single-bottleneck FIFO loop as A.15 (bandwidth changes mid-flight, which the
5161 /// shared `Sim` can't express), inverted: start at 100 Mbit/s, then cut to 10 Mbit/s. A finite
5162 /// bottleneck buffer (~1 BDP of the high rate) makes the 10x cut produce real tail-drop loss:
5163 /// the flow runs cleanly at 100 Mbit/s but overflows the moment the link slows.
5164 /// 1. `BW_HI` = 100 Mbit/s. Ramp cleanly into PROBE_BW with `max_bw` ~= `BW_HI`.
5165 /// 2. Cut the bottleneck rate 10x (`BW_LO` = 10 Mbit/s). The overflowing queue drives loss;
5166 /// track the minimum `bw_shortterm` seen afterwards and the `cycle_count` at which `max_bw`
5167 /// first collapses to the new rate.
5168 ///
5169 /// Asserts:
5170 /// - at the cut the flow was in the high-rate regime (`max_bw` ~= `BW_HI`);
5171 /// - `bw_shortterm` adapts down rapidly after the cut: its post-cut minimum falls at least one
5172 /// `BETA` step below `BW_HI`, throttling the flow within the cycle;
5173 /// - `max_bw` collapses to the new `BW_LO` (within ~15%) once the filter window expires, and
5174 /// does so within a small number of PROBE_BW cycles (`MAX_BW_FILTER_LEN` + headroom).
5175 #[test]
5176 fn max_bw_adapts_down_after_10x_decrease() {
5177 /// packet size in bytes
5178 const MSS: u64 = 1200;
5179 /// simulated propagation round-trip time (100ms), matching A.1
5180 const RTT_NS: u64 = 100_000_000;
5181 /// high link rate before the cut: 100 Mbit/s in bytes/sec
5182 const BW_HI: f64 = 12_500_000.0;
5183 /// low link rate after the cut: 10 Mbit/s in bytes/sec (1/10th)
5184 const BW_LO: f64 = 1_250_000.0;
5185 const FWD_NS: u64 = RTT_NS / 2;
5186 const RET_NS: u64 = RTT_NS / 2;
5187 /// bottleneck buffer, in bytes. Sized at ~1 BDP of the high rate so the 100 Mbit/s flow
5188 /// runs loss-free (BBR holds ~1 BDP inflight with only a small standing queue), but the
5189 /// instant the rate is cut 10x the ~1 BDP still in flight drains at a tenth the rate: the
5190 /// queue overflows this buffer and packets are tail-dropped. That loss is the signal that
5191 /// drives `bw_shortterm` down and, once the max-bw filter window expires, `max_bw`.
5192 const BUFFER_BYTES: f64 = BW_HI * (RTT_NS as f64 / 1e9);
5193
5194 // Seed the probe RNG so the PROBE_BW cycle timing is deterministic.
5195 let seed: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
5196 let config = Bbr3Config {
5197 probe_rng_seed: Some(seed),
5198 ..Bbr3Config::default()
5199 };
5200 let mut bbr = Bbr3::new(Arc::new(config), MSS as u16);
5201 assert_eq!(bbr.state, BbrState::Startup);
5202
5203 let base = Instant::now();
5204 let at = |off_ns: u64| base + Duration::from_nanos(off_ns);
5205 let mut rtt_est = RttEstimator::new(Duration::from_nanos(RTT_NS));
5206
5207 struct InFlight {
5208 pn: u64,
5209 send_ns: u64,
5210 // time this packet resolves: its ACK arrival, or (for a tail drop) the instant its
5211 // loss is detected.
5212 event_ns: u64,
5213 lost: bool,
5214 }
5215 let mut flight: VecDeque<InFlight> = VecDeque::new();
5216
5217 let mut now_ns: u64 = 0;
5218 let mut next_send_ns: u64 = 0;
5219 // time at which the bottleneck finishes serving everything queued so far
5220 let mut btl_free_ns: u64 = 0;
5221 let mut inflight: u64 = 0;
5222 let mut pn: u64 = 0;
5223
5224 // bottleneck serialization time for one MSS-sized packet; raised 10x at the cut.
5225 let mut btl_service_ns: u64 = (MSS as f64 / BW_HI * 1e9).round() as u64;
5226 // The 10x cut fires once PROBE_BW has been reached at the high rate (max_bw ~= BW_HI).
5227 let mut cut = false;
5228 // max_bw / cycle_count captured at the cut (the high-rate operating point).
5229 let mut cut_max_bw = 0.0f64;
5230 let mut cut_cycle: u64 = 0;
5231 // Minimum finite bw_shortterm seen after the cut: the short-term model's rapid descent.
5232 let mut min_shortterm_after = f64::INFINITY;
5233 // PROBE_BW cycles (cycle_count advances) elapsed when max_bw first collapses to ~BW_LO.
5234 let mut adapt_cycles: Option<u64> = None;
5235
5236 for _ in 0..5_000_000 {
5237 let cwnd = bbr.window();
5238 // Always-backlogged, paced sender, as in A.15. Report the cwnd-blocked signal exactly
5239 // as the connection layer does whenever the window (not pacing) stops the next send.
5240 let can_send = inflight + MSS <= cwnd;
5241 if !can_send {
5242 bbr.on_cwnd_limited();
5243 }
5244 let next_ack = flight.front().map(|p| p.event_ns);
5245 let do_send = can_send && next_ack.is_none_or(|ev| next_send_ns <= ev);
5246
5247 if do_send {
5248 let send_ns = now_ns.max(next_send_ns);
5249 now_ns = send_ns;
5250 let arrival = send_ns + FWD_NS;
5251 let service_start = arrival.max(btl_free_ns);
5252 // Bytes already queued behind the bottleneck when this packet arrives. A tail drop
5253 // occurs if the standing queue already exceeds the buffer.
5254 let queue_bytes = service_start.saturating_sub(arrival) as f64
5255 / btl_service_ns as f64
5256 * MSS as f64;
5257 let lost = queue_bytes > BUFFER_BYTES;
5258
5259 let event_ns = if lost {
5260 // Dropped: never served, so the bottleneck is not advanced. Its loss is
5261 // detected roughly when the packets around it would have
5262 // been served/acked.
5263 service_start + RET_NS
5264 } else {
5265 let finish = service_start + btl_service_ns;
5266 btl_free_ns = finish;
5267 finish + RET_NS
5268 };
5269
5270 bbr.on_packet_sent(at(send_ns), MSS as u16, pn, SpaceKind::Data);
5271 inflight += MSS;
5272 flight.push_back(InFlight {
5273 pn,
5274 send_ns,
5275 event_ns,
5276 lost,
5277 });
5278 // pace the next send at BBR's chosen pacing rate
5279 let pacing = bbr.pacing_rate.max(1.0);
5280 next_send_ns = send_ns + (MSS as f64 / pacing * 1e9).round() as u64;
5281 pn += 1;
5282 } else if let Some(p) = flight.pop_front() {
5283 now_ns = now_ns.max(p.event_ns);
5284 inflight -= MSS;
5285 if p.lost {
5286 bbr.on_packet_lost(MSS as u16, p.pn, SpaceKind::Data, at(now_ns));
5287 } else {
5288 rtt_est.update(Duration::ZERO, Duration::from_nanos(now_ns - p.send_ns));
5289 bbr.on_ack(
5290 at(now_ns),
5291 at(p.send_ns),
5292 MSS,
5293 p.pn,
5294 SpaceKind::Data,
5295 false,
5296 &rtt_est,
5297 );
5298 bbr.on_end_acks(at(now_ns), inflight, false, Some(p.pn), SpaceKind::Data);
5299 }
5300
5301 // Phase 1 -> 2: once settled in PROBE_BW at the high rate (max_bw ~= BW_HI), cut
5302 // the link 10x. The ~1 BDP still in flight now overflows the buffer
5303 // -> loss.
5304 if !cut && matches!(bbr.state, BbrState::ProbeBw(_)) && bbr.max_bw >= 0.9 * BW_HI {
5305 cut = true;
5306 cut_max_bw = bbr.max_bw;
5307 cut_cycle = bbr.cycle_count;
5308 btl_service_ns = (MSS as f64 / BW_LO * 1e9).round() as u64;
5309 }
5310
5311 // After the cut, watch the short-term model descend and the long-term filter
5312 // expire.
5313 if cut {
5314 if bbr.bw_shortterm.is_finite() {
5315 min_shortterm_after = min_shortterm_after.min(bbr.bw_shortterm);
5316 }
5317 // max_bw collapses to the new rate once the stale high sample ages out of the
5318 // MAX_BW_FILTER_LEN-wide window (keyed on cycle_count).
5319 if adapt_cycles.is_none() && bbr.max_bw <= 1.15 * BW_LO {
5320 adapt_cycles = Some(bbr.cycle_count - cut_cycle);
5321 break;
5322 }
5323 }
5324 } else {
5325 panic!("simulation stalled: window full but nothing in flight");
5326 }
5327 }
5328
5329 // The link was cut 10x while genuinely in the high-rate regime (max_bw ~= BW_HI).
5330 assert!(cut, "flow never reached PROBE_BW at the high rate to cut");
5331 assert!(
5332 cut_max_bw >= 0.9 * BW_HI,
5333 "at the cut the flow should be in the high-rate regime, got max_bw {cut_max_bw}"
5334 );
5335
5336 // The short-term model reacted to the loss immediately: bw_shortterm was pulled below the
5337 // high operating point by at least one BETA (0.7) decay of loss_lower_bounds. This is the
5338 // *rapid* response: `bw = min(max_bw, bw_shortterm)` so this throttles sending within the
5339 // current cycle, long before max_bw moves. It only steps down ~one BETA per cycle because
5340 // each PROBE_BW refill resets bw_shortterm to INFINITY and re-seeds it from the (still
5341 // stale-high) max_bw; the deep collapse all the way to BW_LO is delivered by the max_bw
5342 // filter expiry below, not by bw_shortterm alone.
5343 assert!(
5344 min_shortterm_after <= 0.75 * BW_HI,
5345 "bw_shortterm should adapt down (>=1 BETA step) after the cut; min seen {min_shortterm_after}"
5346 );
5347
5348 // max_bw collapsed to the new 10 Mbit/s limit (matching the path delivery rate) once the
5349 // filter window expired, and within a small number of PROBE_BW cycles (MAX_BW_FILTER_LEN
5350 // is 2; the bound leaves headroom for the cycle in which the cut was recorded).
5351 let adapt_cycles = adapt_cycles.expect("max_bw never collapsed to the new BW_LO");
5352 assert!(
5353 adapt_cycles <= (MAX_BW_FILTER_LEN as u64) + 2,
5354 "expected max_bw to adapt within ~MAX_BW_FILTER_LEN PROBE_BW cycles, took {adapt_cycles}"
5355 );
5356 // The break fired on max_bw <= 1.15*BW_LO, so only the lower bound informs here:
5357 // confirm the estimate collapsed to (not below) the new rate.
5358 assert!(
5359 bbr.max_bw >= 0.85 * BW_LO,
5360 "max_bw should track the new path delivery rate BW_LO, got {}",
5361 bbr.max_bw
5362 );
5363 }
5364
5365 /// A.17: Handling token bucket policers.
5366 /// Exercises the short-term loss response (`init_lower_bounds` + `loss_lower_bounds`, driven
5367 /// from `adapt_lower_bounds_from_congestion`) settling the flow to a token-bucket policer's
5368 /// token rate: <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.10.3-8>
5369 ///
5370 /// A token-bucket policer is not a queue. It holds a bucket of `BURST` bytes that refills at
5371 /// the token rate, admits a packet only if a token is available, and *drops* (never
5372 /// buffers) any packet that arrives with the bucket empty. This is the classic BBR failure
5373 /// mode: during the initial burst every packet passes at line rate, so BBR's `max_bw`
5374 /// filter latches onto a delivery rate well above the token rate; once the burst is spent
5375 /// the policer starts dropping, and (because the policer adds no delay, so RTT never grows
5376 /// and there is no queueing signal) only the short-term model's loss response can pull the
5377 /// flow back down to the token rate.
5378 ///
5379 /// 1. The short-term model reacts within the PROBE_BW cruise/down cycle. A loss round there
5380 /// runs `init_lower_bounds` (seeding `bw_shortterm`/`inflight_shortterm` finite from the
5381 /// current `max_bw`/`cwnd`) then `loss_lower_bounds`, decaying `bw_shortterm` by `BETA`
5382 /// toward the measured `bw_latest` and `inflight_shortterm` by `BETA` toward
5383 /// `inflight_latest`. Because `bw = min(max_bw, bw_shortterm)` and the window is capped at
5384 /// `inflight_shortterm`, this throttles pacing and inflight even though the stale-high
5385 /// `max_bw` never moves.
5386 /// 2. Repeated across cycles the short-term bounds settle the flow so its send rate matches
5387 /// the token rate: the bucket stays near empty but drops become rare rather than
5388 /// continuous.
5389 ///
5390 /// Bespoke single-bottleneck loop in the spirit of A.15/A.16, with the FIFO buffer replaced by
5391 /// a token bucket (refill at `TOKEN_RATE`, cap `BURST`, no queue). A packet passes iff a
5392 /// token is available on arrival, otherwise it is dropped with only propagation delay (no
5393 /// serialization, no queueing), so RTT is constant and loss is the only congestion signal.
5394 /// Run for a fixed simulated duration; the last `WINDOW_NS` is the stable-point measurement
5395 /// window.
5396 ///
5397 /// Asserts:
5398 /// - the flow reaches PROBE_BW (past STARTUP) with a burst-inflated `max_bw` above the token
5399 /// rate;
5400 /// - once the burst is exhausted and the policer drops, the short-term model engages:
5401 /// `bw_shortterm` drops below the stale-high `max_bw` and `inflight_shortterm` becomes
5402 /// finite;
5403 /// - the flow settles to a stable operating point conforming to the token rate: over the late
5404 /// window the delivered goodput tracks `TOKEN_RATE` and the loss rate stays low (no
5405 /// excessive continuous loss).
5406 #[test]
5407 fn probe_bw_settles_to_token_rate_under_policer() {
5408 /// packet size in bytes
5409 const MSS: u64 = 1200;
5410 /// simulated propagation round-trip time (100ms), matching A.1
5411 const RTT_NS: u64 = 100_000_000;
5412 /// policer token (fill) rate: 10 Mbit/s in bytes/sec
5413 const TOKEN_RATE: f64 = 1_250_000.0;
5414 /// initial (and maximum) bucket depth in bytes. ~2 BDP of the token rate: big enough that
5415 /// STARTUP's ramp passes cleanly (the flow reaches PROBE_BW with a healthy, burst-inflated
5416 /// bw estimate), small enough that continued over-sending in PROBE_BW spends it and exposes
5417 /// the policer.
5418 const BURST: f64 = 2.0 * TOKEN_RATE * (RTT_NS as f64 / 1e9);
5419 const FWD_NS: u64 = RTT_NS / 2;
5420 const RET_NS: u64 = RTT_NS / 2;
5421 /// total simulated time and the trailing stable-point measurement window.
5422 const TOTAL_NS: u64 = 20_000_000_000;
5423 const WINDOW_NS: u64 = 5_000_000_000;
5424
5425 // Seed the probe RNG so the PROBE_BW cycle timing is deterministic.
5426 let seed: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
5427 let config = Bbr3Config {
5428 probe_rng_seed: Some(seed),
5429 ..Bbr3Config::default()
5430 };
5431 let mut bbr = Bbr3::new(Arc::new(config), MSS as u16);
5432 assert_eq!(bbr.state, BbrState::Startup);
5433
5434 let base = Instant::now();
5435 let at = |off_ns: u64| base + Duration::from_nanos(off_ns);
5436 let mut rtt_est = RttEstimator::new(Duration::from_nanos(RTT_NS));
5437
5438 struct InFlight {
5439 pn: u64,
5440 send_ns: u64,
5441 // time this packet resolves: its ACK arrival, or (for a policed drop) the instant its
5442 // loss is detected.
5443 event_ns: u64,
5444 lost: bool,
5445 }
5446 let mut flight: VecDeque<InFlight> = VecDeque::new();
5447
5448 let mut now_ns: u64 = 0;
5449 let mut next_send_ns: u64 = 0;
5450 let mut inflight: u64 = 0;
5451 let mut pn: u64 = 0;
5452
5453 // Token bucket: starts full, refills at TOKEN_RATE up to BURST, drained MSS per admitted
5454 // packet. Advanced by packet arrival time (send_ns + FWD_NS), which is monotonic in
5455 // send_ns.
5456 let mut tokens: f64 = BURST;
5457 let mut last_refill_ns: u64 = 0;
5458
5459 let mut reached_probe_bw = false;
5460 // max_bw captured when PROBE_BW is first reached (the burst-inflated operating point).
5461 let mut max_bw_at_probe_bw = 0.0f64;
5462 // short-term model signals after the burst first drives loss in PROBE_BW.
5463 let mut min_shortterm_after = f64::INFINITY;
5464 let mut inflight_shortterm_engaged = false;
5465
5466 // late-window goodput/loss accounting.
5467 let mut win_start_ns: Option<u64> = None;
5468 let mut win_last_ns: u64 = 0;
5469 let mut win_acked: u64 = 0;
5470 let mut win_lost: u64 = 0;
5471
5472 for _ in 0..50_000_000 {
5473 if now_ns >= TOTAL_NS {
5474 break;
5475 }
5476 let cwnd = bbr.window();
5477 // Always-backlogged, paced sender, as in A.15/A.16. Report the cwnd-blocked signal
5478 // exactly as the connection layer does whenever the window (not pacing) stops the send.
5479 let can_send = inflight + MSS <= cwnd;
5480 if !can_send {
5481 bbr.on_cwnd_limited();
5482 }
5483 let next_ack = flight.front().map(|p| p.event_ns);
5484 let do_send = can_send && next_ack.is_none_or(|ev| next_send_ns <= ev);
5485
5486 if do_send {
5487 let send_ns = now_ns.max(next_send_ns);
5488 now_ns = send_ns;
5489 let arrival = send_ns + FWD_NS;
5490
5491 // Refill the bucket up to its arrival time, cap at BURST, then admit-or-drop.
5492 tokens = (tokens + TOKEN_RATE * (arrival - last_refill_ns) as f64 / 1e9).min(BURST);
5493 last_refill_ns = arrival;
5494 let lost = tokens < MSS as f64;
5495 if !lost {
5496 tokens -= MSS as f64;
5497 }
5498 // Policer adds no queueing/serialization delay: passed packets are acked, dropped
5499 // packets are detected, purely after propagation.
5500 let event_ns = arrival + RET_NS;
5501
5502 bbr.on_packet_sent(at(send_ns), MSS as u16, pn, SpaceKind::Data);
5503 inflight += MSS;
5504 flight.push_back(InFlight {
5505 pn,
5506 send_ns,
5507 event_ns,
5508 lost,
5509 });
5510 // pace the next send at BBR's chosen pacing rate
5511 let pacing = bbr.pacing_rate.max(1.0);
5512 next_send_ns = send_ns + (MSS as f64 / pacing * 1e9).round() as u64;
5513 pn += 1;
5514 } else if let Some(p) = flight.pop_front() {
5515 now_ns = now_ns.max(p.event_ns);
5516 inflight -= MSS;
5517 if p.lost {
5518 bbr.on_packet_lost(MSS as u16, p.pn, SpaceKind::Data, at(now_ns));
5519 } else {
5520 rtt_est.update(Duration::ZERO, Duration::from_nanos(now_ns - p.send_ns));
5521 bbr.on_ack(
5522 at(now_ns),
5523 at(p.send_ns),
5524 MSS,
5525 p.pn,
5526 SpaceKind::Data,
5527 false,
5528 &rtt_est,
5529 );
5530 bbr.on_end_acks(at(now_ns), inflight, false, Some(p.pn), SpaceKind::Data);
5531 }
5532
5533 if !reached_probe_bw && matches!(bbr.state, BbrState::ProbeBw(_)) {
5534 reached_probe_bw = true;
5535 max_bw_at_probe_bw = bbr.max_bw;
5536 }
5537 // After PROBE_BW, watch the short-term model react to the policer's drops.
5538 if reached_probe_bw {
5539 if bbr.bw_shortterm.is_finite() {
5540 min_shortterm_after = min_shortterm_after.min(bbr.bw_shortterm);
5541 }
5542 if bbr.inflight_shortterm != u64::MAX {
5543 inflight_shortterm_engaged = true;
5544 }
5545 }
5546
5547 // Late-window goodput/loss accounting.
5548 if now_ns >= TOTAL_NS - WINDOW_NS {
5549 win_start_ns.get_or_insert(now_ns);
5550 win_last_ns = now_ns;
5551 if p.lost {
5552 win_lost += MSS;
5553 } else {
5554 win_acked += MSS;
5555 }
5556 }
5557 } else {
5558 panic!("simulation stalled: window full but nothing in flight");
5559 }
5560 }
5561
5562 let win_start = win_start_ns.expect("no packets resolved in the measurement window");
5563 let win_secs = (win_last_ns - win_start) as f64 / 1e9;
5564 let goodput = win_acked as f64 / win_secs.max(1e-9);
5565 let loss_rate = win_lost as f64 / (win_acked + win_lost).max(1) as f64;
5566
5567 // The flow reached PROBE_BW past STARTUP, with a burst-inflated max_bw above the token
5568 // rate.
5569 assert!(reached_probe_bw, "flow never reached PROBE_BW");
5570 assert!(
5571 max_bw_at_probe_bw > TOKEN_RATE,
5572 "the burst should inflate max_bw above the token rate on reaching PROBE_BW, got {max_bw_at_probe_bw}"
5573 );
5574
5575 // The short-term model engaged on the policer's drops: bw_shortterm fell below the
5576 // stale-high max_bw (throttling via bw = min(max_bw, bw_shortterm)), and
5577 // inflight_shortterm went finite (capping the window).
5578 assert!(
5579 min_shortterm_after < max_bw_at_probe_bw,
5580 "bw_shortterm should drop below the stale-high max_bw {max_bw_at_probe_bw}, got {min_shortterm_after}"
5581 );
5582 assert!(
5583 inflight_shortterm_engaged,
5584 "inflight_shortterm should become finite when the policer drops packets"
5585 );
5586
5587 // goodput tracks TOKEN_RATE, loss stays low. The lower bound is load-bearing (BBR
5588 // keeps the pipe full); the upper bound is the policer's own cap, so it corroborates
5589 // rather than tests BBR.
5590 assert!(
5591 (0.75 * TOKEN_RATE..=1.25 * TOKEN_RATE).contains(&goodput),
5592 "late-window goodput should track the token rate, got {goodput} ({:.2}x)",
5593 goodput / TOKEN_RATE
5594 );
5595 assert!(
5596 loss_rate <= 0.10,
5597 "policer loss should settle to a low rate, got {loss_rate}"
5598 );
5599 }
5600
5601 /// A.18: Handling spurious Fast Recovery (the loss-undo path).
5602 /// Exercises `save_state_upon_loss` (BBRSaveStateUponLoss) and `on_spurious_congestion_event`
5603 /// (BBRHandleSpuriousLossDetection):
5604 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.11>
5605 ///
5606 /// Packet reordering (later packets delivered while earlier ones sit apparently missing) makes
5607 /// the transport's loss detector declare a Fast Recovery that never really happened: the "lost"
5608 /// packets were only reordered, and arrive (or are DSACK'd) shortly after. BBR guards against
5609 /// this by snapshotting the pre-loss model on every declared loss (`note_loss` ->
5610 /// `save_state_upon_loss`) and restoring it if the transport later reports the episode
5611 /// spurious (in QUIC, the original packet's delivery is confirmed by packet number / a
5612 /// DSACK-equivalent, so the retransmission was spurious). The connection layer signals this
5613 /// via `Controller::on_spurious_congestion_event`.
5614 ///
5615 /// The reordering is introduced while the flow is in PROBE_UP:
5616 /// 1. Reach PROBE_UP loss-free, so the short-term model is at its reset sentinels
5617 /// (`bw_shortterm` = +inf, `inflight_shortterm` = u64::MAX) and `inflight_longterm` is
5618 /// still u64::MAX (only a loss makes it finite). Snapshot those.
5619 /// 2. Declare the oldest still-in-flight packets lost, a reordering picture: later packets are
5620 /// being delivered while these earlier ones look missing. Feed them one at a time until the
5621 /// accumulated loss trips `is_inflight_too_high` (> `LOSS_THRESH` of tx_in_flight): that
5622 /// runs `handle_inflight_too_high`, which clamps `inflight_longterm` to a finite value and
5623 /// moves PROBE_UP -> PROBE_DOWN. Stop declaring losses the instant the state leaves
5624 /// PROBE_UP, so the last `note_loss` (which runs before the transition inside the same
5625 /// call) saved `undo_state` = PROBE_UP.
5626 /// 3. The transport detects the loss was spurious -> `on_spurious_congestion_event`.
5627 ///
5628 /// Asserts:
5629 /// - `save_state_upon_loss` captured the pre-loss PROBE_UP model into the undo fields:
5630 /// `undo_state` = PROBE_UP, `undo_bw_shortterm` = +inf, `undo_inflight_shortterm` =
5631 /// u64::MAX, `undo_inflight_longterm` = u64::MAX.
5632 /// - the spurious Fast Recovery actually moved the flow off PROBE_UP and clamped
5633 /// `inflight_longterm` finite.
5634 /// - `on_spurious_congestion_event` restored the saved model:
5635 /// `bw_shortterm`/`inflight_shortterm` to `max(current, undo)` (their +inf/u64::MAX
5636 /// sentinels) and `inflight_longterm` back to u64::MAX, and seamlessly returned the flow to
5637 /// its previous state, PROBE_UP.
5638 ///
5639 /// Note on the short-term fields: for a spurious episode that restores to PROBE_UP they are
5640 /// necessarily at their sentinels. `adapt_lower_bounds_from_congestion` skips PROBE_UP, so no
5641 /// loss taken in PROBE_UP moves them; and any loss taken *after* the PROBE_UP -> PROBE_DOWN
5642 /// transition would re-run `note_loss` and overwrite `undo_state` to PROBE_DOWN (losing the
5643 /// return-to-PROBE_UP). So the meaningful restored quantities here are `inflight_longterm` and
5644 /// the state; the short-term fields are verified saved and restored at their reset
5645 /// sentinels.
5646 #[test]
5647 fn probe_up_restores_state_on_spurious_loss_detection() {
5648 /// packet size in bytes
5649 const MSS: u64 = 1200;
5650 /// simulated propagation round-trip time (100ms), matching A.1
5651 const RTT_NS: u64 = 100_000_000;
5652 /// bottleneck bandwidth: 10 Mbit/s in bytes/sec. Modest BDP keeps `LOSS_THRESH` (2% of
5653 /// tx_in_flight) small, so a short reordering burst trips `is_inflight_too_high`.
5654 const BW: f64 = 1_250_000.0;
5655 const FWD_NS: u64 = RTT_NS / 2;
5656 const RET_NS: u64 = RTT_NS / 2;
5657
5658 // Seed the probe RNG so the PROBE_BW cycle timing (hence when PROBE_UP is entered) is
5659 // deterministic.
5660 let seed: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
5661 let config = Bbr3Config {
5662 probe_rng_seed: Some(seed),
5663 ..Bbr3Config::default()
5664 };
5665 let mut bbr = Bbr3::new(Arc::new(config), MSS as u16);
5666 assert_eq!(bbr.state, BbrState::Startup);
5667
5668 let base = Instant::now();
5669 let at = |off_ns: u64| base + Duration::from_nanos(off_ns);
5670 let mut rtt_est = RttEstimator::new(Duration::from_nanos(RTT_NS));
5671
5672 struct InFlight {
5673 pn: u64,
5674 send_ns: u64,
5675 ack_ns: u64,
5676 }
5677 let mut flight: VecDeque<InFlight> = VecDeque::new();
5678
5679 let mut now_ns: u64 = 0;
5680 let mut next_send_ns: u64 = 0;
5681 // time at which the bottleneck finishes serving everything queued so far
5682 let mut btl_free_ns: u64 = 0;
5683 let mut inflight: u64 = 0;
5684 let mut pn: u64 = 0;
5685 let btl_service_ns: u64 = (MSS as f64 / BW * 1e9).round() as u64;
5686
5687 // Pre-loss PROBE_UP snapshot, taken the first time the flow is in PROBE_UP,
5688 // before any reordering is introduced.
5689 let mut pre: Option<UndoSnapshot> = None;
5690 // Set once the reordering-induced loss has moved the flow off PROBE_UP; carries the
5691 // undo snapshot + post-loss state/inflight_longterm for the assertions below.
5692 let mut episode: Option<LossEpisode> = None;
5693
5694 for _ in 0..5_000_000 {
5695 if episode.is_some() {
5696 break;
5697 }
5698 let cwnd = bbr.window();
5699 // Always-backlogged, paced sender (as in A.15/A.16/A.17): report the cwnd-blocked
5700 // signal exactly as the connection layer does whenever the window (not
5701 // pacing) stops the send.
5702 let can_send = inflight + MSS <= cwnd;
5703 if !can_send {
5704 bbr.on_cwnd_limited();
5705 }
5706 let next_ack = flight.front().map(|p| p.ack_ns);
5707 // Once in PROBE_UP we stop sending and drain the reordering burst out of the queue, so
5708 // a send is only due while we have not yet snapshotted PROBE_UP.
5709 let do_send =
5710 pre.is_none() && can_send && next_ack.is_none_or(|ack| next_send_ns <= ack);
5711
5712 if do_send {
5713 let send_ns = now_ns.max(next_send_ns);
5714 now_ns = send_ns;
5715 let arrival = send_ns + FWD_NS;
5716 let service_start = arrival.max(btl_free_ns);
5717 let finish = service_start + btl_service_ns;
5718 btl_free_ns = finish;
5719 let ack_ns = finish + RET_NS;
5720
5721 bbr.on_packet_sent(at(send_ns), MSS as u16, pn, SpaceKind::Data);
5722 inflight += MSS;
5723 flight.push_back(InFlight {
5724 pn,
5725 send_ns,
5726 ack_ns,
5727 });
5728 // pace the next send at BBR's chosen pacing rate
5729 let pacing = bbr.pacing_rate.max(1.0);
5730 next_send_ns = send_ns + (MSS as f64 / pacing * 1e9).round() as u64;
5731 pn += 1;
5732
5733 // First time in PROBE_UP: snapshot the pre-loss model, then stop sending and begin
5734 // introducing reordering on the packets already in flight.
5735 if pre.is_none() && bbr.state == BbrState::ProbeBw(ProbeBwSubstate::Up) {
5736 pre = Some((
5737 bbr.state,
5738 bbr.bw_shortterm,
5739 bbr.inflight_shortterm,
5740 bbr.inflight_longterm,
5741 ));
5742 }
5743 } else if let Some(p) = flight.pop_front() {
5744 now_ns = now_ns.max(p.ack_ns);
5745 inflight -= MSS;
5746
5747 // Before PROBE_UP: normal delivery, ramping the flow up.
5748 // In PROBE_UP: introduce reordering by declaring the oldest still-in-flight packets
5749 // lost (later packets are being delivered while these look missing). Keep declaring
5750 // until the accumulated loss trips is_inflight_too_high and the flow leaves
5751 // PROBE_UP; these declarations are spurious: the packets were only
5752 // reordered.
5753 let reordering =
5754 pre.is_some() && bbr.state == BbrState::ProbeBw(ProbeBwSubstate::Up);
5755 if reordering {
5756 bbr.on_packet_lost(MSS as u16, p.pn, SpaceKind::Data, at(now_ns));
5757
5758 // The moment handle_inflight_too_high moved us off PROBE_UP, record the
5759 // episode: the undo snapshot save_state_upon_loss captured
5760 // on this loss, plus the post-loss
5761 // state and inflight_longterm. The last note_loss ran while still in PROBE_UP,
5762 // so undo_state is PROBE_UP.
5763 if bbr.state != BbrState::ProbeBw(ProbeBwSubstate::Up) {
5764 episode = Some((
5765 (
5766 bbr.undo_state,
5767 bbr.undo_bw_shortterm,
5768 bbr.undo_inflight_shortterm,
5769 bbr.undo_inflight_longterm,
5770 ),
5771 bbr.state,
5772 bbr.inflight_longterm,
5773 ));
5774 }
5775 } else {
5776 rtt_est.update(Duration::ZERO, Duration::from_nanos(now_ns - p.send_ns));
5777 bbr.on_ack(
5778 at(now_ns),
5779 at(p.send_ns),
5780 MSS,
5781 p.pn,
5782 SpaceKind::Data,
5783 false,
5784 &rtt_est,
5785 );
5786 bbr.on_end_acks(at(now_ns), inflight, false, Some(p.pn), SpaceKind::Data);
5787 }
5788 } else {
5789 panic!("simulation stalled: window full but nothing in flight");
5790 }
5791 }
5792
5793 let (pre_state, pre_bw_st, pre_inflight_st, pre_inflight_lt) =
5794 pre.expect("flow never reached PROBE_UP");
5795 let ((undo_state, undo_bw_st, undo_inflight_st, undo_inflight_lt), post_state, post_lt) =
5796 episode.expect("reordering never triggered a Fast Recovery out of PROBE_UP");
5797
5798 // The pre-loss PROBE_UP model was at its reset sentinels (loss-free ramp).
5799 assert_eq!(pre_state, BbrState::ProbeBw(ProbeBwSubstate::Up));
5800 assert_eq!(
5801 pre_bw_st,
5802 f64::INFINITY,
5803 "bw_shortterm should be at its reset sentinel entering PROBE_UP"
5804 );
5805 assert_eq!(
5806 pre_inflight_st,
5807 u64::MAX,
5808 "inflight_shortterm should be at its reset sentinel entering PROBE_UP"
5809 );
5810 assert_eq!(
5811 pre_inflight_lt,
5812 u64::MAX,
5813 "inflight_longterm should still be unset (u64::MAX) before any loss"
5814 );
5815
5816 // save_state_upon_loss captured the pre-loss PROBE_UP model into the undo fields.
5817 assert_eq!(
5818 undo_state,
5819 BbrState::ProbeBw(ProbeBwSubstate::Up),
5820 "save_state_upon_loss should have saved BBR.state = PROBE_UP"
5821 );
5822 assert_eq!(
5823 undo_bw_st, pre_bw_st,
5824 "save_state_upon_loss should have saved BBR.bw_shortterm"
5825 );
5826 assert_eq!(
5827 undo_inflight_st, pre_inflight_st,
5828 "save_state_upon_loss should have saved BBR.inflight_shortterm to undo_inflight_shortterm"
5829 );
5830 assert_eq!(
5831 undo_inflight_lt, pre_inflight_lt,
5832 "save_state_upon_loss should have saved BBR.inflight_longterm to undo_inflight_longterm"
5833 );
5834
5835 // The spurious Fast Recovery actually moved the flow off PROBE_UP (into PROBE_DOWN) and
5836 // clamped inflight_longterm to a finite value.
5837 assert_eq!(
5838 post_state,
5839 BbrState::ProbeBw(ProbeBwSubstate::Down),
5840 "the loss should drive PROBE_UP -> PROBE_DOWN via handle_inflight_too_high"
5841 );
5842 assert!(
5843 post_lt < u64::MAX,
5844 "handle_inflight_too_high should clamp inflight_longterm finite, got u64::MAX"
5845 );
5846
5847 // The transport detects the loss was spurious (original packet delivered; the Fast Recovery
5848 // should never have happened) and reports it.
5849 bbr.on_spurious_congestion_event();
5850
5851 // on_spurious_congestion_event restored the saved model and returned to PROBE_UP.
5852 assert_eq!(
5853 bbr.state,
5854 BbrState::ProbeBw(ProbeBwSubstate::Up),
5855 "on_spurious_congestion_event should seamlessly return the flow to PROBE_UP"
5856 );
5857 assert_eq!(
5858 bbr.inflight_longterm,
5859 u64::MAX,
5860 "inflight_longterm should be restored to max(current, undo) = u64::MAX"
5861 );
5862 assert_eq!(
5863 bbr.bw_shortterm,
5864 f64::INFINITY,
5865 "bw_shortterm should be restored to max(current, undo) = +inf"
5866 );
5867 assert_eq!(
5868 bbr.inflight_shortterm,
5869 u64::MAX,
5870 "inflight_shortterm should be restored to max(current, undo) = u64::MAX"
5871 );
5872 }
5873
5874 /// A.19: Handling spurious RTO Recovery (the loss-undo path, RTO variant).
5875 /// Exercises `save_state_upon_loss` (BBRSaveStateUponLoss) and `on_spurious_congestion_event`
5876 /// (BBRHandleSpuriousLossDetection):
5877 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.11>
5878 ///
5879 /// Where A.18 covers a spurious *Fast* Recovery (later packets keep being delivered while a few
5880 /// earlier ones look missing), this covers a spurious *RTO* Recovery: acknowledgements stop
5881 /// arriving entirely, the transport's PTO/RTO timer fires, and the whole outstanding tail is
5882 /// declared lost in one burst. The tail was not really lost: the ACKs (or the packets) were
5883 /// only delayed by reordering on the path, so once the delayed acknowledgements arrive (in
5884 /// QUIC the original packet numbers confirm the delivery, a DSACK-equivalent), the episode
5885 /// is reported spurious and the model must be rolled back.
5886 ///
5887 /// Both recovery kinds reach BBR through the same loss path: the connection layer calls
5888 /// `on_packet_lost` per timed-out packet, which runs `process_lost_packet` -> `note_loss` ->
5889 /// `save_state_upon_loss`. (bbr3's `on_congestion_event` only acts on ECN, so a non-ECN RTO /
5890 /// persistent-congestion batch reaches BBR purely as these per-packet losses; see the trait
5891 /// note on `on_congestion_event`.) The RTO character here is the *shape* of the loss: a
5892 /// single tail burst with no interleaved deliveries, i.e. a timeout, not a SACK-driven Fast
5893 /// Recovery.
5894 ///
5895 /// The timeout is introduced while the flow is in PROBE_UP:
5896 /// 1. Reach PROBE_UP loss-free with a full window outstanding, so the short-term model is at
5897 /// its reset sentinels (`bw_shortterm` = +inf, `inflight_shortterm` = u64::MAX) and
5898 /// `inflight_longterm` is still u64::MAX (only a loss makes it finite). Snapshot those.
5899 /// 2. Simulate the RTO: stop delivering ACKs and time out the entire outstanding tail,
5900 /// declaring the packets lost oldest-first in one burst. Feed them until the accumulated
5901 /// loss trips `is_inflight_too_high` (> `LOSS_THRESH` of tx_in_flight): that runs
5902 /// `handle_inflight_too_high`, which clamps `inflight_longterm` to a finite value and moves
5903 /// PROBE_UP -> PROBE_DOWN. Stop the instant the state leaves PROBE_UP, so the last
5904 /// `note_loss` (which runs before the transition inside the same call) saved `undo_state` =
5905 /// PROBE_UP.
5906 /// 3. The delayed acknowledgements arrive; the transport detects the RTO was spurious ->
5907 /// `on_spurious_congestion_event`.
5908 ///
5909 /// Asserts:
5910 /// - `save_state_upon_loss` captured the pre-loss PROBE_UP model into the undo fields:
5911 /// `undo_state` = PROBE_UP, `undo_bw_shortterm` = +inf, `undo_inflight_shortterm` =
5912 /// u64::MAX, `undo_inflight_longterm` = u64::MAX.
5913 /// - the spurious RTO actually moved the flow off PROBE_UP and clamped `inflight_longterm`
5914 /// finite.
5915 /// - `on_spurious_congestion_event` restored the saved model:
5916 /// `bw_shortterm`/`inflight_shortterm` to `max(current, undo)` (their +inf/u64::MAX
5917 /// sentinels) and `inflight_longterm` back to u64::MAX, and seamlessly returned the flow to
5918 /// its previous state, PROBE_UP.
5919 ///
5920 /// Note on the short-term fields: as in A.18, for a spurious episode that restores to PROBE_UP
5921 /// they are necessarily at their sentinels (`adapt_lower_bounds_from_congestion` skips
5922 /// PROBE_UP, and any loss taken *after* the PROBE_UP -> PROBE_DOWN transition would
5923 /// overwrite `undo_state`). So the meaningful restored quantities here are
5924 /// `inflight_longterm` and the state; the short-term fields are verified saved and restored
5925 /// at their reset sentinels.
5926 #[test]
5927 fn probe_up_restores_state_on_spurious_rto_detection() {
5928 /// packet size in bytes
5929 const MSS: u64 = 1200;
5930 /// simulated propagation round-trip time (100ms), matching A.1
5931 const RTT_NS: u64 = 100_000_000;
5932 /// bottleneck bandwidth: 10 Mbit/s in bytes/sec. Modest BDP keeps `LOSS_THRESH` (2% of
5933 /// tx_in_flight) small, so a short tail burst trips `is_inflight_too_high`.
5934 const BW: f64 = 1_250_000.0;
5935 const FWD_NS: u64 = RTT_NS / 2;
5936 const RET_NS: u64 = RTT_NS / 2;
5937
5938 // Seed the probe RNG so the PROBE_BW cycle timing (hence when PROBE_UP is entered) is
5939 // deterministic.
5940 let seed: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
5941 let config = Bbr3Config {
5942 probe_rng_seed: Some(seed),
5943 ..Bbr3Config::default()
5944 };
5945 let mut bbr = Bbr3::new(Arc::new(config), MSS as u16);
5946 assert_eq!(bbr.state, BbrState::Startup);
5947
5948 let base = Instant::now();
5949 let at = |off_ns: u64| base + Duration::from_nanos(off_ns);
5950 let mut rtt_est = RttEstimator::new(Duration::from_nanos(RTT_NS));
5951
5952 struct InFlight {
5953 pn: u64,
5954 send_ns: u64,
5955 ack_ns: u64,
5956 }
5957 let mut flight: VecDeque<InFlight> = VecDeque::new();
5958
5959 let mut now_ns: u64 = 0;
5960 let mut next_send_ns: u64 = 0;
5961 // time at which the bottleneck finishes serving everything queued so far
5962 let mut btl_free_ns: u64 = 0;
5963 let mut inflight: u64 = 0;
5964 let mut pn: u64 = 0;
5965 let btl_service_ns: u64 = (MSS as f64 / BW * 1e9).round() as u64;
5966
5967 // Pre-loss PROBE_UP snapshot, taken the first time the flow is in PROBE_UP,
5968 // before the RTO is introduced.
5969 let mut pre: Option<UndoSnapshot> = None;
5970 // Set once the RTO-induced loss burst has moved the flow off PROBE_UP; carries the
5971 // undo snapshot + post-loss state/inflight_longterm for the assertions below.
5972 let mut episode: Option<LossEpisode> = None;
5973
5974 for _ in 0..5_000_000 {
5975 if episode.is_some() {
5976 break;
5977 }
5978 let cwnd = bbr.window();
5979 // Always-backlogged, paced sender (as in A.15/A.16/A.17/A.18): report the cwnd-blocked
5980 // signal exactly as the connection layer does whenever the window (not pacing) stops
5981 // the send.
5982 let can_send = inflight + MSS <= cwnd;
5983 if !can_send {
5984 bbr.on_cwnd_limited();
5985 }
5986 let next_ack = flight.front().map(|p| p.ack_ns);
5987 // Once in PROBE_UP we stop sending: the RTO scenario is a *silence*, no more packets go
5988 // out and no ACKs come back while the outstanding tail times out. A send is only due
5989 // while we have not yet snapshotted PROBE_UP.
5990 let do_send =
5991 pre.is_none() && can_send && next_ack.is_none_or(|ack| next_send_ns <= ack);
5992
5993 if do_send {
5994 let send_ns = now_ns.max(next_send_ns);
5995 now_ns = send_ns;
5996 let arrival = send_ns + FWD_NS;
5997 let service_start = arrival.max(btl_free_ns);
5998 let finish = service_start + btl_service_ns;
5999 btl_free_ns = finish;
6000 let ack_ns = finish + RET_NS;
6001
6002 bbr.on_packet_sent(at(send_ns), MSS as u16, pn, SpaceKind::Data);
6003 inflight += MSS;
6004 flight.push_back(InFlight {
6005 pn,
6006 send_ns,
6007 ack_ns,
6008 });
6009 // pace the next send at BBR's chosen pacing rate
6010 let pacing = bbr.pacing_rate.max(1.0);
6011 next_send_ns = send_ns + (MSS as f64 / pacing * 1e9).round() as u64;
6012 pn += 1;
6013
6014 // First time in PROBE_UP: snapshot the pre-loss model, then stop sending so a full
6015 // window is left outstanding for the RTO to time out.
6016 if pre.is_none() && bbr.state == BbrState::ProbeBw(ProbeBwSubstate::Up) {
6017 pre = Some((
6018 bbr.state,
6019 bbr.bw_shortterm,
6020 bbr.inflight_shortterm,
6021 bbr.inflight_longterm,
6022 ));
6023 }
6024 } else if let Some(p) = flight.pop_front() {
6025 now_ns = now_ns.max(p.ack_ns);
6026 inflight -= MSS;
6027
6028 // Before PROBE_UP: normal delivery, ramping the flow up.
6029 // In PROBE_UP: the RTO has fired, no ACKs are arriving, so the whole outstanding
6030 // tail times out. Declare the packets lost oldest-first, in one
6031 // burst with no interleaved deliveries (a timeout, not a Fast
6032 // Recovery). Keep declaring until the accumulated loss trips
6033 // is_inflight_too_high and the flow leaves PROBE_UP; these declarations are
6034 // spurious: the tail was only delayed by reordering, not lost.
6035 let rto_timeout =
6036 pre.is_some() && bbr.state == BbrState::ProbeBw(ProbeBwSubstate::Up);
6037 if rto_timeout {
6038 bbr.on_packet_lost(MSS as u16, p.pn, SpaceKind::Data, at(now_ns));
6039
6040 // The moment handle_inflight_too_high moved us off PROBE_UP, record the
6041 // episode: the undo snapshot save_state_upon_loss captured
6042 // on this loss, plus the post-loss
6043 // state and inflight_longterm. The last note_loss ran while still in PROBE_UP,
6044 // so undo_state is PROBE_UP.
6045 if bbr.state != BbrState::ProbeBw(ProbeBwSubstate::Up) {
6046 episode = Some((
6047 (
6048 bbr.undo_state,
6049 bbr.undo_bw_shortterm,
6050 bbr.undo_inflight_shortterm,
6051 bbr.undo_inflight_longterm,
6052 ),
6053 bbr.state,
6054 bbr.inflight_longterm,
6055 ));
6056 }
6057 } else {
6058 rtt_est.update(Duration::ZERO, Duration::from_nanos(now_ns - p.send_ns));
6059 bbr.on_ack(
6060 at(now_ns),
6061 at(p.send_ns),
6062 MSS,
6063 p.pn,
6064 SpaceKind::Data,
6065 false,
6066 &rtt_est,
6067 );
6068 bbr.on_end_acks(at(now_ns), inflight, false, Some(p.pn), SpaceKind::Data);
6069 }
6070 } else {
6071 panic!("simulation stalled: window full but nothing in flight");
6072 }
6073 }
6074
6075 let (pre_state, pre_bw_st, pre_inflight_st, pre_inflight_lt) =
6076 pre.expect("flow never reached PROBE_UP");
6077 let ((undo_state, undo_bw_st, undo_inflight_st, undo_inflight_lt), post_state, post_lt) =
6078 episode.expect("RTO tail burst never triggered a recovery out of PROBE_UP");
6079
6080 // The pre-loss PROBE_UP model was at its reset sentinels (loss-free ramp).
6081 assert_eq!(pre_state, BbrState::ProbeBw(ProbeBwSubstate::Up));
6082 assert_eq!(
6083 pre_bw_st,
6084 f64::INFINITY,
6085 "bw_shortterm should be at its reset sentinel entering PROBE_UP"
6086 );
6087 assert_eq!(
6088 pre_inflight_st,
6089 u64::MAX,
6090 "inflight_shortterm should be at its reset sentinel entering PROBE_UP"
6091 );
6092 assert_eq!(
6093 pre_inflight_lt,
6094 u64::MAX,
6095 "inflight_longterm should still be unset (u64::MAX) before any loss"
6096 );
6097
6098 // save_state_upon_loss captured the pre-loss PROBE_UP model into the undo fields.
6099 assert_eq!(
6100 undo_state,
6101 BbrState::ProbeBw(ProbeBwSubstate::Up),
6102 "save_state_upon_loss should have saved BBR.state = PROBE_UP"
6103 );
6104 assert_eq!(
6105 undo_bw_st, pre_bw_st,
6106 "save_state_upon_loss should have saved BBR.bw_shortterm"
6107 );
6108 assert_eq!(
6109 undo_inflight_st, pre_inflight_st,
6110 "save_state_upon_loss should have saved BBR.inflight_shortterm to undo_inflight_shortterm"
6111 );
6112 assert_eq!(
6113 undo_inflight_lt, pre_inflight_lt,
6114 "save_state_upon_loss should have saved BBR.inflight_longterm to undo_inflight_longterm"
6115 );
6116
6117 // The spurious RTO actually moved the flow off PROBE_UP (into PROBE_DOWN) and clamped
6118 // inflight_longterm to a finite value.
6119 assert_eq!(
6120 post_state,
6121 BbrState::ProbeBw(ProbeBwSubstate::Down),
6122 "the RTO loss should drive PROBE_UP -> PROBE_DOWN via handle_inflight_too_high"
6123 );
6124 assert!(
6125 post_lt < u64::MAX,
6126 "handle_inflight_too_high should clamp inflight_longterm finite, got u64::MAX"
6127 );
6128
6129 // The delayed acknowledgements arrive: the transport detects the RTO was spurious (original
6130 // packets delivered; the RTO recovery should never have happened) and reports it.
6131 bbr.on_spurious_congestion_event();
6132
6133 // on_spurious_congestion_event restored the saved model and returned to PROBE_UP.
6134 assert_eq!(
6135 bbr.state,
6136 BbrState::ProbeBw(ProbeBwSubstate::Up),
6137 "on_spurious_congestion_event should seamlessly return the flow to PROBE_UP"
6138 );
6139 assert_eq!(
6140 bbr.inflight_longterm,
6141 u64::MAX,
6142 "inflight_longterm should be restored to max(current, undo) = u64::MAX"
6143 );
6144 assert_eq!(
6145 bbr.bw_shortterm,
6146 f64::INFINITY,
6147 "bw_shortterm should be restored to max(current, undo) = +inf"
6148 );
6149 assert_eq!(
6150 bbr.inflight_shortterm,
6151 u64::MAX,
6152 "inflight_shortterm should be restored to max(current, undo) = u64::MAX"
6153 );
6154 }
6155
6156 /// A.20: Entering and exiting PROBE_RTT during STARTUP.
6157 /// equivalent to BBRCheckProbeRTT / BBRHandleProbeRTT / BBRExitProbeRTT:
6158 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.3.4.3>
6159 ///
6160 /// A.10 covers the PROBE_RTT interlude that fires *after* PROBE_BW, where
6161 /// `full_bw_reached` is true and `exit_probe_rtt` therefore routes on to
6162 /// PROBE_BW. This is the STARTUP-phase counterpart: the periodic min-RTT
6163 /// re-probe fires while the flow is *still in STARTUP* with `full_bw_reached`
6164 /// false, so `exit_probe_rtt` must route back to STARTUP (`enter_startup`) to
6165 /// keep searching for the max bandwidth, never forward to PROBE_BW. A.7 shows
6166 /// this same STARTUP <-> PROBE_RTT oscillation as a side effect of its "never
6167 /// exits STARTUP" premise; A.20 isolates and asserts the entry / duration /
6168 /// exit mechanics of one such interlude.
6169 ///
6170 /// Extended, slow STARTUP (same construction as A.7): the app is held to a
6171 /// small fixed window (`APP_WINDOW`, well below cwnd) from the first packet, so
6172 /// every sample is application-limited and `check_full_bw_reached` bails on its
6173 /// `is_app_limited` guard, `full_bw_reached` never arms and STARTUP persists.
6174 /// With a constant RTT the min-RTT floor never drops, so `update_min_rtt`
6175 /// freezes `probe_rtt_min_stamp` and flips `BBR.probe_rtt_expired` true one
6176 /// `BBR.ProbeRTTInterval` (5 s) after it was last stamped; `check_probe_rtt`
6177 /// then enters PROBE_RTT, all while `full_bw_reached` is still false.
6178 ///
6179 /// A transient PROBE_RTT also fires on the very first ack (`probe_rtt_min_stamp`
6180 /// starts unset, so `probe_rtt_expired` is true at t~0); its exit re-stamps
6181 /// `probe_rtt_min_stamp`, so the *next* expiry (the one this test targets)
6182 /// lands a full ProbeRTTInterval later, ~5 s into the still-running STARTUP.
6183 /// The t~0 transient is filtered out by requiring the entry at
6184 /// >= ProbeRTTInterval.
6185 ///
6186 /// On entry `check_probe_rtt` runs `enter_probe_rtt` (state -> ProbeRtt,
6187 /// `cwnd_gain` -> ProbeRTTCwndGain 0.5) and clears `probe_rtt_done_stamp`;
6188 /// `bound_cwnd_for_probe_rtt` caps cwnd at `BBRProbeRTTCwnd` (~0.5*BDP) so the
6189 /// sender stalls until `C.inflight` drains below the cap. When it does,
6190 /// `handle_probe_rtt` arms `probe_rtt_done_stamp = now + ProbeRTTDuration`
6191 /// (200 ms) and starts a fresh round; PROBE_RTT then holds until *both* one
6192 /// packet-timed round has elapsed (`probe_rtt_round_done`) *and*
6193 /// `now > probe_rtt_done_stamp`. `check_probe_rtt_done` then restores the cwnd
6194 /// and calls `exit_probe_rtt`, which (`full_bw_reached` being false) runs
6195 /// `enter_startup`, returning the flow to STARTUP.
6196 ///
6197 /// Asserts that: the flow only ever occupied STARTUP or PROBE_RTT (never
6198 /// advanced to DRAIN/PROBE_BW) and `full_bw_reached` was never set; the targeted
6199 /// PROBE_RTT was entered only after ProbeRTTInterval (5 s) had elapsed, with
6200 /// `cwnd_gain == ProbeRTTCwndGain` (0.5) and `full_bw_reached` still false;
6201 /// `probe_rtt_done_stamp` armed once inflight drained below the cap; the
6202 /// interlude held for at least ProbeRTTDuration (200 ms) *and* one round after
6203 /// arming; and the exit returned to STARTUP (not PROBE_BW) with
6204 /// `full_bw_reached` still false.
6205 #[test]
6206 fn startup_enters_and_exits_probe_rtt_back_to_startup() {
6207 /// packet size in bytes
6208 const MSS: u64 = 1200;
6209 /// simulated bottleneck bandwidth: 100 Mbit/s in bytes/sec
6210 const BW: f64 = 12_500_000.0;
6211 /// simulated propagation round-trip time (100ms), matching A.7/A.10
6212 const RTT_NS: u64 = 100_000_000;
6213 const FWD_NS: u64 = RTT_NS / 2;
6214 const RET_NS: u64 = RTT_NS / 2;
6215 /// bytes the app keeps outstanding, from the first packet on. Fixed and
6216 /// well below cwnd so the sender is application-limited, never cwnd-limited,
6217 /// keeping `full_bw_reached` false and STARTUP alive (cf. A.7). Comfortably
6218 /// above `min_pipe_cwnd` (4*MSS).
6219 const APP_WINDOW: u64 = 20 * MSS;
6220 /// round cap (~1 RTT each, so ~12 s): more than one ProbeRTTInterval (5 s)
6221 /// plus a ProbeRTTDuration (200 ms), enough to capture the interval-expiry
6222 /// interlude and its exit if the loop does not break earlier.
6223 const ROUNDS_CAP: u64 = 120;
6224
6225 // bottleneck serialization time for one MSS-sized packet
6226 let btl_service_ns: u64 = (MSS as f64 / BW * 1e9).round() as u64;
6227
6228 // Drive the production default configuration.
6229 let mut bbr = Bbr3::new(Arc::new(Bbr3Config::default()), MSS as u16);
6230 assert_eq!(bbr.state, BbrState::Startup);
6231
6232 let base = Instant::now();
6233 let at = |off_ns: u64| base + Duration::from_nanos(off_ns);
6234 let mut rtt_est = RttEstimator::new(Duration::from_nanos(RTT_NS));
6235
6236 struct InFlight {
6237 pn: u64,
6238 send_ns: u64,
6239 ack_ns: u64,
6240 }
6241 let mut flight: VecDeque<InFlight> = VecDeque::new();
6242
6243 let mut now_ns: u64 = 0;
6244 let mut next_send_ns: u64 = 0;
6245 // time at which the bottleneck finishes serving everything queued so far
6246 let mut btl_free_ns: u64 = 0;
6247 let mut inflight: u64 = 0;
6248 let mut pn: u64 = 0;
6249
6250 // Captured on the targeted PROBE_RTT entry edge (first entry at or after
6251 // ProbeRTTInterval, i.e. the genuine interval-expiry interlude, not the t~0
6252 // transient): (now_ns, cwnd_gain, full_bw_reached).
6253 let mut entry: Option<(u64, f64, bool)> = None;
6254 // Captured when probe_rtt_done_stamp is first armed inside that interlude
6255 // (C.inflight has drained below the ProbeRTT cwnd cap): (now_ns, round).
6256 let mut done_armed: Option<(u64, u64)> = None;
6257 // Captured on the PROBE_RTT -> STARTUP exit edge:
6258 // (now_ns, state, round, full_bw_reached).
6259 let mut exit: Option<(u64, BbrState, u64, bool)> = None;
6260 // Set the moment any forbidden (past-STARTUP) state is entered.
6261 let mut advanced_past_startup: Option<BbrState> = None;
6262 // Whether full_bw_reached was ever set (must stay false throughout).
6263 let mut full_bw_reached_ever = false;
6264
6265 for _ in 0..1_000_000 {
6266 let cwnd = bbr.window();
6267 // The app never wants more than APP_WINDOW outstanding.
6268 let window_cap = APP_WINDOW.min(cwnd);
6269 let can_send = inflight + MSS <= window_cap;
6270 let next_ack = flight.front().map(|p| p.ack_ns);
6271
6272 // Send whenever the small app window allows and a paced send is due no
6273 // later than the next ack; otherwise process an ack. The app window is
6274 // always the binding limit, not cwnd (cf. A.7).
6275 let do_send = can_send && next_ack.is_none_or(|ack| next_send_ns <= ack);
6276
6277 if do_send {
6278 now_ns = now_ns.max(next_send_ns);
6279 let send_ns = now_ns;
6280 let arrival = send_ns + FWD_NS;
6281 let service_start = arrival.max(btl_free_ns);
6282 let finish = service_start + btl_service_ns;
6283 btl_free_ns = finish;
6284 let ack_ns = finish + RET_NS;
6285
6286 bbr.on_packet_sent(at(send_ns), MSS as u16, pn, SpaceKind::Data);
6287 inflight += MSS;
6288 flight.push_back(InFlight {
6289 pn,
6290 send_ns,
6291 ack_ns,
6292 });
6293
6294 // Emulate MarkConnectionAppLimited so the next packet is stamped
6295 // app-limited at send time (same shape as A.7).
6296 bbr.app_limited = Ord::max(bbr.delivered + bbr.inflight, 1);
6297
6298 // pace the next send at BBR's chosen pacing rate
6299 let pacing = bbr.pacing_rate.max(1.0);
6300 next_send_ns = send_ns + (MSS as f64 / pacing * 1e9).round() as u64;
6301 pn += 1;
6302 } else if let Some(p) = flight.pop_front() {
6303 now_ns = now_ns.max(p.ack_ns);
6304 inflight -= MSS;
6305 rtt_est.update(Duration::ZERO, Duration::from_nanos(now_ns - p.send_ns));
6306 bbr.on_ack(
6307 at(now_ns),
6308 at(p.send_ns),
6309 MSS,
6310 p.pn,
6311 SpaceKind::Data,
6312 true,
6313 &rtt_est,
6314 );
6315 bbr.on_end_acks(at(now_ns), inflight, true, Some(p.pn), SpaceKind::Data);
6316
6317 full_bw_reached_ever |= bbr.full_bw_reached;
6318
6319 match bbr.state {
6320 BbrState::Startup => {
6321 // Once the targeted interlude has been entered, the next time
6322 // we are back in STARTUP is the PROBE_RTT -> STARTUP exit.
6323 if entry.is_some() && exit.is_none() {
6324 exit = Some((now_ns, bbr.state, bbr.round_count, bbr.full_bw_reached));
6325 }
6326 }
6327 BbrState::ProbeRtt => {
6328 // Target only the interval-expiry interlude (>= ProbeRTTInterval),
6329 // skipping the t~0 unset-stamp transient.
6330 if entry.is_none() && now_ns >= PROBE_RTT_INTERVAL_SEC * 1_000_000_000 {
6331 entry = Some((now_ns, bbr.cwnd_gain, bbr.full_bw_reached));
6332 }
6333 // The moment probe_rtt_done_stamp is armed inside that interlude:
6334 // C.inflight has drained below the ProbeRTT cwnd cap.
6335 if entry.is_some()
6336 && done_armed.is_none()
6337 && bbr.probe_rtt_done_stamp.is_some()
6338 {
6339 done_armed = Some((now_ns, bbr.round_count));
6340 }
6341 }
6342 // Any other state means STARTUP was actually left for the next
6343 // phase: the failure this test guards against.
6344 other => {
6345 advanced_past_startup.get_or_insert(other);
6346 }
6347 }
6348
6349 if advanced_past_startup.is_some()
6350 || exit.is_some()
6351 || bbr.round_count >= ROUNDS_CAP
6352 {
6353 break;
6354 }
6355 } else {
6356 panic!("simulation stalled: window full but nothing in flight");
6357 }
6358 }
6359
6360 // Never advanced past STARTUP: only STARTUP and the scheduled PROBE_RTT
6361 // min-RTT refresh were ever entered.
6362 assert!(
6363 advanced_past_startup.is_none(),
6364 "BBR left STARTUP for {:?} during the extended slow-STARTUP PROBE_RTT interlude",
6365 advanced_past_startup,
6366 );
6367 // The plateau path never armed: check_full_bw_reached short-circuits on
6368 // app-limited samples, so full_bw_reached stayed false: the precondition
6369 // for PROBE_RTT routing back to STARTUP rather than on to PROBE_BW.
6370 assert!(
6371 !full_bw_reached_ever,
6372 "full_bw_reached must never be set on application-limited samples"
6373 );
6374
6375 // Entered PROBE_RTT only after ProbeRTTInterval (5 s), still in STARTUP. `entry`
6376 // is only set inside the `>= ProbeRTTInterval` guard above, so a successful
6377 // `expect` already pins the interval-expiry timing.
6378 let (entry_ns, entry_cwnd_gain, entry_full_bw) =
6379 entry.expect("BBR never entered the interval-expiry PROBE_RTT during STARTUP");
6380 assert!(
6381 !entry_full_bw,
6382 "full_bw_reached should be false on the STARTUP-phase PROBE_RTT entry"
6383 );
6384 // cwnd_gain was set to ProbeRTTCwndGain (0.5) on entry.
6385 assert_eq!(
6386 entry_cwnd_gain, bbr.probe_rtt_cwnd_gain,
6387 "PROBE_RTT cwnd_gain should be ProbeRTTCwndGain"
6388 );
6389 assert_eq!(
6390 entry_cwnd_gain, PROBE_RTT_CWND_GAIN,
6391 "ProbeRTTCwndGain should be 0.5"
6392 );
6393
6394 // The ProbeRTTDuration clock was armed once inflight drained below the cap.
6395 let (done_ns, done_round) = done_armed.expect("PROBE_RTT never armed probe_rtt_done_stamp");
6396
6397 // Exited PROBE_RTT back to STARTUP (not PROBE_BW), full_bw_reached still false.
6398 let (exit_ns, exit_state, exit_round, exit_full_bw) =
6399 exit.expect("BBR never exited PROBE_RTT back to STARTUP");
6400 assert_eq!(
6401 exit_state,
6402 BbrState::Startup,
6403 "PROBE_RTT should exit back to STARTUP when full_bw_reached is false"
6404 );
6405 assert!(
6406 !exit_full_bw,
6407 "full_bw_reached should remain false across the PROBE_RTT -> STARTUP exit"
6408 );
6409
6410 // Held for at least ProbeRTTDuration (200 ms) after the clock was armed
6411 // (check_probe_rtt_done uses a strict `now > probe_rtt_done_stamp`)...
6412 assert!(
6413 exit_ns - done_ns >= PROBE_RTT_DURATION_MS * 1_000_000,
6414 "PROBE_RTT exited before ProbeRTTDuration elapsed \
6415 (held {} ns vs duration {} ms)",
6416 exit_ns - done_ns,
6417 PROBE_RTT_DURATION_MS
6418 );
6419 // ...and for at least one packet-timed round after arming.
6420 assert!(
6421 exit_round > done_round,
6422 "PROBE_RTT should hold at least one round after arming \
6423 (arm round {done_round} vs exit round {exit_round})"
6424 );
6425 // Sanity: entry preceded the exit.
6426 assert!(exit_ns > entry_ns);
6427
6428 // Ends in STARTUP, still searching for max bandwidth.
6429 assert_eq!(
6430 bbr.state,
6431 BbrState::Startup,
6432 "BBR should be back in STARTUP after the PROBE_RTT interlude"
6433 );
6434 }
6435
6436 /// A.21: Handling loss during PROBE_UP after `inflight_longterm` is set.
6437 /// equivalent to BBRHandleInflightTooHigh:
6438 /// <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.10.2-1>
6439 ///
6440 /// The counterpart to A.6 (loss in PROBE_UP while application-limited, where the
6441 /// `!is_app_limited` guard makes `handle_inflight_too_high` leave
6442 /// `inflight_longterm` untouched) and to A.18/A.19 (loss in PROBE_UP while
6443 /// `inflight_longterm` is still at its `u64::MAX` init, where the loss merely
6444 /// clamps it *from infinity* to a finite value for the first time). A.21 isolates
6445 /// the remaining case: a non-application-limited loss in PROBE_UP while
6446 /// `inflight_longterm` is **already established finite**, so the loss actively
6447 /// scales the standing long-term estimate *down* via the beta-scaled reduction
6448 /// rule `inflight_longterm = max(tx_in_flight, target_inflight * BETA)`.
6449 ///
6450 /// Loss cannot make `inflight_longterm` finite except through a loss: in PROBE_UP
6451 /// `adapt_long_term_model` short-circuits on `inflight_longterm == u64::MAX`, so
6452 /// nothing raises it until a first loss (`handle_inflight_too_high`) or the
6453 /// STARTUP high-loss escape seeds it. The scenario therefore runs two loss
6454 /// episodes over one non-app-limited, full-window flow (bottleneck bandwidth
6455 /// modest, as in A.18/A.19, so a short oldest-first loss burst trips
6456 /// `is_inflight_too_high`, `lost > LOSS_THRESH * tx_in_flight`):
6457 ///
6458 /// 1. Establish. Reach PROBE_UP loss-free, then declare the oldest in-flight packets lost
6459 /// until the accumulated loss trips `is_inflight_too_high`. `handle_inflight_too_high`
6460 /// clamps `inflight_longterm` from `u64::MAX` to a finite value and moves PROBE_UP ->
6461 /// PROBE_DOWN. Loss injection then stops; this is the "established `inflight_longterm`"
6462 /// precondition.
6463 /// 2. Ride loss-free back up. PROBE_DOWN -> (cruise) -> refill -> PROBE_UP again. With
6464 /// `inflight_longterm` now finite, `adapt_long_term_model` /
6465 /// `probe_inflight_long_term_upward` carry it forward (it only ever grows) as the standing
6466 /// long-term operating point.
6467 /// 3. Deciding loss. In this second PROBE_UP, before any bandwidth plateau forms
6468 /// (`start_probe_bw_up` resets `full_bw`, and a plateau needs `MAX_FULL_BW_COUNT` rounds,
6469 /// so injecting immediately keeps `full_bw_now` false, `BBRIsTimeToGoDown`/`maybe_go_down`
6470 /// never fires), declare the oldest in-flight packets lost until `is_inflight_too_high`
6471 /// trips again. Because the sample is non-app-limited, `handle_inflight_too_high` runs the
6472 /// reduction and resets `inflight_longterm` to `max(tx_in_flight, target_inflight * BETA)`
6473 /// (below the established value) then aborts PROBE_UP straight into PROBE_DOWN.
6474 ///
6475 /// Asserts on the deciding loss that: `inflight_longterm` was established finite
6476 /// (and, only ever growing, was still >= that value entering the loss); the
6477 /// deciding sample was non-app-limited; `inflight_longterm` was reset exactly to
6478 /// the beta-scaled rule `max(tx_in_flight, target_inflight * BETA)` and strictly
6479 /// lower than before the loss (scaled down); the exit was loss-driven, not the
6480 /// plateau path (`full_bw_now` false); and PROBE_UP aborted immediately to
6481 /// PROBE_DOWN.
6482 #[test]
6483 fn probe_up_loss_scales_down_established_inflight_longterm() {
6484 /// packet size in bytes
6485 const MSS: u64 = 1200;
6486 /// simulated propagation round-trip time (100ms), matching A.18/A.19
6487 const RTT_NS: u64 = 100_000_000;
6488 /// bottleneck bandwidth: 10 Mbit/s in bytes/sec. Modest BDP keeps `LOSS_THRESH`
6489 /// (2% of tx_in_flight) small, so a short oldest-first loss burst trips
6490 /// `is_inflight_too_high`.
6491 const BW: f64 = 1_250_000.0;
6492 const FWD_NS: u64 = RTT_NS / 2;
6493 const RET_NS: u64 = RTT_NS / 2;
6494
6495 // Seed the probe RNG so the PROBE_BW cycle timing (hence when each PROBE_UP is
6496 // entered) is deterministic.
6497 let seed: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
6498 let config = Bbr3Config {
6499 probe_rng_seed: Some(seed),
6500 ..Bbr3Config::default()
6501 };
6502 let mut bbr = Bbr3::new(Arc::new(config), MSS as u16);
6503 assert_eq!(bbr.state, BbrState::Startup);
6504
6505 let base = Instant::now();
6506 let at = |off_ns: u64| base + Duration::from_nanos(off_ns);
6507 let mut rtt_est = RttEstimator::new(Duration::from_nanos(RTT_NS));
6508
6509 struct InFlight {
6510 pn: u64,
6511 send_ns: u64,
6512 ack_ns: u64,
6513 }
6514 let mut flight: VecDeque<InFlight> = VecDeque::new();
6515
6516 let mut now_ns: u64 = 0;
6517 let mut next_send_ns: u64 = 0;
6518 let mut btl_free_ns: u64 = 0;
6519 let mut inflight: u64 = 0;
6520 let mut pn: u64 = 0;
6521 let btl_service_ns: u64 = (MSS as f64 / BW * 1e9).round() as u64;
6522
6523 // Episode 1: the value inflight_longterm was clamped to when the first PROBE_UP loss made
6524 // it finite (from u64::MAX). Marks the "established" precondition and switches
6525 // episode 1 off.
6526 let mut established: Option<u64> = None;
6527 // Episode 2 (deciding loss): (inflight_longterm before/after the deciding loss, the
6528 // deciding sample's tx_in_flight, target_inflight = min(bdp, cwnd) at the loss,
6529 // whether the sample was app-limited, full_bw_now at the edge, and the post-loss
6530 // state).
6531 let mut ep2: Option<(u64, u64, u64, u64, bool, bool, BbrState)> = None;
6532
6533 for _ in 0..5_000_000 {
6534 if ep2.is_some() {
6535 break;
6536 }
6537 let cwnd = bbr.window();
6538 let can_send = inflight + MSS <= cwnd;
6539 if !can_send {
6540 bbr.on_cwnd_limited();
6541 }
6542 let next_ack = flight.front().map(|p| p.ack_ns);
6543
6544 let in_up = bbr.state == BbrState::ProbeBw(ProbeBwSubstate::Up);
6545 // Drop (declare oldest-first lost) whenever in PROBE_UP and the relevant episode is
6546 // still pending: episode 1 while inflight_longterm is unset, episode 2
6547 // (still pending, ep2 None) on the next PROBE_UP. Between the two (any
6548 // non-PROBE_UP state) delivery is loss-free, so inflight_longterm carries
6549 // forward untouched and the flow rides back up to PROBE_UP.
6550 let dropping = in_up && (established.is_none() || ep2.is_none());
6551 let do_send = !dropping && can_send && next_ack.is_none_or(|ack| next_send_ns <= ack);
6552
6553 if do_send {
6554 let send_ns = now_ns.max(next_send_ns);
6555 now_ns = send_ns;
6556 let arrival = send_ns + FWD_NS;
6557 let service_start = arrival.max(btl_free_ns);
6558 let finish = service_start + btl_service_ns;
6559 btl_free_ns = finish;
6560 let ack_ns = finish + RET_NS;
6561
6562 bbr.on_packet_sent(at(send_ns), MSS as u16, pn, SpaceKind::Data);
6563 inflight += MSS;
6564 flight.push_back(InFlight {
6565 pn,
6566 send_ns,
6567 ack_ns,
6568 });
6569 let pacing = bbr.pacing_rate.max(1.0);
6570 next_send_ns = send_ns + (MSS as f64 / pacing * 1e9).round() as u64;
6571 pn += 1;
6572 } else if let Some(p) = flight.pop_front() {
6573 now_ns = now_ns.max(p.ack_ns);
6574 inflight -= MSS;
6575 if dropping {
6576 // The PROBE_UP -> PROBE_DOWN transition happens inside on_packet_lost (via
6577 // handle_inflight_too_high), never on an ack, so any Up->Down move here is
6578 // attributable to this loss. inflight_longterm is only touched by the tripping
6579 // loss (non-tripping burst losses leave it unchanged), so `before` captured
6580 // here is exactly the pre-reduction value.
6581 let before = bbr.inflight_longterm;
6582 let was_up = bbr.state == BbrState::ProbeBw(ProbeBwSubstate::Up);
6583 bbr.on_packet_lost(MSS as u16, p.pn, SpaceKind::Data, at(now_ns));
6584 if was_up && bbr.state != BbrState::ProbeBw(ProbeBwSubstate::Up) {
6585 if established.is_none() {
6586 // Episode 1: the first loss clamped inflight_longterm finite.
6587 established = Some(bbr.inflight_longterm);
6588 } else {
6589 // Episode 2: the deciding loss scaled the established value down. Read
6590 // the formula inputs (tx_in_flight was set
6591 // to inflight_at_loss, target_inflight
6592 // = min(bdp, cwnd)) live: set_cwnd does not run inside the loss path,
6593 // so bdp/cwnd match what
6594 // handle_inflight_too_high used.
6595 let txif = bbr.rs.map(|rs| rs.tx_in_flight).unwrap();
6596 let target = Ord::min(bbr.bdp, bbr.cwnd);
6597 ep2 = Some((
6598 before,
6599 bbr.inflight_longterm,
6600 txif,
6601 target,
6602 bbr.rs.is_some_and(|rs| rs.is_app_limited),
6603 bbr.full_bw_now,
6604 bbr.state,
6605 ));
6606 }
6607 }
6608 } else {
6609 rtt_est.update(Duration::ZERO, Duration::from_nanos(now_ns - p.send_ns));
6610 bbr.on_ack(
6611 at(now_ns),
6612 at(p.send_ns),
6613 MSS,
6614 p.pn,
6615 SpaceKind::Data,
6616 false,
6617 &rtt_est,
6618 );
6619 bbr.on_end_acks(at(now_ns), inflight, false, Some(p.pn), SpaceKind::Data);
6620 }
6621 } else {
6622 panic!("simulation stalled: window full but nothing in flight");
6623 }
6624 }
6625
6626 let established =
6627 established.expect("episode 1 never established a finite inflight_longterm");
6628 let (before, after, txif, target, app_lim, full_bw_now, post_state) =
6629 ep2.expect("episode 2 deciding loss never fired out of PROBE_UP");
6630
6631 // Precondition: inflight_longterm was established finite by episode 1, and (only ever
6632 // growing between the episodes, loss-free) was still >= that value entering the
6633 // deciding loss.
6634 assert!(
6635 established < u64::MAX,
6636 "episode 1 should have clamped inflight_longterm finite"
6637 );
6638 assert!(
6639 before >= established && before < u64::MAX,
6640 "the standing inflight_longterm entering the deciding loss should be the established \
6641 finite value carried forward (before {before} vs established {established})"
6642 );
6643
6644 // The deciding loss sample was non-application-limited, so handle_inflight_too_high runs
6645 // the reduction rather than skipping it (the A.6 path).
6646 assert!(
6647 !app_lim,
6648 "deciding loss sample should be non-application-limited so the reduction applies"
6649 );
6650
6651 // handle_inflight_too_high reset inflight_longterm to the beta-scaled rule
6652 // max(tx_in_flight, target_inflight * BETA)...
6653 assert_eq!(
6654 after,
6655 Ord::max(txif, (target as f64 * BETA) as u64),
6656 "inflight_longterm should be reset to max(tx_in_flight, target_inflight * BETA)"
6657 );
6658 // ...scaling the established estimate strictly down.
6659 assert!(
6660 after < before,
6661 "inflight_longterm should be scaled down from its established value \
6662 (after {after} vs before {before})"
6663 );
6664
6665 // Loss, not the plateau path, drove the exit: the deciding loss was injected before any
6666 // bandwidth plateau formed, so full_bw_now (BBRIsTimeToGoDown's signal) never got set.
6667 assert!(
6668 !full_bw_now,
6669 "expected a loss-driven exit, but the plateau signal full_bw_now was set"
6670 );
6671 // PROBE_UP aborted immediately into PROBE_DOWN.
6672 assert_eq!(
6673 post_state,
6674 BbrState::ProbeBw(ProbeBwSubstate::Down),
6675 "the loss should abort PROBE_UP straight to PROBE_DOWN via handle_inflight_too_high"
6676 );
6677 }
6678
6679 /// A.22: Handling application-limited sending during PROBE_REFILL.
6680 /// equivalent to BBRUpdateMaxBw <https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-05.html#section-5.5.5>
6681 ///
6682 /// The `!is_app_limited` half of the `update_max_bw` guard
6683 /// (`delivery_rate >= BBR.max_bw || !RS.is_app_limited`) in isolation: an
6684 /// application-limited delivery-rate sample whose rate is *below* the standing
6685 /// `max_bw` must NOT be folded into the max-bandwidth filter, so a pause in the
6686 /// application during a probe cannot pull the bandwidth estimate down toward the
6687 /// artificially low app-limited rate. (The complementary half, an app-limited
6688 /// sample that is `>= max_bw` still trusted to raise it, is not exercised
6689 /// here; only the "ignore low app-limited samples" behavior is.)
6690 ///
6691 /// Same single-bottleneck simulator as A.5/A.6, run in two phases:
6692 /// 1. Not application-limited, no loss, full-cwnd until the flow cycles STARTUP -> DRAIN ->
6693 /// PROBE_BW, which establishes `max_bw` at ~`BW` (set from the non-app-limited STARTUP
6694 /// delivery-rate samples).
6695 /// 2. The moment PROBE_BW is entered the app is throttled to a small fixed window
6696 /// (`APP_WINDOW`, far below the ~1*BDP..2*BDP cwnd) and every sent packet is stamped
6697 /// app-limited, exactly as A.6. The pipe drains to `APP_WINDOW` during the PROBE_DOWN
6698 /// phase, so by the time the probe timer fires the cycle into PROBE_REFILL every in-flight
6699 /// sample is app-limited and its delivery rate (~`APP_WINDOW / RTT`) sits well below
6700 /// `max_bw`.
6701 ///
6702 /// Across that PROBE_REFILL round trip each ack carries an app-limited sample
6703 /// with `RS.delivery_rate < BBR.max_bw`, so `update_max_bw`'s guard is false and
6704 /// `max_bw` is left untouched, asserted both per-ack (the estimate does not move
6705 /// across any blocked sample) and across the whole round (its value on entering
6706 /// PROBE_UP equals its value on entering PROBE_REFILL). Note the max-bw filter's
6707 /// cycle counter only advances on non-app-limited round-start samples
6708 /// (`adapt_long_term_model`), so the established estimate cannot even age out
6709 /// while the app stays limited.
6710 ///
6711 /// PROBE_REFILL then advances to PROBE_UP on its round boundary
6712 /// (`update_probe_bw_cycle_phase`). At that edge the app un-pauses (full sending
6713 /// resumes); a purely app-limited PROBE_UP would never plateau nor be
6714 /// cwnd-limited (`maybe_go_down` could never fire), so resuming is what lets
6715 /// probing proceed. Asserts the estimate survived (`max_bw` still ~`BW`, never
6716 /// collapsed toward the app-limited rate) and the ProbeBW cycle keeps turning:
6717 /// the flow leaves that PROBE_UP and re-enters a fresh one. (On this constant-RTT,
6718 /// infinite-buffer link PROBE_UP is exited by the periodic min-RTT probe rather
6719 /// than a queue plateau, so the re-probe is the "still probing" signal.)
6720 #[test]
6721 fn probe_refill_ignores_app_limited_low_bw_samples() {
6722 /// packet size in bytes
6723 const MSS: u64 = 1200;
6724 /// simulated bottleneck bandwidth: 100 Mbit/s in bytes/sec
6725 const BW: f64 = 12_500_000.0;
6726 /// simulated propagation round-trip time (100ms), matching A.5/A.6
6727 const RTT_NS: u64 = 100_000_000;
6728 const FWD_NS: u64 = RTT_NS / 2;
6729 const RET_NS: u64 = RTT_NS / 2;
6730 /// application window used once PROBE_BW is reached: bytes the app keeps
6731 /// outstanding. Fixed and far below the PROBE_BW cwnd (~1000 packets here) so
6732 /// the sender is application-limited (never cwnd-limited) and the resulting
6733 /// delivery-rate samples (~`APP_WINDOW / RTT`) sit well below `max_bw`.
6734 /// Matches A.6's window.
6735 const APP_WINDOW: u64 = 200 * MSS;
6736
6737 // bottleneck serialization time for one MSS-sized packet
6738 let btl_service_ns: u64 = (MSS as f64 / BW * 1e9).round() as u64;
6739
6740 // Production default, but pin the probe RNG: once app-limiting lifts at the
6741 // PROBE_REFILL -> PROBE_UP edge the flow does genuine PROBE_BW cycling, whose
6742 // phase timing is RNG-driven, and default() seeds from entropy (cf. sibling
6743 // probing tests).
6744 let config = Bbr3Config {
6745 probe_rng_seed: Some([6; 16]),
6746 ..Bbr3Config::default()
6747 };
6748 let mut bbr = Bbr3::new(Arc::new(config), MSS as u16);
6749 assert_eq!(bbr.state, BbrState::Startup);
6750
6751 let base = Instant::now();
6752 let at = |off_ns: u64| base + Duration::from_nanos(off_ns);
6753 let mut rtt_est = RttEstimator::new(Duration::from_nanos(RTT_NS));
6754
6755 struct InFlight {
6756 pn: u64,
6757 send_ns: u64,
6758 ack_ns: u64,
6759 }
6760 let mut flight: VecDeque<InFlight> = VecDeque::new();
6761
6762 let mut now_ns: u64 = 0;
6763 let mut next_send_ns: u64 = 0;
6764 // time at which the bottleneck finishes serving everything queued so far
6765 let mut btl_free_ns: u64 = 0;
6766 let mut inflight: u64 = 0;
6767 let mut pn: u64 = 0;
6768
6769 // Phase 2 begins once PROBE_BW is entered: from then the app is limited to
6770 // APP_WINDOW and every sent packet is stamped app-limited. Flipped back off at
6771 // the PROBE_REFILL -> PROBE_UP edge so subsequent probing runs full-cwnd.
6772 let mut app_limited_phase = false;
6773 // One-shot latch: arm app-limiting exactly once, on first PROBE_BW entry.
6774 // Otherwise the check below re-fires next ack (still PROBE_BW) and undoes the
6775 // reset at the REFILL -> UP edge, trapping the flow app-limited forever.
6776 let mut app_limited_armed = false;
6777
6778 // max_bw sampled right as PROBE_REFILL is entered and right as it advances to
6779 // PROBE_UP; equal iff the app-limited round left the estimate untouched.
6780 let mut max_bw_at_refill_entry: Option<f64> = None;
6781 let mut max_bw_at_up: Option<f64> = None;
6782 // app-limited PROBE_REFILL samples whose delivery_rate < max_bw (the guard's
6783 // target case), and the largest such rate seen (to show it really was below
6784 // max_bw). Each of these acks is asserted inline to not move max_bw.
6785 let mut blocked_samples: u64 = 0;
6786 let mut max_app_limited_dr: f64 = 0.0;
6787 // Whether the flow advanced PROBE_REFILL -> PROBE_UP, then (after full sending
6788 // resumed) reached the plateau-driven PROBE_UP -> PROBE_DOWN exit.
6789 let mut refill_to_up = false;
6790 // Set once the flow leaves that first post-refill PROBE_UP, then again when it
6791 // re-enters a fresh PROBE_UP, i.e. the ProbeBW cycle kept turning.
6792 let mut left_post_refill_up = false;
6793 let mut post_refill_reprobed = false;
6794 let mut max_bw_final: f64 = 0.0;
6795
6796 for _ in 0..3_000_000 {
6797 if post_refill_reprobed {
6798 break;
6799 }
6800 let cwnd = bbr.window();
6801 let window_cap = if app_limited_phase {
6802 APP_WINDOW.min(cwnd)
6803 } else {
6804 cwnd
6805 };
6806 let can_send = inflight + MSS <= window_cap;
6807 // In the full-cwnd phases a blocked send is a genuine cwnd limit; in the
6808 // app-limited phase the small window is the binding limit, not cwnd, so it
6809 // must not be reported as cwnd-limited.
6810 if !app_limited_phase && !can_send {
6811 bbr.on_cwnd_limited();
6812 }
6813 let next_ack = flight.front().map(|p| p.ack_ns);
6814
6815 let do_send = can_send && next_ack.is_none_or(|ack| next_send_ns <= ack);
6816
6817 if do_send {
6818 now_ns = now_ns.max(next_send_ns);
6819 let send_ns = now_ns;
6820 // enqueue at the FIFO bottleneck, served at BW
6821 let arrival = send_ns + FWD_NS;
6822 let service_start = arrival.max(btl_free_ns);
6823 let finish = service_start + btl_service_ns;
6824 btl_free_ns = finish;
6825 let ack_ns = finish + RET_NS;
6826
6827 bbr.on_packet_sent(at(send_ns), MSS as u16, pn, SpaceKind::Data);
6828 inflight += MSS;
6829 flight.push_back(InFlight {
6830 pn,
6831 send_ns,
6832 ack_ns,
6833 });
6834
6835 if app_limited_phase {
6836 // Emulate MarkConnectionAppLimited so the next packet is stamped
6837 // app-limited at send time. Same shape as A.2/A.6.
6838 bbr.app_limited = Ord::max(bbr.delivered + bbr.inflight, 1);
6839 }
6840
6841 // pace the next send at BBR's chosen pacing rate
6842 let pacing = bbr.pacing_rate.max(1.0);
6843 next_send_ns = send_ns + (MSS as f64 / pacing * 1e9).round() as u64;
6844 pn += 1;
6845 } else if let Some(p) = flight.pop_front() {
6846 now_ns = now_ns.max(p.ack_ns);
6847 inflight -= MSS;
6848 rtt_est.update(Duration::ZERO, Duration::from_nanos(now_ns - p.send_ns));
6849
6850 // update_max_bw runs inside on_ack, on the sample already pending from
6851 // the previous on_end_acks. Snapshot max_bw and state around on_ack, and
6852 // read that sample (bbr.rs) between on_ack and on_end_acks: that is
6853 // exactly what update_max_bw's guard consumed.
6854 let state_before = bbr.state;
6855 let max_bw_before = bbr.max_bw;
6856 bbr.on_ack(
6857 at(now_ns),
6858 at(p.send_ns),
6859 MSS,
6860 p.pn,
6861 SpaceKind::Data,
6862 app_limited_phase,
6863 &rtt_est,
6864 );
6865 let max_bw_after = bbr.max_bw;
6866 let state_after = bbr.state;
6867 let sample = bbr.rs;
6868 bbr.on_end_acks(
6869 at(now_ns),
6870 inflight,
6871 app_limited_phase,
6872 Some(p.pn),
6873 SpaceKind::Data,
6874 );
6875
6876 // Flip to the application-limited phase the moment PROBE_BW is entered,
6877 // so the pipe drains to APP_WINDOW during PROBE_DOWN and the first
6878 // PROBE_REFILL round is entirely app-limited. Same timing as A.6.
6879 if !app_limited_armed && matches!(bbr.state, BbrState::ProbeBw(_)) {
6880 app_limited_armed = true;
6881 app_limited_phase = true;
6882 }
6883
6884 // Capture max_bw as the first PROBE_REFILL is entered.
6885 if state_after == BbrState::ProbeBw(ProbeBwSubstate::Refill)
6886 && max_bw_at_refill_entry.is_none()
6887 {
6888 max_bw_at_refill_entry = Some(bbr.max_bw);
6889 }
6890
6891 // Acks processed while already in PROBE_REFILL carry this round's
6892 // samples. Every one is app-limited and low, so the guard must leave
6893 // max_bw untouched.
6894 if state_before == BbrState::ProbeBw(ProbeBwSubstate::Refill) {
6895 if let Some(rs) = sample
6896 .filter(|rs| rs.is_app_limited)
6897 .filter(|rs| rs.delivery_rate > 0.0)
6898 .filter(|rs| rs.delivery_rate < max_bw_before)
6899 {
6900 blocked_samples += 1;
6901 max_app_limited_dr = max_app_limited_dr.max(rs.delivery_rate);
6902 assert_eq!(
6903 max_bw_after, max_bw_before,
6904 "a low app-limited PROBE_REFILL sample \
6905 (delivery_rate {} < max_bw {max_bw_before}) must not update max_bw",
6906 rs.delivery_rate
6907 );
6908 }
6909 // PROBE_REFILL -> PROBE_UP fires on this round's boundary ack. Snapshot
6910 // max_bw and un-pause the app so subsequent probing runs full-cwnd.
6911 if state_after == BbrState::ProbeBw(ProbeBwSubstate::Up) && !refill_to_up {
6912 refill_to_up = true;
6913 max_bw_at_up = Some(bbr.max_bw);
6914 app_limited_phase = false;
6915 }
6916 }
6917
6918 // Subsequent probing: with full sending resumed, the ProbeBW cycle must
6919 // keep turning. Once the flow leaves the first post-refill PROBE_UP and
6920 // then re-enters a fresh PROBE_UP, it is probing normally again. (This
6921 // constant-RTT, infinite-buffer link exits PROBE_UP via the periodic
6922 // min-RTT probe rather than a queue plateau, so the re-probe (not a
6923 // PROBE_UP -> PROBE_DOWN edge) is the signal to check.)
6924 if refill_to_up && state_after != BbrState::ProbeBw(ProbeBwSubstate::Up) {
6925 left_post_refill_up = true;
6926 }
6927 if left_post_refill_up && state_after == BbrState::ProbeBw(ProbeBwSubstate::Up) {
6928 post_refill_reprobed = true;
6929 max_bw_final = bbr.max_bw;
6930 }
6931 } else {
6932 panic!("simulation stalled: window full but nothing in flight");
6933 }
6934 }
6935
6936 let max_bw_at_refill_entry =
6937 max_bw_at_refill_entry.expect("flow never entered PROBE_REFILL");
6938 let max_bw_at_up = max_bw_at_up.expect("PROBE_REFILL never advanced to PROBE_UP");
6939
6940 // Precondition: PROBE_REFILL was reached with max_bw established at ~BW by the
6941 // non-app-limited STARTUP samples.
6942 let entry_err = (max_bw_at_refill_entry - BW).abs() / BW;
6943 assert!(
6944 entry_err < 0.05,
6945 "max_bw entering PROBE_REFILL ({max_bw_at_refill_entry}) should be within 5% of the \
6946 simulated {BW} (rel err {entry_err})"
6947 );
6948
6949 // The app-limited round produced the guard's target case: samples flagged
6950 // app-limited with delivery_rate strictly below max_bw.
6951 assert!(
6952 blocked_samples > 0,
6953 "expected app-limited PROBE_REFILL samples with delivery_rate < max_bw"
6954 );
6955 assert!(
6956 max_app_limited_dr < max_bw_at_refill_entry,
6957 "the app-limited PROBE_REFILL rate ({max_app_limited_dr}) should sit below max_bw \
6958 ({max_bw_at_refill_entry})"
6959 );
6960
6961 // Across the whole PROBE_REFILL round the artificially low app-limited samples
6962 // left max_bw exactly unchanged (the guard rejected every one; the max-bw
6963 // filter's cycle counter also never advanced on app-limited samples, so the
6964 // estimate could not age out either).
6965 assert_eq!(
6966 max_bw_at_up, max_bw_at_refill_entry,
6967 "max_bw must be unchanged across the app-limited PROBE_REFILL round \
6968 (entry {max_bw_at_refill_entry}, PROBE_UP {max_bw_at_up})"
6969 );
6970
6971 // Subsequent probing was handled correctly: PROBE_REFILL advanced to PROBE_UP
6972 // and, once full sending resumed, the ProbeBW cycle kept turning: the flow
6973 // left that PROBE_UP and re-entered a fresh one, with the bandwidth estimate
6974 // intact (~BW, never collapsed to the app-limited rate).
6975 assert!(
6976 refill_to_up,
6977 "PROBE_REFILL should advance to PROBE_UP after its round trip"
6978 );
6979 assert!(
6980 post_refill_reprobed,
6981 "flow should keep probing: re-enter PROBE_UP after the PROBE_REFILL round"
6982 );
6983 // "Estimate intact" bar: genuine PROBE_BW cycling leaves max_bw a few % below the
6984 // bottleneck (samples taken while pacing_gain < 1), so this guards against collapse
6985 // to the app-limited rate (~0.19*BW), not tight tracking. 10% keeps ~4.7x margin;
6986 // exact preservation across the app-limited round is asserted strictly above.
6987 let final_err = (max_bw_final - BW).abs() / BW;
6988 assert!(
6989 final_err < 0.10,
6990 "max_bw after subsequent probing ({max_bw_final}) should still be within 10% of the \
6991 simulated {BW} (rel err {final_err})"
6992 );
6993 }
6994}