Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
641f8e18c3 | ||
|
|
d2d5f87e91 | ||
|
|
5b5e38a059 | ||
|
|
97bd903769 | ||
|
|
f02f9c0714 | ||
|
|
5957e395a2 | ||
|
|
5b5f5b2609 | ||
|
|
c8a911297d |
No files matched your search
@@ -240,6 +240,20 @@ release. The format is loosely based on
|
||||
|
||||
## irtt-rs
|
||||
|
||||
### 0.10.2
|
||||
|
||||
#### Added
|
||||
|
||||
- Client `--events` filters output by event type, using comma-separated names or repeated options. For example, `--events echo_reply` prints only echo replies. Omitting the option preserves normal output behavior.
|
||||
- Explicitly selecting `echo_sent` with `--events` shows those events even with default table columns.
|
||||
|
||||
### 0.10.1
|
||||
|
||||
#### Fixed
|
||||
|
||||
- The client now responds to SIGINT and SIGTERM while stdout is blocked, so a stalled output reader no longer prevents shutdown.
|
||||
- Interrupted measurements preserve partial statistics summaries when output can be written. A shared 250 ms grace period bounds output flushing during shutdown.
|
||||
|
||||
### 0.10.0
|
||||
|
||||
#### Added
|
||||
|
||||
Generated
+3
-3
@@ -890,7 +890,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "irtt-rs"
|
||||
version = "0.10.0"
|
||||
version = "0.10.2"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
@@ -2142,9 +2142,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
checksum = "ca718471876992059f251e33aafc334346ada08042b7661dd1fe313a9bced557"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
|
||||
@@ -1,97 +1,67 @@
|
||||
# irtt-rs
|
||||
|
||||
`irtt-rs` is a Rust implementation of an IRTT-compatible client and server:
|
||||
finite or continuous UDP latency probing, plus the server side those probes
|
||||
run against.
|
||||
`irtt-rs` is an IRTT-compatible client and server written in Rust. You can use
|
||||
it to measure UDP latency for a fixed duration or leave it running
|
||||
continuously, and run your own server for the probes to talk to.
|
||||
|
||||
This is not the upstream IRTT project. For the original implementation,
|
||||
protocol background, and broader documentation, see
|
||||
[heistp/irtt](https://github.com/heistp/irtt). The installed dispatcher is
|
||||
named `irtt-rs` rather than `irtt` to avoid conflicting with or being
|
||||
mistaken for it.
|
||||
The original IRTT implementation is [heistp/irtt](https://github.com/heistp/irtt),
|
||||
which also has protocol background and more documentation. This project is
|
||||
independent, so the installed dispatcher is named `irtt-rs` to keep it from
|
||||
conflicting with or being mistaken for `irtt`.
|
||||
|
||||
## Features
|
||||
|
||||
* Finite and continuous probe sessions
|
||||
* Concurrent probing of multiple targets, with staggered or burst pacing
|
||||
* Reusable client library with a structured event stream (blocking, Tokio,
|
||||
- Finite and continuous probe sessions
|
||||
- Concurrent probing of multiple targets, with staggered or burst pacing
|
||||
- Reusable client library with a structured event stream (blocking, Tokio,
|
||||
or managed)
|
||||
* Human-readable table output, plus CSV, TSV, and JSON Lines for scripts and
|
||||
- Human-readable table output, plus CSV, TSV, and JSON Lines for scripts and
|
||||
monitoring systems, with selectable output columns
|
||||
* Optional terminal UI
|
||||
* Reusable Tokio-native server library
|
||||
* UDP server applet with sensible zero-argument defaults and explicit bind
|
||||
and session-policy options
|
||||
* Router-friendly multi-applet binary layout
|
||||
- Optional terminal UI
|
||||
- Reusable Tokio-native server library
|
||||
- UDP server applet you can run without arguments, with options for bind
|
||||
addresses and session policy when you need them
|
||||
- A single binary that can run several applets, useful on routers
|
||||
|
||||
## Comparison with upstream IRTT
|
||||
|
||||
`irtt-rs` is an independent, compatible reimplementation, not a successor or
|
||||
replacement for upstream IRTT — [heistp/irtt](https://github.com/heistp/irtt)
|
||||
remains the original implementation. Interoperability is a project goal; see
|
||||
[`docs/INTEROP_COMPARISON.md`](docs/INTEROP_COMPARISON.md) and
|
||||
[`docs/protocol/`](docs/protocol/) for the black-box interoperability
|
||||
methodology and protocol baseline this project verifies against. Choose
|
||||
between them based on the capabilities and deployment model you need.
|
||||
`irtt-rs` is useful if you want to watch several targets at once, compare
|
||||
them in a terminal dashboard, or feed selected measurements into a script.
|
||||
You can leave it running with statistics and automatic reconnection, use
|
||||
its Rust libraries in your own application, or deploy a single binary on
|
||||
a router.
|
||||
|
||||
The table below was checked against upstream release
|
||||
[v0.9.2](https://github.com/heistp/irtt/releases/tag/v0.9.2) (2026-07-17).
|
||||
This project's own interop testing and protocol baseline are currently
|
||||
pinned to the prior release, v0.9.1; the wire protocol version number is
|
||||
unchanged between the two, but v0.9.2 has not itself been exercised against
|
||||
this project's interop harness.
|
||||
Upstream IRTT is much more mature, and if you don't need the features
|
||||
specific to `irtt-rs`, you should probably use it. It also has more tools
|
||||
for working with saved results and controlling how probes run, including
|
||||
replaying saved JSON reports, timer and clock diagnostics, and client
|
||||
payload fill and timing algorithms.
|
||||
|
||||
| Capability | irtt-rs | upstream IRTT (v0.9.2) |
|
||||
| --- | --- | --- |
|
||||
| Wire protocol | Protocol version 1, verified against upstream v0.9.1 | Protocol version 1 (unchanged in v0.9.2) |
|
||||
| Finite probing | Supported, default 10s | Supported, default 1m |
|
||||
| Continuous/indefinite operation | Supported (`--continuous` or `--duration 0`), with optional per-target reconnection | Supported via streaming mode (`-s`, added in v0.9.2); computes no statistics at all in that mode |
|
||||
| Statistics in continuous mode | Optional running count/min/max/mean/variance per session; no exact medians; bounded IPDV tracking. Use `--no-stats` or omit the `stats` feature to disable collection | None in streaming mode — raw/event output only, by design, to avoid unbounded memory |
|
||||
| Multi-target probing (one invocation) | Supported, with labels and staggered/burst pacing | Not supported — one remote address per invocation |
|
||||
| Terminal UI | `irtt-tui`, a live graph/dashboard applet | Not present |
|
||||
| Output formats | table, csv, tsv, jsonl, with selectable columns | Text summary, JSON (optionally gzip to file), and a fixed-field raw mode; no CSV/TSV and no column selection |
|
||||
| Client/server library API | Separate, independently versioned Rust crates (`irtt-client`, `irtt-server`) | One combined Go package exposing both roles |
|
||||
| Server: multiple listeners | `ServerSet` supervises several listeners in one process | `ListenAndServe` also serves multiple requested addresses |
|
||||
| Server: wildcard reply-source | Default behavior on Linux/macOS/FreeBSD; refused elsewhere rather than served incorrectly | Opt-in via `--set-src-ip`, with a documented per-packet allocation cost |
|
||||
| Server: zero-argument bind | Binds the wildcard address on both families by default, gated by the platform check above | Binds the wildcard address on both families by default; no documented platform gate |
|
||||
| Prebuilt release binaries | Published for six target triples | None published; install via `go install` or manual build |
|
||||
|
||||
Upstream has capabilities this project does not implement: a `report`
|
||||
subcommand for replaying saved JSON results, benchmarking and
|
||||
timer/clock-diagnostic subcommands, client-side payload fill and
|
||||
don't-fragment/wait-strategy/timer-algorithm controls, an operator-configurable
|
||||
server-side fill policy (this project's server always honors a session's
|
||||
negotiated fill request, with a fixed fallback pattern, but has no equivalent
|
||||
to upstream's flags for restricting or overriding it), syslog logging, and
|
||||
bundled systemd/OpenRC service files.
|
||||
|
||||
This project has capabilities upstream does not implement: multi-target
|
||||
probing with labels, the terminal UI, CSV/TSV output with selectable
|
||||
columns, a quantified and actively-warned memory-retention model for
|
||||
long-running client sessions, and best-effort Linux kernel-timestamp
|
||||
preference for its own send/receive endpoint.
|
||||
See the [detailed comparison](docs/INTEROP_COMPARISON.md) for individual
|
||||
features, version coverage, and black-box interoperability testing.
|
||||
|
||||
## Installation
|
||||
|
||||
The build requires Rust 1.88 or newer.
|
||||
Prebuilt binaries for supported platforms are available on the
|
||||
[GitHub Releases](https://github.com/Lochnair/irtt-rs/releases) page.
|
||||
|
||||
Install the latest release from crates.io:
|
||||
Alternatively, install from crates.io using Rust 1.88 or newer:
|
||||
|
||||
```sh
|
||||
cargo install irtt-rs --locked
|
||||
```
|
||||
|
||||
This installs the `irtt-rs` dispatcher plus dedicated `irtt-client`,
|
||||
`irtt-tui`, and `irtt-server` binaries. 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 without statistics:
|
||||
This installs the `irtt-rs` dispatcher along with dedicated `irtt-client`,
|
||||
`irtt-tui`, and `irtt-server` binaries. If you only need the client, you can
|
||||
disable the other applets and statistics:
|
||||
|
||||
```sh
|
||||
cargo install irtt-rs --locked --no-default-features --features client
|
||||
```
|
||||
|
||||
Add `stats` (`--features client,stats`) to retain statistics in a client-only
|
||||
build. Statistics are enabled by default; the TUI always enables them.
|
||||
This installs `irtt-client` and the `irtt-rs` dispatcher. Add `stats`
|
||||
(`--features client,stats`) to retain statistics in a client-only build.
|
||||
Statistics are enabled by default; the TUI always enables them.
|
||||
|
||||
For development, or to install directly from a checkout:
|
||||
|
||||
@@ -131,30 +101,56 @@ Watch the same session live in the terminal UI:
|
||||
irtt-tui netperf-eu.bufferbloat.net:2112
|
||||
```
|
||||
|
||||
Run a server with sensible defaults — no `--bind` needed:
|
||||
To run your own server, you can leave out `--bind` and use the defaults:
|
||||
|
||||
```sh
|
||||
irtt-server
|
||||
```
|
||||
|
||||
### Dynamic targets from stdin
|
||||
|
||||
If another application decides which servers to probe, `--targets-stdin`
|
||||
lets it change targets without restarting the client. It works in
|
||||
continuous mode:
|
||||
|
||||
```sh
|
||||
irtt-client --continuous --targets-stdin --reconnect --no-stats \
|
||||
--format csv --columns event,target,seq,rd_us,sd_us
|
||||
```
|
||||
|
||||
Send one comma-separated target set per line to stdin, for example:
|
||||
|
||||
```text
|
||||
ams=ams.example:2112,lhr=lhr.example:2112
|
||||
ams=ams.example:2112
|
||||
[]
|
||||
```
|
||||
|
||||
Each line gives the complete desired target set. Unchanged targets keep
|
||||
running, removed targets stop, and new or changed targets start. Use `[]`
|
||||
to clear all targets without exiting.
|
||||
|
||||
The CSV includes an `event` column so your script can distinguish
|
||||
`echo_reply` measurements from other events.
|
||||
|
||||
Keep stdin open while controlling the client. Closing it sends EOF and stops the run gracefully.
|
||||
|
||||
See [Dynamic target sets from standard input](docs/irtt-client.md#dynamic-target-sets-from-standard-input)
|
||||
for the full syntax and behavior.
|
||||
|
||||
## Documentation
|
||||
|
||||
* [`docs/irtt-client.md`](docs/irtt-client.md) — target syntax, output
|
||||
- [`docs/irtt-client.md`](docs/irtt-client.md): target syntax, output
|
||||
formats and columns, measurement field definitions, finite/continuous
|
||||
memory behavior, kernel timestamps.
|
||||
* [`docs/irtt-tui.md`](docs/irtt-tui.md) — controls, continuous default,
|
||||
- [`docs/irtt-tui.md`](docs/irtt-tui.md): controls, continuous default,
|
||||
retained history.
|
||||
* [`docs/irtt-server.md`](docs/irtt-server.md) — bind defaults and
|
||||
- [`docs/irtt-server.md`](docs/irtt-server.md): bind defaults and
|
||||
overrides, wildcard listener behavior, session policy, HMAC.
|
||||
* [`docs/irtt-rs.md`](docs/irtt-rs.md) — the multicall dispatcher, applet
|
||||
- [`docs/irtt-rs.md`](docs/irtt-rs.md): the multicall dispatcher, applet
|
||||
binary names, feature-gated builds.
|
||||
* [docs.rs](https://docs.rs) for the library crates below.
|
||||
* [`docs/protocol/`](docs/protocol/) and
|
||||
[`docs/INTEROP_COMPARISON.md`](docs/INTEROP_COMPARISON.md) for the wire
|
||||
protocol specification and interoperability validation against upstream
|
||||
`irtt`.
|
||||
|
||||
Every applet also documents its full option list with `--help`.
|
||||
You can also run any applet with `--help` for its full option list.
|
||||
|
||||
## Crates
|
||||
|
||||
@@ -166,9 +162,6 @@ Every applet also documents its full option list with `--help`.
|
||||
| [`irtt-stats`](crates/irtt-stats) | [crates.io](https://crates.io/crates/irtt-stats) | Statistics aggregation over `irtt-client` events ([docs.rs](https://docs.rs/irtt-stats)) |
|
||||
| [`irtt-proto`](crates/irtt-proto) | [crates.io](https://crates.io/crates/irtt-proto) | Low-level wire protocol encode/decode ([docs.rs](https://docs.rs/irtt-proto)) |
|
||||
|
||||
Each library crate has its own crates.io README describing what it's for and
|
||||
when to use it instead of `irtt-client`/`irtt-server` directly.
|
||||
|
||||
## Binaries and features
|
||||
|
||||
| Build | Binaries |
|
||||
@@ -188,7 +181,3 @@ cargo test --workspace --locked
|
||||
cargo test --workspace --doc --all-features --locked
|
||||
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
|
||||
```
|
||||
|
||||
See [`AGENTS.md`](AGENTS.md) for architecture, testing policy, and the
|
||||
clean-room provenance boundary this project maintains against upstream
|
||||
`irtt`.
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "irtt-rs"
|
||||
version = "0.10.0"
|
||||
version = "0.10.2"
|
||||
description = "IRTT-compatible client, TUI, and server applications"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -101,12 +101,22 @@ pub struct ClientArgs {
|
||||
)]
|
||||
pub format: OutputFormat,
|
||||
|
||||
/// Filter output by event type.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "EVENTS",
|
||||
value_delimiter = ',',
|
||||
value_enum,
|
||||
long_help = "Filter output by event type. Accepts comma-separated names or repeated options. If omitted, the normal output behavior is unchanged."
|
||||
)]
|
||||
pub events: Vec<EventKind>,
|
||||
|
||||
/// Comma-separated event row columns, or default/all.
|
||||
#[arg(
|
||||
short = 'c',
|
||||
long,
|
||||
value_name = "COLUMNS",
|
||||
long_help = "Comma-separated event row columns, or default/all.\n\nThe default table columns are compact and hide echo_sent rows. Custom table columns include all event rows. Run --list-columns to see valid names."
|
||||
long_help = "Comma-separated event row columns, or default/all.\n\nThe default table columns are compact and hide echo_sent rows unless explicitly selected with --events. Custom table columns include all event rows. Run --list-columns to see valid names."
|
||||
)]
|
||||
pub columns: Option<String>,
|
||||
|
||||
@@ -189,6 +199,29 @@ impl std::ops::Deref for ClientArgs {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
||||
#[value(rename_all = "snake_case")]
|
||||
pub enum EventKind {
|
||||
SessionStarted,
|
||||
NoTestCompleted,
|
||||
SessionClosed,
|
||||
EchoSent,
|
||||
EchoReply,
|
||||
Loss,
|
||||
Duplicate,
|
||||
Late,
|
||||
Warning,
|
||||
}
|
||||
|
||||
impl EventKind {
|
||||
pub(super) fn name(self) -> String {
|
||||
self.to_possible_value()
|
||||
.expect("all event kinds have a CLI value")
|
||||
.get_name()
|
||||
.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
||||
pub enum OutputFormat {
|
||||
/// Readable terminal table output.
|
||||
|
||||
@@ -12,12 +12,13 @@ use irtt_client::{
|
||||
#[cfg(feature = "stats")]
|
||||
use irtt_stats::{EventStatsUpdate, IpdvPairUpdate};
|
||||
|
||||
use super::args::{HeaderMode, OutputFormat};
|
||||
use super::args::{EventKind, HeaderMode, OutputFormat};
|
||||
use crate::cmd::format::{format_duration, format_signed_duration, ABSENT};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct OutputConfig {
|
||||
format: OutputFormat,
|
||||
events: Vec<EventKind>,
|
||||
columns: Vec<Column>,
|
||||
header: HeaderMode,
|
||||
default_table_rows: bool,
|
||||
@@ -30,6 +31,7 @@ impl OutputConfig {
|
||||
columns: Option<&str>,
|
||||
header: HeaderMode,
|
||||
verbose: bool,
|
||||
events: Vec<EventKind>,
|
||||
) -> Result<Self, String> {
|
||||
let uses_default_columns = columns
|
||||
.map(|columns| columns.trim() == "default")
|
||||
@@ -42,6 +44,7 @@ impl OutputConfig {
|
||||
|
||||
Ok(Self {
|
||||
format,
|
||||
events,
|
||||
columns,
|
||||
header,
|
||||
default_table_rows,
|
||||
@@ -102,7 +105,10 @@ impl OutputConfig {
|
||||
#[cfg(feature = "stats")] stats: Option<&EventStatsUpdate>,
|
||||
) -> Option<String> {
|
||||
let row = OutputRow::from_event(event);
|
||||
if self.default_table_rows && row.is_default_table_hidden() {
|
||||
if !self.events.is_empty() && !self.events.contains(&row.event_kind()) {
|
||||
return None;
|
||||
}
|
||||
if self.events.is_empty() && self.default_table_rows && row.is_default_table_hidden() {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -322,17 +328,17 @@ impl OutputRow {
|
||||
}
|
||||
}
|
||||
|
||||
fn event_name(&self) -> &'static str {
|
||||
fn event_kind(&self) -> EventKind {
|
||||
match self {
|
||||
Self::SessionStarted(_) => "session_started",
|
||||
Self::NoTestCompleted(_) => "no_test_completed",
|
||||
Self::SessionClosed { .. } => "session_closed",
|
||||
Self::EchoSent { .. } => "echo_sent",
|
||||
Self::EchoReply(_) => "echo_reply",
|
||||
Self::Loss { .. } => "loss",
|
||||
Self::Duplicate { .. } => "duplicate",
|
||||
Self::Late { .. } => "late",
|
||||
Self::Warning { .. } => "warning",
|
||||
Self::SessionStarted(_) => EventKind::SessionStarted,
|
||||
Self::NoTestCompleted(_) => EventKind::NoTestCompleted,
|
||||
Self::SessionClosed { .. } => EventKind::SessionClosed,
|
||||
Self::EchoSent { .. } => EventKind::EchoSent,
|
||||
Self::EchoReply(_) => EventKind::EchoReply,
|
||||
Self::Loss { .. } => EventKind::Loss,
|
||||
Self::Duplicate { .. } => EventKind::Duplicate,
|
||||
Self::Late { .. } => EventKind::Late,
|
||||
Self::Warning { .. } => EventKind::Warning,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -784,7 +790,7 @@ fn render_jsonl_row(row: &OutputRow, columns: &[Column], context: RenderContext<
|
||||
fn cell_for(row: &OutputRow, column: Column, context: RenderContext<'_>) -> Option<CellValue> {
|
||||
match column {
|
||||
Column::Target => context.target.map(CellValue::text),
|
||||
Column::Event => Some(CellValue::text(row.event_name())),
|
||||
Column::Event => Some(CellValue::text(row.event_kind().name())),
|
||||
Column::Seq => row.seq().map(|seq| CellValue::Unsigned(u128::from(seq))),
|
||||
Column::Remote => row
|
||||
.remote()
|
||||
|
||||
@@ -3,12 +3,13 @@ use std::{
|
||||
io::{self, BufRead, Write},
|
||||
sync::Arc,
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use irtt_client::{
|
||||
managed::{
|
||||
ManagedClient, ManagedCommandApplyError, ManagedEndReason, ManagedEvent,
|
||||
ManagedTargetEndReason, ManagedTargetOutcome, TargetInstance,
|
||||
ManagedEventTryRecvError, ManagedTargetEndReason, ManagedTargetOutcome, TargetInstance,
|
||||
},
|
||||
ClientEvent,
|
||||
};
|
||||
@@ -18,8 +19,7 @@ use super::{args::ClientArgs, output::OutputConfig};
|
||||
use crate::shared::client::{
|
||||
parse_stdin_target_set,
|
||||
session::{
|
||||
drain_final_events, peer_close_run_error, request_managed_stop_for_peer_close,
|
||||
request_managed_stop_once,
|
||||
peer_close_run_error, request_managed_stop_for_peer_close, request_managed_stop_once,
|
||||
},
|
||||
worker::ManagedWorker,
|
||||
ManagedRunSetup, TargetSpec, STDIN_MAX_DESIRED_TARGETS, STDIN_OUTCOME_HISTORY_LIMIT,
|
||||
@@ -36,6 +36,63 @@ use irtt_client::managed::ManagedStatus;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
const MAX_STDIN_RECORD_BYTES: usize = 64 * 1024;
|
||||
const INTERRUPTED_OUTPUT_GRACE: Duration = Duration::from_millis(250);
|
||||
|
||||
// The frontend only renders into memory. This thread is the sole stdout owner;
|
||||
// dropping its JoinHandle deliberately detaches a writer blocked in a pipe.
|
||||
struct StdoutWriter {
|
||||
sender: tokio::sync::mpsc::Sender<Vec<u8>>,
|
||||
shutdown: tokio::sync::watch::Receiver<bool>,
|
||||
grace_deadline: Option<tokio::time::Instant>,
|
||||
}
|
||||
|
||||
impl StdoutWriter {
|
||||
// Measurement output stops waiting for capacity as soon as interrupted.
|
||||
async fn send(&self, buffer: &mut Vec<u8>) -> io::Result<()> {
|
||||
let mut shutdown = self.shutdown.clone();
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.wait_for(|requested| *requested) => {
|
||||
buffer.clear();
|
||||
Ok(())
|
||||
}
|
||||
result = self.enqueue(buffer) => result,
|
||||
}
|
||||
}
|
||||
|
||||
// Summaries and writer completion share one grace period, regardless of
|
||||
// the number of targets. Normal output never has a delivery deadline.
|
||||
async fn send_summary(&mut self, buffer: &mut Vec<u8>) -> io::Result<()> {
|
||||
let mut shutdown = self.shutdown.clone();
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.wait_for(|requested| *requested) => {
|
||||
let deadline = *self.grace_deadline.get_or_insert_with(|| {
|
||||
tokio::time::Instant::now() + INTERRUPTED_OUTPUT_GRACE
|
||||
});
|
||||
match tokio::time::timeout_at(deadline, self.enqueue(buffer)).await {
|
||||
Ok(result) => result,
|
||||
Err(_) => { buffer.clear(); Ok(()) }
|
||||
}
|
||||
}
|
||||
result = self.enqueue(buffer) => result,
|
||||
}
|
||||
}
|
||||
|
||||
async fn enqueue(&self, buffer: &mut Vec<u8>) -> io::Result<()> {
|
||||
if !buffer.is_empty() {
|
||||
// Take ownership only once capacity is available. Normal operation
|
||||
// preserves ordering without dropping rendered chunks.
|
||||
let permit = self
|
||||
.sender
|
||||
.reserve()
|
||||
.await
|
||||
.map_err(|_| io::Error::other("stdout writer stopped accepting output"))?;
|
||||
permit.send(std::mem::take(buffer));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_stream(
|
||||
args: ClientArgs,
|
||||
@@ -45,6 +102,64 @@ pub async fn run_stream(
|
||||
print!("{}", OutputConfig::list_columns());
|
||||
return Ok(());
|
||||
}
|
||||
let (sender, mut receiver) = tokio::sync::mpsc::channel::<Vec<u8>>(64);
|
||||
let (completed, mut completion) = tokio::sync::oneshot::channel();
|
||||
let writer = thread::Builder::new()
|
||||
.name("irtt-stdout".to_owned())
|
||||
.spawn(move || {
|
||||
let result = (|| {
|
||||
let mut stdout = io::LineWriter::new(io::stdout().lock());
|
||||
while let Some(chunk) = receiver.blocking_recv() {
|
||||
stdout.write_all(&chunk)?;
|
||||
}
|
||||
stdout.flush()
|
||||
})();
|
||||
let _ = completed.send(result);
|
||||
})?;
|
||||
let mut output = StdoutWriter {
|
||||
sender,
|
||||
shutdown: shutdown.clone(),
|
||||
grace_deadline: None,
|
||||
};
|
||||
let run_shutdown = shutdown.clone();
|
||||
let result = tokio::select! {
|
||||
biased;
|
||||
result = &mut completion => {
|
||||
// Dropping the run future stops and joins its ManagedWorker first.
|
||||
writer.join().map_err(|_| io::Error::other("stdout writer thread panicked"))?;
|
||||
result.map_err(|_| io::Error::other("stdout writer completed without a result"))??;
|
||||
return Err(io::Error::other("stdout writer stopped unexpectedly").into());
|
||||
}
|
||||
result = run_stream_output(args, run_shutdown, &mut output) => result,
|
||||
};
|
||||
let grace_deadline = output.grace_deadline;
|
||||
drop(output);
|
||||
// An interrupt during normal flushing also starts the bounded grace period.
|
||||
let written = tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.wait_for(|requested| *requested) => {
|
||||
let deadline = grace_deadline.unwrap_or_else(|| {
|
||||
tokio::time::Instant::now() + INTERRUPTED_OUTPUT_GRACE
|
||||
});
|
||||
match tokio::time::timeout_at(deadline, &mut completion).await {
|
||||
Ok(written) => written,
|
||||
Err(_) => return result,
|
||||
}
|
||||
}
|
||||
result = &mut completion => result,
|
||||
};
|
||||
writer
|
||||
.join()
|
||||
.map_err(|_| io::Error::other("stdout writer thread panicked"))?;
|
||||
written.map_err(|_| io::Error::other("stdout writer completed without a result"))??;
|
||||
result
|
||||
}
|
||||
|
||||
async fn run_stream_output(
|
||||
args: ClientArgs,
|
||||
mut shutdown: tokio::sync::watch::Receiver<bool>,
|
||||
output: &mut StdoutWriter,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let setup = tokio::select! {
|
||||
result = args.prepare() => result.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?,
|
||||
_ = shutdown.wait_for(|requested| *requested) => return Ok(()),
|
||||
@@ -56,6 +171,7 @@ pub async fn run_stream(
|
||||
args.columns.as_deref(),
|
||||
args.header,
|
||||
args.verbose,
|
||||
args.events.clone(),
|
||||
)
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
|
||||
#[cfg(feature = "stats")]
|
||||
@@ -63,7 +179,7 @@ pub async fn run_stream(
|
||||
eprintln!("{warning}");
|
||||
}
|
||||
if setup.stdin_controlled {
|
||||
return run_stdin_stream(setup, output_config, shutdown, &args).await;
|
||||
return run_stdin_stream(setup, output_config, shutdown, &args, output).await;
|
||||
}
|
||||
let continuous = args.is_continuous();
|
||||
|
||||
@@ -87,7 +203,7 @@ pub async fn run_stream(
|
||||
let mut events = handle.subscribe()?;
|
||||
let mut status = handle.subscribe_status();
|
||||
let owner = ManagedWorker::start(task, handle.clone())?;
|
||||
let mut stdout = io::LineWriter::new(io::stdout().lock());
|
||||
let mut stdout = Vec::new();
|
||||
let mut stream_output = StreamOutput {
|
||||
#[cfg(feature = "stats")]
|
||||
continuous: args.is_continuous(),
|
||||
@@ -134,14 +250,18 @@ pub async fn run_stream(
|
||||
) {
|
||||
drop(handle.stop());
|
||||
}
|
||||
if snapshot.final_outcome.is_some() {
|
||||
if interrupted || snapshot.final_outcome.is_some() {
|
||||
break;
|
||||
}
|
||||
tokio::select! {
|
||||
_ = shutdown.changed(), if !interrupted => {}
|
||||
_ = shutdown.changed() => {}
|
||||
changed = status.changed() => { if changed.is_err() { break; } }
|
||||
event = events.recv() => match event {
|
||||
Ok(event) => process_event(event, &mut stream_output, #[cfg(feature = "stats")] &mut stats, &mut terminal_targets)?,
|
||||
Ok(event) => {
|
||||
if *shutdown.borrow() { continue; }
|
||||
process_event(event, &mut stream_output, #[cfg(feature = "stats")] &mut stats, &mut terminal_targets)?;
|
||||
output.send(stream_output.out).await?;
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
|
||||
dropped_events = dropped_events.saturating_add(count);
|
||||
}
|
||||
@@ -153,15 +273,24 @@ pub async fn run_stream(
|
||||
eprintln!("interrupted, closing managed run...");
|
||||
}
|
||||
let outcome = owner.join().await?;
|
||||
drain_final_events(&mut events, &mut dropped_events, |event| {
|
||||
process_event(
|
||||
event,
|
||||
&mut stream_output,
|
||||
#[cfg(feature = "stats")]
|
||||
&mut stats,
|
||||
&mut terminal_targets,
|
||||
)
|
||||
})?;
|
||||
while !*shutdown.borrow() {
|
||||
match events.try_recv() {
|
||||
Ok(event) => {
|
||||
process_event(
|
||||
event,
|
||||
&mut stream_output,
|
||||
#[cfg(feature = "stats")]
|
||||
&mut stats,
|
||||
&mut terminal_targets,
|
||||
)?;
|
||||
output.send(stream_output.out).await?;
|
||||
}
|
||||
Err(ManagedEventTryRecvError::Lagged(count)) => {
|
||||
dropped_events = dropped_events.saturating_add(count);
|
||||
}
|
||||
Err(ManagedEventTryRecvError::Empty | ManagedEventTryRecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
if let Some(warning) = dropped_event_warning(dropped_events) {
|
||||
eprintln!("{warning}");
|
||||
}
|
||||
@@ -212,9 +341,10 @@ pub async fn run_stream(
|
||||
writeln!(stream_output.out, "target: {}", target.label)?;
|
||||
}
|
||||
stream_output.print_summary(target_stats)?;
|
||||
output.send_summary(stream_output.out).await?;
|
||||
}
|
||||
}
|
||||
stream_output.out.flush()?;
|
||||
output.send_summary(stream_output.out).await?;
|
||||
if let Some(error) = terminal_error {
|
||||
return Err(error.into());
|
||||
}
|
||||
@@ -476,6 +606,7 @@ async fn run_stdin_stream(
|
||||
output_config: OutputConfig,
|
||||
mut shutdown: tokio::sync::watch::Receiver<bool>,
|
||||
args: &ClientArgs,
|
||||
output: &mut StdoutWriter,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if *shutdown.borrow() {
|
||||
return Ok(());
|
||||
@@ -496,7 +627,7 @@ async fn run_stdin_stream(
|
||||
let (updates, mut stdin) = tokio::sync::watch::channel(StdinUpdate::Targets(Vec::new()));
|
||||
spawn_stdin_target_reader(updates)?;
|
||||
|
||||
let mut stdout = io::LineWriter::new(io::stdout().lock());
|
||||
let mut stdout = Vec::new();
|
||||
let mut stream_output = StreamOutput {
|
||||
#[cfg(feature = "stats")]
|
||||
continuous: args.is_continuous(),
|
||||
@@ -528,10 +659,19 @@ async fn run_stdin_stream(
|
||||
let mut summary_targets = None;
|
||||
loop {
|
||||
let snapshot = status.borrow_and_update().clone();
|
||||
if *shutdown.borrow() {
|
||||
stop = Some(StdinStop::Interrupted);
|
||||
#[cfg(feature = "stats")]
|
||||
{
|
||||
summary_targets = Some(stats.keys().cloned().collect());
|
||||
}
|
||||
if request_managed_stop_once(&mut stop_requested) {
|
||||
drop(handle.stop());
|
||||
}
|
||||
break;
|
||||
}
|
||||
if stop.is_none() {
|
||||
if *shutdown.borrow() {
|
||||
stop = Some(StdinStop::Interrupted);
|
||||
} else if stdin_changed || stdin.has_changed().unwrap_or(true) {
|
||||
if stdin_changed || stdin.has_changed().unwrap_or(true) {
|
||||
stdin_changed = false;
|
||||
match stdin.borrow_and_update().clone() {
|
||||
StdinUpdate::Targets(specs) => {
|
||||
@@ -595,7 +735,7 @@ async fn run_stdin_stream(
|
||||
break;
|
||||
}
|
||||
tokio::select! {
|
||||
_ = shutdown.changed(), if stop.is_none() => {}
|
||||
_ = shutdown.changed() => {}
|
||||
_ = stdin.changed(), if stop.is_none() => { stdin_changed = true; }
|
||||
changed = status.changed() => { if changed.is_err() { break; } }
|
||||
result = async { preparing.as_mut().expect("guarded target preparation").await }, if preparing.is_some() && stop.is_none() => {
|
||||
@@ -645,9 +785,11 @@ async fn run_stdin_stream(
|
||||
}
|
||||
event = events.recv() => match event {
|
||||
Ok(event) => {
|
||||
if *shutdown.borrow() { continue; }
|
||||
process_stdin_event(event, &mut stream_output, #[cfg(feature = "stats")] &mut stats, &mut terminal_targets)?;
|
||||
#[cfg(feature = "stats")]
|
||||
reconcile_stdin_stats(&mut stats, &status.borrow(), summary_targets.as_ref());
|
||||
output.send(stream_output.out).await?;
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
|
||||
dropped_events = dropped_events.saturating_add(count);
|
||||
@@ -658,15 +800,24 @@ async fn run_stdin_stream(
|
||||
}
|
||||
|
||||
let outcome = owner.join().await?;
|
||||
drain_final_events(&mut events, &mut dropped_events, |event| {
|
||||
process_stdin_event(
|
||||
event,
|
||||
&mut stream_output,
|
||||
#[cfg(feature = "stats")]
|
||||
&mut stats,
|
||||
&mut terminal_targets,
|
||||
)
|
||||
})?;
|
||||
while !*shutdown.borrow() {
|
||||
match events.try_recv() {
|
||||
Ok(event) => {
|
||||
process_stdin_event(
|
||||
event,
|
||||
&mut stream_output,
|
||||
#[cfg(feature = "stats")]
|
||||
&mut stats,
|
||||
&mut terminal_targets,
|
||||
)?;
|
||||
output.send(stream_output.out).await?;
|
||||
}
|
||||
Err(ManagedEventTryRecvError::Lagged(count)) => {
|
||||
dropped_events = dropped_events.saturating_add(count);
|
||||
}
|
||||
Err(ManagedEventTryRecvError::Empty | ManagedEventTryRecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
if let Some(warning) = dropped_event_warning(dropped_events) {
|
||||
eprintln!("{warning}");
|
||||
}
|
||||
@@ -682,6 +833,13 @@ async fn run_stdin_stream(
|
||||
);
|
||||
}
|
||||
|
||||
if *shutdown.borrow() && !matches!(stop, Some(StdinStop::Interrupted)) {
|
||||
stop = Some(StdinStop::Interrupted);
|
||||
#[cfg(feature = "stats")]
|
||||
{
|
||||
summary_targets = Some(stats.keys().cloned().collect());
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "stats")]
|
||||
{
|
||||
let print_summary =
|
||||
@@ -703,10 +861,11 @@ async fn run_stdin_stream(
|
||||
)?;
|
||||
}
|
||||
stream_output.print_summary(stats)?;
|
||||
output.send_summary(stream_output.out).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
stream_output.out.flush()?;
|
||||
output.send_summary(stream_output.out).await?;
|
||||
|
||||
match stop {
|
||||
Some(StdinStop::Fatal(error)) => Err(error.into()),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#[cfg(feature = "tui")]
|
||||
use irtt_client::managed::{ManagedEvent, ManagedEventSubscription, ManagedEventTryRecvError};
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
pub(crate) fn drain_final_events<E>(
|
||||
events: &mut ManagedEventSubscription,
|
||||
dropped_events: &mut u64,
|
||||
|
||||
@@ -1,4 +1,38 @@
|
||||
# Interop Comparison
|
||||
# Interop comparison
|
||||
|
||||
## Feature comparison
|
||||
|
||||
This compares `irtt-rs` with the original [heistp/irtt](https://github.com/heistp/irtt)
|
||||
release [v0.9.2](https://github.com/heistp/irtt/releases/tag/v0.9.2) (2026-07-17).
|
||||
The aim is for the two implementations to interoperate, though the interop
|
||||
tests and verified protocol baseline still use upstream v0.9.1. Both
|
||||
upstream releases use wire protocol version 1, but v0.9.2 hasn't been run
|
||||
through this project's interop harness. See [`docs/protocol/`](protocol/)
|
||||
for the verified protocol baseline.
|
||||
|
||||
| Capability | irtt-rs | Upstream IRTT (v0.9.2) |
|
||||
| --- | --- | --- |
|
||||
| Wire protocol | Version 1, verified against upstream v0.9.1 | Version 1, unchanged from v0.9.1 |
|
||||
| Finite probing | Default duration of 10s | Default duration of 1m |
|
||||
| Continuous probing | `--continuous` or `--duration 0`, with optional per-target reconnection | Streaming mode (`-s`, added in v0.9.2) |
|
||||
| Statistics in continuous mode | Optional count/min/max/mean/variance per session, with bounded IPDV tracking and no exact medians. Disable with `--no-stats` or omit the `stats` feature | No statistics; raw/event output only to avoid unbounded memory |
|
||||
| Multiple targets | Labels and staggered/burst pacing in one invocation | One remote address per invocation |
|
||||
| Terminal UI | Live graph and dashboard through `irtt-tui` | None |
|
||||
| Output formats | table, csv, tsv, jsonl, with selectable columns | Text summary, JSON (optionally gzip to file), and fixed-field raw output; no CSV/TSV or column selection |
|
||||
| Replay saved results | Not implemented | `report` subcommand for saved JSON results |
|
||||
| Benchmarking and clock diagnostics | Not implemented | Benchmarking and timer/clock-diagnostic subcommands |
|
||||
| Client payload and timing controls | No client-side payload fill or don't-fragment/wait-strategy/timer-algorithm controls | Client-side payload fill and controls for don't-fragment, wait strategy, and timer algorithm |
|
||||
| Kernel timestamps | Best-effort preference on Linux for its own send/receive endpoint | Not implemented |
|
||||
| Server listeners | Several listeners in one process, supervised by `ServerSet` | Multiple requested addresses through `ListenAndServe` |
|
||||
| Server wildcard reply-source selection | Default on Linux/macOS/FreeBSD; other platforms refuse wildcard binds | Opt-in through `--set-src-ip`, with a documented per-packet allocation cost |
|
||||
| Server default bind | Wildcard addresses for both families, subject to the platform restriction above | Wildcard addresses for both families; no documented platform restriction |
|
||||
| Server payload fill policy | Honors the session's negotiated request, with a fixed fallback pattern; no options to restrict or override it | Options to restrict or override client fill requests |
|
||||
| Syslog logging | Not implemented | Supported |
|
||||
| Bundled service files | None | systemd and OpenRC |
|
||||
| Client/server library API | Separate, independently versioned Rust crates (`irtt-client`, `irtt-server`) | One Go package exposing both roles |
|
||||
| Prebuilt release binaries | Published for six target triples | None published; install through `go install` or build manually |
|
||||
|
||||
## Black-box testing
|
||||
|
||||
`scripts/compare_irtt_clients.py` is a black-box comparison harness for running
|
||||
upstream `irtt` and `irtt-rs` clients concurrently against the same server. It
|
||||
@@ -8,7 +42,7 @@ summaries, and comparison JSON under `target/interop-comparisons` by default.
|
||||
The harness does not inspect upstream source or tests. It only invokes the
|
||||
upstream executable.
|
||||
|
||||
## Remote or Existing Local Server
|
||||
### Remote or Existing Local Server
|
||||
|
||||
Build `irtt-rs` and ensure an upstream `irtt` executable is available:
|
||||
|
||||
@@ -36,7 +70,7 @@ python3 scripts/compare_irtt_clients.py \
|
||||
Remote comparisons are useful for smoke coverage, but Internet paths are noisy
|
||||
and can make RTT/loss differences hard to interpret.
|
||||
|
||||
## Linux Netem Interop
|
||||
### Linux Netem Interop
|
||||
|
||||
`scripts/run_netem_interop_ci.py` provides deterministic Linux CI coverage using
|
||||
network namespaces, a veth pair, and `tc netem`. It is intended for GitHub
|
||||
@@ -69,7 +103,7 @@ Netem qdiscs are applied on veth egress:
|
||||
Cleanup deletes qdiscs, stops the server process, and removes both namespaces in
|
||||
a `finally` path.
|
||||
|
||||
### Scenarios
|
||||
#### Scenarios
|
||||
|
||||
The CI harness implements:
|
||||
|
||||
@@ -86,7 +120,7 @@ both clients run in the same client namespace against the same server namespace,
|
||||
relative one-way shape is still useful for diagnostics, but the CI pass/fail
|
||||
policy is based on RTT and packet behavior.
|
||||
|
||||
### Pass/Fail Policy
|
||||
#### Pass/Fail Policy
|
||||
|
||||
Deterministic scenarios fail when:
|
||||
|
||||
@@ -113,7 +147,7 @@ python3 scripts/run_netem_interop_ci.py \
|
||||
--loss-rate-warning-pct 12
|
||||
```
|
||||
|
||||
### Manual Linux Usage
|
||||
#### Manual Linux Usage
|
||||
|
||||
On Linux:
|
||||
|
||||
|
||||
+215
-162
@@ -1,29 +1,31 @@
|
||||
# irtt-client
|
||||
|
||||
## NAME
|
||||
## Name
|
||||
|
||||
irtt-client - IRTT-compatible stream client
|
||||
|
||||
## SYNOPSIS
|
||||
## Synopsis
|
||||
|
||||
`irtt-client` [*OPTIONS*] [*[LABEL=]TARGET[@hmac=KEY]*]...
|
||||
|
||||
`irtt-client` `--list-columns`
|
||||
|
||||
## DESCRIPTION
|
||||
## Description
|
||||
|
||||
`irtt-client` probes one or more IRTT-compatible servers over UDP and prints
|
||||
per-probe measurement events as they arrive, plus a final summary for
|
||||
eligible runs.
|
||||
Use `irtt-client` to probe one or more IRTT-compatible servers over UDP.
|
||||
It prints measurement events as they arrive, and a final summary when the
|
||||
run qualifies for one.
|
||||
|
||||
## TARGETS
|
||||
## Targets
|
||||
|
||||
Every target argument accepts `[LABEL=]TARGET[@hmac=KEY]`. An optional
|
||||
`LABEL=` prefix assigns the logical target name used in output; without it,
|
||||
the target string itself is used as the label. Labeled and unlabeled targets
|
||||
can be freely mixed in one argument list. Prefer explicit labels for
|
||||
long-running dynamic target sets: the label is the stable target identity
|
||||
across replacements.
|
||||
Write targets as `[LABEL=]TARGET[@hmac=KEY]`. You can add a `LABEL=` prefix
|
||||
to choose the name shown in output. If you leave it out, the client uses
|
||||
the target string as its label. You can mix labeled and unlabeled targets
|
||||
in the same command.
|
||||
|
||||
If you're changing targets during a long run, give them explicit labels.
|
||||
The label identifies the same logical target when you replace its
|
||||
configuration.
|
||||
|
||||
```sh
|
||||
irtt-client host-a:2112
|
||||
@@ -37,29 +39,31 @@ overrides it for that target, and `@hmac=` explicitly disables HMAC for that
|
||||
target. The modifier uses `@`, not a shell command separator; quote an argument
|
||||
if its key needs shell quoting.
|
||||
|
||||
Global `--hmac KEY` and per-target `@hmac=KEY` accept plain-text keys or hexadecimal keys prefixed with `0x`. For example, `secret` and `0x736563726574` are equivalent.
|
||||
Both the global `--hmac KEY` and per-target `@hmac=KEY` accept plain-text
|
||||
keys or hexadecimal keys prefixed with `0x`. For example, `secret` and
|
||||
`0x736563726574` are equivalent.
|
||||
|
||||
Target syntax supports these backslash escapes in labels, addresses, and HMAC
|
||||
values: `\\` for a literal backslash, `\@` for a literal `@`, `\=` for a
|
||||
literal `=`, and `\;` for a literal semicolon. Escaping `@` is necessary in a
|
||||
label or address when it would otherwise begin `@hmac=`; escaping the first
|
||||
`=` is how an unlabeled target or a label can contain that character. An `=` in an HMAC value does not need escaping. Positional
|
||||
target arguments may contain a raw comma; stdin target sets have the additional
|
||||
`\,` escape described below.
|
||||
`=` lets an unlabeled target or a label contain that character. An `=` in
|
||||
an HMAC value does not need escaping. Positional target arguments may
|
||||
contain a raw comma; stdin target sets have the additional `\,` escape
|
||||
described below.
|
||||
|
||||
Shells process unquoted backslashes and characters such as semicolons before
|
||||
the client sees them. Single-quote a target when it contains parser escapes,
|
||||
for example `'west\=edge=host\@hmac=literal@hmac=a\;b\\c'`, so the
|
||||
backslashes reach `irtt-client` unchanged.
|
||||
|
||||
Multiple targets are probed concurrently. By default, a multi-target session
|
||||
uses **staggered** pacing, spacing active targets across the probe interval;
|
||||
pass `--pacing burst` to send one probe to every active target back-to-back
|
||||
once per interval instead. Final per-target summaries print in the order
|
||||
targets were supplied on the command line, not the order in which they
|
||||
finish or their labels sort alphabetically.
|
||||
The client probes multiple targets concurrently. By default it staggers
|
||||
them across the probe interval. If you want to send one probe to every
|
||||
active target back-to-back once per interval, use `--pacing burst`.
|
||||
Final summaries follow the command-line target order, regardless of which
|
||||
target finished first or how their labels sort.
|
||||
|
||||
## ADDRESS FAMILY
|
||||
## Address family
|
||||
|
||||
By default, each target uses the first address returned by the system resolver,
|
||||
whether IPv4 or IPv6. Use `-4` / `--ipv4` to select IPv4 only, or
|
||||
@@ -94,42 +98,58 @@ labels; choose another alias if one collides with an explicit label.
|
||||
ordinary multi-target pacing and each sends its own probes, so dual-stack can
|
||||
double the number of probes per interval compared with one target per hostname.
|
||||
|
||||
## LOCAL SOCKET CONFIGURATION
|
||||
## Local socket configuration
|
||||
|
||||
The client supports configuring its local UDP sockets using the following options:
|
||||
If you need to control where probes leave the machine, use these socket
|
||||
options:
|
||||
|
||||
- `--bind-addr IP[:PORT]` binds sockets to the specified local IP address and optional port. If no port is specified, or the port is `0`, the operating system assigns an ephemeral port. IPv6 addresses with an explicit port must use `[ADDR]:PORT` syntax. The address family must match the target's resolved address family.
|
||||
- `--bind-device DEVICE` (Linux, Android, Fuchsia) binds sockets to a network interface using `SO_BINDTODEVICE`.
|
||||
- `--mark MARK` (Linux, Android, Fuchsia) sets the socket's firewall mark using `SO_MARK`, allowing traffic to be selected by policy routing or firewall rules. Accepts a decimal or 0x-prefixed hexadecimal unsigned 32-bit integer.
|
||||
- `--fib FIB` (FreeBSD) selects a routing table using `SO_SETFIB`. The FIB is specified as a decimal unsigned 32-bit integer.
|
||||
- `--bind-addr IP[:PORT]` binds sockets to a local IP address and optional
|
||||
port. Leave out the port or use `0` to let the OS assign an ephemeral port.
|
||||
IPv6 addresses with a port need `[ADDR]:PORT` syntax. The address family
|
||||
must match the target's resolved address family.
|
||||
- `--bind-device DEVICE` (Linux, Android, Fuchsia) binds sockets to a
|
||||
network interface using `SO_BINDTODEVICE`.
|
||||
- `--mark MARK` (Linux, Android, Fuchsia) sets the socket's firewall mark
|
||||
using `SO_MARK`, so policy routing or firewall rules can select the
|
||||
traffic. It accepts a decimal or 0x-prefixed hexadecimal unsigned 32-bit
|
||||
integer.
|
||||
- `--fib FIB` (FreeBSD) selects a routing table using `SO_SETFIB`. Supply
|
||||
the FIB as a decimal unsigned 32-bit integer.
|
||||
|
||||
For example, bind IPv4 probes to a specific source address and interface, with a firewall mark:
|
||||
For example, to bind IPv4 probes to a specific source address and interface
|
||||
with a firewall mark:
|
||||
|
||||
```sh
|
||||
irtt-client -4 --bind-addr 192.0.2.10 --bind-device wan0 --mark 1234 host.example
|
||||
```
|
||||
|
||||
Setting a firewall mark or binding to a network device may require elevated privileges or appropriate capabilities. If the operating system rejects a socket option, socket setup fails.
|
||||
Setting a firewall mark or binding to a network device may require elevated
|
||||
privileges or appropriate capabilities. If the OS rejects a socket option,
|
||||
socket setup fails.
|
||||
|
||||
A fixed local port may cause binding conflicts when multiple target sockets are created. Use an ephemeral port unless a specific source port is required. A family-specific bind address is also incompatible with targets using the other address family.
|
||||
A fixed local port can cause bind conflicts when the client creates sockets
|
||||
for multiple targets. Unless you need a particular source port, let the OS
|
||||
assign one. A bind address for one family also cannot be used with targets
|
||||
in the other family.
|
||||
|
||||
## DYNAMIC TARGET SETS FROM STANDARD INPUT
|
||||
## Dynamic target sets from standard input
|
||||
|
||||
`--targets-stdin` is available only with continuous mode (`--continuous` or
|
||||
`--duration 0`).
|
||||
Positional targets remain an initial desired set, and may be omitted to
|
||||
start empty:
|
||||
`--targets-stdin` only works in continuous mode (`--continuous` or
|
||||
`--duration 0`). Use it to change targets while the client is running.
|
||||
Positional targets form the initial set. You can leave them out to start
|
||||
with none:
|
||||
|
||||
~~~sh
|
||||
irtt-client --duration 0 --targets-stdin
|
||||
irtt-client --duration 0 --targets-stdin ams=ams.example:2112
|
||||
~~~
|
||||
|
||||
Each non-empty stdin record declares the complete desired target set, rather
|
||||
than a delta. A later record replaces an earlier set: targets absent from it
|
||||
are retired, unchanged target configurations retain their generation, and a
|
||||
changed address or HMAC setting creates a fresh generation. Under transient
|
||||
live-generation backpressure, only the latest unapplied desired set is kept.
|
||||
Each non-empty stdin record gives the complete set of targets you want.
|
||||
A new record replaces the previous set: the client retires targets you leave
|
||||
out, keeps the generation for unchanged configurations, and starts a fresh
|
||||
generation when an address or HMAC setting changes. If live-generation
|
||||
capacity temporarily prevents an update, only the latest unapplied set is
|
||||
kept.
|
||||
|
||||
The same address-family preparation applies to initial and stdin targets.
|
||||
With `--dual-stack`, unchanged labels and endpoints retain their discovered
|
||||
@@ -138,11 +158,12 @@ and later adding it again performs fresh discovery. New declarations are
|
||||
prepared asynchronously while existing measurements continue. A preparation
|
||||
error stops the stream gracefully without applying a partial set.
|
||||
|
||||
Commas frame targets in stdin records; escape a literal comma within one
|
||||
target as `\,`. Target parsing inside each element is otherwise the same as
|
||||
for positional arguments, including the other escapes above, `@hmac=KEY`, and
|
||||
`@hmac=`. Use a persistent producer or keep a pipe/FIFO writer open while the
|
||||
client is being controlled. For example, in one terminal:
|
||||
Separate targets in stdin records with commas, and escape a comma inside a
|
||||
target as `\,`. Each target otherwise uses the same syntax as positional
|
||||
arguments, including the escapes above, `@hmac=KEY`, and `@hmac=`.
|
||||
|
||||
Keep the producer or pipe/FIFO writer open while controlling the client.
|
||||
For example, run this in one terminal:
|
||||
|
||||
~~~sh
|
||||
mkfifo /tmp/irtt-targets
|
||||
@@ -150,8 +171,8 @@ irtt-client --duration 0 --targets-stdin --hmac default-secret \
|
||||
< /tmp/irtt-targets
|
||||
~~~
|
||||
|
||||
Then open the FIFO once from another terminal and keep that descriptor open
|
||||
while publishing complete desired sets:
|
||||
Then, in another terminal, open the FIFO once and keep that descriptor open
|
||||
while sending complete target sets:
|
||||
|
||||
~~~sh
|
||||
exec 3> /tmp/irtt-targets
|
||||
@@ -161,19 +182,21 @@ printf '%s\n' 'sg=sg.example:2112@hmac=sg-secret,nyc=nyc.example:2112' >&3
|
||||
exec 3>&-
|
||||
~~~
|
||||
|
||||
The exact record `[]` selects an empty desired set. Empty records are
|
||||
ignored; whitespace is otherwise payload, not trimming. EOF is immediate loss
|
||||
or closure of the control plane and requests a graceful stop. A desired-state
|
||||
revision that was read but has not yet been acknowledged as applied may be
|
||||
discarded when EOF is observed; EOF does not wait for a capacity-rejected
|
||||
revision to become applicable.
|
||||
Send exactly `[]` to select an empty target set. The client ignores empty
|
||||
records, but otherwise treats whitespace as part of the input without
|
||||
trimming it.
|
||||
|
||||
Closing stdin sends EOF and asks the client to stop gracefully. An update
|
||||
that has been read but not yet acknowledged as applied may be discarded
|
||||
at that point. EOF does not wait for an update blocked by capacity to
|
||||
become applicable.
|
||||
|
||||
The maximum stdin record payload is 64 KiB. Stdin-controlled mode bounds each
|
||||
desired set to 128 targets and retains at most 256 live target generations
|
||||
while replacements drain. The 128-target limit applies after dual-stack
|
||||
expansion: a hostname producing both families consumes two target slots.
|
||||
|
||||
## NEGOTIATION ONLY
|
||||
## Negotiation only
|
||||
|
||||
`-n` / `--no-test` connects to each target, negotiates the requested parameters,
|
||||
and exits without sending measurement probes. For example:
|
||||
@@ -182,17 +205,18 @@ and exits without sending measurement probes. For example:
|
||||
irtt-client --no-test host.example:2112 --duration 30s --interval 100ms
|
||||
```
|
||||
|
||||
The `no_test_completed` event reports successful negotiation, including the
|
||||
On successful negotiation, the `no_test_completed` event includes the
|
||||
negotiated duration, interval, and packet length. Output format and column
|
||||
selection work as usual; no measurement summary or statistics-memory warning
|
||||
is printed. Negotiation failures use the normal target diagnostics and exit
|
||||
policy: an uninterrupted run exits nonzero if no target succeeds.
|
||||
selection work as usual. The client prints no measurement summary or
|
||||
statistics-memory warning. Negotiation failures use the normal target
|
||||
diagnostics and exit policy: an uninterrupted run exits nonzero if no
|
||||
target succeeds.
|
||||
|
||||
Duration options still specify the duration to negotiate; even with
|
||||
`--continuous` or `--duration 0`, the client exits after negotiation.
|
||||
`--no-test` cannot be combined with `--targets-stdin`.
|
||||
|
||||
## FINITE VERSUS CONTINUOUS OPERATION
|
||||
## Finite versus continuous operation
|
||||
|
||||
`--duration` and `--interval` accept fractional and composite durations, such as
|
||||
`2.5s`, `1m30s`, and `1h`, as well as `ms`, `us` (or `µs`), and `ns` units
|
||||
@@ -208,24 +232,25 @@ Omitting the option preserves the library default schedule.
|
||||
|
||||
`--duration` controls the run length:
|
||||
|
||||
- Any nonzero duration (default `10s`) is a **finite** run: probing stops
|
||||
after that long, and a final summary is printed for table output when
|
||||
statistics are enabled (see [OUTPUT](#output) below — CSV, TSV, and
|
||||
JSON Lines never print one).
|
||||
- `--continuous` (or `--duration 0`) is **continuous**: the client runs until
|
||||
interrupted (`Ctrl-C`). With statistics enabled and table output, a final
|
||||
summary is printed only if the run was interrupted. Stdin-controlled mode
|
||||
is the exception: EOF also requests a graceful stop and prints the final
|
||||
retained summary. An uninterrupted
|
||||
continuous run that exits for another reason does not get one.
|
||||
- A nonzero duration (default `10s`) gives you a finite run. Probing stops
|
||||
after that long. If statistics are enabled and you're using table
|
||||
[output](#output), the client prints a final summary. CSV, TSV, and
|
||||
JSON Lines never include a summary.
|
||||
- With `--continuous` (or `--duration 0`), the client runs until interrupted
|
||||
(`Ctrl-C`). With statistics and table output enabled, interrupting the run
|
||||
prints a final summary. In stdin-controlled mode, EOF also stops the run
|
||||
gracefully and prints the retained summary. A continuous run that exits
|
||||
for another reason without being interrupted does not print one.
|
||||
|
||||
See [MEASUREMENTS AND MEMORY](#measurements-and-memory) below for why this
|
||||
distinction also determines what statistics can be computed and how much
|
||||
memory a long run retains.
|
||||
The choice also affects which statistics are available and how much memory
|
||||
a long run retains. See [Measurements and memory](#measurements-and-memory).
|
||||
|
||||
### Optional recovery and event-only runs
|
||||
|
||||
`--no-stats` disables statistics collection and final summaries while preserving individual measurement output. Statistics-dependent columns (such as IPDV) are unavailable. This runtime option does not remove the `irtt-stats` dependency.
|
||||
If you only need individual measurements, use `--no-stats` to disable
|
||||
statistics collection and final summaries. Columns that need statistics,
|
||||
such as IPDV, are unavailable. The runtime flag still leaves the
|
||||
`irtt-stats` dependency in the build.
|
||||
|
||||
To omit statistics at compile time, build with:
|
||||
|
||||
@@ -240,15 +265,31 @@ warnings are disabled, and IPDV columns retain their names with absent values
|
||||
(`-` in tables, empty CSV/TSV fields, and JSON `null`). Measurement events and
|
||||
protocol-level received statistics remain available.
|
||||
|
||||
`--reconnect` enables automatic recovery for individual targets in continuous mode (`--continuous` or `--duration 0`). Failed connections, closed sessions, and unresponsive servers are retried without interrupting other targets. Retries double the delay after each failed attempt, from 250 ms up to 10 seconds by default. A valid reply resets the delay to the initial value; each target backs off independently.
|
||||
Use `--reconnect` to recover individual targets in continuous mode
|
||||
(`--continuous` or `--duration 0`). The client retries failed connections,
|
||||
closed sessions, and unresponsive servers while the other targets keep
|
||||
running. By default, the delay doubles after each failed attempt, from
|
||||
250 ms up to 10 seconds. A valid reply resets it to the initial delay.
|
||||
Each target backs off independently.
|
||||
|
||||
`--reconnect-initial-delay DURATION` (default: `250ms`) and `--reconnect-max-delay DURATION` (default: `10s`) configure these delays. Both require `--reconnect`, must be positive and schedulable, and the maximum must be at least twice the initial delay. For example, `--reconnect-initial-delay 1s --reconnect-max-delay 5s` retries after `1s`, `2s`, `4s`, `5s`, then `5s` thereafter.
|
||||
You can change the delays with `--reconnect-initial-delay DURATION`
|
||||
(default: `250ms`) and `--reconnect-max-delay DURATION` (default: `10s`).
|
||||
Both require `--reconnect` and must be positive and schedulable. The maximum
|
||||
must be at least twice the initial delay. For example,
|
||||
`--reconnect-initial-delay 1s --reconnect-max-delay 5s` retries after `1s`,
|
||||
`2s`, `4s`, `5s`, then `5s` from there on.
|
||||
|
||||
`--reconnect-timeout DURATION` controls how long a session can go without valid replies before reconnection is attempted (default: `10s`). The timeout applies only while actively sending probes and should account for the probe interval and expected packet loss.
|
||||
`--reconnect-timeout DURATION` sets how long a session can go without valid
|
||||
replies before the client tries to reconnect (default: `10s`). It only
|
||||
applies while actively sending probes, so allow for your probe interval and
|
||||
expected packet loss.
|
||||
|
||||
**Statistics reset on reconnection**, so final summaries only reflect the latest session per target. A warning is emitted when reconnection is enabled with statistics collection. Use `--no-stats` to disable collection and suppress the warning.
|
||||
Reconnecting resets statistics for that target, so its final summary only
|
||||
covers the latest session. The client warns about this when reconnection
|
||||
and statistics collection are both enabled. Use `--no-stats` to disable
|
||||
collection and suppress the warning.
|
||||
|
||||
Example for continuous, multi-target CSV output without statistics:
|
||||
For continuous CSV output from multiple targets without statistics:
|
||||
|
||||
```sh
|
||||
irtt-client --continuous --reconnect --no-stats \
|
||||
@@ -259,11 +300,11 @@ irtt-client --continuous --reconnect --no-stats \
|
||||
reflector1 reflector2
|
||||
```
|
||||
|
||||
## OUTPUT
|
||||
## Output
|
||||
|
||||
### `--format FORMAT`
|
||||
|
||||
One of four event-row formats, default `table`:
|
||||
Choose one of four event-row formats. The default is `table`:
|
||||
|
||||
- `table`: human-readable terminal output; the only format that prints a
|
||||
final summary.
|
||||
@@ -271,23 +312,36 @@ One of four event-row formats, default `table`:
|
||||
- `tsv`: tab-separated output.
|
||||
- `jsonl`: one JSON object per event.
|
||||
|
||||
CSV, TSV, and JSON Lines default to **all** columns for structured export;
|
||||
`table` defaults to a compact column set and hides `echo_sent` rows. An
|
||||
explicit `--columns` selection other than `default` shows all event rows,
|
||||
including `echo_sent`; `--columns default` is equivalent to omitting the
|
||||
option and keeps `echo_sent` hidden for table output.
|
||||
CSV, TSV, and JSON Lines include all columns by default for structured export;
|
||||
`table` defaults to a compact column set and hides `echo_sent` events.
|
||||
You can show those events by explicitly selecting them with `--events`
|
||||
or by using custom columns.
|
||||
|
||||
### `--events EVENTS`
|
||||
|
||||
Choose which events to print with a comma-separated list, or repeat the
|
||||
option to select more events. The available names are `session_started`,
|
||||
`no_test_completed`, `session_closed`, `echo_sent`, `echo_reply`, `loss`,
|
||||
`duplicate`, `late`, and `warning`. Without this option, the client prints
|
||||
all events normally shown by the selected format.
|
||||
|
||||
For CSV output containing only echo replies:
|
||||
|
||||
```sh
|
||||
irtt-client <server> --events echo_reply --format csv --header never --no-stats
|
||||
```
|
||||
|
||||
### `--columns COLUMNS` / `-c COLUMNS`
|
||||
|
||||
Comma-separated event row columns, or the special values `default` (the
|
||||
format's default set) or `all` (every column). List every available column,
|
||||
its meaning, and its aliases with:
|
||||
Choose event-row columns with a comma-separated list, or use `default`
|
||||
for the format's default set and `all` for every column. To see the
|
||||
available columns, their meanings, and aliases, run:
|
||||
|
||||
```sh
|
||||
irtt-client --list-columns
|
||||
```
|
||||
|
||||
(`--list-columns` does not require a target.)
|
||||
You don't need a target for `--list-columns`.
|
||||
|
||||
### `--header auto|always|never`
|
||||
|
||||
@@ -296,10 +350,9 @@ Lines never prints a header line.
|
||||
|
||||
### `--verbose`
|
||||
|
||||
Includes extra fields in table output and final summaries. Has no effect on
|
||||
CSV, TSV, or JSON Lines output either way — those formats show their default
|
||||
all-column set or an explicit `--columns` selection exactly as given,
|
||||
regardless of `--verbose`.
|
||||
Include extra fields in table output and final summaries. CSV, TSV, and
|
||||
JSON Lines keep their default all-column set or your explicit `--columns`
|
||||
selection regardless of `--verbose`.
|
||||
|
||||
### Examples
|
||||
|
||||
@@ -309,13 +362,13 @@ irtt-client <server> --format jsonl
|
||||
irtt-client <server> --format csv --columns event,seq,remote,effective_rtt_us
|
||||
```
|
||||
|
||||
A stream containing only effective RTT values in microseconds:
|
||||
To print only effective RTT values in microseconds:
|
||||
|
||||
```sh
|
||||
irtt-client <server> --format tsv --columns effective_rtt_us --header never
|
||||
```
|
||||
|
||||
## MEASUREMENT FIELDS
|
||||
## Measurement fields
|
||||
|
||||
Useful measurement columns:
|
||||
|
||||
@@ -336,95 +389,95 @@ Useful measurement columns:
|
||||
|
||||
### Kernel timestamps (Linux)
|
||||
|
||||
On Linux, one-way delay estimates 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. 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 best-effort: an unsupported or implausible kernel timestamp falls
|
||||
back to userspace timing. `raw_rtt_us` is unaffected either way;
|
||||
`adjusted_rtt_us`/`effective_rtt_us` can still shift slightly, since server
|
||||
processing time is derived from the same server receive endpoint.
|
||||
On Linux, the client prefers kernel timestamps for its own send/receive
|
||||
endpoint when calculating one-way delays, where the platform and socket
|
||||
support them. This reduces the effect of scheduling and queuing delay.
|
||||
The `irtt-server` applet makes the same choice for its endpoint on Linux.
|
||||
With another IRTT-compatible server, the client uses the wall-clock value
|
||||
that server reports as-is.
|
||||
|
||||
## MEASUREMENTS AND MEMORY
|
||||
If a kernel timestamp is unsupported or implausible, timing falls back to
|
||||
userspace. This leaves `raw_rtt_us` unchanged. The
|
||||
`adjusted_rtt_us`/`effective_rtt_us` values can still shift slightly, since
|
||||
server processing time uses the same server receive endpoint.
|
||||
|
||||
The statistics retention below applies only when the `stats` feature is compiled
|
||||
in and `--no-stats` is absent. Disabling collection keeps the client
|
||||
classification bounds but allocates no statistics collectors.
|
||||
## Measurements and memory
|
||||
|
||||
Finite and continuous modes make a different tradeoff between exact
|
||||
statistics and bounded memory.
|
||||
If the `stats` feature is compiled in and you haven't used `--no-stats`,
|
||||
the client collects statistics as described here. Disabling collection
|
||||
allocates no statistics collectors, though the client still keeps its
|
||||
bounded reply-classification state.
|
||||
|
||||
**Finite mode** retains exact timing samples (and adjacent-sequence IPDV
|
||||
state) so the final summary can report exact medians. Retained memory
|
||||
therefore grows with the number of probes sent, at a small constant cost per
|
||||
probe (on the order of a few hundred bytes of retained sample state per
|
||||
probe per target, before allocator overhead) — a very long finite run at a
|
||||
short interval is the case to plan capacity for. The client warns on stderr
|
||||
when its own estimate of this retained state crosses roughly 128 MiB, and
|
||||
again more strongly past roughly 512 MiB and 1 GiB; treat those numbers as
|
||||
practical guidance, not a promised RSS ceiling, since allocator and OS
|
||||
overhead sit outside the estimate.
|
||||
In finite mode, the client keeps exact timing samples and adjacent-sequence
|
||||
IPDV state so it can report exact medians in the final summary. Memory
|
||||
grows with the number of probes sent, at roughly a few hundred bytes per
|
||||
probe per target before allocator overhead. So if you're planning a long
|
||||
finite run with a short interval, allow for that growth.
|
||||
|
||||
**Continuous mode** (`--duration 0`) does not retain a growing sample
|
||||
history: running statistics keep count/min/max/mean/variance only, so exact
|
||||
medians are unavailable. It still keeps a bounded adjacent-sequence IPDV
|
||||
store, capped at 4,096 sequences per target, so nearby replies can still
|
||||
form IPDV pairs. Retained statistics state therefore approaches a fixed
|
||||
per-target bound rather than growing with elapsed run time.
|
||||
The client prints a warning on stderr when its estimate of retained state
|
||||
crosses roughly 128 MiB, with stronger warnings past roughly 512 MiB and
|
||||
1 GiB. These are useful planning figures, but they don't promise an RSS
|
||||
ceiling: allocator and OS overhead are outside the estimate.
|
||||
|
||||
Reply classification (independent of the statistics choice above) is also
|
||||
bounded per target: each of the pending, timed-out, and completed/duplicate
|
||||
sequence stores has its own 4,096-entry limit — not one shared total across
|
||||
the three. A full pending store makes the client fail and drain that target
|
||||
as resource-exhausted rather than wait for capacity to free. Timed-out and
|
||||
completed state evict their oldest entries, so a sufficiently old late reply
|
||||
can still be seen and counted, but no longer has retained send state for
|
||||
measurements such as RTT.
|
||||
In continuous mode (`--duration 0`), running statistics keep only
|
||||
count/min/max/mean/variance, so exact medians are unavailable. The client
|
||||
still keeps up to 4,096 sequences per target for adjacent-sequence IPDV
|
||||
pairs. Statistics memory approaches a fixed limit per target as the run
|
||||
continues.
|
||||
|
||||
Overall, retained state scales with the **number of configured targets**
|
||||
and these fixed per-target limits, not with total probes sent in continuous
|
||||
mode. Output is written as events arrive rather than accumulated; the
|
||||
managed event stream feeding it is itself bounded and lossy (16,384 events),
|
||||
so a sufficiently slow consumer can miss events, in which case the client
|
||||
reports that resulting output/statistics may be incomplete.
|
||||
Reply classification has its own bounds, regardless of whether you collect
|
||||
statistics. Each target has separate pending, timed-out, and
|
||||
completed/duplicate sequence stores, each with a 4,096-entry limit. If the
|
||||
pending store fills, the client fails and drains that target as
|
||||
resource-exhausted. It does not wait for space to free up.
|
||||
|
||||
The timed-out and completed stores evict their oldest entries. A very late
|
||||
reply can still be seen and counted after eviction, but without its retained
|
||||
send state the client cannot calculate measurements such as RTT.
|
||||
|
||||
In continuous mode, retained state scales with the number of configured
|
||||
targets and these fixed limits per target. The client writes output as
|
||||
events arrive. The managed event stream has a 16,384-event capacity and can
|
||||
drop events if the consumer falls behind. If that happens, the client
|
||||
reports that output and statistics may be incomplete.
|
||||
|
||||
For capacity planning on constrained devices (for example an OpenWrt
|
||||
router): a finite run's statistics memory is roughly proportional to
|
||||
`targets × probes_sent`, while a continuous run's statistics memory
|
||||
approaches a fixed multiple of `targets` regardless of how long it runs.
|
||||
|
||||
## PEER CLOSE AND EXIT BEHAVIOR
|
||||
## Peer close and exit behavior
|
||||
|
||||
A peer closing a finite run is accepted as a terminal, successful outcome.
|
||||
Without `--reconnect`, in continuous mode, an uninterrupted peer closure is treated as an error —
|
||||
the client exits nonzero — so a process supervisor can restart it; an
|
||||
interrupted run (`Ctrl-C`) never reports peer closure as an error, whichever
|
||||
mode it was in.
|
||||
If a peer closes a finite run, the client treats that as successful
|
||||
completion. In continuous mode without `--reconnect`, a peer closure ends
|
||||
an uninterrupted run with a nonzero exit status so a process supervisor
|
||||
can restart it. If you interrupted the run (`Ctrl-C`), peer closure is
|
||||
never reported as an error in either mode.
|
||||
|
||||
With `--reconnect`, a desired target reopens automatically after peer closure.
|
||||
Without recovery, `--targets-stdin` makes peer closure target-local: other
|
||||
desired targets and the stdin controller continue running. Including that
|
||||
target in a later complete set starts a fresh generation.
|
||||
|
||||
## EXIT STATUS
|
||||
## Exit status
|
||||
|
||||
`0` on success (including a finite run's ordinary completion, and an
|
||||
interrupted run of either mode). Nonzero if the managed driver failed, if an
|
||||
uninterrupted continuous run ended by peer closure, or if the run was not
|
||||
interrupted and no target completed successfully. An interrupted run never
|
||||
fails on the last of these conditions — for example, interrupting a
|
||||
continuous run whose only target never opened still exits `0`.
|
||||
The client exits `0` on success, including normal finite completion and
|
||||
interrupted runs in either mode. It exits nonzero if the managed driver
|
||||
fails, an uninterrupted continuous run ends by peer closure, or no target
|
||||
completes successfully in an uninterrupted run.
|
||||
|
||||
## SERVER FILL REQUEST
|
||||
Interrupting the run prevents the last condition from counting as a failure.
|
||||
For example, if you interrupt a continuous run whose only target never
|
||||
opened, it still exits `0`.
|
||||
|
||||
## Server fill request
|
||||
|
||||
`--sfill DESCRIPTOR` requests the server's echo-reply payload fill: `none`
|
||||
(zero-filled), `rand` (random bytes), or `pattern:HEX` (a repeating
|
||||
hexadecimal pattern). See `irtt-server(1)` SERVER FILL for what the server
|
||||
does with an unset or unparseable request.
|
||||
hexadecimal pattern). See the Server fill section in `irtt-server(1)` for
|
||||
what the server does with an unset or unparseable request.
|
||||
|
||||
## EXAMPLES
|
||||
## Examples
|
||||
|
||||
```sh
|
||||
irtt-client netperf-eu.bufferbloat.net:2112
|
||||
@@ -433,7 +486,7 @@ irtt-client netperf-eu.bufferbloat.net:2112 --duration 0
|
||||
irtt-client host-a:2112 host-b:2112 --pacing burst
|
||||
```
|
||||
|
||||
## SEE ALSO
|
||||
## See also
|
||||
|
||||
`irtt-tui(1)`, `irtt-server(1)`, `irtt-rs(1)`, and `irtt-client --help` for
|
||||
the full option list, including HMAC, clock/timestamp negotiation, DSCP, and
|
||||
|
||||
+34
-36
@@ -1,10 +1,10 @@
|
||||
# irtt-rs
|
||||
|
||||
## NAME
|
||||
## Name
|
||||
|
||||
irtt-rs - IRTT-compatible multi-applet dispatcher
|
||||
|
||||
## SYNOPSIS
|
||||
## Synopsis
|
||||
|
||||
`irtt-rs` `client` [*OPTIONS*] [*[LABEL=]TARGET*]...
|
||||
|
||||
@@ -12,41 +12,40 @@ irtt-rs - IRTT-compatible multi-applet dispatcher
|
||||
|
||||
`irtt-rs` `server` [*OPTIONS*]
|
||||
|
||||
## DESCRIPTION
|
||||
## Description
|
||||
|
||||
`irtt-rs` is a multicall dispatcher: one binary that runs the client, TUI,
|
||||
or server applet depending on the subcommand given, or on the name it was
|
||||
invoked/copied as. It exists for router-friendly single-binary deployment;
|
||||
it does not add behavior beyond what `irtt-client(1)`, `irtt-tui(1)`, and
|
||||
`irtt-server(1)` already provide.
|
||||
`irtt-rs` lets you run the client, TUI, or server from one binary. You choose
|
||||
the applet with a subcommand, or by the name you invoke or copy the binary
|
||||
as. This is useful on a router where you want a single binary to deploy.
|
||||
The applets behave the same as `irtt-client(1)`, `irtt-tui(1)`, and
|
||||
`irtt-server(1)`.
|
||||
|
||||
Each subcommand behaves exactly like its dedicated binary — `irtt-rs client
|
||||
...` is equivalent to running `irtt-client ...` directly. See those manuals
|
||||
for target syntax, options, output, and exit behavior.
|
||||
For example, `irtt-rs client ...` does the same thing as running `irtt-client ...` directly. See the
|
||||
individual manuals for target syntax, options, output, and exit behavior.
|
||||
|
||||
## APPLET BINARY NAMES
|
||||
## Applet binary names
|
||||
|
||||
The build also produces dedicated binaries: `irtt-client`, `irtt-tui`, and
|
||||
`irtt-server`. A dedicated binary always runs its own role, regardless of
|
||||
what it is invoked or copied as — it does not consult argv0 dispatch logic.
|
||||
The build also produces dedicated `irtt-client`, `irtt-tui`, and
|
||||
`irtt-server` binaries. These always run their own applet, even if you copy
|
||||
or invoke them under a different name. They do not use argv0 to choose an
|
||||
applet.
|
||||
|
||||
`irtt-rs` itself inspects argv0 first: if invoked (or symlinked/copied) under
|
||||
one of the recognized applet names — `irtt-client`, `irtt-tui`, or
|
||||
`irtt-server` — it runs that applet directly, without needing a subcommand.
|
||||
Any other name starting with `irtt-` (other than `irtt-rs` itself) is
|
||||
rejected as an unknown applet name before subcommand parsing is even
|
||||
attempted — `irtt-custom server` is a hard error, not a fallthrough. Only
|
||||
`irtt-rs` itself, or a name that doesn't start with `irtt-` at all, falls
|
||||
through to ordinary `client`/`tui`/`server` subcommand parsing.
|
||||
The `irtt-rs` dispatcher checks argv0 first. If you invoke it through a
|
||||
symlink or copy named `irtt-client`, `irtt-tui`, or `irtt-server`, it runs
|
||||
that applet without a subcommand.
|
||||
|
||||
## FEATURE-DEPENDENT APPLET AVAILABILITY
|
||||
Other names starting with `irtt-`, except `irtt-rs` itself, are rejected
|
||||
before subcommands are parsed. So `irtt-custom server` fails with an unknown
|
||||
applet error. If the name is `irtt-rs`, or doesn't start with `irtt-`, the
|
||||
dispatcher parses the usual `client`/`tui`/`server` subcommands.
|
||||
|
||||
Each applet is gated by a Cargo feature on the `irtt-rs` package: `client`,
|
||||
`tui`, `server`. All three are in the default feature set. A build with a
|
||||
feature disabled still produces the `irtt-rs` binary, but that applet is
|
||||
unavailable at runtime — running it (by subcommand or by binary name)
|
||||
reports which feature to rebuild with, and `irtt-rs --help` lists which
|
||||
applets the current build actually contains.
|
||||
## Feature-dependent applet availability
|
||||
|
||||
Each applet has a Cargo feature on the `irtt-rs` package: `client`,
|
||||
`tui`, `server`. All three are enabled by default. If you disable one, the
|
||||
build still produces `irtt-rs`, but you cannot run that applet. Trying it
|
||||
through either a subcommand or a binary name tells you which feature to
|
||||
rebuild with. Use `irtt-rs --help` to see which applets your build includes.
|
||||
|
||||
```sh
|
||||
cargo install irtt-rs --locked --no-default-features --features client,stats
|
||||
@@ -55,13 +54,12 @@ cargo install irtt-rs --locked --no-default-features --features client,stats
|
||||
Use `--features client` to omit statistics. The default build includes the
|
||||
`stats` feature, and `tui` always enables it.
|
||||
|
||||
## RELATIONSHIP TO irtt-client, irtt-tui, irtt-server
|
||||
## Relationship to irtt-client, irtt-tui, irtt-server
|
||||
|
||||
`irtt-rs` is packaging and dispatch only. All probing, display, and server
|
||||
behavior lives in `irtt-client(1)`, `irtt-tui(1)`, and `irtt-server(1)` —
|
||||
consult those manuals for anything observable about running a probe or
|
||||
serving one.
|
||||
The `irtt-rs` dispatcher handles packaging and applet selection. For how to run
|
||||
probes, use the dashboard, or serve requests, see `irtt-client(1)`,
|
||||
`irtt-tui(1)`, and `irtt-server(1)`.
|
||||
|
||||
## SEE ALSO
|
||||
## See also
|
||||
|
||||
`irtt-client(1)`, `irtt-tui(1)`, `irtt-server(1)`.
|
||||
+104
-104
@@ -1,22 +1,22 @@
|
||||
# irtt-server
|
||||
|
||||
## NAME
|
||||
## Name
|
||||
|
||||
irtt-server - IRTT-compatible UDP server
|
||||
|
||||
## SYNOPSIS
|
||||
## Synopsis
|
||||
|
||||
`irtt-server` [`--bind` *ADDR*]... [*OPTIONS*]
|
||||
|
||||
`irtt-rs server` [`--bind` *ADDR*]... [*OPTIONS*]
|
||||
|
||||
## DESCRIPTION
|
||||
## Description
|
||||
|
||||
`irtt-server` answers IRTT echo requests over UDP. It is thin orchestration
|
||||
over the reusable `irtt-server` library: one current-thread Tokio runtime,
|
||||
one or more listeners, and the library's own session/policy logic.
|
||||
`irtt-server` answers IRTT echo requests over UDP. It uses the reusable
|
||||
`irtt-server` library for sessions and policy, with one current-thread
|
||||
Tokio runtime running one or more listeners.
|
||||
|
||||
Run with no arguments for the ordinary case:
|
||||
To start a server with the defaults, run:
|
||||
|
||||
```sh
|
||||
irtt-server
|
||||
@@ -28,34 +28,31 @@ wildcard address for both address families, in this fixed order:
|
||||
1. `[::]:2112`
|
||||
2. `0.0.0.0:2112`
|
||||
|
||||
This is available wherever the platform supports safe wildcard reply-source
|
||||
selection (see [Wildcard binds](#wildcard-binds) below). Any explicit
|
||||
`--bind` **replaces** this default pair entirely rather than adding to it —
|
||||
`irtt-server --bind 127.0.0.1:2112` binds only that one address.
|
||||
The defaults work on platforms that support wildcard reply-source
|
||||
selection (see [Wildcard binds](#wildcard-binds)). If you supply any
|
||||
`--bind`, it replaces both defaults. For example,
|
||||
`irtt-server --bind 127.0.0.1:2112` listens only on that address.
|
||||
|
||||
If one of the two default addresses cannot be bound because its address
|
||||
family itself has no local support at all — IPv6 disabled on an otherwise
|
||||
IPv4-capable host, for example — the server falls back to serving just the
|
||||
other one instead of failing outright, and says so on startup. Any other
|
||||
reason a default listener fails — the port already in use, permission
|
||||
denied, no safe wildcard reply-source path — is not a family-support
|
||||
problem and is not silently worked around; the invocation fails as it would
|
||||
for an explicit `--bind`. This fallback applies only to the implicit default
|
||||
pair: an explicit `--bind` list always keeps the ordinary all-or-nothing
|
||||
behavior described below.
|
||||
If the host has no support for one address family, for example because IPv6
|
||||
is disabled, the server uses the other default listener and tells you on
|
||||
startup. Other bind failures still stop startup. This includes a port
|
||||
already in use, permission denied, or a platform without safe wildcard
|
||||
reply-source selection.
|
||||
|
||||
## OPTIONS
|
||||
The address-family fallback only applies to the default pair. An explicit
|
||||
`--bind` list always uses the all-or-nothing behavior described below.
|
||||
|
||||
## Options
|
||||
|
||||
### `--bind ADDR`
|
||||
|
||||
Local UDP address to bind, as `ADDR:PORT`, for example `127.0.0.1:2112` or
|
||||
`[::1]:2112`. Host names are not resolved. Repeatable.
|
||||
Use `ADDR:PORT` to choose a local UDP address, for example `127.0.0.1:2112`
|
||||
or `[::1]:2112`. Hostnames are not resolved.
|
||||
|
||||
Repeat the option to serve several addresses from one process, in the order
|
||||
given. Every listener applies the same policy options, but each keeps its
|
||||
own sessions and tokens, so a session belongs to the address it was opened
|
||||
on. Binding is all-or-nothing: if any address cannot be bound, none are
|
||||
served.
|
||||
Repeat `--bind` to serve several addresses from one process, in the order
|
||||
given. Every listener uses the same policy options, but keeps its own
|
||||
sessions and tokens. A session belongs to the address it was opened on.
|
||||
Binding is all-or-nothing: if any address cannot be bound, none are served.
|
||||
|
||||
A port of `0` selects an unused port per listener, so two such binds get two
|
||||
different ports.
|
||||
@@ -63,71 +60,75 @@ different ports.
|
||||
With no `--bind` at all, see [Description](#description) above for the
|
||||
default wildcard pair and the address-family fallback it allows.
|
||||
|
||||
When reply DSCP/traffic-class marking for IPv4 peers matters, do not configure
|
||||
one explicit dual-stack `[::]:PORT` listener by itself. On Linux, IPv4 peers
|
||||
served through that socket are IPv4-mapped and their replies cannot retain the
|
||||
negotiated marking. Configure the ordinary paired `[::]:PORT` and
|
||||
`0.0.0.0:PORT` listeners instead; the IPv6 listener is made IPv6-only for that
|
||||
same-port pair, so each family uses the correct socket option. This is a
|
||||
configuration workaround, not a reason to force `IPV6_V6ONLY` for every lone
|
||||
IPv6 listener.
|
||||
If you need DSCP/traffic-class marking on replies to IPv4 peers, use care
|
||||
with a lone dual-stack `[::]:PORT` listener. On Linux, those peers use
|
||||
IPv4-mapped addresses and their replies cannot retain the negotiated
|
||||
marking. Use paired `[::]:PORT` and `0.0.0.0:PORT` listeners on the same
|
||||
port. The server makes the IPv6 listener IPv6-only for that pair, so each
|
||||
family gets the correct socket option. A lone IPv6 listener keeps its usual
|
||||
behavior; the server does not force `IPV6_V6ONLY` on it.
|
||||
|
||||
### `--hmac KEY`
|
||||
|
||||
Accepts a plain-text key or a hexadecimal key prefixed with `0x`. For example, `secret` and `0x736563726574` are equivalent. The `IRTT_SERVER_HMAC` environment variable uses the same syntax.
|
||||
You can use a plain-text key or a hexadecimal key prefixed with `0x`.
|
||||
For example, `secret` and `0x736563726574` are equivalent. The
|
||||
`IRTT_SERVER_HMAC` environment variable accepts the same syntax.
|
||||
|
||||
With a key configured, every request must carry a valid MAC and every reply is authenticated;
|
||||
without one, authenticated requests are dropped. The key is visible in the
|
||||
process arguments.
|
||||
With a key configured, every request must carry a valid MAC and the server
|
||||
authenticates every reply. Without a key, the server drops authenticated
|
||||
requests. A key passed on the command line is visible in the process
|
||||
arguments.
|
||||
|
||||
### `--max-sessions COUNT`
|
||||
|
||||
Maximum number of simultaneously live sessions **per listener**, not per
|
||||
process. Once a listener's table is full, a session-creating open to it is
|
||||
dropped silently; nothing is evicted. Zero refuses every session-creating
|
||||
open.
|
||||
Set the maximum number of live sessions per listener. This limit applies
|
||||
to each listener separately, so the process can have more sessions in total.
|
||||
Once a listener's table is full, it silently drops opens that would create
|
||||
a session and keeps the existing sessions. Zero refuses every open that
|
||||
would create a session.
|
||||
|
||||
### `--max-packet-length BYTES`
|
||||
|
||||
Maximum echo datagram size a session may negotiate, in bytes. A longer
|
||||
requested length is reduced during negotiation, an open whose mandatory
|
||||
field block would not fit is refused, and inbound echo requests are admitted
|
||||
against the same limit. This is a resource bound, not an MTU.
|
||||
Set the maximum echo datagram size a session may negotiate, in bytes. The
|
||||
server reduces longer requests during negotiation and refuses an open if
|
||||
its mandatory fields would not fit. Incoming echo requests must fit the
|
||||
same limit. Use this to bound resource use; it does not set the network MTU.
|
||||
|
||||
### `--min-interval DURATION`
|
||||
|
||||
Floor on the send interval a session may negotiate. The negotiated interval
|
||||
is still capped at a quarter of the idle timeout afterwards, so a
|
||||
`--min-interval` above that cap is not the value actually negotiated.
|
||||
Set the minimum send interval a session may negotiate. The server then caps
|
||||
the negotiated interval at a quarter of the idle timeout, so a
|
||||
`--min-interval` above that cap will be reduced.
|
||||
|
||||
A session's reply allowance refills at the shorter of this configured floor
|
||||
and the interval it actually negotiated. Ordinarily that is the configured
|
||||
floor itself, since the negotiated interval is normally at least as long —
|
||||
a 10ms `--min-interval` against a 1s negotiated interval refills every
|
||||
10ms. The two differ only when the idle-timeout cap above pulled the
|
||||
negotiated interval below the configured floor, in which case the shorter,
|
||||
negotiated value is what the allowance actually refills at: a server must
|
||||
not enforce a cadence it did not advertise to the client. Use `0` for no
|
||||
time-based throttling.
|
||||
The session's reply allowance refills at the shorter of the configured
|
||||
minimum and the negotiated interval. Usually this is the configured
|
||||
minimum: a 10ms `--min-interval` with a 1s negotiated interval refills every
|
||||
10ms.
|
||||
|
||||
If the idle-timeout cap reduces the negotiated interval below the configured
|
||||
minimum, the allowance refills at that shorter interval too. This keeps the
|
||||
server from enforcing a slower cadence than it told the client to use.
|
||||
Use `0` for no time-based throttling.
|
||||
|
||||
### `--burst COUNT`
|
||||
|
||||
Echo replies one session may have answered before its allowance has to
|
||||
refill. Use `0` for no allowance at all, which rate-limits every echo
|
||||
request whatever the interval is.
|
||||
Set how many echo replies a session can send before its allowance needs to
|
||||
refill. Use `0` for no allowance, which rate-limits every
|
||||
echo request regardless of the interval.
|
||||
|
||||
### `--idle-timeout DURATION`
|
||||
|
||||
Release a session after this long without a served or rate-limited echo
|
||||
request. The deadline runs from the open, and release is silent. Use `0` to
|
||||
expire a session at the next evaluation; it is not a way to disable expiry.
|
||||
request. The timeout starts at open, and the server releases the session
|
||||
without sending a close. If you use `0`, the session expires at the next
|
||||
evaluation; zero does not disable expiry.
|
||||
|
||||
### `--max-duration DURATION`
|
||||
|
||||
Maximum test duration a session may negotiate. A longer request is reduced
|
||||
to it, and a continuous request is answered with it. Omit this flag for no
|
||||
maximum; a maximum of zero cannot be expressed, because a negotiated
|
||||
duration of zero means continuous.
|
||||
Set the maximum test duration a session may negotiate. The server reduces
|
||||
longer requests to this value and uses it for continuous requests too.
|
||||
Leave the flag out if you don't want a maximum. You cannot set a maximum of
|
||||
zero, since a negotiated duration of zero means continuous.
|
||||
|
||||
### `--timestamp-allowance MODE`
|
||||
|
||||
@@ -147,13 +148,12 @@ and they are sent unmarked. The session is not refused.
|
||||
Durations everywhere in this command accept an integer followed by a unit:
|
||||
`ms`, `s`, or `m`.
|
||||
|
||||
## ENVIRONMENT
|
||||
## Environment
|
||||
|
||||
Every option above can also be supplied through an environment variable, for
|
||||
deployments that configure by environment rather than by command line
|
||||
(containers, orchestrators, unit files). A flag given on the command line
|
||||
overrides the corresponding variable entirely; an unset variable leaves the
|
||||
built-in default in place.
|
||||
You can also set each option through an environment variable, which is
|
||||
useful for containers, orchestrators, and unit files. A command-line flag
|
||||
overrides the corresponding variable entirely. If neither is set, the
|
||||
server uses its built-in default.
|
||||
|
||||
| Variable | Option |
|
||||
| --- | --- |
|
||||
@@ -176,10 +176,10 @@ more than one address needs repeated `--bind` options.
|
||||
an empty value leave traffic-class marking enabled; any other value disables
|
||||
it, and an unset variable leaves it enabled.
|
||||
|
||||
## OUTPUT
|
||||
## Output
|
||||
|
||||
Each bound endpoint is printed on startup, once every listener is up, which
|
||||
also resolves a port of `0`:
|
||||
Once every listener is up, the server prints each bound endpoint. If you
|
||||
requested port `0`, this shows which port was assigned:
|
||||
|
||||
```
|
||||
irtt-server: listening on [::]:2112
|
||||
@@ -188,21 +188,22 @@ irtt-server: listening on 0.0.0.0:2112
|
||||
|
||||
`Ctrl-C` stops the server gracefully.
|
||||
|
||||
## MULTIPLE LISTENERS
|
||||
## Multiple listeners
|
||||
|
||||
Every invocation runs through the same multi-listener path, so one address —
|
||||
whether from the default pair or a single explicit `--bind` — is an ordinary
|
||||
set of one rather than a separate mode. The policy options above apply to
|
||||
every listener, but listeners are otherwise independent: each has its own
|
||||
sessions and tokens, and `--max-sessions` bounds each listener rather than
|
||||
the process. If any listener fails while running, the others are shut down
|
||||
with it rather than leaving a service configured for two families answering
|
||||
on one.
|
||||
The server manages listeners the same way whether you have one or several,
|
||||
including when only one address from the default pair is available or you
|
||||
supply a single `--bind`. Each listener uses the same policy options, but
|
||||
keeps its own sessions and tokens. The `--max-sessions` limit applies to
|
||||
each listener separately.
|
||||
|
||||
If any listener fails while running, the server shuts down the others too.
|
||||
So a service configured for both address families won't carry on answering
|
||||
on just one after a listener failure.
|
||||
|
||||
Wildcard IPv4 and IPv6 listeners may share one port, as the default pair
|
||||
does.
|
||||
|
||||
## SERVER FILL
|
||||
## Server fill
|
||||
|
||||
Echo replies fill their payload according to the negotiated `ServerFill`
|
||||
mode, which a client requests with `irtt-client`'s `--sfill` (see
|
||||
@@ -212,11 +213,11 @@ mode, which a client requests with `irtt-client`'s `--sfill` (see
|
||||
- `rand`: random bytes.
|
||||
- `pattern:HEX`: the given repeating hexadecimal pattern.
|
||||
|
||||
Every valid descriptor is honored. A descriptor the server cannot parse, and
|
||||
a client that expresses no fill preference at all, both get the default
|
||||
`pattern:69727474`, i.e. the repeating bytes `69 72 74 74` (`irtt`).
|
||||
The server honors every valid descriptor. If it cannot parse the descriptor,
|
||||
or the client sends no fill preference, it uses `pattern:69727474`: the
|
||||
repeating bytes `69 72 74 74` (`irtt`).
|
||||
|
||||
## WILDCARD BINDS
|
||||
## Wildcard binds
|
||||
|
||||
An explicit interface address works on every supported system. A wildcard
|
||||
bind such as `0.0.0.0:2112` or `[::]:2112` reads each request's local
|
||||
@@ -224,14 +225,13 @@ destination from packet metadata and sends that request's reply from the
|
||||
same address, so a client on a multi-homed host is answered from the
|
||||
endpoint it actually contacted.
|
||||
|
||||
That reply-source selection is implemented on **Linux, macOS, and FreeBSD**.
|
||||
Elsewhere, a wildcard bind is refused at construction rather than served
|
||||
from a routing-table-selected source address a client would discard — this
|
||||
also applies to the zero-argument default, which will fail with a clear
|
||||
error asking for an explicit `--bind` on such a platform. This correctness
|
||||
rule is deliberate and is not weakened by the zero-argument default.
|
||||
Reply-source selection is supported on Linux, macOS, and FreeBSD. On other
|
||||
platforms, the server refuses wildcard binds because the routing table
|
||||
could select a source address the client would discard. This also means
|
||||
running without arguments fails on those platforms, with an error asking
|
||||
you to supply an explicit `--bind`.
|
||||
|
||||
## EXAMPLES
|
||||
## Examples
|
||||
|
||||
Run with the default wildcard listeners on the standard port:
|
||||
|
||||
@@ -283,19 +283,19 @@ Require authentication:
|
||||
irtt-server --bind 192.0.2.10:2112 --hmac secret
|
||||
```
|
||||
|
||||
## EXIT STATUS
|
||||
## Exit status
|
||||
|
||||
`0` on a clean shutdown (`Ctrl-C`), nonzero if binding fails or a listener
|
||||
fails while running.
|
||||
|
||||
## LIMITATIONS
|
||||
## Limitations
|
||||
|
||||
- No hostname resolution, interface expansion, daemonization, or
|
||||
configuration file — binds are explicit addresses only.
|
||||
configuration file. Binds must use explicit addresses.
|
||||
- Wildcard reply-source selection is unavailable outside Linux, macOS, and
|
||||
FreeBSD; such platforms must bind explicit addresses.
|
||||
|
||||
## SEE ALSO
|
||||
## See also
|
||||
|
||||
`irtt-client(1)`, `irtt-tui(1)`, `irtt-rs(1)`, and `irtt-server --help` for
|
||||
the full option list.
|
||||
+84
-77
@@ -1,25 +1,24 @@
|
||||
# irtt-tui
|
||||
|
||||
## NAME
|
||||
## Name
|
||||
|
||||
irtt-tui - IRTT-compatible terminal UI client
|
||||
|
||||
## SYNOPSIS
|
||||
## Synopsis
|
||||
|
||||
`irtt-tui` [*OPTIONS*] *[LABEL=]TARGET*...
|
||||
|
||||
## DESCRIPTION
|
||||
## Description
|
||||
|
||||
`irtt-tui` is a live dashboard over the same probing engine as
|
||||
`irtt-client(1)`: a graph and status view instead of a printed event stream.
|
||||
At least one target is required.
|
||||
`irtt-tui` gives you a live graph and status dashboard using the same probing
|
||||
engine as `irtt-client(1)`. You need at least one target to start it.
|
||||
|
||||
Target syntax, multiple targets, and pacing are identical to
|
||||
`irtt-client(1)` — see that manual for `[LABEL=]TARGET` syntax and the
|
||||
`--pacing staggered|burst` option. The client configuration flags
|
||||
Targets and pacing work the same as in `irtt-client(1)`. See that manual
|
||||
for `[LABEL=]TARGET` syntax, multiple targets, and the
|
||||
`--pacing staggered|burst` option. The configuration flags
|
||||
(`--interval`, `--timeouts`, `--length`, `--hmac`, `--clock`, `--tstamp`,
|
||||
`--stats`, `--sfill`, `--dscp`, `--ttl`, `--loose`) are shared with `irtt-client` as
|
||||
well; consult that manual for their meaning.
|
||||
`--stats`, `--sfill`, `--dscp`, `--ttl`, `--loose`) are also shared with
|
||||
`irtt-client`, so the same descriptions apply.
|
||||
|
||||
Address-family controls are shared too: `-4` / `--ipv4` uses IPv4 only,
|
||||
`-6` / `--ipv6` uses IPv6 only, and `--dual-stack` gives each available family
|
||||
@@ -36,11 +35,12 @@ The second command creates `cov/v4` and/or `cov/v6` according to the system
|
||||
resolver's initial results, letting you compare the two paths directly.
|
||||
See the client manual's address-family section for discovery errors and labels.
|
||||
|
||||
## CONTINUOUS DEFAULT
|
||||
## Continuous default
|
||||
|
||||
Unlike `irtt-client`, **the TUI defaults to continuous mode**
|
||||
(`--duration 0`): it runs until you quit. Pass an explicit `--duration` for
|
||||
a finite run. Both `--duration` and `--interval` use the
|
||||
The TUI defaults to continuous mode (`--duration 0`) and runs until you
|
||||
quit. If you're used to `irtt-client`, which defaults to a finite run,
|
||||
keep that difference in mind. To set a finite run, pass an explicit
|
||||
`--duration`. Both `--duration` and `--interval` use the
|
||||
[client duration syntax](irtt-client.md#finite-versus-continuous-operation),
|
||||
including fractional values (`2.5s`), composite values (`1m30s`), and units
|
||||
from nanoseconds (`ns`) through hours (`h`):
|
||||
@@ -49,26 +49,29 @@ from nanoseconds (`ns`) through hours (`h`):
|
||||
irtt-tui <server> --duration 30s
|
||||
```
|
||||
|
||||
Continuous runs reconnect after target-local failures, peer closure, or a
|
||||
session completing because the server restricted its duration. After a
|
||||
1.5-second delay, one coalesced retry reopens completed targets while healthy
|
||||
sessions continue undisturbed. Even if every target fails, the TUI stays alive
|
||||
until you quit. Explicit stops, removed or replaced targets, and no-test
|
||||
completion do not trigger retries. Quitting cancels pending retry work.
|
||||
In continuous mode, the TUI reconnects after a target fails, the peer closes
|
||||
the session, or the session ends because the server limited its duration.
|
||||
After a 1.5-second delay, it groups completed targets into one retry while
|
||||
healthy sessions keep running. Even if every target fails, the TUI stays
|
||||
open until you quit.
|
||||
|
||||
Explicit stops, removed or replaced targets, and no-test completion do not
|
||||
trigger retries. Quitting cancels any pending retry work.
|
||||
|
||||
With `--dual-stack`, a failed family reconnects independently while its healthy
|
||||
sibling continues. Reconnect resolves the original hostname again within the
|
||||
target's family. It does not discover additional families or pin the address
|
||||
returned by initial discovery.
|
||||
|
||||
Each reconnect keeps the same logical target row. Statistics reset when a new
|
||||
Reconnecting keeps the target in the same row. Statistics reset when a new
|
||||
session generation is accepted, including attempts that fail before opening.
|
||||
Retained graph history and the age of the last actual reply remain; the graph
|
||||
leaves a gap between generations instead of connecting their samples.
|
||||
The TUI keeps graph history and the age of the last actual reply, with a
|
||||
gap in the graph between generations.
|
||||
|
||||
## DASHBOARD
|
||||
## Dashboard
|
||||
|
||||
One dashboard is used for every target count and terminal size. It shows:
|
||||
The dashboard uses the same layout for any target count and terminal size.
|
||||
It shows:
|
||||
|
||||
- Run status, elapsed time, and finite duration or continuous mode.
|
||||
- The selected target's label and remote address, plus latest signed
|
||||
@@ -78,30 +81,32 @@ One dashboard is used for every target count and terminal size. It shows:
|
||||
- A graph overlaying all targets, with matching target colors and a visible
|
||||
live/history indicator and window size.
|
||||
|
||||
An active session with no primary replies shows `waiting`. Reply age makes a
|
||||
stale RTT distinguishable from a fresh measurement. Loss follows the shared
|
||||
statistics collector: outstanding sends can temporarily contribute to the
|
||||
cumulative loss estimate. Jitter is `-` until an IPDV pair is available.
|
||||
An active session shows `waiting` until it gets a primary reply. Check the
|
||||
reply age to see how fresh the displayed RTT is. The loss estimate comes
|
||||
from the shared statistics collector, so outstanding sends can temporarily
|
||||
count towards cumulative loss. Jitter stays `-` until an IPDV pair is
|
||||
available.
|
||||
|
||||
`Tab` / `Shift-Tab` selects a target, bringing its row into view when necessary.
|
||||
Up to six target rows are shown; smaller terminals show fewer. Selecting a target
|
||||
changes the identification, one-way summary and details; the graph continues to
|
||||
compare every target.
|
||||
Use `Tab` / `Shift-Tab` to select a target and bring its row into view.
|
||||
The dashboard shows up to six target rows, with fewer on smaller terminals.
|
||||
Selecting a target changes the identification, one-way summary, and details.
|
||||
The graph still compares every target.
|
||||
|
||||
`d` opens a details sheet in the graph's space. It contains the selected target's
|
||||
session and negotiation, packet counters, progress, latest timing sample, full
|
||||
timing statistics, warnings, and recent events from all targets. Scroll it with
|
||||
the arrows or page keys. Long lines and timing columns can be read with horizontal
|
||||
scrolling, including full labels and remote addresses that do not fit in the
|
||||
header. `d` or `Esc` returns to the graph, preserving its metric and viewport.
|
||||
The former large/compact Dashboard screens have been replaced by this sheet;
|
||||
`g` remains an alias for `d`.
|
||||
Press `d` to open details in the graph's space. You'll find the selected
|
||||
target's session and negotiation, packet counters, progress, latest timing
|
||||
sample, full timing statistics, and warnings, along with recent events
|
||||
from all targets.
|
||||
|
||||
The minimum terminal size is 56 columns by 18 rows. Below that size the UI asks
|
||||
for a resize and continues acquiring measurements. Resize and important status
|
||||
redraws still work while the display is paused.
|
||||
Scroll with the arrows or page keys. You can scroll horizontally to read
|
||||
long lines and timing columns, including labels and remote addresses that
|
||||
don't fit in the header. Press `d` or `Esc` to return to the graph with its
|
||||
metric and viewport preserved. You can also use `g` as an alias for `d`.
|
||||
|
||||
## CONTROLS
|
||||
You need a terminal at least 56 columns wide and 18 rows tall. Below that
|
||||
size, the TUI asks you to resize while continuing to collect measurements.
|
||||
Resizing and important status redraws still work when the display is paused.
|
||||
|
||||
## Controls
|
||||
|
||||
| Key | Action |
|
||||
| --- | --- |
|
||||
@@ -126,50 +131,52 @@ Signed effective/adjusted RTT and one-way values are preserved. Graph windows
|
||||
range from 5 seconds to 24 hours and default to one minute. The footer shows
|
||||
controls appropriate to the currently open graph or details sheet.
|
||||
|
||||
## RETAINED HISTORY
|
||||
## Retained history
|
||||
|
||||
The TUI keeps its own bounded presentation state on top of the statistics
|
||||
retention described in `irtt-client(1)`:
|
||||
Along with the statistics described in `irtt-client(1)`, the TUI keeps
|
||||
some history for the display:
|
||||
|
||||
- Up to 500,000 graph samples per target.
|
||||
- Up to 80 recent status/log messages.
|
||||
|
||||
At 500,000 replies per target, retention is approximately 13h53m20s at
|
||||
100ms intervals, 27h46m40s at 200ms, or 5d18h53m20s at 1s. Retained graph
|
||||
samples store both clocks, six graph-ready values, and a session-boundary
|
||||
flag; latest details keep the full typed timing values and sequence number.
|
||||
On 64-bit macOS, an 88-byte retained sample gives about 42 MiB per target
|
||||
at the cap, excluding statistics and temporary rendering buffers. Histories
|
||||
grow on demand and reserve exactly to the cap on their final growth step;
|
||||
they do not preallocate the full history for every target at startup.
|
||||
At 500,000 replies per target, you get approximately 13h53m20s of history
|
||||
with 100ms intervals, 27h46m40s at 200ms, or 5d18h53m20s at 1s. Each graph
|
||||
sample stores both clocks, six values ready for graphing, and a flag marking
|
||||
session boundaries. The latest details keep the full typed timing values
|
||||
and sequence number.
|
||||
|
||||
`r` clears graph history for all targets and returns the viewport to
|
||||
live; it does not change the 500,000-sample cap itself. These are
|
||||
presentation bounds on top of the client's own statistics retention (see
|
||||
`irtt-client(1)` MEASUREMENTS AND MEMORY) — a long continuous TUI session's
|
||||
total memory is dominated by whichever of the two is larger for your target
|
||||
count.
|
||||
On 64-bit macOS, each retained sample takes 88 bytes, giving about 42 MiB
|
||||
per target at the cap. Statistics and temporary rendering buffers use
|
||||
additional memory. History grows as needed and reserves exactly to the
|
||||
cap on its final growth step, so you don't pay for the full history of
|
||||
every target at startup.
|
||||
|
||||
A **finite** `--duration` TUI run retains exact statistics samples exactly as
|
||||
`irtt-client(1)` does in finite mode, so its retained state grows with the
|
||||
probe count rather than approaching a bound. Unlike `irtt-client`, the TUI
|
||||
prints **no** memory warning when that estimate gets large: it owns the
|
||||
alternate screen, and a stderr warning there would be invisible or would
|
||||
corrupt the display. Size a long finite TUI run from the figures in
|
||||
`irtt-client(1)` MEASUREMENTS AND MEMORY, or use the continuous default.
|
||||
Press `r` to clear graph history for all targets and return to the live
|
||||
view. This leaves the 500,000-sample cap unchanged. Display history is kept
|
||||
in addition to the client's statistics (see Measurements and memory in
|
||||
`irtt-client(1)`). During a long continuous TUI session, whichever uses more
|
||||
memory for your target count will dominate the total.
|
||||
|
||||
## EXIT BEHAVIOR
|
||||
If you set a finite `--duration`, the TUI retains exact statistics samples
|
||||
just as `irtt-client(1)` does in finite mode. Memory then grows with the
|
||||
probe count. The TUI does not print the memory warnings you get from
|
||||
`irtt-client`, since a stderr warning on the alternate screen could be
|
||||
invisible or corrupt the display. For a long finite run, plan memory using
|
||||
the figures in `irtt-client(1)` under Measurements and memory, or use the
|
||||
continuous default.
|
||||
|
||||
Quitting with `q` or `Ctrl-C` is an interrupted, successful exit. In continuous
|
||||
mode, peer closure and target failures trigger reconnect rather than ending the
|
||||
run. A managed driver failure or an unexpected reconnect-update failure still
|
||||
ends the TUI with an error.
|
||||
## Exit behavior
|
||||
|
||||
Quitting with `q` or `Ctrl-C` counts as an interrupted, successful exit.
|
||||
In continuous mode, the TUI reconnects after peer closure and target failures.
|
||||
A managed driver failure or an unexpected reconnect-update failure still
|
||||
ends the run with an error.
|
||||
|
||||
Finite runs do not reconnect. They complete after their target sessions end;
|
||||
a peer closure is a normal session ending, while a run whose targets all fail
|
||||
exits with an error.
|
||||
|
||||
## EXAMPLES
|
||||
## Examples
|
||||
|
||||
```sh
|
||||
irtt-tui host.example
|
||||
@@ -178,7 +185,7 @@ irtt-tui eu=host-a.example us=host-b.example --pacing burst
|
||||
irtt-tui host.example --duration 30s
|
||||
```
|
||||
|
||||
## SEE ALSO
|
||||
## See also
|
||||
|
||||
`irtt-client(1)` for target syntax, pacing, negotiation flags, and
|
||||
finite/continuous memory behavior; `irtt-server(1)`; `irtt-rs(1)`.
|
||||
Reference in new issue
Block a user