The capacity probe divided client-side bytes by the HOST's burst duration — a window wrong on both edges (base snapshotted before the burst reached the host, frozen only when the ProbeResult landed, while the host's clock stops the moment ITS send window closes, before the switch/kernel queue finishes draining toward the client). On a 1 GbE link a 2 Gbps burst target "measured" 1266 Mbps and set an 886 Mbps climb ceiling the link could never carry — permanent for the session, because set_ceiling never lowers. The reassembler now stamps probe-scoped counters (bytes, packets, first/last arrival, monotonic ns) at its FLAG_PROBE routing, so video around the burst contaminates neither numerator nor denominator; the throughput divisor is the client's first→last arrival interval, with the host duration kept as the fallback when fewer than two probe packets arrived. The user-facing speed test shares the corrected computation (ProbeOutcome/PunktfunkProbeResult layouts unchanged; elapsed_ms docs updated to the new semantics). Two guards ride along: - PUNKTFUNK_ABR_MAX_MBPS clamps inside set_ceiling — the one funnel every learned ceiling passes through — so a user cap binds no matter what any probe concludes. - The controller latches decode_cap_kbps when two CONSECUTIVE backoffs carry decode-severe evidence (deep decode excursion or jump-to-live flush) at a similar pre-backoff rate, mirroring host_cap_kbps for the client decoder: a knee below the link ceiling was a permanent 30-60 s sawtooth costing a flush + dropped-frame burst per cycle (1440p120 HEVC field case, knee ~490 Mbps). One spurious flush never latches; the cap re-probes on the CAP_REPROBE_WINDOWS clock, so it lifts when the decoder recovers. Also rights the three stale "3 Gbps" probe-clamp comments (the host constant has been 10 Gbps since MAX_PROBE_KBPS moved). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
111 lines
5.6 KiB
Rust
111 lines
5.6 KiB
Rust
//! Live counters for the frame-pacing / quality logic and the web UI.
|
|
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
use std::sync::OnceLock;
|
|
use std::time::Instant;
|
|
|
|
/// Monotonic now, in ns since an arbitrary process-wide epoch — the basis for the probe
|
|
/// arrival stamps below. Monotonic on purpose: the stamps are only ever DIFFERENCED on this
|
|
/// machine (the burst's receive interval), and a wall-clock step mid-burst — the exact event
|
|
/// the clock re-sync machinery exists for — must not corrupt the one measurement the ABR
|
|
/// ceiling is built from, so the CLOCK_REALTIME basis `pts_ns` uses is wrong here. Floored
|
|
/// at 1 so a stamp can never collide with the 0 = "unset" sentinel.
|
|
pub(crate) fn now_monotonic_ns() -> u64 {
|
|
static EPOCH: OnceLock<Instant> = OnceLock::new();
|
|
(EPOCH.get_or_init(Instant::now).elapsed().as_nanos() as u64).max(1)
|
|
}
|
|
|
|
/// Immutable snapshot, copied across the C ABI as `PunktfunkStats`.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct Stats {
|
|
pub frames_submitted: u64,
|
|
pub frames_completed: u64,
|
|
pub frames_dropped: u64,
|
|
pub packets_sent: u64,
|
|
pub packets_received: u64,
|
|
pub packets_dropped: u64,
|
|
/// Packets the host could NOT hand to the kernel because the send buffer was full (WouldBlock)
|
|
/// — the dominant loss mode at very high bitrate. Distinct from `packets_dropped` (recv-side
|
|
/// reassembler rejects). A non-zero, growing value means the link/encoder is outrunning the
|
|
/// send path; raise `net.core.wmem_max` / lower the bitrate, or wait for paced batched sending.
|
|
pub packets_send_dropped: u64,
|
|
pub fec_recovered_shards: u64,
|
|
/// Shards counted into [`fec_recovered_shards`](Self::fec_recovered_shards) that later ARRIVED
|
|
/// — reordered delivery lets a block reconstruct early from parity, so the still-in-flight
|
|
/// shards it "recovered" were late, not lost. Loss estimators must net this out
|
|
/// (`recovered - late`, see [`window_loss_ppm`](crate::quic::window_loss_ppm)) or plain
|
|
/// reordering reads as packet loss and spooks adaptive FEC + the bitrate controller.
|
|
/// Deliberately NOT mirrored into the C-ABI `PunktfunkStats` (loss windows run in-core).
|
|
pub fec_late_shards: u64,
|
|
pub bytes_sent: u64,
|
|
pub bytes_received: u64,
|
|
/// Probe-scoped receive counters: wire packets / plaintext bytes carrying
|
|
/// [`FLAG_PROBE`](crate::packet::FLAG_PROBE) (speed-test filler), counted at the
|
|
/// reassembler's probe routing decision. `bytes_received` counts EVERY accepted datagram,
|
|
/// so a speed-test numerator built from it inherits whatever video was in flight around
|
|
/// the burst — these keep video out of the probe math. Deliberately NOT mirrored into the
|
|
/// C-ABI `PunktfunkStats` (probe measurements surface via `ProbeOutcome`).
|
|
pub probe_packets_received: u64,
|
|
pub probe_bytes_received: u64,
|
|
/// First / last probe-packet arrival (monotonic ns, see [`now_monotonic_ns`]; 0 = none
|
|
/// since the last probe arm). Their difference is the burst's client-side receive
|
|
/// interval — the honest speed-test denominator: the host's send window closes while the
|
|
/// switch/kernel queue toward the client is still draining, so dividing client bytes by
|
|
/// the HOST duration overstates the link (a 1 GbE link "measured" 1266 Mbps). The client
|
|
/// pump zeroes both when it arms a probe (`Session::reset_probe_arrivals`).
|
|
pub probe_first_arrival_ns: u64,
|
|
pub probe_last_arrival_ns: u64,
|
|
}
|
|
|
|
/// Atomic accumulators owned by a [`Session`](crate::session::Session). Snapshot to
|
|
/// [`Stats`] for readers. `Relaxed` ordering is fine: these are monotonic counters
|
|
/// read for display, never used to synchronize other memory. (The two probe arrival
|
|
/// stamps are the exception — slots, not counters — but they carry no synchronization
|
|
/// duty either: they are read hundreds of ms after the last write.)
|
|
#[derive(Default)]
|
|
pub struct StatsCounters {
|
|
pub frames_submitted: AtomicU64,
|
|
pub frames_completed: AtomicU64,
|
|
pub frames_dropped: AtomicU64,
|
|
pub packets_sent: AtomicU64,
|
|
pub packets_received: AtomicU64,
|
|
pub packets_dropped: AtomicU64,
|
|
pub packets_send_dropped: AtomicU64,
|
|
pub fec_recovered_shards: AtomicU64,
|
|
pub fec_late_shards: AtomicU64,
|
|
pub bytes_sent: AtomicU64,
|
|
pub bytes_received: AtomicU64,
|
|
pub probe_packets_received: AtomicU64,
|
|
pub probe_bytes_received: AtomicU64,
|
|
pub probe_first_arrival_ns: AtomicU64,
|
|
pub probe_last_arrival_ns: AtomicU64,
|
|
}
|
|
|
|
impl StatsCounters {
|
|
#[inline]
|
|
pub fn add(counter: &AtomicU64, n: u64) {
|
|
counter.fetch_add(n, Ordering::Relaxed);
|
|
}
|
|
|
|
pub fn snapshot(&self) -> Stats {
|
|
let l = Ordering::Relaxed;
|
|
Stats {
|
|
frames_submitted: self.frames_submitted.load(l),
|
|
frames_completed: self.frames_completed.load(l),
|
|
frames_dropped: self.frames_dropped.load(l),
|
|
packets_sent: self.packets_sent.load(l),
|
|
packets_received: self.packets_received.load(l),
|
|
packets_dropped: self.packets_dropped.load(l),
|
|
packets_send_dropped: self.packets_send_dropped.load(l),
|
|
fec_recovered_shards: self.fec_recovered_shards.load(l),
|
|
fec_late_shards: self.fec_late_shards.load(l),
|
|
bytes_sent: self.bytes_sent.load(l),
|
|
bytes_received: self.bytes_received.load(l),
|
|
probe_packets_received: self.probe_packets_received.load(l),
|
|
probe_bytes_received: self.probe_bytes_received.load(l),
|
|
probe_first_arrival_ns: self.probe_first_arrival_ns.load(l),
|
|
probe_last_arrival_ns: self.probe_last_arrival_ns.load(l),
|
|
}
|
|
}
|
|
}
|