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}