Merge pull request #127 from Lochnair/docs/release-readiness-audit-fixes

docs: implement release-readiness audit findings
This commit was merged in pull request #127.
This commit is contained in:
Nils Andreas Svee authored and GitHub committed 2026-08-20 15:33:08 +07:00
commit 2521959364
5 files changed
+87 -17

No files matched your search

+22 -7
View File
@@ -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
@@ -133,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
@@ -179,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 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.
A peer closing a finite CLI or TUI run is accepted as a terminal outcome. In
@@ -294,7 +302,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]
+29 -3
View File
@@ -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<i128>) -> String {
}
/// Format an optional nanosecond scalar, rendering `None` as [`ABSENT`].
#[cfg(feature = "client")]
pub(crate) fn format_optional_ns_f64(value: Option<f64>) -> 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<u64>) -> 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");
+16 -1
View File
@@ -351,7 +351,22 @@ 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 whatever wall time the
/// 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
/// endpoint.
pub client_to_server: Option<SignedDuration>,
/// Server-to-client delay.
///
+15 -1
View File
@@ -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<ManagedTargetConfig>,
+5 -5
View File
@@ -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};