Skip to main content

irtt_stats/
lib.rs

1//! Statistics aggregation for `irtt-client` events.
2//!
3//! The crate consumes `irtt-client` events and produces cumulative or rolling
4//! snapshots for reporting and integration code.
5
6#![forbid(unsafe_code)]
7#![warn(missing_docs)]
8#![warn(rustdoc::missing_crate_level_docs)]
9
10use std::time::Duration;
11
12use irtt_client::ClientEvent;
13
14mod core;
15mod ipdv;
16mod loss;
17mod normalization;
18mod rolling;
19mod time_stats;
20
21use core::CoreStats;
22pub use loss::LossStats;
23use normalization::normalize_event;
24use rolling::RollingEvents;
25pub use time_stats::TimeStats;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28/// Configuration for statistics collection.
29pub struct StatsConfig {
30    /// How timing samples are retained for median-capable metrics.
31    pub samples: SampleMode,
32    /// Number of recent normalized events retained for count-based rolling snapshots.
33    ///
34    /// A successful probe usually contributes two normalized events: one send event
35    /// and one unique reply event.
36    pub rolling_count: Option<usize>,
37    /// Time span of recent normalized events retained for time-based rolling snapshots.
38    pub rolling_time: Option<Duration>,
39}
40
41impl StatsConfig {
42    /// Returns the default configuration for finite runs.
43    ///
44    /// Finite mode retains exact samples where needed for medians and keeps
45    /// unbounded adjacent-sequence IPDV tracking so late adjacent replies can
46    /// still complete IPDV pairs.
47    pub fn finite() -> Self {
48        Self {
49            samples: SampleMode::Exact,
50            rolling_count: None,
51            rolling_time: None,
52        }
53    }
54
55    /// Returns a configuration for long-running use.
56    ///
57    /// Continuous mode uses running statistics, does not retain exact samples
58    /// for medians, and bounds adjacent-sequence IPDV tracking for long-running
59    /// sessions.
60    pub fn continuous() -> Self {
61        Self {
62            samples: SampleMode::RunningOnly,
63            rolling_count: None,
64            rolling_time: None,
65        }
66    }
67}
68
69impl Default for StatsConfig {
70    fn default() -> Self {
71        Self::finite()
72    }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76/// Controls whether exact timing samples are retained.
77pub enum SampleMode {
78    /// Keep only running statistics; exact medians are not available.
79    RunningOnly,
80    /// Retain exact samples for metrics that report exact medians.
81    Exact,
82}
83
84#[derive(Debug, Clone, PartialEq)]
85/// Stateful statistics collector for `irtt-client` events.
86///
87/// A collector maintains cumulative statistics and, when configured, rolling
88/// windows. Rolling snapshots are recomputed from retained normalized events.
89pub struct StatsCollector {
90    cumulative: CoreStats,
91    rolling: RollingEvents,
92}
93
94impl StatsCollector {
95    /// Creates a collector with the supplied configuration.
96    pub fn new(config: StatsConfig) -> Self {
97        Self {
98            cumulative: CoreStats::new(config.samples),
99            rolling: RollingEvents::new(config),
100        }
101    }
102
103    /// Processes one client event and returns the per-event statistics update.
104    ///
105    /// Updates currently report whether the event contributed a unique reply
106    /// timing sample and any adjacent-sequence IPDV pairs completed by this
107    /// event.
108    pub fn process(&mut self, event: &ClientEvent) -> EventStatsUpdate {
109        let Some(stats_event) = normalize_event(event) else {
110            return EventStatsUpdate::default();
111        };
112
113        let update = self.cumulative.apply(stats_event.clone());
114        self.rolling.push(stats_event);
115        update
116    }
117
118    /// Returns a snapshot of all events processed by this collector.
119    pub fn snapshot(&self) -> Snapshot {
120        self.cumulative.snapshot()
121    }
122
123    /// Returns the configured count-based rolling snapshot, if enabled.
124    ///
125    /// The snapshot is recomputed from the retained rolling events.
126    pub fn rolling_count(&self) -> Option<Snapshot> {
127        self.rolling.count_snapshot()
128    }
129
130    /// Returns the configured time-based rolling snapshot, if enabled.
131    ///
132    /// The snapshot is recomputed from the retained rolling events.
133    pub fn rolling_time(&self) -> Option<Snapshot> {
134        self.rolling.time_snapshot()
135    }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Default)]
139/// Per-event statistics produced by [`StatsCollector::process`].
140pub struct EventStatsUpdate {
141    /// Whether the processed event contributed a unique reply timing sample.
142    pub contributed_sample: bool,
143    /// Adjacent-sequence IPDV pairs completed by the processed event.
144    pub ipdv_pairs: Vec<IpdvPairUpdate>,
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148/// Adjacent-sequence IPDV pair completed by a processed event.
149pub struct IpdvPairUpdate {
150    /// Sequence number of the earlier reply in the adjacent pair.
151    pub previous_seq: u32,
152    /// Sequence number of the later reply in the adjacent pair.
153    pub current_seq: u32,
154    /// Absolute round-trip IPDV between the adjacent replies.
155    pub rtt_ipdv: Duration,
156    /// Absolute send-side IPDV when send one-way delay is available for both replies.
157    pub send_ipdv: Option<Duration>,
158    /// Absolute receive-side IPDV when receive one-way delay is available for both replies.
159    pub receive_ipdv: Option<Duration>,
160}
161
162#[derive(Debug, Clone, PartialEq)]
163/// Point-in-time statistics summary.
164pub struct Snapshot {
165    /// Event counters grouped by event class.
166    pub events: EventCounts,
167    /// Packet and byte counters.
168    pub packets: PacketCounts,
169    /// Packet loss, duplicate, and late-packet percentages.
170    pub loss: LossStats,
171    /// Duration of send calls, in nanoseconds.
172    pub send_call: TimeStats,
173    /// Sender scheduling error, in nanoseconds.
174    pub timer_error: TimeStats,
175    /// Round-trip timing statistics.
176    pub rtt: RttStats,
177    /// Inter-packet delay variation statistics.
178    pub ipdv: IpdvStats,
179    /// One-way delay statistics.
180    pub one_way_delay: OneWayDelayStats,
181    /// Server processing time statistics.
182    pub server_processing: ServerProcessingStats,
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
186/// Counts of normalized client events.
187pub struct EventCounts {
188    /// Probe send events processed.
189    pub sent_events: u64,
190    /// On-time unique echo replies processed.
191    pub echo_replies: u64,
192    /// Late unique echo replies processed.
193    pub late_unique_replies: u64,
194    /// Duplicate echo replies processed.
195    pub duplicate_replies: u64,
196    /// Loss events processed.
197    pub loss_events: u64,
198    /// Warning events processed.
199    pub warning_events: u64,
200    /// Late replies that could not be matched to retained in-flight state.
201    ///
202    /// These replies are observed on the socket, but cannot contribute RTT,
203    /// one-way delay, server-processing, or IPDV samples.
204    pub untracked_late_replies: u64,
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
208/// Packet, byte, and server-reported receive counters.
209pub struct PacketCounts {
210    /// Probe packets sent by the local client.
211    pub packets_sent: u64,
212    /// Reply packets observed by the local client.
213    ///
214    /// This includes unique measurable replies, duplicate replies, and late
215    /// replies that cannot be matched to retained in-flight state.
216    pub packets_received: u64,
217    /// Unique replies that can contribute timing measurements.
218    pub unique_replies: u64,
219    /// Duplicate replies received by the local client.
220    pub duplicates: u64,
221    /// Late reply packets received by the local client, including untracked-late replies.
222    pub late_packets: u64,
223    /// Probe bytes sent by the local client.
224    pub bytes_sent: u64,
225    /// Reply bytes observed by the local client.
226    ///
227    /// This includes bytes from unique measurable replies, duplicate replies,
228    /// and untracked-late replies. Per-category byte counts are not currently
229    /// exposed separately.
230    pub bytes_received: u64,
231    /// Highest cumulative server-reported packets received, when available.
232    pub server_packets_received: Option<u64>,
233    /// Server-reported receive window, when available.
234    pub server_received_window: Option<u64>,
235}
236
237#[derive(Debug, Clone, PartialEq)]
238/// Round-trip time statistics.
239pub struct RttStats {
240    /// Effective RTT used for primary RTT reporting and IPDV input.
241    pub primary: TimeStats,
242    /// Client-observed raw RTT.
243    pub raw: TimeStats,
244    /// RTT adjusted for server processing time when available.
245    pub adjusted: TimeStats,
246}
247
248#[derive(Debug, Clone, PartialEq)]
249/// Inter-packet delay variation statistics.
250pub struct IpdvStats {
251    /// Round-trip IPDV.
252    pub round_trip: TimeStats,
253    /// Send-side IPDV, when send one-way delay is available.
254    pub send: TimeStats,
255    /// Receive-side IPDV, when receive one-way delay is available.
256    pub receive: TimeStats,
257}
258
259#[derive(Debug, Clone, PartialEq)]
260/// One-way delay statistics.
261pub struct OneWayDelayStats {
262    /// Client-to-server delay.
263    pub send_delay: TimeStats,
264    /// Server-to-client delay.
265    pub receive_delay: TimeStats,
266}
267
268#[derive(Debug, Clone, PartialEq)]
269/// Server processing time statistics.
270pub struct ServerProcessingStats {
271    /// Time spent processing a probe at the server.
272    pub processing: TimeStats,
273}