Skip to main content

irtt_client/
event.rs

1use std::{
2    net::SocketAddr,
3    time::{Duration, Instant},
4};
5
6use crate::{session::NegotiatedParams, timing::ClientTimestamp};
7
8/// Result of the IRTT open exchange.
9///
10/// The lower-level [`Client`](crate::Client) API returns this before any echo
11/// probes are driven. Managed sessions publish the contained lifecycle event to
12/// subscribers immediately after a successful open.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum OpenOutcome {
15    /// A normal probe session was opened.
16    Started {
17        /// Resolved remote socket address used by the UDP socket.
18        remote: SocketAddr,
19        /// Session token assigned by the server and used on echo/close packets.
20        token: u64,
21        /// Parameters returned by the server after negotiation.
22        negotiated: NegotiatedParams,
23        /// Lifecycle event corresponding to this outcome.
24        event: ClientEvent,
25    },
26    /// A no-test open exchange completed without starting a probe session.
27    NoTestCompleted {
28        /// Resolved remote socket address used by the UDP socket.
29        remote: SocketAddr,
30        /// Parameters returned by the server after negotiation.
31        negotiated: NegotiatedParams,
32        /// Lifecycle event corresponding to this outcome.
33        event: ClientEvent,
34    },
35}
36
37/// Event emitted by an IRTT client session.
38///
39/// Events form the public lifecycle and measurement stream for callers. UIs
40/// usually display lifecycle, warning, and per-packet events directly; summary
41/// reporting should aggregate `EchoSent`, `EchoReply`, `EchoLoss`,
42/// `LateReply`, and `DuplicateReply` events or pass the stream to
43/// `irtt-stats`.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum ClientEvent {
46    /// A normal session has opened and is ready to send echo probes.
47    SessionStarted {
48        /// Resolved remote socket address.
49        remote: SocketAddr,
50        /// Session token assigned by the server.
51        token: u64,
52        /// Negotiated protocol parameters for this session.
53        negotiated: NegotiatedParams,
54        /// Client timestamp when the open reply was accepted.
55        at: ClientTimestamp,
56    },
57    /// A negotiation-only no-test exchange completed.
58    ///
59    /// No `EchoSent`, `EchoReply`, `EchoLoss`, or `SessionClosed` events are
60    /// expected for this open outcome.
61    NoTestCompleted {
62        /// Resolved remote socket address.
63        remote: SocketAddr,
64        /// Negotiated protocol parameters returned by the server.
65        negotiated: NegotiatedParams,
66        /// Client timestamp when the open reply was accepted.
67        at: ClientTimestamp,
68    },
69    /// The session closed locally or through an authenticated peer close.
70    ///
71    /// For a peer close-flagged echo reply, the reply's measurement or
72    /// classification event is emitted immediately before this event.
73    /// Managed sessions disconnect event subscribers after the worker exits,
74    /// while leaving already queued events available to drain.
75    SessionClosed {
76        /// Resolved remote socket address.
77        remote: SocketAddr,
78        /// Session token that was closed.
79        token: u64,
80        /// Client timestamp when the close request was sent.
81        at: ClientTimestamp,
82    },
83
84    /// An echo request was sent.
85    ///
86    /// This records local scheduling and send-call timing. It does not imply a
87    /// reply will arrive; callers should pair it with `EchoReply`, `EchoLoss`,
88    /// `LateReply`, or `DuplicateReply` by sequence number when displaying
89    /// packet lifecycle state.
90    EchoSent {
91        /// Wire sequence number sent in the echo request.
92        seq: u32,
93        /// Resolved remote socket address.
94        remote: SocketAddr,
95        /// Monotonic deadline at which the probe was scheduled to be sent.
96        scheduled_at: Instant,
97        /// Paired client wall/monotonic timestamp captured immediately after
98        /// the successful UDP send completed. This is the RTT and userspace
99        /// upstream one-way-delay fallback endpoint. The operational probe
100        /// timeout deadline is computed from an earlier, private, pre-send
101        /// timestamp instead — see the client crate's `AGENTS.md`.
102        sent_at: ClientTimestamp,
103        /// Number of bytes passed to the UDP socket.
104        bytes: usize,
105        /// Elapsed time spent in the socket send call, excluding protocol and
106        /// scheduler commit work.
107        send_call: Duration,
108        /// Absolute difference between the scheduled send time and the
109        /// post-send `sent_at` timestamp above.
110        timer_error: Duration,
111    },
112
113    /// First in-window reply for a pending echo request.
114    ///
115    /// This is the primary successful measurement event. RTT and optional
116    /// server timing/statistics fields should generally be aggregated from this
117    /// variant.
118    EchoReply {
119        /// Wire sequence number from the echo reply.
120        seq: u32,
121        /// Resolved remote socket address.
122        remote: SocketAddr,
123        /// Client timestamp recorded immediately after the matching request
124        /// finished sending (see `ClientEvent::EchoSent::sent_at`).
125        sent_at: ClientTimestamp,
126        /// Client timestamp recorded when the reply was received.
127        received_at: ClientTimestamp,
128        /// Round-trip timing sample for this reply.
129        rtt: RttSample,
130        /// Optional server-reported timestamps and processing time.
131        server_timing: Option<ServerTiming>,
132        /// Optional one-way delay values derived from client/server wall clocks.
133        one_way: Option<OneWayDelaySample>,
134        /// Optional server-reported received-statistics sample.
135        received_stats: Option<ReceivedStatsSample>,
136        /// UDP datagram size received from the socket.
137        bytes: usize,
138        /// Receive-side metadata observed outside the IRTT wire payload.
139        packet_meta: PacketMeta,
140    },
141
142    /// A sent echo request exceeded the local probe timeout before a reply was
143    /// matched.
144    ///
145    /// A later datagram for the same sequence may still arrive and will be
146    /// reported as [`ClientEvent::LateReply`] rather than changing this event.
147    EchoLoss {
148        /// Wire sequence number that timed out.
149        seq: u32,
150        /// Client timestamp recorded immediately after the request finished
151        /// sending (see `ClientEvent::EchoSent::sent_at`). The timeout
152        /// deadline below is anchored to an earlier, private, pre-send
153        /// timestamp, not to this one.
154        sent_at: ClientTimestamp,
155        /// Monotonic deadline at which the probe was declared lost.
156        timeout_at: std::time::Instant,
157    },
158
159    /// Additional reply for a sequence that was already completed.
160    ///
161    /// Duplicates are not primary RTT samples. They are useful for diagnostics
162    /// and packet counters, but should not be aggregated as successful first
163    /// replies.
164    DuplicateReply {
165        /// Wire sequence number from the duplicate reply.
166        seq: u32,
167        /// Resolved remote socket address.
168        remote: SocketAddr,
169        /// Client timestamp recorded when the duplicate was received.
170        received_at: ClientTimestamp,
171        /// UDP datagram size received from the socket.
172        bytes: usize,
173    },
174
175    /// Reply for a sequence that was no longer pending.
176    ///
177    /// This commonly means the probe had already been reported as lost, or the
178    /// reply arrived out of the client's current tracking window. When the
179    /// original send metadata is still retained, RTT and one-way timing are
180    /// included; otherwise those fields are `None`.
181    LateReply {
182        /// Wire sequence number from the late reply.
183        seq: u32,
184        /// Highest sequence number accepted as an in-window first reply so far.
185        highest_seen: u32,
186        /// Resolved remote socket address.
187        remote: SocketAddr,
188        /// Original post-send client timestamp, when still retained (see
189        /// `ClientEvent::EchoSent::sent_at`).
190        sent_at: Option<ClientTimestamp>,
191        /// Client timestamp recorded when the late reply was received.
192        received_at: ClientTimestamp,
193        /// RTT sample computed when the original send timestamp is available.
194        rtt: Option<RttSample>,
195        /// Optional server-reported timestamps and processing time.
196        server_timing: Option<ServerTiming>,
197        /// Optional one-way delay values derived from client/server wall clocks.
198        one_way: Option<OneWayDelaySample>,
199        /// Optional server-reported received-statistics sample.
200        received_stats: Option<ReceivedStatsSample>,
201        /// UDP datagram size received from the socket.
202        bytes: usize,
203        /// Receive-side metadata observed outside the IRTT wire payload.
204        packet_meta: PacketMeta,
205    },
206
207    /// Non-fatal condition observed while receiving or classifying packets.
208    ///
209    /// Warning events are diagnostic. They do not close the session by
210    /// themselves and are not measurement samples.
211    Warning {
212        /// Machine-readable warning category.
213        kind: WarningKind,
214        /// Human-readable diagnostic text.
215        message: String,
216        /// Client timestamp when the warning was emitted.
217        at: ClientTimestamp,
218    },
219}
220
221/// Category for a non-fatal [`ClientEvent::Warning`].
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223#[non_exhaustive]
224pub enum WarningKind {
225    /// A received datagram could not be decoded as a relevant IRTT packet.
226    MalformedOrUnrelatedPacket,
227    /// A decoded packet used a token that does not match the open session.
228    WrongToken,
229    /// A decoded reply could not be matched to tracked probe state.
230    UntrackedReply,
231}
232
233/// Round-trip timing for one echo reply.
234///
235/// `raw` is the client-observed elapsed time from sending the echo request to
236/// receiving the reply and is therefore non-negative. When the server reports
237/// enough monotonic timing to compute processing time, `adjusted` subtracts
238/// that server processing from `raw`. `effective` is the value used by reporting
239/// and statistics: adjusted when available, otherwise raw converted to a signed
240/// value.
241///
242/// Adjusted and effective RTT values are signed. They may be negative if the
243/// reported server processing time is greater than the raw client-observed RTT.
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub struct RttSample {
246    /// Client-observed RTT from send to receive.
247    pub raw: Duration,
248    /// Signed RTT adjusted for server processing when server processing is available.
249    pub adjusted: Option<SignedDuration>,
250    /// Signed effective RTT used by stats and CLI reporting.
251    ///
252    /// This is `adjusted` when server processing is available, otherwise `raw`
253    /// converted to a signed duration.
254    pub effective: SignedDuration,
255}
256
257/// Signed nanosecond-backed duration used for derived timing values.
258///
259/// This type preserves negative values that can arise after subtracting server
260/// processing time or comparing wall-clock timestamps from different hosts. It
261/// should not be used to represent raw elapsed wall-clock or monotonic time,
262/// which is non-negative and represented by [`Duration`].
263///
264/// # Example
265///
266/// Signed timing values retain a negative result instead of clamping clock
267/// skew or server processing adjustments to zero:
268///
269/// ```
270/// use irtt_client::SignedDuration;
271///
272/// let adjusted_rtt = SignedDuration::from_nanos(-250_000);
273/// assert!(adjusted_rtt.is_negative());
274/// assert_eq!(adjusted_rtt.as_micros(), -250);
275/// ```
276#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
277pub struct SignedDuration {
278    ns: i128,
279}
280
281impl SignedDuration {
282    /// Construct a signed duration from nanoseconds.
283    pub const fn from_nanos(ns: i128) -> Self {
284        Self { ns }
285    }
286
287    /// Convert a non-negative [`Duration`] into a signed duration.
288    ///
289    /// Durations larger than `i128::MAX` nanoseconds saturate to `i128::MAX`.
290    pub fn from_duration(duration: Duration) -> Self {
291        Self {
292            ns: i128::try_from(duration.as_nanos()).unwrap_or(i128::MAX),
293        }
294    }
295
296    /// Return the duration in nanoseconds.
297    pub const fn as_nanos(self) -> i128 {
298        self.ns
299    }
300
301    /// Return the duration in whole microseconds, truncated toward zero.
302    pub const fn as_micros(self) -> i128 {
303        self.ns / 1_000
304    }
305
306    /// Return the duration in whole milliseconds, truncated toward zero.
307    pub const fn as_millis(self) -> i128 {
308        self.ns / 1_000_000
309    }
310
311    /// Return whether this duration is less than zero.
312    pub const fn is_negative(self) -> bool {
313        self.ns < 0
314    }
315}
316
317impl From<Duration> for SignedDuration {
318    fn from(value: Duration) -> Self {
319        Self::from_duration(value)
320    }
321}
322
323/// Server-reported timing fields decoded from an echo reply.
324///
325/// These values are optional because the negotiated timestamp mode, clock mode,
326/// server implementation, or packet contents may omit individual fields.
327/// Timestamp values are nanoseconds in the server's reported wall-clock or
328/// monotonic clock domain. `None` means unavailable; `Some(0)` means the server
329/// reported a zero timestamp.
330#[derive(Debug, Clone, Copy, PartialEq, Eq)]
331pub struct ServerTiming {
332    /// Server wall-clock receive timestamp, in nanoseconds.
333    pub receive_wall_ns: Option<i64>,
334    /// Server monotonic receive timestamp, in nanoseconds.
335    pub receive_mono_ns: Option<i64>,
336    /// Server wall-clock send timestamp, in nanoseconds.
337    pub send_wall_ns: Option<i64>,
338    /// Server monotonic send timestamp, in nanoseconds.
339    pub send_mono_ns: Option<i64>,
340    /// Server wall-clock midpoint timestamp, in nanoseconds.
341    pub midpoint_wall_ns: Option<i64>,
342    /// Server monotonic midpoint timestamp, in nanoseconds.
343    pub midpoint_mono_ns: Option<i64>,
344    /// Server processing time computed from monotonic receive/send timestamps.
345    ///
346    /// `None` means the required monotonic timestamps were unavailable or
347    /// unusable. `Some(Duration::ZERO)` means the computed processing interval
348    /// was available and zero.
349    pub processing: Option<Duration>,
350}
351
352/// Directional one-way delay samples computed from wall-clock timestamps.
353///
354/// Each directional field is an `Option<SignedDuration>`. `None` means the
355/// required client or server wall-clock timestamp was unavailable, or the
356/// timestamp arithmetic was out of range. `Some(value)` means the delay was
357/// computed from available timestamps.
358///
359/// Negative computed values are preserved as `Some` instead of being clamped to
360/// zero or dropped. A negative one-way delay usually indicates wall-clock skew
361/// between the client and server.
362#[derive(Debug, Clone, Copy, PartialEq, Eq)]
363pub struct OneWayDelaySample {
364    /// Client-to-server delay.
365    ///
366    /// This is computed from the client send wall time and the server receive
367    /// wall time when both are available. The client send endpoint is the
368    /// correlated Linux kernel TX timestamp for this send when one is
369    /// available and plausible for this send, and the userspace post-send wall
370    /// time otherwise. The server receive endpoint is whatever wall time the
371    /// peer reported; against this crate's own Tokio server runtime on Linux
372    /// (the `irtt-server` applet), that is its kernel receive timestamp when
373    /// one is available and plausible, and its userspace receive wall time
374    /// otherwise. Against another IRTT-compatible peer, or an application
375    /// driving `irtt_server::ServerCore::handle_datagram` directly, the
376    /// client has no way to know which clock source produced it — the public
377    /// `ServerCore` API never supplies a kernel timestamp of its own accord.
378    /// `RttSample::raw` is unaffected and remains a userspace
379    /// monotonic measurement, but `adjusted` and `effective` can still shift:
380    /// when the session negotiated wall-clock timing rather than monotonic,
381    /// server processing time is derived from this same server receive
382    /// endpoint.
383    pub client_to_server: Option<SignedDuration>,
384    /// Server-to-client delay.
385    ///
386    /// This is computed from the server send wall time and the client receive
387    /// wall time when both are available. The client receive endpoint is the
388    /// kernel receive timestamp reported with the datagram when one is
389    /// available and plausible for this receive, and the userspace receive wall
390    /// time otherwise. RTT is unaffected and remains a userspace monotonic
391    /// measurement.
392    pub server_to_client: Option<SignedDuration>,
393}
394
395/// Server-reported receive counters decoded from an echo reply.
396///
397/// These fields are optional because the negotiated received-statistics mode or
398/// server implementation may omit them. `None` means unavailable; `Some(0)`
399/// means the server reported the value and it was zero.
400#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401pub struct ReceivedStatsSample {
402    /// Server-reported count of received packets, when available.
403    pub count: Option<u32>,
404    /// Raw server-reported receive window bitmap, when available.
405    ///
406    /// The bitmap is preserved exactly as it appeared on the wire; the client
407    /// does not interpret its bits.
408    pub window: Option<u64>,
409}
410
411/// Receive-side packet metadata observed outside the IRTT wire protocol.
412///
413/// `None` means the metadata was unavailable. `Some(0)` means the metadata was
414/// observed and its value was zero.
415#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
416pub struct PacketMeta {
417    pub traffic_class: Option<u8>,
418    pub dscp: Option<u8>,
419    pub ecn: Option<u8>,
420    /// Kernel software receive timestamp reported with the datagram, where the
421    /// platform provides one.
422    ///
423    /// This is the raw observed value, reported whether or not it was accepted
424    /// as the downstream one-way delay endpoint. It is a kernel software
425    /// timestamp, not a NIC hardware ingress time.
426    pub kernel_rx_timestamp: Option<std::time::SystemTime>,
427}