Files
punktfunk/crates/punktfunk-core/src/stats.rs
T
enricobuehler e9a7373c76 fix(client/abr): measure delivered throughput in media bytes, not wire bytes
The controller's two throughput-driven gates both compare "what the pipeline
carried" against the ENCODER's target: the utilization gate asks whether a clean
window actually tested that target (a calm menu proves nothing), and the
never-decaying proven mark bounds how far every later climb may step.

Both were fed `bytes_received`, which counts every accepted datagram — headers,
FEC parity, probe filler, audio. So the figure rose with the redundancy the host
adds in ANSWER to loss: at 25 % FEC the gate passed with the encoder emitting
~55 % of target, and the proven mark inherited the same inflation permanently.
The signal was weakest exactly on the lossy links it exists for.

Count data-shard payload separately at the reassembler's routing decision — the
same place, and for the same reason, the probe counters are already stamped —
and feed the ABR that. First time both gates are dimensionally honest: a media
rate compared against a media target.
2026-08-03 18:53:02 +02:00

123 lines
6.5 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`).
/// Media bytes delivered to the video reassembler: DATA-shard payload only — no packet
/// headers, no FEC parity, no probe filler, no audio. This is the rate the encoder's target
/// is a promise about, and the only honest thing to compare that target against.
/// `bytes_received` counts every accepted datagram, so a "delivered throughput" built from
/// it rises with the FEC redundancy the host adds in answer to loss — which meant the
/// adaptive-bitrate utilization gate ("did the pipeline actually carry ~the target?") read
/// 25 % high exactly on the lossy links it exists for, and the never-decaying
/// proven-throughput mark inherited the same inflation. Deliberately NOT mirrored into the
/// C-ABI `PunktfunkStats`.
pub media_bytes_received: u64,
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 media_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),
media_bytes_received: self.media_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),
}
}
}