feat(app): make statistics collection optional at compile time
Rust CI / cargo fmt (push) Successful in 17s
Rust CI / cargo clippy (push) Successful in 29s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Successful in 26s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 11s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 6s
Rust CI / cargo doc (push) Successful in 27s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Successful in 1m6s
Netem Interop / netem interop (push) Failing after 17s
Supply chain / cargo-deny advisories (push) Failing after 3s
Fuzz Smoke / cargo fuzz smoke (push) Failing after 31s
Supply chain / cargo-deny bans licenses sources (push) Failing after 4s
Supply chain / actionlint (push) Successful in 4s
Build binaries / Build (arm-musleabi) (push) Successful in 4m31s
Build binaries / Build (aarch64-musl) (push) Successful in 4m41s
Build binaries / Build (arm-musleabihf) (push) Successful in 4m7s
Build binaries / Build (armv7-musleabi) (push) Successful in 4m2s
Build binaries / Build (armv7-musleabihf) (push) Successful in 4m26s
Build binaries / Build (i686-musl) (push) Successful in 4m20s
Build binaries / Build (mips-musl) (push) Successful in 4m18s
Build binaries / Build (mips64-muslabi64) (push) Successful in 4m19s
Build binaries / Build (mips64el-muslabi64) (push) Successful in 4m8s
Build binaries / Build (mipsel-musl) (push) Successful in 4m7s
Build binaries / Build (riscv64gc-musl) (push) Successful in 4m11s
Build binaries / Build (x86_64-musl) (push) Successful in 4m17s
Rust CI / cargo fmt (push) Successful in 17s
Rust CI / cargo clippy (push) Successful in 29s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Successful in 26s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 11s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 6s
Rust CI / cargo doc (push) Successful in 27s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Successful in 1m6s
Netem Interop / netem interop (push) Failing after 17s
Supply chain / cargo-deny advisories (push) Failing after 3s
Fuzz Smoke / cargo fuzz smoke (push) Failing after 31s
Supply chain / cargo-deny bans licenses sources (push) Failing after 4s
Supply chain / actionlint (push) Successful in 4s
Build binaries / Build (arm-musleabi) (push) Successful in 4m31s
Build binaries / Build (aarch64-musl) (push) Successful in 4m41s
Build binaries / Build (arm-musleabihf) (push) Successful in 4m7s
Build binaries / Build (armv7-musleabi) (push) Successful in 4m2s
Build binaries / Build (armv7-musleabihf) (push) Successful in 4m26s
Build binaries / Build (i686-musl) (push) Successful in 4m20s
Build binaries / Build (mips-musl) (push) Successful in 4m18s
Build binaries / Build (mips64-muslabi64) (push) Successful in 4m19s
Build binaries / Build (mips64el-muslabi64) (push) Successful in 4m8s
Build binaries / Build (mipsel-musl) (push) Successful in 4m7s
Build binaries / Build (riscv64gc-musl) (push) Successful in 4m11s
Build binaries / Build (x86_64-musl) (push) Successful in 4m17s
Decouple statistics collection into a dedicated feature to enable lighter client-only builds.
This commit is contained in:
11 files changed
+215
-93
No files matched your search
@@ -84,12 +84,15 @@ cargo install irtt-rs --locked
|
|||||||
This installs the `irtt-rs` dispatcher plus dedicated `irtt-client`,
|
This installs the `irtt-rs` dispatcher plus dedicated `irtt-client`,
|
||||||
`irtt-tui`, and `irtt-server` binaries. Each applet can also be built in
|
`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
|
isolation with `--no-default-features` plus the applet's own feature, for
|
||||||
example a lightweight client-only install:
|
example a lightweight client-only install without statistics:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo install irtt-rs --locked --no-default-features --features client
|
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.
|
||||||
|
|
||||||
For development, or to install directly from a checkout:
|
For development, or to install directly from a checkout:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -165,6 +168,7 @@ when to use it instead of `irtt-client`/`irtt-server` directly.
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `--no-default-features` | `irtt-rs` |
|
| `--no-default-features` | `irtt-rs` |
|
||||||
| `--no-default-features --features client` | `irtt-rs`, `irtt-client` |
|
| `--no-default-features --features client` | `irtt-rs`, `irtt-client` |
|
||||||
|
| `--no-default-features --features client,stats` | `irtt-rs`, `irtt-client` (with statistics) |
|
||||||
| `--no-default-features --features server` | `irtt-rs`, `irtt-server` |
|
| `--no-default-features --features server` | `irtt-rs`, `irtt-server` |
|
||||||
| `--no-default-features --features tui` | `irtt-rs`, `irtt-tui` |
|
| `--no-default-features --features tui` | `irtt-rs`, `irtt-tui` |
|
||||||
| Default features (or `--all-features`) | `irtt-rs`, `irtt-client`, `irtt-tui`, `irtt-server` |
|
| Default features (or `--all-features`) | `irtt-rs`, `irtt-client`, `irtt-tui`, `irtt-server` |
|
||||||
|
|||||||
@@ -41,10 +41,11 @@ required-features = ["server"]
|
|||||||
doc = false
|
doc = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["client", "server", "tui"]
|
default = ["client", "server", "tui", "stats"]
|
||||||
client = ["dep:humantime", "dep:tokio", "dep:irtt-client", "irtt-client/ancillary", "irtt-client/tokio", "dep:irtt-proto", "dep:irtt-stats"]
|
client = ["dep:humantime", "dep:tokio", "dep:irtt-client", "irtt-client/ancillary", "irtt-client/tokio", "dep:irtt-proto"]
|
||||||
|
stats = ["dep:irtt-stats"]
|
||||||
server = ["dep:irtt-server", "dep:irtt-proto", "dep:tokio"]
|
server = ["dep:irtt-server", "dep:irtt-proto", "dep:tokio"]
|
||||||
tui =["dep:humantime", "dep:tokio", "dep:futures-core", "dep:irtt-client", "irtt-client/ancillary", "irtt-client/tokio", "dep:irtt-proto", "dep:irtt-stats", "dep:ratatui", "dep:crossterm", "dep:chrono"]
|
tui =["dep:humantime", "dep:tokio", "dep:futures-core", "dep:irtt-client", "irtt-client/ancillary", "irtt-client/tokio", "dep:irtt-proto", "stats", "dep:ratatui", "dep:crossterm", "dep:chrono"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { version = "0.4.45", default-features = false, features = ["clock"], optional = true }
|
chrono = { version = "0.4.45", default-features = false, features = ["clock"], optional = true }
|
||||||
|
|||||||
@@ -11,8 +11,13 @@ Installing this crate builds:
|
|||||||
- `irtt-tui` — the interactive terminal UI (feature `tui`).
|
- `irtt-tui` — the interactive terminal UI (feature `tui`).
|
||||||
- `irtt-server` — the dedicated server binary (feature `server`).
|
- `irtt-server` — the dedicated server binary (feature `server`).
|
||||||
|
|
||||||
All three features are enabled by default, so a plain `cargo install irtt-rs`
|
The `client`, `server`, `tui`, and `stats` features are enabled by default, so a
|
||||||
installs everything.
|
plain `cargo install irtt-rs` installs everything. The TUI requires statistics
|
||||||
|
and enables `stats` automatically.
|
||||||
|
|
||||||
|
For a client without statistics, use
|
||||||
|
`cargo install irtt-rs --locked --no-default-features --features client --bin irtt-client`.
|
||||||
|
Use `--features client,stats` for a client-only build with statistics.
|
||||||
|
|
||||||
## This is an application, not a library
|
## This is an application, not a library
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ pub struct ClientArgs {
|
|||||||
default_value = "10s",
|
default_value = "10s",
|
||||||
value_parser = parse_test_duration,
|
value_parser = parse_test_duration,
|
||||||
help = "Test duration (e.g. 2.5s, 1m30s, 1h); use --continuous for continuous mode (0 is also accepted)",
|
help = "Test duration (e.g. 2.5s, 1m30s, 1h); use --continuous for continuous mode (0 is also accepted)",
|
||||||
long_help = "Test duration (e.g. 2.5s, 1m30s, 500us, 100ns, 1h); use --continuous for continuous mode (0 is also accepted).\n\nUnless --no-stats is selected, finite runs retain exact statistics for final summaries. Continuous mode uses bounded-memory running statistics and prints a final summary when interrupted; --targets-stdin also prints the final retained summary on EOF."
|
long_help = "Test duration (e.g. 2.5s, 1m30s, 500us, 100ns, 1h); use --continuous for continuous mode (0 is also accepted).\n\nWhen statistics support is compiled in and --no-stats is not selected, finite runs retain exact statistics for final summaries. Continuous mode uses bounded-memory running statistics and prints a final summary when interrupted; --targets-stdin also prints the final retained summary on EOF."
|
||||||
)]
|
)]
|
||||||
pub duration: Duration,
|
pub duration: Duration,
|
||||||
|
|
||||||
@@ -64,6 +64,7 @@ pub struct ClientArgs {
|
|||||||
pub continuous: bool,
|
pub continuous: bool,
|
||||||
|
|
||||||
/// Disable statistics collection and final summaries; retain measurement event output.
|
/// Disable statistics collection and final summaries; retain measurement event output.
|
||||||
|
/// Accepted even when statistics support is not compiled in.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub no_stats: bool,
|
pub no_stats: bool,
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod args;
|
pub mod args;
|
||||||
pub mod output;
|
pub mod output;
|
||||||
pub mod run;
|
pub mod run;
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
pub mod summary;
|
pub mod summary;
|
||||||
|
|
||||||
pub use args::{ClientArgs, HeaderMode, OutputFormat};
|
pub use args::{ClientArgs, HeaderMode, OutputFormat};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use irtt_client::{
|
|||||||
ServerTiming, SignedDuration, WarningKind,
|
ServerTiming, SignedDuration, WarningKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
use irtt_stats::{EventStatsUpdate, IpdvPairUpdate};
|
use irtt_stats::{EventStatsUpdate, IpdvPairUpdate};
|
||||||
|
|
||||||
use super::args::{HeaderMode, OutputFormat};
|
use super::args::{HeaderMode, OutputFormat};
|
||||||
@@ -48,10 +49,12 @@ impl OutputConfig {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
pub(super) fn prints_summary(&self) -> bool {
|
pub(super) fn prints_summary(&self) -> bool {
|
||||||
self.format.prints_summary()
|
self.format.prints_summary()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
pub(super) fn summary_verbose(&self) -> bool {
|
pub(super) fn summary_verbose(&self) -> bool {
|
||||||
self.verbose
|
self.verbose
|
||||||
}
|
}
|
||||||
@@ -96,15 +99,22 @@ impl OutputConfig {
|
|||||||
&self,
|
&self,
|
||||||
event: &ClientEvent,
|
event: &ClientEvent,
|
||||||
target: Option<&str>,
|
target: Option<&str>,
|
||||||
stats: Option<&EventStatsUpdate>,
|
#[cfg(feature = "stats")] stats: Option<&EventStatsUpdate>,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
let row = OutputRow::from_event(event);
|
let row = OutputRow::from_event(event);
|
||||||
if self.default_table_rows && row.is_default_table_hidden() {
|
if self.default_table_rows && row.is_default_table_hidden() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
|
let ipdv_ns = row
|
||||||
|
.seq()
|
||||||
|
.and_then(|seq| ipdv_pair(stats, seq))
|
||||||
|
.map(|pair| pair.rtt_ipdv_ns);
|
||||||
|
#[cfg(not(feature = "stats"))]
|
||||||
|
let ipdv_ns = None;
|
||||||
let context = RenderContext {
|
let context = RenderContext {
|
||||||
stats,
|
ipdv_ns,
|
||||||
target,
|
target,
|
||||||
verbose: self.verbose,
|
verbose: self.verbose,
|
||||||
};
|
};
|
||||||
@@ -155,7 +165,7 @@ impl OutputConfig {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
struct RenderContext<'a> {
|
struct RenderContext<'a> {
|
||||||
stats: Option<&'a EventStatsUpdate>,
|
ipdv_ns: Option<i128>,
|
||||||
target: Option<&'a str>,
|
target: Option<&'a str>,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
}
|
}
|
||||||
@@ -817,18 +827,12 @@ fn cell_for(row: &OutputRow, column: Column, context: RenderContext<'_>) -> Opti
|
|||||||
.reply()
|
.reply()
|
||||||
.and_then(|reply| reply.one_way.and_then(|one_way| one_way.client_to_server))
|
.and_then(|reply| reply.one_way.and_then(|one_way| one_way.client_to_server))
|
||||||
.map(|value| CellValue::Integer(signed_duration_us(value))),
|
.map(|value| CellValue::Integer(signed_duration_us(value))),
|
||||||
Column::Ipdv => row
|
Column::Ipdv => context.ipdv_ns.map(|value| {
|
||||||
.seq()
|
CellValue::text(format_signed_duration(SignedDuration::from_nanos(value)))
|
||||||
.and_then(|seq| ipdv_pair(context.stats, seq))
|
}),
|
||||||
.map(|pair| {
|
Column::IpdvUs => context
|
||||||
CellValue::text(format_signed_duration(SignedDuration::from_nanos(
|
.ipdv_ns
|
||||||
pair.rtt_ipdv_ns,
|
.map(|value| CellValue::Integer(value / 1_000)),
|
||||||
)))
|
|
||||||
}),
|
|
||||||
Column::IpdvUs => row
|
|
||||||
.seq()
|
|
||||||
.and_then(|seq| ipdv_pair(context.stats, seq))
|
|
||||||
.map(|pair| CellValue::Integer(pair.rtt_ipdv_ns / 1_000)),
|
|
||||||
Column::ServerProcessing => row
|
Column::ServerProcessing => row
|
||||||
.reply()
|
.reply()
|
||||||
.and_then(|reply| reply.server_timing.and_then(|timing| timing.processing))
|
.and_then(|reply| reply.server_timing.and_then(|timing| timing.processing))
|
||||||
@@ -935,6 +939,7 @@ fn server_timing_i64(
|
|||||||
.map(|value| CellValue::Integer(i128::from(value)))
|
.map(|value| CellValue::Integer(i128::from(value)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
fn ipdv_pair(stats: Option<&EventStatsUpdate>, seq: u32) -> Option<&IpdvPairUpdate> {
|
fn ipdv_pair(stats: Option<&EventStatsUpdate>, seq: u32) -> Option<&IpdvPairUpdate> {
|
||||||
let stats = stats?;
|
let stats = stats?;
|
||||||
stats
|
stats
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, BTreeSet, HashSet, VecDeque},
|
collections::{HashSet, VecDeque},
|
||||||
io::{self, BufRead, Write},
|
io::{self, BufRead, Write},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
thread,
|
thread,
|
||||||
@@ -7,7 +7,7 @@ use std::{
|
|||||||
|
|
||||||
use irtt_client::{
|
use irtt_client::{
|
||||||
managed::{
|
managed::{
|
||||||
ManagedClient, ManagedCommandApplyError, ManagedEndReason, ManagedEvent, ManagedStatus,
|
ManagedClient, ManagedCommandApplyError, ManagedEndReason, ManagedEvent,
|
||||||
ManagedTargetEndReason, ManagedTargetOutcome, TargetInstance,
|
ManagedTargetEndReason, ManagedTargetOutcome, TargetInstance,
|
||||||
},
|
},
|
||||||
ClientEvent,
|
ClientEvent,
|
||||||
@@ -16,26 +16,25 @@ use irtt_client::{
|
|||||||
use super::{args::ClientArgs, output::OutputConfig};
|
use super::{args::ClientArgs, output::OutputConfig};
|
||||||
|
|
||||||
use crate::shared::client::{
|
use crate::shared::client::{
|
||||||
expected_probe_count, parse_stdin_target_set,
|
parse_stdin_target_set,
|
||||||
session::{
|
session::{
|
||||||
drain_final_events, peer_close_run_error, request_managed_stop_for_peer_close,
|
drain_final_events, peer_close_run_error, request_managed_stop_for_peer_close,
|
||||||
request_managed_stop_once, should_print_final_summary,
|
request_managed_stop_once,
|
||||||
},
|
},
|
||||||
worker::ManagedWorker,
|
worker::ManagedWorker,
|
||||||
ManagedRunSetup, TargetSpec, STDIN_MAX_DESIRED_TARGETS, STDIN_OUTCOME_HISTORY_LIMIT,
|
ManagedRunSetup, TargetSpec, STDIN_MAX_DESIRED_TARGETS, STDIN_OUTCOME_HISTORY_LIMIT,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
use irtt_stats::{EventStatsUpdate, StatsCollector, StatsConfig};
|
use irtt_stats::{EventStatsUpdate, StatsCollector, StatsConfig};
|
||||||
|
|
||||||
const MIB: u64 = 1024 * 1024;
|
#[cfg(feature = "stats")]
|
||||||
|
use crate::shared::client::{expected_probe_count, session::should_print_final_summary};
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
|
use irtt_client::managed::ManagedStatus;
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
const GIB: u64 = 1024 * MIB;
|
|
||||||
|
|
||||||
const FINITE_STATS_MEMORY_WARNING_BYTES: u64 = 128 * MIB;
|
|
||||||
|
|
||||||
const FINITE_STATS_MEMORY_STRONG_WARNING_BYTES: u64 = 512 * MIB;
|
|
||||||
|
|
||||||
const FINITE_STATS_MEMORY_VERY_STRONG_WARNING_BYTES: u64 = GIB;
|
|
||||||
const MAX_STDIN_RECORD_BYTES: usize = 64 * 1024;
|
const MAX_STDIN_RECORD_BYTES: usize = 64 * 1024;
|
||||||
|
|
||||||
pub async fn run_stream(
|
pub async fn run_stream(
|
||||||
@@ -50,6 +49,7 @@ pub async fn run_stream(
|
|||||||
result = args.prepare() => result.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?,
|
result = args.prepare() => result.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?,
|
||||||
_ = shutdown.wait_for(|requested| *requested) => return Ok(()),
|
_ = shutdown.wait_for(|requested| *requested) => return Ok(()),
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
let multi_target = setup.is_multi_target();
|
let multi_target = setup.is_multi_target();
|
||||||
let output_config = OutputConfig::new(
|
let output_config = OutputConfig::new(
|
||||||
args.format,
|
args.format,
|
||||||
@@ -58,6 +58,7 @@ pub async fn run_stream(
|
|||||||
args.verbose,
|
args.verbose,
|
||||||
)
|
)
|
||||||
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
|
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
if let Some(warning) = reconnect_stats_warning(&args) {
|
if let Some(warning) = reconnect_stats_warning(&args) {
|
||||||
eprintln!("{warning}");
|
eprintln!("{warning}");
|
||||||
}
|
}
|
||||||
@@ -65,9 +66,9 @@ pub async fn run_stream(
|
|||||||
return run_stdin_stream(setup, output_config, shutdown, &args).await;
|
return run_stdin_stream(setup, output_config, shutdown, &args).await;
|
||||||
}
|
}
|
||||||
let continuous = args.is_continuous();
|
let continuous = args.is_continuous();
|
||||||
let target_count = setup.target_count();
|
|
||||||
|
|
||||||
if let Some(warning) = finite_stats_memory_warning(&args, target_count) {
|
#[cfg(feature = "stats")]
|
||||||
|
if let Some(warning) = finite_stats_memory_warning(&args, setup.target_count()) {
|
||||||
eprintln!("{warning}");
|
eprintln!("{warning}");
|
||||||
}
|
}
|
||||||
if *shutdown.borrow() {
|
if *shutdown.borrow() {
|
||||||
@@ -86,15 +87,20 @@ pub async fn run_stream(
|
|||||||
let owner = ManagedWorker::start(task, handle.clone())?;
|
let owner = ManagedWorker::start(task, handle.clone())?;
|
||||||
let mut stdout = io::LineWriter::new(io::stdout().lock());
|
let mut stdout = io::LineWriter::new(io::stdout().lock());
|
||||||
let mut stream_output = StreamOutput {
|
let mut stream_output = StreamOutput {
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
continuous: args.is_continuous(),
|
continuous: args.is_continuous(),
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
collect_stats: !args.no_stats,
|
collect_stats: !args.no_stats,
|
||||||
config: output_config,
|
config: output_config,
|
||||||
header_printed: false,
|
header_printed: false,
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
print_final_summary: false,
|
print_final_summary: false,
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
show_running_only_summary_note: false,
|
show_running_only_summary_note: false,
|
||||||
out: &mut stdout,
|
out: &mut stdout,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
let mut stats = setup
|
let mut stats = setup
|
||||||
.targets
|
.targets
|
||||||
.iter()
|
.iter()
|
||||||
@@ -133,7 +139,7 @@ pub async fn run_stream(
|
|||||||
_ = shutdown.changed(), if !interrupted => {}
|
_ = shutdown.changed(), if !interrupted => {}
|
||||||
changed = status.changed() => { if changed.is_err() { break; } }
|
changed = status.changed() => { if changed.is_err() { break; } }
|
||||||
event = events.recv() => match event {
|
event = events.recv() => match event {
|
||||||
Ok(event) => process_event(event, &mut stream_output, &mut stats, &mut terminal_targets)?,
|
Ok(event) => process_event(event, &mut stream_output, #[cfg(feature = "stats")] &mut stats, &mut terminal_targets)?,
|
||||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
|
Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
|
||||||
dropped_events = dropped_events.saturating_add(count);
|
dropped_events = dropped_events.saturating_add(count);
|
||||||
}
|
}
|
||||||
@@ -146,7 +152,13 @@ pub async fn run_stream(
|
|||||||
}
|
}
|
||||||
let outcome = owner.join().await?;
|
let outcome = owner.join().await?;
|
||||||
drain_final_events(&mut events, &mut dropped_events, |event| {
|
drain_final_events(&mut events, &mut dropped_events, |event| {
|
||||||
process_event(event, &mut stream_output, &mut stats, &mut terminal_targets)
|
process_event(
|
||||||
|
event,
|
||||||
|
&mut stream_output,
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
|
&mut stats,
|
||||||
|
&mut terminal_targets,
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
if let Some(warning) = dropped_event_warning(dropped_events) {
|
if let Some(warning) = dropped_event_warning(dropped_events) {
|
||||||
eprintln!("{warning}");
|
eprintln!("{warning}");
|
||||||
@@ -180,22 +192,25 @@ pub async fn run_stream(
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
stream_output.print_final_summary =
|
#[cfg(feature = "stats")]
|
||||||
!args.no_stats && !args.no_test && should_print_final_summary(continuous, interrupted);
|
{
|
||||||
stream_output.show_running_only_summary_note =
|
stream_output.print_final_summary =
|
||||||
continuous && interrupted && stream_output.print_final_summary;
|
!args.no_stats && !args.no_test && should_print_final_summary(continuous, interrupted);
|
||||||
for target in &setup.targets {
|
stream_output.show_running_only_summary_note =
|
||||||
let Some(target_stats) = stats.get(&target.label) else {
|
continuous && interrupted && stream_output.print_final_summary;
|
||||||
continue;
|
for target in &setup.targets {
|
||||||
};
|
let Some(target_stats) = stats.get(&target.label) else {
|
||||||
if multi_target
|
continue;
|
||||||
&& stream_output.print_final_summary
|
};
|
||||||
&& stream_output.config.prints_summary()
|
if multi_target
|
||||||
{
|
&& stream_output.print_final_summary
|
||||||
writeln!(stream_output.out)?;
|
&& stream_output.config.prints_summary()
|
||||||
writeln!(stream_output.out, "target: {}", target.label)?;
|
{
|
||||||
|
writeln!(stream_output.out)?;
|
||||||
|
writeln!(stream_output.out, "target: {}", target.label)?;
|
||||||
|
}
|
||||||
|
stream_output.print_summary(target_stats)?;
|
||||||
}
|
}
|
||||||
stream_output.print_summary(target_stats)?;
|
|
||||||
}
|
}
|
||||||
stream_output.out.flush()?;
|
stream_output.out.flush()?;
|
||||||
if let Some(error) = terminal_error {
|
if let Some(error) = terminal_error {
|
||||||
@@ -370,16 +385,18 @@ impl BoundedTargetSet {
|
|||||||
fn process_stdin_event<W: Write>(
|
fn process_stdin_event<W: Write>(
|
||||||
event: ManagedEvent,
|
event: ManagedEvent,
|
||||||
stream_output: &mut StreamOutput<'_, W>,
|
stream_output: &mut StreamOutput<'_, W>,
|
||||||
stats: &mut BTreeMap<TargetInstance, StatsCollector>,
|
#[cfg(feature = "stats")] stats: &mut BTreeMap<TargetInstance, StatsCollector>,
|
||||||
terminal_targets: &mut BoundedTargetSet,
|
terminal_targets: &mut BoundedTargetSet,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
match event {
|
match event {
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
ManagedEvent::TargetStateChanged { target, .. } if stream_output.collect_stats => {
|
ManagedEvent::TargetStateChanged { target, .. } if stream_output.collect_stats => {
|
||||||
stats
|
stats
|
||||||
.entry(target)
|
.entry(target)
|
||||||
.or_insert_with(|| StatsCollector::new(stats_config(true)));
|
.or_insert_with(|| StatsCollector::new(stats_config(true)));
|
||||||
}
|
}
|
||||||
ManagedEvent::Client { target, event } => {
|
ManagedEvent::Client { target, event } => {
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
let update = stream_output.collect_stats.then(|| {
|
let update = stream_output.collect_stats.then(|| {
|
||||||
let collector = stats
|
let collector = stats
|
||||||
.entry(target.clone())
|
.entry(target.clone())
|
||||||
@@ -389,7 +406,12 @@ fn process_stdin_event<W: Write>(
|
|||||||
}
|
}
|
||||||
collector.process(&event)
|
collector.process(&event)
|
||||||
});
|
});
|
||||||
stream_output.print_event(&event, Some(target.id.as_str()), update.as_ref())?;
|
stream_output.print_event(
|
||||||
|
&event,
|
||||||
|
Some(target.id.as_str()),
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
|
update.as_ref(),
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
ManagedEvent::TargetFinished { outcome }
|
ManagedEvent::TargetFinished { outcome }
|
||||||
if terminal_targets.insert(outcome.target.clone()) =>
|
if terminal_targets.insert(outcome.target.clone()) =>
|
||||||
@@ -401,6 +423,7 @@ fn process_stdin_event<W: Write>(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
fn reconcile_stdin_stats(
|
fn reconcile_stdin_stats(
|
||||||
stats: &mut BTreeMap<TargetInstance, StatsCollector>,
|
stats: &mut BTreeMap<TargetInstance, StatsCollector>,
|
||||||
status: &ManagedStatus,
|
status: &ManagedStatus,
|
||||||
@@ -417,6 +440,7 @@ fn reconcile_stdin_stats(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
fn snapshot_stdin_summary_targets(
|
fn snapshot_stdin_summary_targets(
|
||||||
stats: &mut BTreeMap<TargetInstance, StatsCollector>,
|
stats: &mut BTreeMap<TargetInstance, StatsCollector>,
|
||||||
status: &ManagedStatus,
|
status: &ManagedStatus,
|
||||||
@@ -470,14 +494,19 @@ async fn run_stdin_stream(
|
|||||||
|
|
||||||
let mut stdout = io::LineWriter::new(io::stdout().lock());
|
let mut stdout = io::LineWriter::new(io::stdout().lock());
|
||||||
let mut stream_output = StreamOutput {
|
let mut stream_output = StreamOutput {
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
continuous: args.is_continuous(),
|
continuous: args.is_continuous(),
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
collect_stats: !args.no_stats,
|
collect_stats: !args.no_stats,
|
||||||
config: output_config,
|
config: output_config,
|
||||||
header_printed: false,
|
header_printed: false,
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
print_final_summary: false,
|
print_final_summary: false,
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
show_running_only_summary_note: false,
|
show_running_only_summary_note: false,
|
||||||
out: &mut stdout,
|
out: &mut stdout,
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
let mut stats = BTreeMap::new();
|
let mut stats = BTreeMap::new();
|
||||||
let mut terminal_targets = BoundedTargetSet::new(STDIN_OUTCOME_HISTORY_LIMIT);
|
let mut terminal_targets = BoundedTargetSet::new(STDIN_OUTCOME_HISTORY_LIMIT);
|
||||||
let mut stdin_changed = false;
|
let mut stdin_changed = false;
|
||||||
@@ -491,6 +520,7 @@ async fn run_stdin_stream(
|
|||||||
let mut dropped_events = 0_u64;
|
let mut dropped_events = 0_u64;
|
||||||
let mut stop_requested = false;
|
let mut stop_requested = false;
|
||||||
let mut stop = None;
|
let mut stop = None;
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
let mut summary_targets = None;
|
let mut summary_targets = None;
|
||||||
loop {
|
loop {
|
||||||
let snapshot = status.borrow_and_update().clone();
|
let snapshot = status.borrow_and_update().clone();
|
||||||
@@ -542,16 +572,20 @@ async fn run_stdin_stream(
|
|||||||
}
|
}
|
||||||
if stop.is_some() {
|
if stop.is_some() {
|
||||||
preparing = None;
|
preparing = None;
|
||||||
summary_targets = Some(snapshot_stdin_summary_targets(
|
#[cfg(feature = "stats")]
|
||||||
&mut stats,
|
{
|
||||||
&snapshot,
|
summary_targets = Some(snapshot_stdin_summary_targets(
|
||||||
!args.no_stats,
|
&mut stats,
|
||||||
));
|
&snapshot,
|
||||||
|
!args.no_stats,
|
||||||
|
));
|
||||||
|
}
|
||||||
if request_managed_stop_once(&mut stop_requested) {
|
if request_managed_stop_once(&mut stop_requested) {
|
||||||
drop(handle.stop());
|
drop(handle.stop());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
reconcile_stdin_stats(&mut stats, &snapshot, summary_targets.as_ref());
|
reconcile_stdin_stats(&mut stats, &snapshot, summary_targets.as_ref());
|
||||||
if snapshot.final_outcome.is_some() {
|
if snapshot.final_outcome.is_some() {
|
||||||
break;
|
break;
|
||||||
@@ -573,7 +607,10 @@ async fn run_stdin_stream(
|
|||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
stop = Some(StdinStop::Fatal(format!("invalid --targets-stdin target set: {error}")));
|
stop = Some(StdinStop::Fatal(format!("invalid --targets-stdin target set: {error}")));
|
||||||
summary_targets = Some(snapshot_stdin_summary_targets(&mut stats, &status.borrow(), !args.no_stats));
|
#[cfg(feature = "stats")]
|
||||||
|
{
|
||||||
|
summary_targets = Some(snapshot_stdin_summary_targets(&mut stats, &status.borrow(), !args.no_stats));
|
||||||
|
}
|
||||||
if request_managed_stop_once(&mut stop_requested) { drop(handle.stop()); }
|
if request_managed_stop_once(&mut stop_requested) { drop(handle.stop()); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -594,14 +631,18 @@ async fn run_stdin_stream(
|
|||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
stop = Some(StdinStop::Fatal("--targets-stdin update was rejected".to_owned()));
|
stop = Some(StdinStop::Fatal("--targets-stdin update was rejected".to_owned()));
|
||||||
summary_targets = Some(snapshot_stdin_summary_targets(&mut stats, &status.borrow(), !args.no_stats));
|
#[cfg(feature = "stats")]
|
||||||
|
{
|
||||||
|
summary_targets = Some(snapshot_stdin_summary_targets(&mut stats, &status.borrow(), !args.no_stats));
|
||||||
|
}
|
||||||
if request_managed_stop_once(&mut stop_requested) { drop(handle.stop()); }
|
if request_managed_stop_once(&mut stop_requested) { drop(handle.stop()); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
event = events.recv() => match event {
|
event = events.recv() => match event {
|
||||||
Ok(event) => {
|
Ok(event) => {
|
||||||
process_stdin_event(event, &mut stream_output, &mut stats, &mut terminal_targets)?;
|
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());
|
reconcile_stdin_stats(&mut stats, &status.borrow(), summary_targets.as_ref());
|
||||||
}
|
}
|
||||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
|
Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
|
||||||
@@ -614,7 +655,13 @@ async fn run_stdin_stream(
|
|||||||
|
|
||||||
let outcome = owner.join().await?;
|
let outcome = owner.join().await?;
|
||||||
drain_final_events(&mut events, &mut dropped_events, |event| {
|
drain_final_events(&mut events, &mut dropped_events, |event| {
|
||||||
process_stdin_event(event, &mut stream_output, &mut stats, &mut terminal_targets)
|
process_stdin_event(
|
||||||
|
event,
|
||||||
|
&mut stream_output,
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
|
&mut stats,
|
||||||
|
&mut terminal_targets,
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
if let Some(warning) = dropped_event_warning(dropped_events) {
|
if let Some(warning) = dropped_event_warning(dropped_events) {
|
||||||
eprintln!("{warning}");
|
eprintln!("{warning}");
|
||||||
@@ -631,25 +678,28 @@ async fn run_stdin_stream(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let print_summary =
|
#[cfg(feature = "stats")]
|
||||||
!args.no_stats && matches!(stop, Some(StdinStop::Interrupted | StdinStop::Eof));
|
{
|
||||||
stream_output.print_final_summary = print_summary;
|
let print_summary =
|
||||||
stream_output.show_running_only_summary_note = print_summary;
|
!args.no_stats && matches!(stop, Some(StdinStop::Interrupted | StdinStop::Eof));
|
||||||
if let Some(summary_targets) = summary_targets {
|
stream_output.print_final_summary = print_summary;
|
||||||
let multi_target = summary_targets.len() > 1;
|
stream_output.show_running_only_summary_note = print_summary;
|
||||||
for target in summary_targets {
|
if let Some(summary_targets) = summary_targets {
|
||||||
let stats = stats
|
let multi_target = summary_targets.len() > 1;
|
||||||
.get(&target)
|
for target in summary_targets {
|
||||||
.expect("summary snapshot retains every selected target collector");
|
let stats = stats
|
||||||
if multi_target && stream_output.config.prints_summary() {
|
.get(&target)
|
||||||
writeln!(stream_output.out)?;
|
.expect("summary snapshot retains every selected target collector");
|
||||||
writeln!(
|
if multi_target && stream_output.config.prints_summary() {
|
||||||
stream_output.out,
|
writeln!(stream_output.out)?;
|
||||||
"target: {} (generation {})",
|
writeln!(
|
||||||
target.id, target.generation
|
stream_output.out,
|
||||||
)?;
|
"target: {} (generation {})",
|
||||||
|
target.id, target.generation
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
stream_output.print_summary(stats)?;
|
||||||
}
|
}
|
||||||
stream_output.print_summary(stats)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stream_output.out.flush()?;
|
stream_output.out.flush()?;
|
||||||
@@ -668,11 +718,12 @@ async fn run_stdin_stream(
|
|||||||
fn process_event<W: Write>(
|
fn process_event<W: Write>(
|
||||||
event: ManagedEvent,
|
event: ManagedEvent,
|
||||||
stream_output: &mut StreamOutput<'_, W>,
|
stream_output: &mut StreamOutput<'_, W>,
|
||||||
stats: &mut BTreeMap<String, StatsCollector>,
|
#[cfg(feature = "stats")] stats: &mut BTreeMap<String, StatsCollector>,
|
||||||
terminal_targets: &mut HashSet<TargetInstance>,
|
terminal_targets: &mut HashSet<TargetInstance>,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
match event {
|
match event {
|
||||||
ManagedEvent::Client { target, event } => {
|
ManagedEvent::Client { target, event } => {
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
let update = stream_output.collect_stats.then(|| {
|
let update = stream_output.collect_stats.then(|| {
|
||||||
let collector = stats
|
let collector = stats
|
||||||
.get_mut(target.id.as_str())
|
.get_mut(target.id.as_str())
|
||||||
@@ -683,7 +734,12 @@ fn process_event<W: Write>(
|
|||||||
}
|
}
|
||||||
collector.process(&event)
|
collector.process(&event)
|
||||||
});
|
});
|
||||||
stream_output.print_event(&event, Some(target.id.as_str()), update.as_ref())?;
|
stream_output.print_event(
|
||||||
|
&event,
|
||||||
|
Some(target.id.as_str()),
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
|
update.as_ref(),
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
ManagedEvent::TargetFinished { outcome } => {
|
ManagedEvent::TargetFinished { outcome } => {
|
||||||
terminal_targets.insert(outcome.target.clone());
|
terminal_targets.insert(outcome.target.clone());
|
||||||
@@ -717,15 +773,24 @@ fn target_failure_messages(target: &ManagedTargetOutcome) -> Vec<String> {
|
|||||||
messages
|
messages
|
||||||
}
|
}
|
||||||
fn dropped_event_warning(dropped_events: u64) -> Option<String> {
|
fn dropped_event_warning(dropped_events: u64) -> Option<String> {
|
||||||
(dropped_events > 0).then(|| format!("irtt-rs: warning: dropped {dropped_events} managed run event{}; output and statistics may be incomplete", if dropped_events == 1 { "" } else { "s" }))
|
let affected = if cfg!(feature = "stats") {
|
||||||
|
"output and statistics"
|
||||||
|
} else {
|
||||||
|
"output"
|
||||||
|
};
|
||||||
|
(dropped_events > 0).then(|| format!("irtt-rs: warning: dropped {dropped_events} managed run event{}; {affected} may be incomplete", if dropped_events == 1 { "" } else { "s" }))
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StreamOutput<'a, W: Write> {
|
struct StreamOutput<'a, W: Write> {
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
continuous: bool,
|
continuous: bool,
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
collect_stats: bool,
|
collect_stats: bool,
|
||||||
config: OutputConfig,
|
config: OutputConfig,
|
||||||
header_printed: bool,
|
header_printed: bool,
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
print_final_summary: bool,
|
print_final_summary: bool,
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
show_running_only_summary_note: bool,
|
show_running_only_summary_note: bool,
|
||||||
out: &'a mut W,
|
out: &'a mut W,
|
||||||
}
|
}
|
||||||
@@ -734,10 +799,15 @@ impl<W: Write> StreamOutput<'_, W> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
event: &ClientEvent,
|
event: &ClientEvent,
|
||||||
target: Option<&str>,
|
target: Option<&str>,
|
||||||
update: Option<&EventStatsUpdate>,
|
#[cfg(feature = "stats")] update: Option<&EventStatsUpdate>,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
self.print_header()?;
|
self.print_header()?;
|
||||||
if let Some(line) = self.config.render_event(event, target, update) {
|
if let Some(line) = self.config.render_event(
|
||||||
|
event,
|
||||||
|
target,
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
|
update,
|
||||||
|
) {
|
||||||
writeln!(self.out, "{line}")?;
|
writeln!(self.out, "{line}")?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -753,6 +823,7 @@ impl<W: Write> StreamOutput<'_, W> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
fn print_summary(&mut self, stats: &StatsCollector) -> io::Result<()> {
|
fn print_summary(&mut self, stats: &StatsCollector) -> io::Result<()> {
|
||||||
if self.print_final_summary && self.config.prints_summary() {
|
if self.print_final_summary && self.config.prints_summary() {
|
||||||
write!(
|
write!(
|
||||||
@@ -771,6 +842,7 @@ impl<W: Write> StreamOutput<'_, W> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
fn stats_config(continuous: bool) -> StatsConfig {
|
fn stats_config(continuous: bool) -> StatsConfig {
|
||||||
if continuous {
|
if continuous {
|
||||||
StatsConfig::continuous()
|
StatsConfig::continuous()
|
||||||
@@ -778,7 +850,17 @@ fn stats_config(continuous: bool) -> StatsConfig {
|
|||||||
StatsConfig::finite()
|
StatsConfig::finite()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
fn finite_stats_memory_warning(args: &ClientArgs, target_count: usize) -> Option<String> {
|
fn finite_stats_memory_warning(args: &ClientArgs, target_count: usize) -> Option<String> {
|
||||||
|
const MIB: u64 = 1024 * 1024;
|
||||||
|
|
||||||
|
const GIB: u64 = 1024 * MIB;
|
||||||
|
|
||||||
|
const FINITE_STATS_MEMORY_WARNING_BYTES: u64 = 128 * MIB;
|
||||||
|
|
||||||
|
const FINITE_STATS_MEMORY_STRONG_WARNING_BYTES: u64 = 512 * MIB;
|
||||||
|
|
||||||
|
const FINITE_STATS_MEMORY_VERY_STRONG_WARNING_BYTES: u64 = GIB;
|
||||||
if args.no_stats || args.no_test || args.is_continuous() || args.duration.is_zero() {
|
if args.no_stats || args.no_test || args.is_continuous() || args.duration.is_zero() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -807,7 +889,7 @@ fn finite_stats_memory_warning(args: &ClientArgs, target_count: usize) -> Option
|
|||||||
Some(format!("irtt-rs: warning: finite exact statistics may retain about {formatted} for this run; {guidance}"))
|
Some(format!("irtt-rs: warning: finite exact statistics may retain about {formatted} for this run; {guidance}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
fn reconnect_stats_warning(args: &ClientArgs) -> Option<&'static str> {
|
fn reconnect_stats_warning(args: &ClientArgs) -> Option<&'static str> {
|
||||||
(args.reconnect && !args.no_stats).then_some("irtt-rs: warning: statistics spanning multiple sessions may not accurately represent one uninterrupted measurement run; use --no-stats for measurement events only. Session statistics reset on reconnect.")
|
(args.reconnect && !args.no_stats).then_some("irtt-rs: warning: statistics spanning multiple sessions may not accurately represent one uninterrupted measurement run; use --no-stats for measurement events only. Session statistics reset on reconnect.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,12 +57,13 @@ pub(crate) fn format_ns_i128(value: i128) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Format an optional nanosecond scalar, rendering `None` as [`ABSENT`].
|
/// Format an optional nanosecond scalar, rendering `None` as [`ABSENT`].
|
||||||
|
#[cfg(any(all(feature = "client", feature = "stats"), feature = "tui"))]
|
||||||
pub(crate) fn format_optional_ns_i128(value: Option<i128>) -> String {
|
pub(crate) fn format_optional_ns_i128(value: Option<i128>) -> String {
|
||||||
value.map(format_ns_i128).unwrap_or_else(absent)
|
value.map(format_ns_i128).unwrap_or_else(absent)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format an optional nanosecond scalar, rendering `None` as [`ABSENT`].
|
/// Format an optional nanosecond scalar, rendering `None` as [`ABSENT`].
|
||||||
#[cfg(feature = "client")]
|
#[cfg(all(feature = "client", feature = "stats"))]
|
||||||
pub(crate) fn format_optional_ns_f64(value: Option<f64>) -> String {
|
pub(crate) fn format_optional_ns_f64(value: Option<f64>) -> String {
|
||||||
value.map(format_ns_f64).unwrap_or_else(absent)
|
value.map(format_ns_f64).unwrap_or_else(absent)
|
||||||
}
|
}
|
||||||
@@ -80,6 +81,7 @@ pub(crate) fn format_signed_duration(value: SignedDuration) -> String {
|
|||||||
|
|
||||||
/// Format a percentage with two decimals; non-finite values render as
|
/// Format a percentage with two decimals; non-finite values render as
|
||||||
/// [`ABSENT`].
|
/// [`ABSENT`].
|
||||||
|
#[cfg(any(all(feature = "client", feature = "stats"), feature = "tui"))]
|
||||||
pub(crate) fn format_percent(value: f64) -> String {
|
pub(crate) fn format_percent(value: f64) -> String {
|
||||||
if value.is_finite() {
|
if value.is_finite() {
|
||||||
format!("{value:.2}%")
|
format!("{value:.2}%")
|
||||||
@@ -112,6 +114,7 @@ pub(crate) fn format_optional_count(value: Option<u64>) -> String {
|
|||||||
value.map(format_count).unwrap_or_else(absent)
|
value.map(format_count).unwrap_or_else(absent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(all(feature = "client", feature = "stats"), feature = "tui"))]
|
||||||
fn absent() -> String {
|
fn absent() -> String {
|
||||||
ABSENT.to_owned()
|
ABSENT.to_owned()
|
||||||
}
|
}
|
||||||
@@ -22,10 +22,14 @@ pub(crate) fn parse<T: clap::Parser>(argv: Vec<OsString>, report: fn() -> String
|
|||||||
|
|
||||||
#[cfg(any(feature = "client", feature = "tui"))]
|
#[cfg(any(feature = "client", feature = "tui"))]
|
||||||
pub(crate) fn client() -> String {
|
pub(crate) fn client() -> String {
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
|
let stats = format!(" stats: {}\n", irtt_stats::VERSION);
|
||||||
|
#[cfg(not(feature = "stats"))]
|
||||||
|
let stats = " stats: disabled\n";
|
||||||
format!(
|
format!(
|
||||||
" client: {}\n stats: {}\n proto: {}\n",
|
" client: {}\n{} proto: {}\n",
|
||||||
irtt_client::VERSION,
|
irtt_client::VERSION,
|
||||||
irtt_stats::VERSION,
|
stats,
|
||||||
irtt_proto::VERSION,
|
irtt_proto::VERSION,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -222,7 +222,20 @@ memory a long run retains.
|
|||||||
|
|
||||||
### Optional recovery and event-only runs
|
### 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 does not remove the `irtt-stats` dependency.
|
`--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.
|
||||||
|
|
||||||
|
To omit statistics at compile time, build with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build --release -p irtt-rs --bin irtt-client --no-default-features --features client
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--features client,stats` to include statistics in a client-only build.
|
||||||
|
Default builds include statistics, and `tui` always enables them. Without
|
||||||
|
compiled statistics, `--no-stats` remains accepted, summaries and statistics
|
||||||
|
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 use exponential backoff from 250 ms up to 10 seconds.
|
`--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 use exponential backoff from 250 ms up to 10 seconds.
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -49,9 +49,12 @@ reports which feature to rebuild with, and `irtt-rs --help` lists which
|
|||||||
applets the current build actually contains.
|
applets the current build actually contains.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo install irtt-rs --locked --no-default-features --features client
|
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
|
`irtt-rs` is packaging and dispatch only. All probing, display, and server
|
||||||
|
|||||||
Reference in new issue
Block a user