fix(client/abr): a granted climb disproves the learned cap
Completing the cap-escape fix. Backing the re-probe clock off to 12 s got the client asking again quickly, but each ask only LIFTED the cap by +12.5 % — so even a host that had fully recovered still granted the session its real ceiling one small step at a time, ~4 minutes from the 20 Mbps default to a 300 Mbps link. The crawl was never the point; re-learning was. A request granted IN FULL at or above the cap is the host's own word that the limit is gone. Drop the cap outright at that point instead of nudging it. A standing limit is unaffected — it answers the same re-probe with another short ack, which re-latches it and doubles its clock, exactly as before. Adds the end-to-end regression the sweep was really about: a session pinned at 20 Mbps by a transient cadence refusal, under a probe-measured 300 Mbps ceiling, now reaches 150 Mbps in 22 windows (~16 s) where it used to need ~17 minutes.
This commit is contained in:
@@ -416,6 +416,21 @@ impl BitrateController {
|
||||
}
|
||||
} else {
|
||||
self.short_acks = 0;
|
||||
// GRANTED in full at or above the learned cap: the limit that taught it is
|
||||
// gone, and we have the host's own word for it. Drop the cap outright rather
|
||||
// than keep crawling up in +12.5 % re-probe steps — for a cap latched from a
|
||||
// transient (a host briefly behind cadence) that crawl is the entire
|
||||
// remaining cost of the transient, and it is measured in minutes.
|
||||
if self.host_cap_kbps.is_some_and(|c| kbps >= c) {
|
||||
tracing::info!(
|
||||
granted_kbps = kbps,
|
||||
"adaptive bitrate: host granted a climb at the learned cap — the \
|
||||
limit has lifted, dropping it"
|
||||
);
|
||||
self.host_cap_kbps = None;
|
||||
self.cap_probe_windows = 0;
|
||||
self.cap_reprobe_after = CAP_REPROBE_WINDOWS_MIN;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.current_kbps = kbps;
|
||||
@@ -1596,6 +1611,62 @@ mod tests {
|
||||
assert_eq!(c.host_cap_kbps, Some(794_000 + 794_000 / 8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_transient_refusal_does_not_pin_the_session() {
|
||||
// The field failure this whole cap-escape change exists for. A host that escalates its
|
||||
// capture/encode pipeline once — a startup hitch is enough — used to refuse every climb
|
||||
// for the rest of the session; the client latched that refusal as a cap, at whatever
|
||||
// rate slow start had reached, which is routinely the 20 Mbps default. Escaping cost
|
||||
// +12.5 % per ~60 s: north of twenty minutes to reach a 300 Mbps link ceiling, which the
|
||||
// user experiences as "Automatic is broken".
|
||||
let mut c = BitrateController::new(20_000);
|
||||
c.set_ceiling(300_000); // the startup probe measured a fat link
|
||||
let start = Instant::now();
|
||||
let mut tick = 0u32;
|
||||
let mut windows_pinned = 0u32;
|
||||
// Two refused climbs at the same rate → the cap latches at 20 Mbps.
|
||||
for _ in 0..2 {
|
||||
let k = run_clean(&mut c, start, tick, 4).expect("slow start should ask to climb");
|
||||
tick += 4;
|
||||
assert!(k > 20_000);
|
||||
c.on_ack(20_000); // "behind cadence — held at the current rate"
|
||||
}
|
||||
assert_eq!(c.host_cap_kbps, Some(20_000));
|
||||
// The host recovers immediately (its bucket drains; the escalation bought the headroom
|
||||
// it was for), but the client has no way to know that except by asking again. Drive
|
||||
// clean windows and grant whatever it asks for.
|
||||
while c.current_kbps < 150_000 && windows_pinned < 400 {
|
||||
if let Some(k) = c.on_window(
|
||||
ticks(start, tick),
|
||||
0,
|
||||
0,
|
||||
Some(10_000),
|
||||
None,
|
||||
None,
|
||||
1_000_000,
|
||||
false,
|
||||
0,
|
||||
) {
|
||||
c.on_ack(k);
|
||||
}
|
||||
tick += 1;
|
||||
windows_pinned += 1;
|
||||
}
|
||||
assert!(
|
||||
c.current_kbps >= 150_000,
|
||||
"still pinned at {} after {windows_pinned} windows",
|
||||
c.current_kbps
|
||||
);
|
||||
// ~750 ms a window: this must be tens of seconds, not the old tens of minutes.
|
||||
assert!(
|
||||
windows_pinned <= 40,
|
||||
"took {windows_pinned} windows (~{} s) to escape a transient refusal",
|
||||
windows_pinned * 3 / 4
|
||||
);
|
||||
// And the disproven cap is gone, not merely nudged upward.
|
||||
assert!(c.host_cap_kbps.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_standing_cap_backs_its_reprobe_clock_off() {
|
||||
// The other half of the re-probe: an encoder's real codec ceiling (794 Mbps, L6.2)
|
||||
|
||||
Reference in New Issue
Block a user