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