// SPDX-License-Identifier: AGPL-3.0-only //! Transport statistics for a client connection. use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; /// Application-level message counters, incremented by the client's chunk task and read by the UI thread. /// /// Held behind an [`Arc`] and mutated with relaxed atomics: each counter is independent, nothing else is ordered against them, and a reader that observes a slightly stale value is reporting a diagnostic figure, not making a decision. #[derive(Debug, Default)] pub(crate) struct NetCounters { /// Chunk deliveries received from the server. chunks_received: AtomicU64, /// Chunk drop notifications received from the server. drops_received: AtomicU64, /// Subscription updates written to the server. subscribes_sent: AtomicU64, } impl NetCounters { /// Records one received chunk delivery. pub(crate) fn record_chunk(&self) { self.chunks_received.fetch_add(1, Ordering::Relaxed); } /// Records one received chunk drop notification. pub(crate) fn record_drop(&self) { self.drops_received.fetch_add(1, Ordering::Relaxed); } /// Records one subscription update written to the server. pub(crate) fn record_subscribe(&self) { self.subscribes_sent.fetch_add(1, Ordering::Relaxed); } } /// A snapshot of one client connection's transport state. /// /// Every field is a value copied at the moment of the call. A snapshot taken before the handshake completes reports `connected == false` and zeroes throughout, which is a meaningful state rather than missing data. #[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct NetStats { /// Whether a QUIC connection is currently established. pub connected: bool, /// Bytes carried in UDP datagrams sent on this connection, including QUIC framing and retransmissions. pub bytes_sent: u64, /// Bytes carried in UDP datagrams received on this connection, including QUIC framing. pub bytes_received: u64, /// UDP datagrams sent on this connection. pub datagrams_sent: u64, /// UDP datagrams received on this connection. pub datagrams_received: u64, /// Chunk deliveries received since the connection was established. pub chunks_received: u64, /// Chunk drop notifications received since the connection was established. pub drops_received: u64, /// Subscription updates written to the server since the connection was established. pub subscribes_sent: u64, /// The QUIC stack's current round-trip-time estimate, in milliseconds. pub rtt_ms: f32, /// Packets the congestion controller has declared lost on the current path. pub lost_packets: u64, /// Current congestion window, in bytes: the ceiling on data in flight. pub congestion_window: u64, /// Largest UDP payload the current path is known to carry, in bytes, as discovered by path MTU probing. pub path_mtu: u16, } /// Reads a connection's transport statistics, or reports the disconnected state. /// /// `connection` is [`None`] until the handshake completes and after the connection closes. pub(crate) fn snapshot( connection: Option<&quinn::Connection>, counters: &Arc, ) -> NetStats { let mut stats = NetStats { chunks_received: counters.chunks_received.load(Ordering::Relaxed), drops_received: counters.drops_received.load(Ordering::Relaxed), subscribes_sent: counters.subscribes_sent.load(Ordering::Relaxed), ..NetStats::default() }; let Some(connection) = connection else { return stats; }; let quic = connection.stats(); // A connection handle outlives the connection itself; a close reason is how a torn-down connection distinguishes itself from a live one, and its final counters stay readable either way. stats.connected = connection.close_reason().is_none(); stats.bytes_sent = quic.udp_tx.bytes; stats.bytes_received = quic.udp_rx.bytes; stats.datagrams_sent = quic.udp_tx.datagrams; stats.datagrams_received = quic.udp_rx.datagrams; stats.rtt_ms = quic.path.rtt.as_secs_f32() * 1000.0; stats.lost_packets = quic.path.lost_packets; stats.congestion_window = quic.path.cwnd; stats.path_mtu = quic.path.current_mtu; stats } #[cfg(test)] #[path = "tests/stats.rs"] mod tests;