fix(core): reordering no longer reads as packet loss — net late shards out of the loss estimate
ci / web (push) Successful in 50s
ci / docs-site (push) Successful in 53s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
decky / build-publish (push) Successful in 19s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
ci / bench (push) Successful in 5m35s
docker / deploy-docs (push) Successful in 25s
arch / build-publish (push) Successful in 11m22s
android / android (push) Successful in 14m48s
deb / build-publish (push) Successful in 14m45s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m12s
ci / rust (push) Successful in 18m20s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m11s
flatpak / build-publish (push) Successful in 5m59s
windows-host / package (push) Successful in 8m3s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m47s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m27s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m6s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m52s
apple / swift (push) Successful in 5m12s
release / apple (push) Successful in 26m35s
apple / screenshots (push) Successful in 19m28s
ci / web (push) Successful in 50s
ci / docs-site (push) Successful in 53s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
decky / build-publish (push) Successful in 19s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
ci / bench (push) Successful in 5m35s
docker / deploy-docs (push) Successful in 25s
arch / build-publish (push) Successful in 11m22s
android / android (push) Successful in 14m48s
deb / build-publish (push) Successful in 14m45s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m12s
ci / rust (push) Successful in 18m20s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m11s
flatpak / build-publish (push) Successful in 5m59s
windows-host / package (push) Successful in 8m3s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m47s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m27s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m6s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m52s
apple / swift (push) Successful in 5m12s
release / apple (push) Successful in 26m35s
apple / screenshots (push) Successful in 19m28s
Reversed/reordered delivery lets a FEC block reconstruct EARLY (data + recovery >= k), counting still-in-flight shards into fec_recovered_shards; window_loss_ppm then reported pure reordering as loss, inflating LossReports — which size adaptive FEC and, since the Automatic overhaul, feed the ABR controller (one severe window ends slow start FOR GOOD, so a reorder burst could permanently kneecap a session's climb). Early reconstruct stays (it's the latency-right choice); the accounting now nets it out. The reassembler counts a new fec_late_shards stat when a parity-restored data shard ARRIVES after all — matched exactly: the completed/abandoned-frame memory (ReassemblyWindow::completed, now a map) remembers which shards each terminal frame reconstructed, and a late arrival must match one (removed on hit), so wire duplicates of delivered shards and stragglers of failed blocks count nothing. In-flight blocks dedup via have_data. window_loss_ppm takes the late delta and estimates from (recovered - late), saturating across window boundaries; both callers (client core + probe) pass it. The e2e reorder tests now assert the NET equals the true kill count in both delivery orders, dup included (previously documented as a known inflation). Not mirrored into the C-ABI PunktfunkStats — the loss windows run in-core on every platform. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1148,13 +1148,19 @@ impl BitrateChanged {
|
||||
}
|
||||
|
||||
/// Compute a [`LossReport`] `loss_ppm` from one window's session-stat deltas: shards FEC recovered
|
||||
/// (the loss it absorbed), shards received, and frames that went unrecoverable. Loss ≈ recovered /
|
||||
/// (received + recovered) — the fraction of shards that arrived missing. A frame drop means loss
|
||||
/// exceeded the current FEC budget (so `recovered` plateaus), so add a fixed bump to push the host's
|
||||
/// FEC up past the cap on the next adjustment. Returns parts-per-million, capped at 1e6.
|
||||
pub fn window_loss_ppm(recovered: u64, received: u64, frames_dropped: u64) -> u32 {
|
||||
let denom = received.saturating_add(recovered);
|
||||
let mut ppm = recovered
|
||||
/// (the loss it absorbed), recovered-but-then-arrived shards (`late` — reordered delivery lets a
|
||||
/// block reconstruct early, so those were never lost; netting them out keeps plain reordering from
|
||||
/// reading as packet loss and spooking adaptive FEC + the bitrate controller), shards received,
|
||||
/// and frames that went unrecoverable. Loss ≈ (recovered − late) / (received + recovered − late) —
|
||||
/// the fraction of shards that truly never arrived (a late shard is inside `received`, so the
|
||||
/// denominator nets it too; saturating, so reorder straddling a window boundary can't go
|
||||
/// negative). A frame drop means loss exceeded the current FEC budget (so `recovered` plateaus),
|
||||
/// so add a fixed bump to push the host's FEC up past the cap on the next adjustment. Returns
|
||||
/// parts-per-million, capped at 1e6.
|
||||
pub fn window_loss_ppm(recovered: u64, late: u64, received: u64, frames_dropped: u64) -> u32 {
|
||||
let lost = recovered.saturating_sub(late);
|
||||
let denom = received.saturating_add(lost);
|
||||
let mut ppm = lost
|
||||
.saturating_mul(1_000_000)
|
||||
.checked_div(denom)
|
||||
.unwrap_or(0) as u32;
|
||||
|
||||
@@ -707,15 +707,22 @@ fn loss_report_roundtrip() {
|
||||
#[test]
|
||||
fn window_loss_ppm_estimates_and_caps() {
|
||||
// No traffic → 0. A clean window (nothing recovered) → 0.
|
||||
assert_eq!(window_loss_ppm(0, 0, 0), 0);
|
||||
assert_eq!(window_loss_ppm(0, 1000, 0), 0);
|
||||
assert_eq!(window_loss_ppm(0, 0, 0, 0), 0);
|
||||
assert_eq!(window_loss_ppm(0, 0, 1000, 0), 0);
|
||||
// 50 recovered of 1000 total (950 received + 50 recovered) = 5%.
|
||||
assert_eq!(window_loss_ppm(50, 950, 0), 50_000);
|
||||
assert_eq!(window_loss_ppm(50, 0, 950, 0), 50_000);
|
||||
// An unrecoverable frame adds the +5% bump (push FEC past the current cap).
|
||||
assert_eq!(window_loss_ppm(50, 950, 1), 100_000);
|
||||
assert_eq!(window_loss_ppm(50, 0, 950, 1), 100_000);
|
||||
// A total-loss window with a drop but nothing received still reports the bump, capped at 1e6.
|
||||
assert_eq!(window_loss_ppm(0, 0, 3), 50_000);
|
||||
assert!(window_loss_ppm(u64::MAX, 1, 9) <= 1_000_000);
|
||||
assert_eq!(window_loss_ppm(0, 0, 0, 3), 50_000);
|
||||
assert!(window_loss_ppm(u64::MAX, 0, 1, 9) <= 1_000_000);
|
||||
// Reordering: shards "recovered" early that then arrived are late, not lost — netted out, so
|
||||
// a pure-reorder window reads 0. Partially late nets to the true loss (20 of 1000 = 2%).
|
||||
assert_eq!(window_loss_ppm(50, 50, 1000, 0), 0);
|
||||
assert_eq!(window_loss_ppm(50, 30, 980, 0), 20_000);
|
||||
// `late` can outrun `recovered` across a window boundary (reorder straddling the report
|
||||
// tick) or via a rare wire duplicate — saturate at a clean window, never underflow.
|
||||
assert_eq!(window_loss_ppm(10, 25, 1000, 0), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user