Skip to main content

subetha_cxc/
path_model_sensor.rs

1//! BBR-style passive path model: bottleneck bandwidth, round-trip
2//! propagation delay, and the bandwidth-delay product, all recovered from
3//! the ACK stream the reliable-UDP sender already drives - no probe traffic.
4//!
5//! The model follows the two-filter structure of BBR (Cardwell, Cheng, Gunn,
6//! Yeganeh, Jacobson, *BBR: Congestion-Based Congestion Control*, ACM Queue
7//! 2016) and its delivery-rate estimator (Cheng, Cardwell, Yeganeh, Jacobson,
8//! *Delivery Rate Estimation*, `draft-cheng-iccrg-delivery-rate-estimation`):
9//!
10//! - **`BtlBw`** is a windowed *maximum* of the delivery rate. A bottleneck
11//!   queue can delay delivery but cannot make data arrive faster than the
12//!   link carries it, so the peak delivery rate over a window of round-trips
13//!   is the true bottleneck capacity. The max filter rejects the
14//!   under-estimates a filling or draining queue injects.
15//! - **`RTprop`** is a windowed *minimum* of the round-trip time. A queue
16//!   inflates RTT, so the minimum over a long window is the queue-free
17//!   propagation delay.
18//!
19//! Because a queue moves the two estimates in opposite directions (it lifts
20//! RTT but never lifts the delivery rate), the max-rate / min-RTT pair
21//! separates capacity from delay from a single passive ACK stream. Their
22//! product is the bandwidth-delay product `BDP = BtlBw * RTprop`, the
23//! in-flight window that keeps the bottleneck busy without standing queue.
24//!
25//! The estimator is fed connection-level samples: each ACK reports the
26//! cumulative `delivered` count and the time, and the rate sample is the
27//! delivered delta over the time delta (the `ack_elapsed` rate of the
28//! delivery-rate draft). The window for `BtlBw` tracks ~10 round-trips of the
29//! current `RTprop`, BBR's round-counted bandwidth window, clamped to a sane
30//! range so a brief idle does not discard the estimate.
31//!
32//! It is a pure model: [`PathModel::on_ack`] takes the cumulative delivered
33//! count, a timestamp, and an RTT, so a synthetic ACK trace exercises it
34//! deterministically and the live sender feeds it from real feedback.
35
36use std::collections::VecDeque;
37
38/// `RTprop` is held over a 10-second window, matching BBR's `RTpropFilterLen`:
39/// long enough to span the lulls between an application's bursts, short enough
40/// that a genuine route change is adopted within seconds.
41const RTPROP_WINDOW_US: u64 = 10_000_000;
42
43/// `BtlBw`'s window is ~10 round-trips of the current `RTprop` (BBR's
44/// `BtlBwFilterLen`), so the bandwidth estimate spans enough round-trips to
45/// see the bottleneck's peak but adapts when capacity drops.
46const BW_WINDOW_RTTS: u64 = 10;
47
48/// Floor on the `BtlBw` window so a brief application-limited idle (a short
49/// gap with no delivery) does not age out the last good bandwidth sample.
50const BW_WINDOW_MIN_US: u64 = 200_000;
51
52/// Ceiling on the `BtlBw` window: even at a very large `RTprop` the bandwidth
53/// estimate should not stretch past the `RTprop` window itself.
54const BW_WINDOW_MAX_US: u64 = 10_000_000;
55
56/// Floor on the delivery-rate *sample* window. Each rate sample spans at least
57/// one `RTprop` but never less than this, so a burst of coalesced ACKs (many
58/// blocks reported at once after the receiver was briefly silent, or after the
59/// unpaced sender bursts into its socket buffer) is averaged over a meaningful
60/// interval instead of dividing a large delivered jump by a near-zero gap.
61const BW_SAMPLE_FLOOR_US: u64 = 20_000;
62
63/// Most backhaul hops the mesh-hop detector reports (8x throughput reduction).
64const MAX_BACKHAUL_HOPS: u8 = 3;
65
66/// `mcs_norm` above which the first hop is judged healthy for mesh detection:
67/// the local radio is fine, so a low end-to-end `BtlBw` is a downstream
68/// backhaul hop, not a weak first hop.
69const MESH_MCS_HEALTHY: f32 = 0.5;
70
71/// Congestion share above which a low `BtlBw` is judged congestion, not a mesh
72/// hop: a congested shared link shows the rising-delay, congestion-classed loss
73/// the item-3 classifier flags, where a structural backhaul hop does not.
74const MESH_MAX_CONGESTION: f32 = 0.5;
75
76/// Sliding-window maximum over timestamped samples, via a monotonic deque:
77/// samples are kept in decreasing-value, increasing-time order, so the front
78/// is always the current maximum. A new sample evicts every older sample no
79/// greater than it (a newer-or-equal value dominates them for all future
80/// windows), so the deque stays short and `get` is O(1). This is the exact
81/// windowed-max filter BBR uses for `BtlBw`.
82struct WindowedMax {
83    samples: VecDeque<(u64, u64)>,
84    window_us: u64,
85}
86
87impl WindowedMax {
88    fn new(window_us: u64) -> Self {
89        Self { samples: VecDeque::new(), window_us }
90    }
91
92    /// Record `value` at `now_us` (timestamps must be non-decreasing).
93    fn push(&mut self, now_us: u64, value: u64) {
94        while let Some(&(_, back)) = self.samples.back() {
95            if back <= value {
96                self.samples.pop_back();
97            } else {
98                break;
99            }
100        }
101        self.samples.push_back((now_us, value));
102        self.expire(now_us);
103    }
104
105    /// Drop samples older than the window relative to `now_us`. The front is
106    /// both the oldest and the maximum, so expiring it promotes the next
107    /// largest in-window sample.
108    fn expire(&mut self, now_us: u64) {
109        let cutoff = now_us.saturating_sub(self.window_us);
110        while let Some(&(t, _)) = self.samples.front() {
111            if t < cutoff {
112                self.samples.pop_front();
113            } else {
114                break;
115            }
116        }
117    }
118
119    fn set_window(&mut self, window_us: u64) {
120        self.window_us = window_us;
121    }
122
123    fn get(&self) -> u64 {
124        self.samples.front().map(|&(_, v)| v).unwrap_or(0)
125    }
126}
127
128/// Sliding-window minimum, the mirror of [`WindowedMax`]: samples kept in
129/// increasing-value, increasing-time order so the front is the current
130/// minimum. Used for `RTprop`.
131struct WindowedMin {
132    samples: VecDeque<(u64, u64)>,
133    window_us: u64,
134}
135
136impl WindowedMin {
137    fn new(window_us: u64) -> Self {
138        Self { samples: VecDeque::new(), window_us }
139    }
140
141    fn push(&mut self, now_us: u64, value: u64) {
142        while let Some(&(_, back)) = self.samples.back() {
143            if back >= value {
144                self.samples.pop_back();
145            } else {
146                break;
147            }
148        }
149        self.samples.push_back((now_us, value));
150        let cutoff = now_us.saturating_sub(self.window_us);
151        while let Some(&(t, _)) = self.samples.front() {
152            if t < cutoff {
153                self.samples.pop_front();
154            } else {
155                break;
156            }
157        }
158    }
159
160    fn get(&self) -> u64 {
161        self.samples.front().map(|&(_, v)| v).unwrap_or(0)
162    }
163}
164
165/// Passive BBR path model. Holds the windowed `BtlBw` / `RTprop` estimates and
166/// exposes them plus the derived BDP. Sized in *blocks* of `block_bytes` so
167/// the sender can read the BDP directly as a flow-window target.
168pub struct PathModel {
169    block_bytes: u64,
170    btlbw: WindowedMax,
171    rtprop: WindowedMin,
172    /// Anchor `(delivered_blocks, ack_time_us, newest_send_us)` of the current
173    /// rate-sample window: the delivered count, the ACK arrival time, and the
174    /// send time of the newest delivered block, all as of the last emitted
175    /// sample. The window grows from here until at least one `RTprop` (floored)
176    /// has elapsed; the rate divides by `max(ack_window, send_span)` so neither
177    /// a burst of coalesced ACKs nor an in-order frontier leap (a retransmit
178    /// unblocking a backlog) can fabricate a peak. `None` until the first ACK
179    /// anchors it.
180    sample_anchor: Option<(u64, u64, u64)>,
181    /// Smoothed recent RTT (SRTT, microseconds); the "RTT_now" the standing-
182    /// queue / bufferbloat estimate compares against `RTprop`. 0 until the
183    /// first RTT sample.
184    srtt_us: u64,
185    /// Running sum and count of RTT samples, for the mean RTT under load - the
186    /// sustained-latency metric a bufferbloat pacer is judged by (the min RTT
187    /// alone only shows the best moment).
188    rtt_sum_us: u64,
189    rtt_n: u64,
190}
191
192impl PathModel {
193    /// New model whose BDP is reported in blocks of `block_bytes` (the data
194    /// payload per block: `k * item_bytes`, excluding parity and headers, so
195    /// the estimate is goodput, not wire rate).
196    pub fn new(block_bytes: usize) -> Self {
197        Self {
198            block_bytes: (block_bytes as u64).max(1),
199            btlbw: WindowedMax::new(BW_WINDOW_MIN_US),
200            rtprop: WindowedMin::new(RTPROP_WINDOW_US),
201            sample_anchor: None,
202            srtt_us: 0,
203            rtt_sum_us: 0,
204            rtt_n: 0,
205        }
206    }
207
208    /// Fold in one ACK: `delivered_blocks` is the cumulative count the
209    /// receiver has delivered, `now_us` the arrival time, `rtt_us` the
210    /// round-trip time the just-delivered block measured (0 if none), and
211    /// `newest_send_us` the send time of the newest block this ACK delivered.
212    ///
213    /// A delivery-rate sample is emitted only once the window from the anchor
214    /// spans at least one `RTprop` (floored at `BW_SAMPLE_FLOOR_US`); its
215    /// rate is the delivered bytes over `max(ack_window, send_span)`. Anchoring
216    /// at the last emitted sample - not the previous ACK - averages a run of
217    /// coalesced ACKs over the real interval they cover; dividing by the
218    /// send-span (the spread of send times across the delivered blocks) caps an
219    /// in-order frontier leap - a retransmit unblocking a buffered backlog - at
220    /// the rate the blocks were actually sent. Neither can fabricate a peak for
221    /// the max filter to latch onto. This is BBR's `max(ack_elapsed,
222    /// send_elapsed)` delivery-rate guard, per round trip, on an unpaced sender.
223    ///
224    /// `delivered_blocks` must be non-decreasing and `now_us` monotonic.
225    pub fn on_ack(&mut self, delivered_blocks: u64, now_us: u64, rtt_us: u64, newest_send_us: u64) {
226        if rtt_us > 0 {
227            self.rtprop.push(now_us, rtt_us);
228            // Smoothed recent RTT (RFC 6298 SRTT, alpha = 1/8) - the "RTT_now"
229            // the standing-queue estimate compares against RTprop.
230            self.srtt_us = if self.srtt_us == 0 {
231                rtt_us
232            } else {
233                self.srtt_us - (self.srtt_us >> 3) + (rtt_us >> 3)
234            };
235            self.rtt_sum_us += rtt_us;
236            self.rtt_n += 1;
237            // Track ~10 round-trips of the current RTprop, clamped.
238            let window =
239                (BW_WINDOW_RTTS * self.rtprop.get()).clamp(BW_WINDOW_MIN_US, BW_WINDOW_MAX_US);
240            self.btlbw.set_window(window);
241            self.btlbw.expire(now_us);
242        }
243        match self.sample_anchor {
244            None => self.sample_anchor = Some((delivered_blocks, now_us, newest_send_us)),
245            Some((anchor_d, anchor_t, anchor_send)) => {
246                let min_window = self.rtprop.get().max(BW_SAMPLE_FLOOR_US);
247                if delivered_blocks > anchor_d && now_us >= anchor_t + min_window {
248                    let delta = delivered_blocks - anchor_d;
249                    let data = delta.saturating_mul(self.block_bytes);
250                    // Data cannot be delivered faster than it was sent: divide by
251                    // the larger of the ACK window and the send span.
252                    let ack_window = now_us - anchor_t;
253                    let send_span = newest_send_us.saturating_sub(anchor_send);
254                    let interval = ack_window.max(send_span).max(1);
255                    let rate = data.saturating_mul(1_000_000) / interval;
256                    self.btlbw.push(now_us, rate);
257                    self.sample_anchor = Some((delivered_blocks, now_us, newest_send_us));
258                }
259            }
260        }
261    }
262
263    /// Bottleneck bandwidth estimate in bits per second.
264    pub fn btlbw_bps(&self) -> u64 {
265        self.btlbw.get().saturating_mul(8)
266    }
267
268    /// Round-trip propagation delay estimate in microseconds (0 until the
269    /// first RTT sample).
270    pub fn rtprop_us(&self) -> u64 {
271        self.rtprop.get()
272    }
273
274    /// Smoothed recent round-trip time in microseconds (SRTT, 0 until the first
275    /// RTT sample) - the "RTT_now" of the standing-queue estimate.
276    pub fn rtt_now_us(&self) -> u64 {
277        self.srtt_us
278    }
279
280    /// Mean RTT in microseconds across all samples - the sustained latency under
281    /// load. A bufferbloat pacer is judged by how far this sits below the
282    /// un-paced mean (the min RTT alone only shows the best moment).
283    pub fn rtt_mean_us(&self) -> u64 {
284        self.rtt_sum_us / self.rtt_n.max(1)
285    }
286
287    /// Self-induced queue delay in microseconds: `RTT_now - RTprop`. A
288    /// sustained value above ~25 ms during our own transfer is bufferbloat we
289    /// are causing - the signal to pace down rather than blast. 0 before the
290    /// first RTT sample, and clamped at 0 (the smoothed RTT can dip a hair
291    /// below the windowed-min RTprop between samples).
292    pub fn queue_delay_us(&self) -> u64 {
293        self.srtt_us.saturating_sub(self.rtprop.get())
294    }
295
296    /// Bandwidth-delay product in bytes (`BtlBw * RTprop`).
297    pub fn bdp_bytes(&self) -> u64 {
298        self.btlbw.get().saturating_mul(self.rtprop.get()) / 1_000_000
299    }
300
301    /// Bandwidth-delay product in blocks - the in-flight window that keeps the
302    /// bottleneck busy with no standing queue.
303    pub fn bdp_blocks(&self) -> u64 {
304        self.bdp_bytes() / self.block_bytes
305    }
306
307    /// Estimated number of Wi-Fi backhaul hops (0..=3) behind the first hop.
308    ///
309    /// A single-radio repeater receives then retransmits on the SAME channel;
310    /// carrier-sense self-interference roughly halves throughput per hop. So
311    /// with `nominal_bps` the single-hop PHY rate (the first-hop MCS, item 5)
312    /// and `BtlBw` the measured end-to-end bottleneck (item 6),
313    /// `round(log2(nominal / BtlBw))` is the backhaul-hop count - 2x for one
314    /// hop, 4x for two, 8x for three.
315    ///
316    /// Gated so real congestion does not read as a mesh hop: the first hop must
317    /// be healthy (`mcs_norm` high - the local radio is fine, so the reduction
318    /// is downstream) AND the loss must NOT be congestion-classed
319    /// (`congestion_fraction` low). A single-radio repeater's penalty is a
320    /// structural bandwidth halving with no extra loss, whereas a congested
321    /// shared link shows the rising-delay, congestion-classed loss the item-3
322    /// classifier flags - so the loss class, not an RTT-inflation proxy, is the
323    /// discriminator (real repeaters add bandwidth penalty, not latency). This
324    /// answers what TTL cannot - an L2-bridged repeater does not decrement the
325    /// IP TTL, but its performance signature is unmistakable.
326    pub fn backhaul_hops(&self, nominal_bps: u64, mcs_norm: f32, congestion_fraction: f32) -> u8 {
327        let btlbw = self.btlbw_bps();
328        if btlbw == 0 || nominal_bps <= btlbw {
329            // No bandwidth estimate yet, or no halving at all (the bottleneck is
330            // at least the single-hop rate - certainly not behind a repeater).
331            return 0;
332        }
333        let hops = (nominal_bps as f64 / btlbw as f64)
334            .log2()
335            .round()
336            .clamp(0.0, MAX_BACKHAUL_HOPS as f64) as u8;
337        let first_hop_healthy = mcs_norm > MESH_MCS_HEALTHY;
338        let not_congested = congestion_fraction < MESH_MAX_CONGESTION;
339        if hops >= 1 && first_hop_healthy && not_congested {
340            hops
341        } else {
342            0
343        }
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    /// A steady stream at a known rate and RTT recovers `BtlBw` within
352    /// tolerance and `RTprop` exactly (min filter, within one sample), and the
353    /// BDP is their product. This is the synthetic-trace proof.
354    #[test]
355    fn recovers_known_rate_and_rtprop() {
356        // 1000-byte blocks, 10 blocks every 1000 us = 10_000 bytes / 1 ms =
357        // 1e7 bytes/s = 80 Mbit/s. Fixed RTT 5 ms.
358        let mut m = PathModel::new(1000);
359        let mut delivered = 0u64;
360        let mut now = 0u64;
361        for _ in 0..200 {
362            delivered += 10;
363            now += 1000;
364            m.on_ack(delivered, now, 5000, now);
365        }
366        let bps = m.btlbw_bps();
367        assert!(
368            (78_000_000..=82_000_000).contains(&bps),
369            "BtlBw {bps} bps not within tolerance of 80 Mbit/s"
370        );
371        assert_eq!(m.rtprop_us(), 5000, "RTprop is the exact min RTT");
372        // BDP = 1e7 bytes/s * 5e-3 s = 50_000 bytes = 50 blocks.
373        assert_eq!(m.bdp_blocks(), 50, "BDP in blocks = BtlBw * RTprop");
374    }
375
376    /// A queue that inflates RTT must NOT lower `BtlBw` (the max filter holds
377    /// the bottleneck peak) and must NOT lift `RTprop` (the min filter holds
378    /// the bloat-free path). This is the capacity/delay separation.
379    #[test]
380    fn queue_does_not_corrupt_estimates() {
381        let mut m = PathModel::new(1000);
382        let mut delivered = 0u64;
383        let mut now = 0u64;
384        // Warm up at the true path: rate 1e7 B/s, RTT 5 ms.
385        for _ in 0..50 {
386            delivered += 10;
387            now += 1000;
388            m.on_ack(delivered, now, 5000, now);
389        }
390        let bw_before = m.btlbw_bps();
391        // A standing queue forms: same delivery rate, RTT climbs to 20 ms.
392        for _ in 0..50 {
393            delivered += 10;
394            now += 1000;
395            m.on_ack(delivered, now, 20_000, now);
396        }
397        assert_eq!(m.rtprop_us(), 5000, "RTprop unmoved by the queue (min filter)");
398        assert!(
399            m.btlbw_bps() >= bw_before,
400            "BtlBw not lowered by the queue (max filter)"
401        );
402    }
403
404    /// A capacity drop is adopted once the old high-rate samples age out of
405    /// the `BtlBw` window.
406    #[test]
407    fn adopts_lower_capacity_after_window() {
408        let mut m = PathModel::new(1000);
409        let mut delivered = 0u64;
410        let mut now = 0u64;
411        // Fast: 1e7 B/s at 5 ms RTT. The BtlBw window is
412        // max(10 * RTprop, 200 ms floor) = 200 ms, so the last fast sample
413        // (at t = 50 ms) ages out only after now passes 50 ms + 200 ms.
414        for _ in 0..50 {
415            delivered += 10;
416            now += 1000;
417            m.on_ack(delivered, now, 5000, now);
418        }
419        let fast = m.btlbw_bps();
420        // Capacity halves: 5 blocks per ms. Run well past the 200 ms window
421        // (to now = 350 ms, comfortably past the 250 ms ageout boundary).
422        for _ in 0..300 {
423            delivered += 5;
424            now += 1000;
425            m.on_ack(delivered, now, 5000, now);
426        }
427        let slow = m.btlbw_bps();
428        assert!(slow < fast, "BtlBw drops once the fast samples age out");
429        assert!(
430            (38_000_000..=42_000_000).contains(&slow),
431            "BtlBw {slow} bps tracks the halved 40 Mbit/s capacity"
432        );
433    }
434
435    /// Backhaul-hop count is `round(log2(nominal / BtlBw))`, clamped 0..=3, but
436    /// only when the first hop is healthy AND the loss is not congestion-classed
437    /// - so a congested shared link does not read as a mesh hop.
438    #[test]
439    fn backhaul_hops_from_capacity_ratio_gated() {
440        let mut m = PathModel::new(1000);
441        let mut d = 0u64;
442        let mut t = 0u64;
443        // Establish BtlBw = 1e7 B/s = 8e7 bit/s.
444        for _ in 0..100 {
445            d += 10;
446            t += 1000;
447            m.on_ack(d, t, 5000, t);
448        }
449        let btlbw = m.btlbw_bps();
450        assert!((78_000_000..=82_000_000).contains(&btlbw));
451        // nominal = 2x BtlBw, first hop healthy, loss not congestion-classed:
452        // one backhaul hop (the repeater halves throughput).
453        assert_eq!(m.backhaul_hops(2 * btlbw, 0.9, 0.0), 1);
454        assert_eq!(m.backhaul_hops(4 * btlbw, 0.9, 0.0), 2, "4x -> two hops");
455        assert_eq!(m.backhaul_hops(16 * btlbw, 0.9, 0.0), 3, "8x+ clamps at three");
456        // Gate 1: a weak first hop (low mcs) means the loss is local.
457        assert_eq!(m.backhaul_hops(2 * btlbw, 0.3, 0.0), 0, "weak first hop is not a hop");
458        // Gate 2: congestion-classed loss reads as congestion, not a mesh hop.
459        assert_eq!(m.backhaul_hops(2 * btlbw, 0.9, 0.9), 0, "congestion is not a mesh hop");
460        // No halving at all -> zero hops.
461        assert_eq!(m.backhaul_hops(btlbw, 0.9, 0.0), 0);
462    }
463
464    /// No RTT samples (RTT always 0) leaves `RTprop` and the BDP at zero
465    /// without panicking - the model degrades cleanly before the first
466    /// round-trip is measured.
467    #[test]
468    fn no_rtt_samples_is_safe() {
469        let mut m = PathModel::new(1000);
470        m.on_ack(10, 1000, 0, 1000);
471        m.on_ack(20, 2000, 0, 2000);
472        assert_eq!(m.rtprop_us(), 0);
473        assert_eq!(m.bdp_blocks(), 0);
474    }
475
476    /// A run of coalesced ACKs that reports a large delivered jump over a
477    /// near-zero inter-ACK gap must NOT explode `BtlBw`. The anchor-based
478    /// minimum window means the burst alone (before the window elapses) emits
479    /// nothing, and the eventual sample divides by the real window, not the
480    /// near-zero gap - so the estimate stays bounded instead of latching a
481    /// divide-by-tiny peak.
482    #[test]
483    fn coalesced_burst_does_not_explode_btlbw() {
484        let mut m = PathModel::new(1000);
485        m.on_ack(0, 0, 5000, 0); // anchor = (0, 0, 0), RTprop = 5 ms
486        // 1000 blocks reported just 1 us later (a coalesced ACK / socket-buffer
487        // burst). The window from the anchor is 1 us, far below the floor, so
488        // no sample is emitted - the per-ACK divide-by-near-zero never happens.
489        m.on_ack(1000, 1, 5000, 1);
490        assert_eq!(
491            m.btlbw_bps(),
492            0,
493            "a sub-window coalesced burst emits no rate sample"
494        );
495        // A full window later a single bounded sample emits - the delivered
496        // bytes over the real window, never the 1e15-ish explosion a per-ACK
497        // 1 us gap would have produced.
498        m.on_ack(1100, 25_000, 5000, 25_000);
499        let bps = m.btlbw_bps();
500        assert!(
501            (1..1_000_000_000).contains(&bps),
502            "after the window a bounded sample emits (got {bps} bps), not a divide-by-near-zero peak"
503        );
504    }
505
506    /// The standing-queue estimate is zero at the propagation RTT and rises to
507    /// the queue depth when a deep buffer fills, while `RTprop` holds the
508    /// bloat-free minimum. This is the bufferbloat signal item 7 reads.
509    #[test]
510    fn queue_delay_tracks_standing_queue() {
511        let mut m = PathModel::new(1000);
512        let mut d = 0u64;
513        let mut t = 0u64;
514        // Establish RTprop at 5 ms with steady delivery.
515        for _ in 0..30 {
516            d += 10;
517            t += 1000;
518            m.on_ack(d, t, 5000, t);
519        }
520        assert_eq!(m.rtprop_us(), 5000);
521        assert!(m.queue_delay_us() < 2000, "no standing queue at the propagation RTT");
522        // A deep queue forms: RTT climbs to 60 ms and stays there.
523        for _ in 0..60 {
524            d += 10;
525            t += 1000;
526            m.on_ack(d, t, 60_000, t);
527        }
528        assert_eq!(m.rtprop_us(), 5000, "RTprop still the bloat-free minimum");
529        let qd = m.queue_delay_us();
530        assert!(qd > 40_000, "queue delay {qd} us reflects the ~55 ms standing queue");
531    }
532
533    /// An in-order frontier leap - a head-of-line loss stalls `ack_through`,
534    /// then a retransmit unblocks a buffered backlog so the delivered count
535    /// jumps hundreds of blocks in one ACK window - must be capped at the rate
536    /// the blocks were actually SENT, not the narrow ACK window the leap landed
537    /// in. This is the contaminant that fabricated a 45 Gbit/s `BtlBw` over
538    /// real lossy Wi-Fi.
539    #[test]
540    fn frontier_leap_capped_by_send_span() {
541        let mut m = PathModel::new(1000);
542        m.on_ack(0, 0, 5000, 0); // anchor: delivered 0, sent at t=0
543        // 500 buffered blocks unblock at once: delivered leaps 0 -> 500 inside a
544        // 20 ms ACK window, but those blocks were SENT over 100 ms. The ACK
545        // window alone would read 500 * 1000 B / 20 ms = 2.5e7 B/s; the send
546        // span caps it at 500 * 1000 B / 100 ms = 5e6 B/s = 40 Mbit/s.
547        m.on_ack(500, 20_000, 5000, 100_000);
548        let bps = m.btlbw_bps();
549        assert!(
550            (38_000_000..=42_000_000).contains(&bps),
551            "leap rate {bps} bps capped by the 100 ms send span, not the 20 ms ACK window"
552        );
553    }
554}