irtt_stats/lib.rs
1//! Statistics aggregation for `irtt-client` events.
2//!
3//! [`StatsCollector`] consumes [`irtt_client::ClientEvent`] values through
4//! [`StatsCollector::process`] and produces a cumulative [`Snapshot`], plus
5//! optional count- and time-based rolling snapshots, for reporting and
6//! integration code.
7//!
8//! # Retention modes
9//!
10//! [`StatsConfig`] selects between two retention modes via [`SampleMode`].
11//! [`StatsConfig::finite`] ([`SampleMode::Exact`]) retains an exact sample per
12//! timing metric, so every metric reports an exact median once it has
13//! samples; retention grows with the number of probes processed. This is the
14//! default and suits a run with a known, bounded duration.
15//! [`StatsConfig::continuous`] ([`SampleMode::RunningOnly`]) keeps only
16//! running statistics — no exact median — and bounds its adjacent-sequence
17//! IPDV tracking, so memory stays bounded for a long-running or unbounded
18//! session. An enabled time-based rolling window has no item cap and is not
19//! hard-bounded; it expires only when new events arrive. Rolling snapshots
20//! always use running statistics regardless of this setting.
21//!
22//! See `examples/` in the repository for a runnable comparison of both modes.
23
24#![forbid(unsafe_code)]
25#![warn(missing_docs)]
26#![warn(rustdoc::missing_crate_level_docs)]
27
28/// Cargo package version of this compiled library.
29pub const VERSION: &str = env!("CARGO_PKG_VERSION");
30
31use std::time::Duration;
32
33use irtt_client::ClientEvent;
34
35mod core;
36mod ipdv;
37mod loss;
38mod normalization;
39mod retention;
40mod rolling;
41
42use core::CoreStats;
43pub use loss::LossStats;
44#[doc(inline)]
45pub use measurement_stats::TimeStats;
46use normalization::normalize_event;
47use rolling::RollingEvents;
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50/// Configuration for statistics collection.
51pub struct StatsConfig {
52 /// How timing samples are retained, and so whether the cumulative snapshot
53 /// reports medians.
54 pub samples: SampleMode,
55 /// Number of recent normalized events retained for count-based rolling snapshots.
56 ///
57 /// A successful probe usually contributes two normalized events: one send event
58 /// and one unique reply event.
59 pub rolling_count: Option<usize>,
60 /// Time span of recent normalized events retained for time-based rolling snapshots.
61 ///
62 /// This window has no item cap, even in continuous mode. Its memory depends
63 /// on event volume inside the interval. Expiry occurs on insertion, anchored
64 /// at the maximum observed normalized event timestamp, not when a snapshot
65 /// is read. Events exactly at the cutoff are included. Backdated events
66 /// cannot move the anchor backwards or restore expired history. Retained
67 /// events are replayed in arrival order.
68 pub rolling_time: Option<Duration>,
69 /// Whether matched late replies contribute measurements.
70 pub late_replies: LateReplyMode,
71}
72
73impl StatsConfig {
74 /// Returns the default configuration for finite runs.
75 ///
76 /// Finite mode retains exact samples for every timing metric, so each of
77 /// them reports a median in the cumulative snapshot once it has samples,
78 /// and keeps unbounded adjacent-sequence IPDV tracking so late adjacent
79 /// replies can still complete IPDV pairs.
80 ///
81 /// # Example
82 ///
83 /// ```
84 /// use irtt_stats::{StatsCollector, StatsConfig};
85 ///
86 /// # use std::net::{IpAddr, Ipv4Addr, SocketAddr};
87 /// # use std::time::{Duration, Instant, SystemTime};
88 /// # use irtt_client::{ClientEvent, ClientTimestamp};
89 /// #
90 /// # fn sent(seq: u32, at_ms: u64, send_call_us: u64) -> ClientEvent {
91 /// # let sent_at = ClientTimestamp {
92 /// # mono: Instant::now() + Duration::from_millis(at_ms),
93 /// # wall: SystemTime::UNIX_EPOCH + Duration::from_millis(at_ms),
94 /// # };
95 /// # ClientEvent::EchoSent {
96 /// # seq,
97 /// # remote: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112),
98 /// # scheduled_at: Some(sent_at.mono),
99 /// # sent_at,
100 /// # bytes: 64,
101 /// # send_call: Duration::from_micros(send_call_us),
102 /// # timer_error: Some(Duration::from_micros(2)),
103 /// # }
104 /// # }
105 /// #
106 /// let mut collector = StatsCollector::new(StatsConfig::finite());
107 /// for (seq, send_call_us) in [10_u64, 30, 20].into_iter().enumerate() {
108 /// collector.process(&sent(seq as u32, seq as u64 * 10, send_call_us));
109 /// }
110 ///
111 /// let snapshot = collector.snapshot();
112 /// // Finite mode retains exact samples, so the send-call metric reports an
113 /// // exact median once it has samples: the middle of the sorted
114 /// // 10, 20, 30 µs values, not their mean.
115 /// assert_eq!(snapshot.send_call.count, 3);
116 /// assert_eq!(snapshot.send_call.median_ns, Some(20_000.0));
117 /// ```
118 pub fn finite() -> Self {
119 Self {
120 samples: SampleMode::Exact,
121 rolling_count: None,
122 rolling_time: None,
123 late_replies: LateReplyMode::Measure,
124 }
125 }
126
127 /// Returns a configuration for long-running use.
128 ///
129 /// Continuous mode uses running statistics, retains no exact samples so no
130 /// timing metric reports a median, and bounds adjacent-sequence IPDV
131 /// tracking for long-running sessions. An explicitly enabled `rolling_time`
132 /// window is not hard-bounded; use `rolling_count` for bounded event storage.
133 ///
134 /// # Example
135 ///
136 /// ```
137 /// use irtt_stats::{StatsCollector, StatsConfig};
138 ///
139 /// # use std::net::{IpAddr, Ipv4Addr, SocketAddr};
140 /// # use std::time::{Duration, Instant, SystemTime};
141 /// # use irtt_client::{ClientEvent, ClientTimestamp};
142 /// #
143 /// # fn sent(seq: u32, at_ms: u64, send_call_us: u64) -> ClientEvent {
144 /// # let sent_at = ClientTimestamp {
145 /// # mono: Instant::now() + Duration::from_millis(at_ms),
146 /// # wall: SystemTime::UNIX_EPOCH + Duration::from_millis(at_ms),
147 /// # };
148 /// # ClientEvent::EchoSent {
149 /// # seq,
150 /// # remote: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112),
151 /// # scheduled_at: Some(sent_at.mono),
152 /// # sent_at,
153 /// # bytes: 64,
154 /// # send_call: Duration::from_micros(send_call_us),
155 /// # timer_error: Some(Duration::from_micros(2)),
156 /// # }
157 /// # }
158 /// #
159 /// let mut collector = StatsCollector::new(StatsConfig::continuous());
160 /// for seq in 0..3 {
161 /// collector.process(&sent(seq, u64::from(seq) * 10, 10));
162 /// }
163 ///
164 /// let snapshot = collector.snapshot();
165 /// // Continuous mode still tracks running statistics — count, mean, min,
166 /// // and max — but retains no exact samples, so it reports no median.
167 /// assert_eq!(snapshot.send_call.count, 3);
168 /// assert!(snapshot.send_call.mean_ns > 0.0);
169 /// assert_eq!(snapshot.send_call.median_ns, None);
170 /// ```
171 pub fn continuous() -> Self {
172 Self {
173 samples: SampleMode::RunningOnly,
174 rolling_count: None,
175 rolling_time: None,
176 late_replies: LateReplyMode::Measure,
177 }
178 }
179
180 /// Returns an approximate estimate of the bytes a [`StatsCollector`] with
181 /// this configuration retains after processing `probe_count` probes.
182 ///
183 /// This is a planning aid — it exists so callers can warn about a run
184 /// before starting it — and deliberately not allocator accounting. It is
185 /// derived from the structures this crate retains and a fixed
186 /// capacity-headroom factor for the amortized-doubling containers those
187 /// structures use, so it errs toward overestimating.
188 ///
189 /// # What is included
190 ///
191 /// - Exact timing samples, for every metric that retains them.
192 /// - Adjacent-sequence IPDV tracking state.
193 /// - Count-based rolling storage, when [`StatsConfig::rolling_count`] is
194 /// set, assuming a probe contributes one send event and one unique
195 /// reply event.
196 ///
197 /// # What is excluded
198 ///
199 /// - Time-based rolling storage. [`StatsConfig::rolling_time`] retains
200 /// whatever arrives inside its window, which cannot be bounded from a
201 /// probe count alone without assuming traffic timing, so it does not
202 /// contribute to this estimate.
203 /// - Everything outside this crate: sockets, client session state,
204 /// rendered output, and caller structures.
205 ///
206 /// # Sample mode
207 ///
208 /// [`SampleMode::Exact`] retains one value per timing metric per ordinary
209 /// successful probe and keeps unbounded IPDV state, so the estimate grows
210 /// with `probe_count`. [`SampleMode::RunningOnly`] retains no exact
211 /// samples and bounds its IPDV state, so the estimate reaches a ceiling
212 /// and stops growing.
213 ///
214 /// # Optional measurements
215 ///
216 /// Adjusted RTT, one-way delay, send/receive IPDV, and server processing
217 /// are only measured when the negotiated session supplies the necessary
218 /// timestamps. The estimate assumes all of them are available, which is
219 /// the upper bound.
220 ///
221 /// # Saturation
222 ///
223 /// The result is deterministic and computed with saturating arithmetic, so
224 /// an enormous `probe_count` saturates at [`u64::MAX`] rather than
225 /// wrapping or panicking.
226 ///
227 /// # Example
228 ///
229 /// ```
230 /// use irtt_stats::StatsConfig;
231 ///
232 /// // Finite retention grows with the probe count...
233 /// let finite = StatsConfig::finite();
234 /// assert!(
235 /// finite.estimated_retained_bytes(1_000_000)
236 /// > finite.estimated_retained_bytes(1_000)
237 /// );
238 ///
239 /// // ...while continuous retention is bounded, so a far longer run is no
240 /// // more expensive.
241 /// let continuous = StatsConfig::continuous();
242 /// assert_eq!(
243 /// continuous.estimated_retained_bytes(1_000_000),
244 /// continuous.estimated_retained_bytes(10_000_000),
245 /// );
246 /// assert!(
247 /// continuous.estimated_retained_bytes(1_000_000)
248 /// < finite.estimated_retained_bytes(1_000_000)
249 /// );
250 ///
251 /// // Enormous probe counts saturate rather than wrapping or panicking.
252 /// assert_eq!(finite.estimated_retained_bytes(u64::MAX), u64::MAX);
253 /// ```
254 pub fn estimated_retained_bytes(&self, probe_count: u64) -> u64 {
255 retention::estimated_retained_bytes(self, probe_count)
256 }
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
260/// Whether a matched late reply contributes measurements.
261///
262/// This is a measurement policy, not a classification. A late reply that the
263/// client matched to retained send state is a late unique reply in both modes,
264/// and a late reply that could not be matched remains an untracked late reply
265/// in both modes. Only what a matched late reply is allowed to measure differs,
266/// so a caller that treats late replies as diagnostics does not have to degrade
267/// the event itself to suppress its timing contribution.
268pub enum LateReplyMode {
269 /// Matched late replies contribute timing, one-way, server-processing, and
270 /// IPDV measurements like any other unique reply.
271 #[default]
272 Measure,
273 /// Matched late replies are counted but measure nothing.
274 ///
275 /// Packet, byte, late, and server-reported receive counters still update,
276 /// so the reply stays visible as a late unique reply. Timing metrics,
277 /// one-way and server-processing measurements, and IPDV do not update, and
278 /// [`EventStatsUpdate::contributed_sample`] is `false`.
279 CountOnly,
280}
281
282impl Default for StatsConfig {
283 fn default() -> Self {
284 Self::finite()
285 }
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq)]
289/// Controls whether exact timing samples are retained.
290///
291/// This governs the cumulative snapshot from [`StatsCollector::snapshot`],
292/// where the choice applies uniformly to every timing metric, so that snapshot
293/// never mixes median-capable and median-incapable timing metrics. Rolling
294/// snapshots are recomputed with running statistics whatever this is set to;
295/// see [`StatsCollector::rolling_count`].
296pub enum SampleMode {
297 /// Keep only running statistics; medians are not available for any timing
298 /// metric, and retention stays bounded for long-running sessions.
299 RunningOnly,
300 /// Retain exact samples for every timing metric, so each one reports a
301 /// median once it has samples in the cumulative snapshot. Retention grows
302 /// with the probe count.
303 Exact,
304}
305
306#[derive(Debug, Clone, PartialEq)]
307/// Stateful statistics collector for `irtt-client` events.
308///
309/// A collector maintains cumulative statistics and, when configured, rolling
310/// windows. Rolling snapshots are recomputed from retained normalized events.
311///
312/// # Example
313///
314/// Feed a short stream of `irtt-client` events to a finite-mode collector and
315/// read back a cumulative snapshot:
316///
317/// ```
318/// use irtt_stats::{StatsCollector, StatsConfig};
319///
320/// # use std::net::{IpAddr, Ipv4Addr, SocketAddr};
321/// # use std::time::{Duration, Instant, SystemTime};
322/// # use irtt_client::{ClientEvent, ClientTimestamp, RttSample, SignedDuration};
323/// #
324/// # /// A synthetic reply; a real session delivers these as `ClientEvent`s.
325/// # fn reply(seq: u32, rtt_ms: u64) -> ClientEvent {
326/// # let remote = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112);
327/// # let sent_at = ClientTimestamp {
328/// # mono: Instant::now() + Duration::from_millis(u64::from(seq) * 10),
329/// # wall: SystemTime::UNIX_EPOCH + Duration::from_millis(u64::from(seq) * 10),
330/// # };
331/// # let raw = Duration::from_millis(rtt_ms);
332/// # ClientEvent::EchoReply {
333/// # seq,
334/// # remote,
335/// # sent_at,
336/// # received_at: ClientTimestamp {
337/// # mono: sent_at.mono + raw,
338/// # wall: sent_at.wall + raw,
339/// # },
340/// # rtt: RttSample {
341/// # raw,
342/// # adjusted: None,
343/// # effective: SignedDuration::from_duration(raw),
344/// # },
345/// # server_timing: None,
346/// # one_way: None,
347/// # received_stats: None,
348/// # bytes: 64,
349/// # packet_meta: irtt_client::PacketMeta::default(),
350/// # }
351/// # }
352/// #
353/// let mut collector = StatsCollector::new(StatsConfig::finite());
354/// for (seq, rtt_ms) in [10_u64, 30, 20].into_iter().enumerate() {
355/// collector.process(&reply(seq as u32, rtt_ms));
356/// }
357///
358/// let snapshot = collector.snapshot();
359/// assert_eq!(snapshot.events.echo_replies, 3);
360/// assert_eq!(snapshot.rtt.primary.count, 3);
361/// // Finite mode retains exact samples, so the round-trip metric reports an
362/// // exact median: the middle of the sorted 10, 20, 30 ms samples.
363/// assert_eq!(snapshot.rtt.primary.median_ns, Some(20_000_000.0));
364/// ```
365pub struct StatsCollector {
366 cumulative: CoreStats,
367 rolling: RollingEvents,
368}
369
370impl StatsCollector {
371 /// Creates a collector with the supplied configuration.
372 pub fn new(config: StatsConfig) -> Self {
373 Self {
374 cumulative: CoreStats::new(config.samples, config.late_replies),
375 rolling: RollingEvents::new(config),
376 }
377 }
378
379 /// Processes one client event and returns the per-event statistics update.
380 ///
381 /// Updates currently report whether the event contributed a unique reply
382 /// timing sample and any adjacent-sequence IPDV pairs completed by this
383 /// event.
384 pub fn process(&mut self, event: &ClientEvent) -> EventStatsUpdate {
385 let Some(stats_event) = normalize_event(event) else {
386 return EventStatsUpdate::default();
387 };
388
389 let update = self.cumulative.apply(stats_event.clone());
390 self.rolling.push(stats_event);
391 update
392 }
393
394 /// Returns a snapshot of all events processed by this collector.
395 ///
396 /// This is the snapshot [`StatsConfig::samples`] applies to, so under
397 /// [`SampleMode::Exact`] every timing metric here reports a median once it
398 /// has samples.
399 pub fn snapshot(&self) -> Snapshot {
400 self.cumulative.snapshot()
401 }
402
403 /// Returns the configured count-based rolling snapshot, if enabled.
404 ///
405 /// The snapshot is recomputed from the retained rolling events using
406 /// running statistics only, whatever [`StatsConfig::samples`] is set to,
407 /// so its timing metrics report no median. A rolling window is a bounded
408 /// recent view rather than the run's retained history.
409 ///
410 /// # Example
411 ///
412 /// ```
413 /// use irtt_stats::{StatsCollector, StatsConfig};
414 ///
415 /// # use std::net::{IpAddr, Ipv4Addr, SocketAddr};
416 /// # use std::time::{Duration, Instant, SystemTime};
417 /// # use irtt_client::{ClientEvent, ClientTimestamp};
418 /// #
419 /// # fn sent(seq: u32, at_ms: u64, send_call_us: u64) -> ClientEvent {
420 /// # let sent_at = ClientTimestamp {
421 /// # mono: Instant::now() + Duration::from_millis(at_ms),
422 /// # wall: SystemTime::UNIX_EPOCH + Duration::from_millis(at_ms),
423 /// # };
424 /// # ClientEvent::EchoSent {
425 /// # seq,
426 /// # remote: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112),
427 /// # scheduled_at: Some(sent_at.mono),
428 /// # sent_at,
429 /// # bytes: 64,
430 /// # send_call: Duration::from_micros(send_call_us),
431 /// # timer_error: Some(Duration::from_micros(2)),
432 /// # }
433 /// # }
434 /// #
435 /// let mut collector = StatsCollector::new(StatsConfig {
436 /// rolling_count: Some(2),
437 /// ..StatsConfig::finite()
438 /// });
439 /// collector.process(&sent(0, 0, 10));
440 /// collector.process(&sent(1, 10, 10));
441 /// collector.process(&sent(2, 20, 10));
442 ///
443 /// // The window keeps only the last two normalized events...
444 /// let rolling = collector.rolling_count().unwrap();
445 /// assert_eq!(rolling.packets.packets_sent, 2);
446 /// // ...and is recomputed with running statistics, so it reports no median
447 /// // even though the collector retains exact samples for its cumulative
448 /// // snapshot.
449 /// assert_eq!(rolling.send_call.median_ns, None);
450 ///
451 /// // The cumulative snapshot still sees every event.
452 /// let cumulative = collector.snapshot();
453 /// assert_eq!(cumulative.packets.packets_sent, 3);
454 /// assert_eq!(cumulative.send_call.median_ns, Some(10_000.0));
455 /// ```
456 pub fn rolling_count(&self) -> Option<Snapshot> {
457 self.rolling.count_snapshot()
458 }
459
460 /// Returns the configured time-based rolling snapshot, if enabled.
461 ///
462 /// Like [`StatsCollector::rolling_count`], this is recomputed with running
463 /// statistics only and reports no medians.
464 ///
465 /// # Example
466 ///
467 /// ```
468 /// use std::time::Duration;
469 ///
470 /// use irtt_stats::{StatsCollector, StatsConfig};
471 ///
472 /// # use std::net::{IpAddr, Ipv4Addr, SocketAddr};
473 /// # use std::time::Instant;
474 /// # use irtt_client::{ClientEvent, ClientTimestamp};
475 /// #
476 /// # fn sent(seq: u32, at_ms: u64, send_call_us: u64) -> ClientEvent {
477 /// # let sent_at = ClientTimestamp {
478 /// # mono: Instant::now() + Duration::from_millis(at_ms),
479 /// # wall: std::time::SystemTime::UNIX_EPOCH + Duration::from_millis(at_ms),
480 /// # };
481 /// # ClientEvent::EchoSent {
482 /// # seq,
483 /// # remote: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112),
484 /// # scheduled_at: Some(sent_at.mono),
485 /// # sent_at,
486 /// # bytes: 64,
487 /// # send_call: Duration::from_micros(send_call_us),
488 /// # timer_error: Some(Duration::from_micros(2)),
489 /// # }
490 /// # }
491 /// #
492 /// let mut collector = StatsCollector::new(StatsConfig {
493 /// rolling_time: Some(Duration::from_millis(15)),
494 /// ..StatsConfig::finite()
495 /// });
496 /// collector.process(&sent(0, 0, 10));
497 /// collector.process(&sent(1, 10, 10));
498 /// collector.process(&sent(2, 30, 10));
499 ///
500 /// // A 15 ms window anchored at the latest event (30 ms) retains only the
501 /// // events from the last 15 ms — here just the final send.
502 /// let rolling = collector.rolling_time().unwrap();
503 /// assert_eq!(rolling.packets.packets_sent, 1);
504 /// ```
505 pub fn rolling_time(&self) -> Option<Snapshot> {
506 self.rolling.time_snapshot()
507 }
508}
509
510#[derive(Debug, Clone, PartialEq, Eq, Default)]
511/// Per-event statistics produced by [`StatsCollector::process`].
512pub struct EventStatsUpdate {
513 /// Whether the processed event contributed a unique reply timing sample.
514 pub contributed_sample: bool,
515 /// Adjacent-sequence IPDV pairs completed by the processed event.
516 pub ipdv_pairs: Vec<IpdvPairUpdate>,
517}
518
519#[derive(Debug, Clone, Copy, PartialEq, Eq)]
520/// Adjacent-sequence IPDV pair completed by a processed event.
521pub struct IpdvPairUpdate {
522 /// Sequence number of the earlier reply in the adjacent pair.
523 pub previous_seq: u32,
524 /// Sequence number of the later reply in the adjacent pair.
525 pub current_seq: u32,
526 /// Signed round-trip delay change (current minus previous) in nanoseconds.
527 pub rtt_ipdv_ns: i128,
528 /// Signed send-side delay change in nanoseconds, when available for both replies.
529 pub send_ipdv_ns: Option<i128>,
530 /// Signed receive-side delay change in nanoseconds, when available for both replies.
531 pub receive_ipdv_ns: Option<i128>,
532 /// Absolute round-trip IPDV between the adjacent replies.
533 pub rtt_ipdv: Duration,
534 /// Absolute send-side IPDV when send one-way delay is available for both replies.
535 pub send_ipdv: Option<Duration>,
536 /// Absolute receive-side IPDV when receive one-way delay is available for both replies.
537 pub receive_ipdv: Option<Duration>,
538}
539
540#[derive(Debug, Clone, PartialEq)]
541/// Point-in-time statistics summary.
542pub struct Snapshot {
543 /// Event counters grouped by event class.
544 pub events: EventCounts,
545 /// Packet and byte counters.
546 pub packets: PacketCounts,
547 /// Packet loss, duplicate, and late-packet percentages.
548 pub loss: LossStats,
549 /// Duration of send calls, in nanoseconds.
550 pub send_call: TimeStats,
551 /// Sender scheduling error, in nanoseconds.
552 pub timer_error: TimeStats,
553 /// Round-trip timing statistics.
554 pub rtt: RttStats,
555 /// Inter-packet delay variation statistics.
556 pub ipdv: IpdvStats,
557 /// One-way delay statistics.
558 pub one_way_delay: OneWayDelayStats,
559 /// Server processing time statistics.
560 pub server_processing: ServerProcessingStats,
561}
562
563#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
564/// Counts of normalized client events.
565pub struct EventCounts {
566 /// Probe send events processed.
567 pub sent_events: u64,
568 /// On-time unique echo replies processed.
569 pub echo_replies: u64,
570 /// Late unique echo replies processed.
571 pub late_unique_replies: u64,
572 /// Duplicate echo replies processed.
573 pub duplicate_replies: u64,
574 /// Loss events processed.
575 pub loss_events: u64,
576 /// Warning events processed.
577 pub warning_events: u64,
578 /// Late replies that could not be matched to retained in-flight state.
579 ///
580 /// These replies are observed on the socket, but cannot contribute RTT,
581 /// one-way delay, server-processing, or IPDV samples.
582 pub untracked_late_replies: u64,
583}
584
585#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
586/// Packet, byte, and server-reported receive counters.
587pub struct PacketCounts {
588 /// Probe packets sent by the local client.
589 pub packets_sent: u64,
590 /// Reply packets observed by the local client.
591 ///
592 /// This includes unique measurable replies, duplicate replies, and late
593 /// replies that cannot be matched to retained in-flight state.
594 pub packets_received: u64,
595 /// Unique replies that can contribute timing measurements.
596 pub unique_replies: u64,
597 /// Duplicate replies received by the local client.
598 pub duplicates: u64,
599 /// Late reply packets received by the local client, including untracked-late replies.
600 pub late_packets: u64,
601 /// Probe bytes sent by the local client.
602 pub bytes_sent: u64,
603 /// Reply bytes observed by the local client.
604 ///
605 /// This includes bytes from unique measurable replies, duplicate replies,
606 /// and untracked-late replies. Per-category byte counts are not currently
607 /// exposed separately.
608 pub bytes_received: u64,
609 /// Highest cumulative server-reported packets received, when available.
610 ///
611 /// Rolling snapshots also preserve this raw cumulative value from retained
612 /// replies. Their directional loss uses an interval-relative increase
613 /// instead; see [`LossStats`] for baseline and availability semantics.
614 pub server_packets_received: Option<u64>,
615 /// Raw server-reported receive window from the most recently processed
616 /// reply that carried one, when available.
617 ///
618 /// The value is stored exactly as the server reported it and is not
619 /// interpreted by this crate. It is a bounded bitmap of recent server
620 /// receive history rather than a fixed 64-packet loss mask, so a value of
621 /// `0x1` means no earlier history is represented and not that the previous
622 /// 63 packets were lost. Replies are aggregated in local arrival order, so
623 /// a late reply can leave an older window here than an already-processed
624 /// reply carried.
625 pub server_received_window: Option<u64>,
626}
627
628#[derive(Debug, Clone, PartialEq)]
629/// Round-trip time statistics.
630pub struct RttStats {
631 /// Effective RTT used for primary RTT reporting and IPDV input.
632 pub primary: TimeStats,
633 /// Client-observed raw RTT.
634 pub raw: TimeStats,
635 /// RTT adjusted for server processing time when available.
636 pub adjusted: TimeStats,
637}
638
639#[derive(Debug, Clone, PartialEq)]
640/// Inter-packet delay variation statistics.
641pub struct IpdvStats {
642 /// Round-trip IPDV.
643 pub round_trip: TimeStats,
644 /// Send-side IPDV, when send one-way delay is available.
645 pub send: TimeStats,
646 /// Receive-side IPDV, when receive one-way delay is available.
647 pub receive: TimeStats,
648}
649
650#[derive(Debug, Clone, PartialEq)]
651/// One-way delay statistics.
652pub struct OneWayDelayStats {
653 /// Client-to-server delay.
654 pub send_delay: TimeStats,
655 /// Server-to-client delay.
656 pub receive_delay: TimeStats,
657}
658
659#[derive(Debug, Clone, PartialEq)]
660/// Server processing time statistics.
661pub struct ServerProcessingStats {
662 /// Time spent processing a probe at the server.
663 pub processing: TimeStats,
664}