From 90178cfb755c015994c1e12ef659a85eae84d94f Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Thu, 20 Aug 2026 14:56:22 +0700 Subject: [PATCH 1/8] docs: point installation and library usage at crates.io The README led application users through cloning the repo and building with cargo install --path, and led library consumers through a local path dependency, even though irtt-rs and irtt-client are now published. Make the registry the default path for both and keep the local-checkout/path forms documented as the alternative for development. --- README.md | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8305182..83aaf0d 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,10 @@ The installed dispatcher is named `irtt-rs` rather than `irtt` to avoid conflict The build requires Rust 1.88 or newer. -From a local checkout: +Install the latest release from crates.io: ```sh -git clone https://github.com/Lochnair/irtt-rs.git -cd irtt-rs -cargo install --path crates/irtt-app +cargo install irtt-rs --locked ``` This installs: @@ -45,7 +43,15 @@ Each applet can also be built in isolation with `--no-default-features` plus the applet's own feature. For example, a lightweight client-only install: ```sh -cargo install --path crates/irtt-app --no-default-features --features client +cargo install irtt-rs --locked --no-default-features --features client +``` + +For development, or to install directly from a checkout: + +```sh +git clone https://github.com/Lochnair/irtt-rs.git +cd irtt-rs +cargo install --path crates/irtt-app --locked ``` ## Quick start @@ -294,7 +300,14 @@ irtt-server --help `irtt-client` exposes the client session and event layer independently of CLI formatting and statistics. -Add it from a local checkout: +Add it from crates.io: + +```toml +[dependencies] +irtt-client = { version = "0.5", features = ["tokio"] } +``` + +For development against an unpublished change, use a path dependency instead: ```toml [dependencies] From 1ce65c0db46cb6a8350e816568328139c4b9bf02 Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Thu, 20 Aug 2026 14:56:34 +0700 Subject: [PATCH 2/8] docs: note that final target summaries follow CLI order Final per-target summaries print in the order targets were supplied on the command line, not finish order or alphabetical label order. This only concerns the end-of-run summary printout; it says nothing about ordering of live/interleaved per-event output. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 83aaf0d..e209d23 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ Multi-target sessions use staggered pacing by default. To send each target's pro irtt-client host-a:2112 host-b:2112 --pacing burst ``` -Target labels are included in the default multi-target output. +Target labels are included in the default multi-target output. Final per-target summaries are printed in the order targets were supplied on the command line, not the order in which they finish or their labels sort alphabetically. ## Output formats From 13a907bc28518e72f14eae31bb69d5a4b43f4a7f Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Thu, 20 Aug 2026 14:56:50 +0700 Subject: [PATCH 3/8] docs: document Linux kernel-timestamp preference for OWD On Linux, one-way delay measurement best-effort prefers kernel send/receive timestamps over userspace timing, falling back whenever a kernel reading is unavailable or implausible. RTT is unaffected and remains userspace-timed. Add a user-facing README note near the measurement/output explanation, and bring OneWayDelaySample::client_to_server's rustdoc in line with the send-endpoint behavior, matching the style already used for server_to_client. Also fix a stale socket_io module doc that still claimed the captured kernel receive timestamp went unconsumed; ServerCore has used it via preferred_receive_wall_ns since d3f93a9. --- README.md | 2 ++ crates/irtt-client/src/event.rs | 8 +++++++- crates/irtt-server/src/socket_io.rs | 10 +++++----- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e209d23..b4bb6f4 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,8 @@ Useful measurement fields include: Adjusted RTT can be negative when server processing exceeds the measured raw RTT. One-way delay estimates can be negative because of clock skew between the client and server. +On Linux, `sd_us` and `rd_us` (and the underlying one-way delay samples) prefer kernel-captured send/receive timestamps over userspace timing where the platform and socket support them, reducing the effect of scheduling and queuing delay on these measurements. This is a best-effort fallback: unsupported or implausible kernel timestamps fall back to userspace timing. RTT always uses userspace timing and is unaffected. + Default table output prints a final summary after completed finite runs and interrupted continuous runs when the run policy permits it. CSV, TSV, and JSON Lines output do not print this summary. A peer closing a finite CLI or TUI run is accepted as a terminal outcome. In diff --git a/crates/irtt-client/src/event.rs b/crates/irtt-client/src/event.rs index 6f74089..ef700c0 100644 --- a/crates/irtt-client/src/event.rs +++ b/crates/irtt-client/src/event.rs @@ -351,7 +351,13 @@ pub struct OneWayDelaySample { /// Client-to-server delay. /// /// This is computed from the client send wall time and the server receive - /// wall time when both are available. + /// wall time when both are available. The client send endpoint is the + /// correlated Linux kernel TX timestamp for this send when one is + /// available and plausible for this send, and the userspace post-send wall + /// time otherwise. The server receive endpoint is the server's kernel + /// receive timestamp when one is available and plausible, and its + /// userspace receive wall time otherwise. RTT is unaffected and remains a + /// userspace monotonic measurement. pub client_to_server: Option, /// Server-to-client delay. /// diff --git a/crates/irtt-server/src/socket_io.rs b/crates/irtt-server/src/socket_io.rs index 1a9b93c..cc2d2e2 100644 --- a/crates/irtt-server/src/socket_io.rs +++ b/crates/irtt-server/src/socket_io.rs @@ -32,11 +32,11 @@ //! instant, is served normally with no timestamp. Nothing about which datagrams //! reach [`ServerCore`](crate::ServerCore) depends on it. //! -//! **Nothing consumes it yet.** It is captured here so a follow-up change can -//! decide how a kernel-observed arrival time enters the protocol core; today -//! the core still samples its own receive instant and no protocol timing is -//! affected. It is a software wall-clock reading only — not a NIC hardware -//! timestamp, not monotonic, and not a transmit timestamp. +//! [`ServerCore`](crate::ServerCore) prefers it over its own receive sample +//! when reporting a request's receive instant, falling back to that sample +//! when no plausible kernel reading is available; see +//! `preferred_receive_wall_ns`. It is a software wall-clock reading only — +//! not a NIC hardware timestamp, not monotonic, and not a transmit timestamp. use std::{io, net::SocketAddr, time::SystemTime}; From a0b51032f23c3fc06e84c0af34687e8eaad1dc35 Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Thu, 20 Aug 2026 14:56:56 +0700 Subject: [PATCH 4/8] docs: document update_targets() replacement semantics ManagedClientHandle::update_targets() had a one-line rustdoc that didn't say targets is a full desired-set replacement, that an unchanged live target continues undisturbed, or that a changed config or a terminal-generation resubmission starts a fresh generation even for byte-identical config. --- crates/irtt-client/src/managed/task.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/irtt-client/src/managed/task.rs b/crates/irtt-client/src/managed/task.rs index b200968..3f190ae 100644 --- a/crates/irtt-client/src/managed/task.rs +++ b/crates/irtt-client/src/managed/task.rs @@ -236,7 +236,21 @@ impl ManagedClientHandle { ManagedStopReceipt::new(Arc::clone(&self.stop)) } - /// Submit one complete desired target set without awaiting queue capacity. + /// Submit the complete desired target set, replacing any previous one. + /// + /// `targets` is not a delta: any live target whose id is missing from + /// `targets` is stopped and removed. A target that is still present with an + /// unchanged configuration continues undisturbed — it is not restarted and + /// does not get a new generation. A target present with a *changed* + /// configuration, or whose current generation already reached a terminal + /// state (e.g. failed, completed, or was closed by the peer), is treated as + /// a fresh start: it is retired and a new generation is created for it, even + /// if the configuration is byte-for-byte identical to the one that just + /// finished. + /// + /// This call enqueues the update without waiting for queue capacity; await + /// the returned [`ManagedCommandReceipt`] to observe whether — and how — it + /// was applied. pub fn update_targets( &self, targets: Vec, From 98e7231ede2967407d12194143bc09808ef049a1 Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Thu, 20 Aug 2026 14:57:02 +0700 Subject: [PATCH 5/8] fix(app): gate feature-specific formatting helpers RUSTFLAGS="-D warnings" cargo check -p irtt-rs --no-default-features --features client (or --features tui) warned on dead code: some format.rs helpers are only called from the client applet, others only from the TUI. Narrowly #[cfg(feature = "...")]-gate each to the applet that actually uses it instead of allowing the warning or introducing a new feature, and split the tests that exercised both groups to match. --- crates/irtt-app/src/cmd/format.rs | 32 ++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/crates/irtt-app/src/cmd/format.rs b/crates/irtt-app/src/cmd/format.rs index 62f284b..0fc1f2f 100644 --- a/crates/irtt-app/src/cmd/format.rs +++ b/crates/irtt-app/src/cmd/format.rs @@ -12,6 +12,7 @@ use std::time::Duration; +#[cfg(feature = "client")] use irtt_client::SignedDuration; /// Rendering used wherever a scalar is unavailable. @@ -61,6 +62,7 @@ pub(crate) fn format_optional_ns_i128(value: Option) -> String { } /// Format an optional nanosecond scalar, rendering `None` as [`ABSENT`]. +#[cfg(feature = "client")] pub(crate) fn format_optional_ns_f64(value: Option) -> String { value.map(format_ns_f64).unwrap_or_else(absent) } @@ -71,6 +73,7 @@ pub(crate) fn format_duration(value: Duration) -> String { } /// Format a signed duration as a nanosecond scalar, preserving its sign. +#[cfg(feature = "client")] pub(crate) fn format_signed_duration(value: SignedDuration) -> String { format_ns_i128(value.as_nanos()) } @@ -88,6 +91,7 @@ pub(crate) fn format_percent(value: f64) -> String { /// Format `value` as a percentage of `total`, capped at 100%. /// /// A zero total has no percentage to report and renders as [`ABSENT`]. +#[cfg(feature = "tui")] pub(crate) fn format_percent_ratio(value: u64, total: u64) -> String { if total == 0 { ABSENT.to_owned() @@ -97,11 +101,13 @@ pub(crate) fn format_percent_ratio(value: u64, total: u64) -> String { } /// Format a scalar count. +#[cfg(feature = "tui")] pub(crate) fn format_count(value: u64) -> String { value.to_string() } /// Format an optional scalar count, rendering `None` as [`ABSENT`]. +#[cfg(feature = "tui")] pub(crate) fn format_optional_count(value: Option) -> String { value.map(format_count).unwrap_or_else(absent) } @@ -152,6 +158,11 @@ mod tests { fn durations_render_as_nanosecond_scalars() { assert_eq!(format_duration(Duration::from_millis(25)), "25.0ms"); assert_eq!(format_duration(Duration::from_secs(1)), "1.000s"); + } + + #[test] + #[cfg(feature = "client")] + fn signed_durations_render_as_nanosecond_scalars() { assert_eq!( format_signed_duration(SignedDuration::from_nanos(-1_500)), "-1.5µs" @@ -161,18 +172,33 @@ mod tests { #[test] fn absent_and_non_finite_scalars_render_as_a_placeholder() { assert_eq!(format_optional_ns_i128(None), ABSENT); - assert_eq!(format_optional_ns_f64(None), ABSENT); - assert_eq!(format_optional_count(None), ABSENT); assert_eq!(format_ns_f64(f64::NAN), ABSENT); assert_eq!(format_ns_f64(f64::INFINITY), ABSENT); assert_eq!(format_percent(f64::NAN), ABSENT); + } + + #[test] + #[cfg(feature = "client")] + fn absent_optional_ns_f64_renders_as_a_placeholder() { + assert_eq!(format_optional_ns_f64(None), ABSENT); + } + + #[test] + #[cfg(feature = "tui")] + fn absent_counts_and_ratios_render_as_a_placeholder() { + assert_eq!(format_optional_count(None), ABSENT); assert_eq!(format_percent_ratio(1, 0), ABSENT); } #[test] - fn percentages_and_counts_have_one_spelling() { + fn percentages_have_one_spelling() { assert_eq!(format_percent(0.0), "0.00%"); assert_eq!(format_percent(12.345), "12.35%"); + } + + #[test] + #[cfg(feature = "tui")] + fn percent_ratios_and_counts_have_one_spelling() { assert_eq!(format_percent_ratio(1, 4), "25.00%"); assert_eq!(format_percent_ratio(5, 4), "100.00%"); assert_eq!(format_count(42), "42"); From c9269ca0ee215ce40ac99e7b9c6161c2d45b1071 Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Thu, 20 Aug 2026 15:12:01 +0700 Subject: [PATCH 6/8] docs: qualify RTT-unaffected claim to raw RTT only adjusted_rtt_us/effective_rtt_us are derived in part from server-reported processing time, which is computed from the same server receive endpoint the client_to_server one-way-delay field documents as kernel-preferred. Only raw RTT is guaranteed to stay on userspace monotonic timing; say so precisely instead of claiming RTT in general is unaffected. --- README.md | 2 +- crates/irtt-client/src/event.rs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b4bb6f4..d5aaa46 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,7 @@ Useful measurement fields include: Adjusted RTT can be negative when server processing exceeds the measured raw RTT. One-way delay estimates can be negative because of clock skew between the client and server. -On Linux, `sd_us` and `rd_us` (and the underlying one-way delay samples) prefer kernel-captured send/receive timestamps over userspace timing where the platform and socket support them, reducing the effect of scheduling and queuing delay on these measurements. This is a best-effort fallback: unsupported or implausible kernel timestamps fall back to userspace timing. RTT always uses userspace timing and is unaffected. +On Linux, `sd_us` and `rd_us` (and the underlying one-way delay samples) prefer kernel-captured send/receive timestamps over userspace timing where the platform and socket support them, reducing the effect of scheduling and queuing delay on these measurements. This is a best-effort fallback: unsupported or implausible kernel timestamps fall back to userspace timing. `raw_rtt_us` always uses userspace timing and is unaffected; `adjusted_rtt_us`/`effective_rtt_us` can still shift slightly, since server processing time is derived from the same server receive endpoint. Default table output prints a final summary after completed finite runs and interrupted continuous runs when the run policy permits it. CSV, TSV, and JSON Lines output do not print this summary. diff --git a/crates/irtt-client/src/event.rs b/crates/irtt-client/src/event.rs index ef700c0..bd5c202 100644 --- a/crates/irtt-client/src/event.rs +++ b/crates/irtt-client/src/event.rs @@ -356,8 +356,11 @@ pub struct OneWayDelaySample { /// available and plausible for this send, and the userspace post-send wall /// time otherwise. The server receive endpoint is the server's kernel /// receive timestamp when one is available and plausible, and its - /// userspace receive wall time otherwise. RTT is unaffected and remains a - /// userspace monotonic measurement. + /// userspace receive wall time otherwise. `RttSample::raw` is unaffected + /// and remains a userspace monotonic measurement, but `adjusted` and + /// `effective` can still shift: when the session negotiated wall-clock + /// timing rather than monotonic, server processing time is derived from + /// this same server receive endpoint. pub client_to_server: Option, /// Server-to-client delay. /// From cab1fd1aab7e7e56ac6ad73afa0c55b284eb5ca4 Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Thu, 20 Aug 2026 15:18:48 +0700 Subject: [PATCH 7/8] docs: scope server kernel-timestamp claim to this crate's server irtt-client can talk to any IRTT-compatible peer, not only this repo's own irtt-server, and has no way to know what clock source produced a peer-reported wall-clock endpoint. Restrict the kernel-receive-timestamp-preference claim to when the peer is this crate's own Linux irtt-server; state plainly that another peer's reported value is used as-is otherwise. --- README.md | 2 +- crates/irtt-client/src/event.rs | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d5aaa46..9857440 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,7 @@ Useful measurement fields include: Adjusted RTT can be negative when server processing exceeds the measured raw RTT. One-way delay estimates can be negative because of clock skew between the client and server. -On Linux, `sd_us` and `rd_us` (and the underlying one-way delay samples) prefer kernel-captured send/receive timestamps over userspace timing where the platform and socket support them, reducing the effect of scheduling and queuing delay on these measurements. This is a best-effort fallback: unsupported or implausible kernel timestamps fall back to userspace timing. `raw_rtt_us` always uses userspace timing and is unaffected; `adjusted_rtt_us`/`effective_rtt_us` can still shift slightly, since server processing time is derived from the same server receive endpoint. +On Linux, `sd_us` and `rd_us` (and the underlying one-way delay samples) prefer a kernel-captured timestamp over userspace timing for this client's own send/receive endpoint where the platform and socket support it, reducing the effect of scheduling and queuing delay on these measurements. The peer's reported endpoint gets the same preference when it is this crate's own `irtt-server` on Linux; against another IRTT-compatible server, whatever wall-clock value it reports is used as-is. This is a best-effort fallback: unsupported or implausible kernel timestamps fall back to userspace timing. `raw_rtt_us` always uses userspace timing and is unaffected; `adjusted_rtt_us`/`effective_rtt_us` can still shift slightly, since server processing time is derived from the same server receive endpoint. Default table output prints a final summary after completed finite runs and interrupted continuous runs when the run policy permits it. CSV, TSV, and JSON Lines output do not print this summary. diff --git a/crates/irtt-client/src/event.rs b/crates/irtt-client/src/event.rs index bd5c202..9b1ada8 100644 --- a/crates/irtt-client/src/event.rs +++ b/crates/irtt-client/src/event.rs @@ -354,13 +354,16 @@ pub struct OneWayDelaySample { /// wall time when both are available. The client send endpoint is the /// correlated Linux kernel TX timestamp for this send when one is /// available and plausible for this send, and the userspace post-send wall - /// time otherwise. The server receive endpoint is the server's kernel - /// receive timestamp when one is available and plausible, and its - /// userspace receive wall time otherwise. `RttSample::raw` is unaffected - /// and remains a userspace monotonic measurement, but `adjusted` and - /// `effective` can still shift: when the session negotiated wall-clock - /// timing rather than monotonic, server processing time is derived from - /// this same server receive endpoint. + /// time otherwise. The server receive endpoint is whatever wall time the + /// peer reported; against this crate's own `irtt-server` on Linux, that is + /// its kernel receive timestamp when one is available and plausible, and + /// its userspace receive wall time otherwise. Against another + /// IRTT-compatible peer, the client has no way to know which clock source + /// produced it. `RttSample::raw` is unaffected and remains a userspace + /// monotonic measurement, but `adjusted` and `effective` can still shift: + /// when the session negotiated wall-clock timing rather than monotonic, + /// server processing time is derived from this same server receive + /// endpoint. pub client_to_server: Option, /// Server-to-client delay. /// From 1754b77ea5014ef2243322492336f5ffbcc1b280 Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Thu, 20 Aug 2026 15:24:43 +0700 Subject: [PATCH 8/8] docs: scope kernel-timestamp claim to the Tokio server runtime irtt_server::ServerCore::handle_datagram is a public API that always passes kernel_rx_timestamp = None; only the crate-private entry point the Tokio runtime (the irtt-server applet) uses supplies one. Say so explicitly instead of implying every use of this crate's server gets the preference. --- README.md | 2 +- crates/irtt-client/src/event.rs | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9857440..f682fd7 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,7 @@ Useful measurement fields include: Adjusted RTT can be negative when server processing exceeds the measured raw RTT. One-way delay estimates can be negative because of clock skew between the client and server. -On Linux, `sd_us` and `rd_us` (and the underlying one-way delay samples) prefer a kernel-captured timestamp over userspace timing for this client's own send/receive endpoint where the platform and socket support it, reducing the effect of scheduling and queuing delay on these measurements. The peer's reported endpoint gets the same preference when it is this crate's own `irtt-server` on Linux; against another IRTT-compatible server, whatever wall-clock value it reports is used as-is. This is a best-effort fallback: unsupported or implausible kernel timestamps fall back to userspace timing. `raw_rtt_us` always uses userspace timing and is unaffected; `adjusted_rtt_us`/`effective_rtt_us` can still shift slightly, since server processing time is derived from the same server receive endpoint. +On Linux, `sd_us` and `rd_us` (and the underlying one-way delay samples) prefer a kernel-captured timestamp over userspace timing for this client's own send/receive endpoint where the platform and socket support it, reducing the effect of scheduling and queuing delay on these measurements. The peer's reported endpoint gets the same preference when it is the `irtt-server` applet running on Linux; against another IRTT-compatible server, whatever wall-clock value it reports is used as-is. This is a best-effort fallback: unsupported or implausible kernel timestamps fall back to userspace timing. `raw_rtt_us` always uses userspace timing and is unaffected; `adjusted_rtt_us`/`effective_rtt_us` can still shift slightly, since server processing time is derived from the same server receive endpoint. Default table output prints a final summary after completed finite runs and interrupted continuous runs when the run policy permits it. CSV, TSV, and JSON Lines output do not print this summary. diff --git a/crates/irtt-client/src/event.rs b/crates/irtt-client/src/event.rs index 9b1ada8..911fba9 100644 --- a/crates/irtt-client/src/event.rs +++ b/crates/irtt-client/src/event.rs @@ -355,11 +355,14 @@ pub struct OneWayDelaySample { /// correlated Linux kernel TX timestamp for this send when one is /// available and plausible for this send, and the userspace post-send wall /// time otherwise. The server receive endpoint is whatever wall time the - /// peer reported; against this crate's own `irtt-server` on Linux, that is - /// its kernel receive timestamp when one is available and plausible, and - /// its userspace receive wall time otherwise. Against another - /// IRTT-compatible peer, the client has no way to know which clock source - /// produced it. `RttSample::raw` is unaffected and remains a userspace + /// peer reported; against this crate's own Tokio server runtime on Linux + /// (the `irtt-server` applet), that is its kernel receive timestamp when + /// one is available and plausible, and its userspace receive wall time + /// otherwise. Against another IRTT-compatible peer, or an application + /// driving `irtt_server::ServerCore::handle_datagram` directly, the + /// client has no way to know which clock source produced it — the public + /// `ServerCore` API never supplies a kernel timestamp of its own accord. + /// `RttSample::raw` is unaffected and remains a userspace /// monotonic measurement, but `adjusted` and `effective` can still shift: /// when the session negotiated wall-clock timing rather than monotonic, /// server processing time is derived from this same server receive