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}