ci / web (push) Successful in 1m3s
ci / docs-site (push) Successful in 1m2s
apple / swift (push) Successful in 1m19s
decky / build-publish (push) Successful in 53s
deb / build-publish (push) Successful in 12m22s
ci / bench (push) Successful in 6m13s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 44s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6m32s
ci / rust (push) Failing after 9m2s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m22s
release / apple (push) Successful in 9m50s
windows-host / package (push) Failing after 13m7s
deb / build-publish-host (push) Successful in 13m1s
arch / build-publish (push) Failing after 14m11s
apple / screenshots (push) Failing after 3m37s
android / android (push) Successful in 14m56s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 6m8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8m32s
flatpak / build-publish (push) Failing after 8m13s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 6m2s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 6m33s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 7m44s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m32s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m13s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 9m0s
docker / deploy-docs (push) Successful in 26s
There is one `ProbeState` per session and no correlation id, and two independent requesters share it: the pump's startup link-capacity probe and the embedder's `NativeClient::request_probe` (the shipped "Test connection" in the Windows GUI and the Linux GTK app). The startup path had a busy check, a watchdog and a window rebase; the embedder path had none of them, and the two collide by default — both shipped speed tests call `request_probe` on the statement right after `connect()`, and the pump's probe fires 2 s later, inside that burst. Four defects, one cluster: - The pump overwrote the shared slot unconditionally. Mid-burst it drops `base_packets`/`base_bytes`, the pump re-snapshots them against the host's full-burst denominator, and the user's speed test reports roughly an order of magnitude low; if the result lands first instead, the reset wipes `done` and the embedder's poll loop never sees its own measurement. Now it defers and retries rather than stealing the slot. - An unanswered embedder probe never timed out. `probe_active` gates the entire report tick — LossReport, the ABR window feed, the standing-latency ladder and a pending ClockResync all live inside it. A host that ignores ProbeRequest is an anticipated configuration (the startup path was given a 6 s timeout for exactly that) so the embedder path could latch `active` forever and silently switch off every adaptation mechanism for the rest of the session. Now a watchdog covers a probe of either origin. - `request_probe` latched `active` before `try_send`, so a full or closed ctrl channel returned `Closed` to the caller while leaving the session wedged in the state above. It now rolls back, mirroring the startup path. - Only the startup path rebased the ABR window past the burst. Probe filler is counted into `bytes_received` for every accepted datagram but never reaches the decoder, and the tick is suppressed for the whole burst, so the first post-burst window read the burst rate as `actual_kbps`. That poisons `proven_kbps`, a monotone high-water mark that is never decayed, which disables the x1.5 evidence-gated climb guard for the session and authorizes a climb into a rate the decoder has never carried. The rebase now happens on the falling edge of any probe, and covers the loss/packet anchors too so the first post-burst LossReport isn't divided by a filler-inflated packet count. Also: `wants_decode_latency` advertised on two of the three terms the pump actually requires to arm ABR, omitting `resolved_bitrate_kbps > 0`, so against an old host that reports no rate an embedder fed decode latency to a controller that never runs. No wire or ABI change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
69 lines
3.8 KiB
Rust
69 lines
3.8 KiB
Rust
//! Speed-test probe state (`ProbeState`, pump-mirrored) and the public `ProbeOutcome`.
|
||
|
||
/// Accumulated state of an in-flight / finished speed test. The data-plane pump mirrors the
|
||
/// session's packet-level receive counters here; the control task finalizes the delivered figure
|
||
/// and folds in the host's [`ProbeResult`] when it lands. Read by [`NativeClient::probe_result`].
|
||
///
|
||
/// Counting at the *packet* level (every delivered wire packet) — not whole reassembled probe AUs —
|
||
/// is what makes the measurement degrade gracefully: once loss exceeds the FEC budget no AU
|
||
/// completes, so the old AU-based count cliffed to zero even though most bytes still arrived.
|
||
#[derive(Default)]
|
||
pub(crate) struct ProbeState {
|
||
/// A probe is in progress: set by `request_probe`, cleared when the host's [`ProbeResult`]
|
||
/// lands (a re-probe just overwrites the whole state — the latest one wins).
|
||
pub(crate) active: bool,
|
||
/// `session.stats()` receive counters at the burst's start (snapshotted by the pump on its first
|
||
/// tick while active) and latest, mirrored every pump iteration.
|
||
pub(crate) base_packets: Option<u64>,
|
||
pub(crate) base_bytes: Option<u64>,
|
||
pub(crate) rx_packets_now: u64,
|
||
pub(crate) rx_bytes_now: u64,
|
||
/// Delivered wire packets / plaintext bytes (header + shard), frozen when the host's report lands
|
||
/// (so resumed video after the burst can't inflate them).
|
||
pub(crate) delivered_packets: u64,
|
||
pub(crate) delivered_bytes: u64,
|
||
/// The host's end-of-burst report.
|
||
pub(crate) host_goodput_bytes: u64,
|
||
pub(crate) host_au: u32,
|
||
/// Wire packets the host actually put on the link, and the ones its send buffer dropped.
|
||
pub(crate) host_wire_packets: u32,
|
||
pub(crate) host_send_dropped: u32,
|
||
/// The host's measured burst duration (the throughput denominator).
|
||
pub(crate) host_duration_ms: u32,
|
||
/// The host's `ProbeResult` arrived → the measurement is final.
|
||
pub(crate) done: bool,
|
||
/// The requested burst length, so the pump can arm a watchdog for a host that never answers.
|
||
/// Without one, an ignored `ProbeRequest` latches `active` forever and the pump's whole report
|
||
/// tick — loss reports, the ABR window feed, the standing-latency ladder and pending clock
|
||
/// re-syncs — stays suppressed for the rest of the session.
|
||
pub(crate) duration_ms: u32,
|
||
}
|
||
|
||
/// A finished/partial speed-test measurement, returned by [`NativeClient::probe_result`].
|
||
#[derive(Clone, Copy, Debug, Default)]
|
||
pub struct ProbeOutcome {
|
||
/// The host's end-of-burst report has arrived — the numbers below are final.
|
||
pub done: bool,
|
||
/// Delivered wire bytes (header + shard) / packets the client received during the burst.
|
||
pub recv_bytes: u64,
|
||
pub recv_packets: u32,
|
||
/// Application goodput bytes / access units the host offered.
|
||
pub host_bytes: u64,
|
||
pub host_packets: u32,
|
||
/// The burst duration the host measured, in milliseconds (the throughput denominator).
|
||
pub elapsed_ms: u32,
|
||
/// Delivered wire throughput = `recv_bytes * 8 / elapsed_ms` (kilobits/second). The figure to
|
||
/// drive a [`Hello::bitrate_kbps`] choice from (allow headroom for the FEC overhead + loss).
|
||
pub throughput_kbps: u32,
|
||
/// Link loss = `(wire_packets_sent − received) / wire_packets_sent`, percent. Packets the host
|
||
/// put on the wire that didn't arrive.
|
||
pub loss_pct: f32,
|
||
/// Host-side drop = `send_dropped / (wire_packets_sent + send_dropped)`, percent. Packets the
|
||
/// host's send buffer couldn't accept (raise `net.core.wmem_max` / lower the rate). Distinct
|
||
/// from `loss_pct`: this is the host failing to keep up, not the link dropping traffic.
|
||
pub host_drop_pct: f32,
|
||
/// Wire packets the host put on the link and the ones its send buffer dropped (raw counts).
|
||
pub wire_packets_sent: u32,
|
||
pub send_dropped: u32,
|
||
}
|