refactor(cli): unify scalar presentation formatting

Stream output, the run summary, and the TUI each carried their own copy of
the same scalar vocabulary, and they had drifted: the TUI spelled
microseconds "us" while the others used "µs", percentages guarded
non-finite values in one place but not another, and the absent-value
placeholder was spelled inline everywhere.

cmd::format now owns nanosecond-scalar, percentage, and count formatting
with one documented unit-selection and rounding policy, µs everywhere, and
signed values preserved. The frontends keep their own metric sets and
layouts; the TUI keeps its coarse wall-clock span policy, now named
format_span and documented as deliberately distinct, and uses the shared
scalar vocabulary below one second.
This commit is contained in:
Nils Andreas Svee committed 2026-08-15 01:11:33 +07:00
1 parent 90c04b6569
commit 69b90dd64a
5 files changed
+257 -148

No files matched your search

+2 -29
View File
@@ -10,6 +10,7 @@ use irtt_client::{
}; };
use super::args::{HeaderMode, OutputFormat}; use super::args::{HeaderMode, OutputFormat};
use crate::cmd::format::{format_duration, format_signed_duration, ABSENT};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(super) struct OutputConfig { pub(super) struct OutputConfig {
@@ -817,7 +818,7 @@ fn render_table_row(row: &OutputRow, columns: &[Column], context: RenderContext<
} }
fn format_table_cell(column: &Column, value: Option<String>) -> String { fn format_table_cell(column: &Column, value: Option<String>) -> String {
let value = value.unwrap_or_else(|| "-".to_owned()); let value = value.unwrap_or_else(|| ABSENT.to_owned());
let width = column.table_width(); let width = column.table_width();
if column.align_right() { if column.align_right() {
format!("{value:>width$}") format!("{value:>width$}")
@@ -1161,34 +1162,6 @@ fn wall_time_ns(wall: SystemTime) -> Option<u128> {
.map(|duration| duration.as_nanos()) .map(|duration| duration.as_nanos())
} }
fn format_duration(duration: Duration) -> String {
format_ns(duration.as_nanos() as f64)
}
fn format_signed_duration(duration: SignedDuration) -> String {
format_signed_ns(duration.as_nanos() as f64)
}
fn format_signed_ns(ns: f64) -> String {
if ns < 0.0 {
format!("-{}", format_ns(-ns))
} else {
format_ns(ns)
}
}
fn format_ns(ns: f64) -> String {
if ns < 1_000.0 {
format!("{ns:.0}ns")
} else if ns < 1_000_000.0 {
format!("{:.1}µs", ns / 1_000.0)
} else if ns < 1_000_000_000.0 {
format!("{:.1}ms", ns / 1_000_000.0)
} else {
format!("{:.3}s", ns / 1_000_000_000.0)
}
}
fn warning_kind(kind: WarningKind) -> &'static str { fn warning_kind(kind: WarningKind) -> &'static str {
match kind { match kind {
WarningKind::MalformedOrUnrelatedPacket => "malformed_or_unrelated_packet", WarningKind::MalformedOrUnrelatedPacket => "malformed_or_unrelated_packet",
+5 -28
View File
@@ -2,6 +2,11 @@ use std::fmt::Write as _;
use irtt_stats::{Snapshot, TimeStats}; use irtt_stats::{Snapshot, TimeStats};
use crate::cmd::format::{
format_ns_f64, format_optional_ns_f64 as format_ns_f64_opt,
format_optional_ns_i128 as format_ns_i128, format_percent,
};
pub fn format_summary(summary: &Snapshot) -> String { pub fn format_summary(summary: &Snapshot) -> String {
format_summary_with_options(summary, SummaryFormatOptions::default()) format_summary_with_options(summary, SummaryFormatOptions::default())
} }
@@ -117,34 +122,6 @@ fn write_time_row(out: &mut String, label: &str, value: &TimeStats) {
.unwrap(); .unwrap();
} }
fn format_percent(value: f64) -> String {
format!("{value:.2}%")
}
fn format_ns_i128(value: Option<i128>) -> String {
value
.map(|value| format_ns_f64(value as f64))
.unwrap_or_else(|| "-".to_owned())
}
fn format_ns_f64_opt(value: Option<f64>) -> String {
value.map(format_ns_f64).unwrap_or_else(|| "-".to_owned())
}
fn format_ns_f64(value: f64) -> String {
let sign = if value.is_sign_negative() { "-" } else { "" };
let value = value.abs();
if value < 1_000.0 {
format!("{sign}{value:.0}ns")
} else if value < 1_000_000.0 {
format!("{sign}{:.1}µs", value / 1_000.0)
} else if value < 1_000_000_000.0 {
format!("{sign}{:.1}ms", value / 1_000_000.0)
} else {
format!("{sign}{:.3}s", value / 1_000_000_000.0)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+181
View File
@@ -0,0 +1,181 @@
//! Scalar presentation vocabulary shared by the CLI frontends.
//!
//! Stream output, the run summary, and the TUI display different metrics in
//! different layouts, but they render the same *scalars*: nanosecond-scale
//! durations, percentages, and counts. This module owns those spellings so the
//! frontends cannot drift apart on units, rounding, or how an absent value is
//! rendered.
//!
//! It is presentation only. Nothing here belongs in `irtt-stats`, and it is
//! deliberately not a general formatting framework: it holds exactly the
//! vocabulary more than one frontend needs.
use std::time::Duration;
use irtt_client::SignedDuration;
/// Rendering used wherever a scalar is unavailable.
pub(crate) const ABSENT: &str = "-";
/// Format a nanosecond scalar.
///
/// Unit selection and rounding are one policy for every frontend:
///
/// | magnitude | unit | precision |
/// |-----------|------|-----------|
/// | `< 1µs` | `ns` | whole |
/// | `< 1ms` | `µs` | 1 decimal |
/// | `< 1s` | `ms` | 1 decimal |
/// | `>= 1s` | `s` | 3 decimals |
///
/// Negative values are meaningful (clock offsets, one-way deltas, IPDV), so
/// the unit is selected from the magnitude and the sign is preserved. Values
/// that are not finite render as [`ABSENT`].
pub(crate) fn format_ns_f64(value: f64) -> String {
if !value.is_finite() {
return ABSENT.to_owned();
}
let sign = if value.is_sign_negative() { "-" } else { "" };
let magnitude = value.abs();
if magnitude < 1_000.0 {
format!("{sign}{magnitude:.0}ns")
} else if magnitude < 1_000_000.0 {
format!("{sign}{:.1}µs", magnitude / 1_000.0)
} else if magnitude < 1_000_000_000.0 {
format!("{sign}{:.1}ms", magnitude / 1_000_000.0)
} else {
format!("{sign}{:.3}s", magnitude / 1_000_000_000.0)
}
}
/// Format an exact nanosecond scalar with [`format_ns_f64`]'s policy.
pub(crate) fn format_ns_i128(value: i128) -> String {
let sign = if value < 0 { "-" } else { "" };
let magnitude = value.saturating_abs() as f64;
format!("{sign}{}", format_ns_f64(magnitude))
}
/// Format an optional nanosecond scalar, rendering `None` as [`ABSENT`].
pub(crate) fn format_optional_ns_i128(value: Option<i128>) -> String {
value.map(format_ns_i128).unwrap_or_else(absent)
}
/// Format an optional nanosecond scalar, rendering `None` as [`ABSENT`].
pub(crate) fn format_optional_ns_f64(value: Option<f64>) -> String {
value.map(format_ns_f64).unwrap_or_else(absent)
}
/// Format a duration as a nanosecond scalar.
pub(crate) fn format_duration(value: Duration) -> String {
format_ns_i128(i128::try_from(value.as_nanos()).unwrap_or(i128::MAX))
}
/// Format a signed duration as a nanosecond scalar, preserving its sign.
pub(crate) fn format_signed_duration(value: SignedDuration) -> String {
format_ns_i128(value.as_nanos())
}
/// Format a percentage with two decimals; non-finite values render as
/// [`ABSENT`].
pub(crate) fn format_percent(value: f64) -> String {
if value.is_finite() {
format!("{value:.2}%")
} else {
ABSENT.to_owned()
}
}
/// Format `value` as a percentage of `total`, capped at 100%.
///
/// A zero total has no percentage to report and renders as [`ABSENT`].
pub(crate) fn format_percent_ratio(value: u64, total: u64) -> String {
if total == 0 {
ABSENT.to_owned()
} else {
format_percent((value as f64 / total as f64 * 100.0).min(100.0))
}
}
/// Format a scalar count.
pub(crate) fn format_count(value: u64) -> String {
value.to_string()
}
/// Format an optional scalar count, rendering `None` as [`ABSENT`].
pub(crate) fn format_optional_count(value: Option<u64>) -> String {
value.map(format_count).unwrap_or_else(absent)
}
fn absent() -> String {
ABSENT.to_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nanosecond_scalars_select_units_at_the_defined_boundaries() {
assert_eq!(format_ns_f64(0.0), "0ns");
assert_eq!(format_ns_f64(999.0), "999ns");
assert_eq!(format_ns_f64(1_000.0), "1.0µs");
assert_eq!(format_ns_f64(999_999.0), "1000.0µs");
assert_eq!(format_ns_f64(1_000_000.0), "1.0ms");
assert_eq!(format_ns_f64(999_999_999.0), "1000.0ms");
assert_eq!(format_ns_f64(1_000_000_000.0), "1.000s");
assert_eq!(format_ns_f64(1_500_000_000.0), "1.500s");
}
#[test]
fn negative_nanosecond_scalars_keep_their_sign() {
assert_eq!(format_ns_f64(-500.0), "-500ns");
assert_eq!(format_ns_f64(-1_500.0), "-1.5µs");
assert_eq!(format_ns_f64(-2_500_000.0), "-2.5ms");
assert_eq!(format_ns_f64(-1_000_000_000.0), "-1.000s");
assert_eq!(format_ns_i128(-1_500), "-1.5µs");
}
#[test]
fn exact_and_floating_nanosecond_scalars_agree() {
for value in [0_i128, 1, 999, 1_000, 1_234_567, -1_234_567, 5_000_000_000] {
assert_eq!(format_ns_i128(value), format_ns_f64(value as f64));
}
}
#[test]
fn microseconds_use_the_micro_sign() {
assert!(format_ns_f64(1_500.0).ends_with("µs"));
assert!(!format_ns_f64(1_500.0).contains("us"));
}
#[test]
fn durations_render_as_nanosecond_scalars() {
assert_eq!(format_duration(Duration::from_millis(25)), "25.0ms");
assert_eq!(format_duration(Duration::from_secs(1)), "1.000s");
assert_eq!(
format_signed_duration(SignedDuration::from_nanos(-1_500)),
"-1.5µs"
);
}
#[test]
fn absent_and_non_finite_scalars_render_as_a_placeholder() {
assert_eq!(format_optional_ns_i128(None), ABSENT);
assert_eq!(format_optional_ns_f64(None), ABSENT);
assert_eq!(format_optional_count(None), ABSENT);
assert_eq!(format_ns_f64(f64::NAN), ABSENT);
assert_eq!(format_ns_f64(f64::INFINITY), ABSENT);
assert_eq!(format_percent(f64::NAN), ABSENT);
assert_eq!(format_percent_ratio(1, 0), ABSENT);
}
#[test]
fn percentages_and_counts_have_one_spelling() {
assert_eq!(format_percent(0.0), "0.00%");
assert_eq!(format_percent(12.345), "12.35%");
assert_eq!(format_percent_ratio(1, 4), "25.00%");
assert_eq!(format_percent_ratio(5, 4), "100.00%");
assert_eq!(format_count(42), "42");
assert_eq!(format_optional_count(Some(0)), "0");
}
}
+2
View File
@@ -1,5 +1,7 @@
#[cfg(feature = "client")] #[cfg(feature = "client")]
pub mod client; pub mod client;
#[cfg(any(feature = "client", feature = "tui"))]
mod format;
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub mod server; pub mod server;
#[cfg(feature = "tui")] #[cfg(feature = "tui")]
+67 -91
View File
@@ -23,7 +23,13 @@ use ratatui::{
}; };
use crate::{ use crate::{
cmd::tui::args::TuiArgs, cmd::{
format::{
format_count, format_duration, format_ns_f64, format_optional_count,
format_optional_ns_i128, format_percent, format_percent_ratio, ABSENT,
},
tui::args::TuiArgs,
},
shared::client::{expected_probe_count, GroupPacingArg}, shared::client::{expected_probe_count, GroupPacingArg},
}; };
@@ -330,7 +336,7 @@ impl TuiState {
recent = Some(format!( recent = Some(format!(
"reply seq={} effective={}", "reply seq={} effective={}",
format_seq(*seq), format_seq(*seq),
format_ns_i128(Some(rtt.effective.as_nanos())) format_optional_ns_i128(Some(rtt.effective.as_nanos()))
)); ));
} }
ClientEvent::EchoLoss { seq, .. } => { ClientEvent::EchoLoss { seq, .. } => {
@@ -349,7 +355,7 @@ impl TuiState {
.map(|sample| { .map(|sample| {
format!( format!(
" effective={}", " effective={}",
format_ns_i128(Some(sample.effective.as_nanos())) format_optional_ns_i128(Some(sample.effective.as_nanos()))
) )
}) })
.unwrap_or_default(); .unwrap_or_default();
@@ -1149,8 +1155,8 @@ fn header(state: &TuiState, density: HeaderDensity) -> Paragraph<'_> {
let session = selected let session = selected
.and_then(|target| target.session.as_deref()) .and_then(|target| target.session.as_deref())
.unwrap_or("-"); .unwrap_or("-");
let elapsed = format_duration(state.started_at.elapsed()); let elapsed = format_span(state.started_at.elapsed());
let duration = format_optional_duration(state.config.duration); let duration = format_optional_span(state.config.duration);
let mode = if state.config.duration.is_some() { let mode = if state.config.duration.is_some() {
"finite" "finite"
} else { } else {
@@ -1186,8 +1192,8 @@ fn header(state: &TuiState, density: HeaderDensity) -> Paragraph<'_> {
)), )),
Line::from(format!( Line::from(format!(
"interval: {} timeout: {}", "interval: {} timeout: {}",
format_duration(state.config.interval), format_span(state.config.interval),
format_duration(state.config.timeout) format_span(state.config.timeout)
)), )),
Line::from(negotiated), Line::from(negotiated),
]); ]);
@@ -1197,7 +1203,7 @@ fn header(state: &TuiState, density: HeaderDensity) -> Paragraph<'_> {
Line::from(format!("session: {session}")), Line::from(format!("session: {session}")),
Line::from(format!( Line::from(format!(
"{mode} elapsed: {elapsed} interval: {}", "{mode} elapsed: {elapsed} interval: {}",
format_duration(state.config.interval) format_span(state.config.interval)
)), )),
]); ]);
} }
@@ -1216,12 +1222,12 @@ fn graph_summary_panel(state: &TuiState, snapshot: &Snapshot) -> Paragraph<'stat
let remote = selected let remote = selected
.and_then(|target| target.remote.as_deref()) .and_then(|target| target.remote.as_deref())
.unwrap_or("-"); .unwrap_or("-");
let elapsed = format_duration(state.started_at.elapsed()); let elapsed = format_span(state.started_at.elapsed());
let packets = snapshot.packets; let packets = snapshot.packets;
let last = state let last = state
.selected_target() .selected_target()
.and_then(|target| target.last_sample) .and_then(|target| target.last_sample)
.map(|sample| format_ns_i128(Some(sample.effective_ns))) .map(|sample| format_optional_ns_i128(Some(sample.effective_ns)))
.unwrap_or_else(|| "-".to_owned()); .unwrap_or_else(|| "-".to_owned());
let target_context = if state.is_multi_target() { let target_context = if state.is_multi_target() {
@@ -1295,7 +1301,7 @@ fn packet_panel(state: &TuiState, snapshot: &Snapshot, panel_height: u16) -> Par
)), )),
Line::from(format!( Line::from(format!(
"server received {} window {}", "server received {} window {}",
format_optional_u64(packets.server_packets_received), format_optional_count(packets.server_packets_received),
format_optional_hex(packets.server_received_window) format_optional_hex(packets.server_received_window)
)), )),
]) ])
@@ -1314,7 +1320,7 @@ fn target_table_panel(state: &TuiState, panel_height: u16) -> Paragraph<'static>
let snapshot = target.stats.snapshot(); let snapshot = target.stats.snapshot();
let last = target let last = target
.last_sample .last_sample
.map(|sample| format_ns_i128(Some(sample.effective_ns))) .map(|sample| format_optional_ns_i128(Some(sample.effective_ns)))
.unwrap_or_else(|| "-".to_owned()); .unwrap_or_else(|| "-".to_owned());
lines.push(Line::from(vec![ lines.push(Line::from(vec![
target_label_span(&target.label, idx, 16), target_label_span(&target.label, idx, 16),
@@ -1695,7 +1701,7 @@ fn viewport_x_bounds(viewport: GraphViewportRange) -> [f64; 2] {
fn viewport_x_axis_labels(viewport: GraphViewportRange) -> Vec<Span<'static>> { fn viewport_x_axis_labels(viewport: GraphViewportRange) -> Vec<Span<'static>> {
vec![ vec![
Span::raw(format!("-{}", format_duration(viewport.window))), Span::raw(format!("-{}", format_span(viewport.window))),
Span::raw(if viewport.is_live { "live" } else { "end" }), Span::raw(if viewport.is_live { "live" } else { "end" }),
] ]
} }
@@ -1704,7 +1710,7 @@ fn graph_context(state: &TuiState, viewport: GraphViewportRange) -> String {
format!( format!(
"{} | window {}", "{} | window {}",
graph_viewport_status(state), graph_viewport_status(state),
format_duration(viewport.window) format_span(viewport.window)
) )
} }
@@ -1744,9 +1750,9 @@ fn format_axis_time_ms(value_ms: f64) -> String {
if abs_ms < 1.0 { if abs_ms < 1.0 {
let us = abs_ms * 1_000.0; let us = abs_ms * 1_000.0;
if us < 10.0 { if us < 10.0 {
format!("{sign}{us:.1}us") format!("{sign}{us:.1}µs")
} else { } else {
format!("{sign}{us:.0}us") format!("{sign}{us:.0}µs")
} }
} else if abs_ms < 1_000.0 { } else if abs_ms < 1_000.0 {
if abs_ms < 10.0 { if abs_ms < 10.0 {
@@ -1804,23 +1810,23 @@ fn sample_panel(state: &TuiState, snapshot: &Snapshot) -> Paragraph<'static> {
)), )),
Line::from(format!( Line::from(format!(
"raw / adjusted / effective: {} / {} / {}", "raw / adjusted / effective: {} / {} / {}",
last.map(|sample| format_ns_i128(Some(sample.raw_ns))) last.map(|sample| format_optional_ns_i128(Some(sample.raw_ns)))
.unwrap_or_else(|| "-".to_owned()), .unwrap_or_else(|| "-".to_owned()),
last.map(|sample| format_ns_i128(sample.adjusted_ns)) last.map(|sample| format_optional_ns_i128(sample.adjusted_ns))
.unwrap_or_else(|| "-".to_owned()), .unwrap_or_else(|| "-".to_owned()),
last.map(|sample| format_ns_i128(Some(sample.effective_ns))) last.map(|sample| format_optional_ns_i128(Some(sample.effective_ns)))
.unwrap_or_else(|| "-".to_owned()) .unwrap_or_else(|| "-".to_owned())
)), )),
Line::from(format!( Line::from(format!(
"one-way c2s / s2c: {} / {}", "one-way c2s / s2c: {} / {}",
last.map(|sample| format_ns_i128(sample.client_to_server_ns)) last.map(|sample| format_optional_ns_i128(sample.client_to_server_ns))
.unwrap_or_else(|| "-".to_owned()), .unwrap_or_else(|| "-".to_owned()),
last.map(|sample| format_ns_i128(sample.server_to_client_ns)) last.map(|sample| format_optional_ns_i128(sample.server_to_client_ns))
.unwrap_or_else(|| "-".to_owned()) .unwrap_or_else(|| "-".to_owned())
)), )),
Line::from(format!( Line::from(format!(
"server processing: {}", "server processing: {}",
last.map(|sample| format_ns_i128(sample.server_processing_ns)) last.map(|sample| format_optional_ns_i128(sample.server_processing_ns))
.unwrap_or_else(|| "-".to_owned()) .unwrap_or_else(|| "-".to_owned())
)), )),
Line::from(format!( Line::from(format!(
@@ -1888,7 +1894,7 @@ fn graph_viewport_status(state: &TuiState) -> String {
let live_end = state.newest_graph_sample_time().unwrap_or(now).max(now); let live_end = state.newest_graph_sample_time().unwrap_or(now).max(now);
format!( format!(
"history -{}", "history -{}",
format_duration(live_end.saturating_duration_since(end)) format_span(live_end.saturating_duration_since(end))
) )
} }
} }
@@ -1906,9 +1912,9 @@ fn push_time_line(lines: &mut Vec<Line<'_>>, label: &str, stats: &TimeStats) {
lines.push(Line::from(format!( lines.push(Line::from(format!(
"{label:<18} {:>5} {:>9} {:>9} {:>9} {:>9}", "{label:<18} {:>5} {:>9} {:>9} {:>9} {:>9}",
format_count(stats.count), format_count(stats.count),
format_ns_i128(stats.min_ns), format_optional_ns_i128(stats.min_ns),
format_ns_f64(stats.mean_ns), format_ns_f64(stats.mean_ns),
format_ns_i128(stats.max_ns), format_optional_ns_i128(stats.max_ns),
format_ns_f64(stats.stddev_ns()) format_ns_f64(stats.stddev_ns())
))); )));
} }
@@ -1918,7 +1924,7 @@ fn format_negotiated(negotiated: &NegotiatedParams) -> String {
let duration = if params.duration_ns == 0 { let duration = if params.duration_ns == 0 {
"-".to_owned() "-".to_owned()
} else { } else {
format_ns_i128(Some(i128::from(params.duration_ns))) format_optional_ns_i128(Some(i128::from(params.duration_ns)))
}; };
let restrictions = if negotiated.restrictions.is_empty() { let restrictions = if negotiated.restrictions.is_empty() {
"none".to_owned() "none".to_owned()
@@ -1928,7 +1934,7 @@ fn format_negotiated(negotiated: &NegotiatedParams) -> String {
format!( format!(
"duration={} interval={} length={} clock={:?} timestamps={:?} stats={:?} restrictions={}", "duration={} interval={} length={} clock={:?} timestamps={:?} stats={:?} restrictions={}",
duration, duration,
format_ns_i128(Some(i128::from(params.interval_ns))), format_optional_ns_i128(Some(i128::from(params.interval_ns))),
params.length, params.length,
params.clock, params.clock,
params.stamp_at, params.stamp_at,
@@ -1937,80 +1943,43 @@ fn format_negotiated(negotiated: &NegotiatedParams) -> String {
) )
} }
fn format_optional_duration(value: Option<Duration>) -> String { fn format_optional_span(value: Option<Duration>) -> String {
value.map(format_duration).unwrap_or_else(|| "-".to_owned()) value.map(format_span).unwrap_or_else(|| ABSENT.to_owned())
} }
fn format_duration(value: Duration) -> String { /// Format a wall-clock span shown in the TUI's headers, config lines, and
/// graph window labels.
///
/// Spans are read at a glance rather than compared numerically, so they are
/// deliberately coarser than the shared scalar policy: a zero span is `0s`,
/// whole seconds carry one decimal, and a span of a minute or more is written
/// as minutes and seconds. Below one second there is nothing to gain from a
/// second spelling, so the shared scalar vocabulary is used directly.
fn format_span(value: Duration) -> String {
if value.is_zero() { if value.is_zero() {
return "0s".to_owned(); return "0s".to_owned();
} }
let nanos = value.as_nanos(); let nanos = value.as_nanos();
if nanos < 1_000 { if nanos < 1_000_000_000 {
format!("{nanos}ns") return format_duration(value);
} else if nanos < 1_000_000 { }
format!("{:.1}us", nanos as f64 / 1_000.0) let secs = value.as_secs();
} else if nanos < 1_000_000_000 { if nanos < 60_000_000_000 {
format!("{:.1}ms", nanos as f64 / 1_000_000.0)
} else if nanos < 60_000_000_000 {
format!("{:.1}s", nanos as f64 / 1_000_000_000.0) format!("{:.1}s", nanos as f64 / 1_000_000_000.0)
} else { } else {
let secs = value.as_secs();
format!("{}m{:02}s", secs / 60, secs % 60) format!("{}m{:02}s", secs / 60, secs % 60)
} }
} }
fn format_ns_i128(value: Option<i128>) -> String {
value.map(format_ns_value).unwrap_or_else(|| "-".to_owned())
}
fn format_ns_f64(value: f64) -> String {
format_ns_value(value.round() as i128)
}
fn format_ns_value(value: i128) -> String {
let sign = if value < 0 { "-" } else { "" };
let value = value.saturating_abs() as f64;
if value < 1_000.0 {
format!("{sign}{value:.0}ns")
} else if value < 1_000_000.0 {
format!("{sign}{:.1}us", value / 1_000.0)
} else if value < 1_000_000_000.0 {
format!("{sign}{:.1}ms", value / 1_000_000.0)
} else {
format!("{sign}{:.3}s", value / 1_000_000_000.0)
}
}
fn format_percent(value: f64) -> String {
if value.is_finite() {
format!("{value:.2}%")
} else {
"-".to_owned()
}
}
fn format_percent_ratio(value: u64, total: u64) -> String {
if total == 0 {
"-".to_owned()
} else {
format_percent((value as f64 / total as f64 * 100.0).min(100.0))
}
}
fn format_rate(count: u64, elapsed: Duration) -> String { fn format_rate(count: u64, elapsed: Duration) -> String {
let secs = elapsed.as_secs_f64(); let secs = elapsed.as_secs_f64();
if secs <= f64::EPSILON { if secs <= f64::EPSILON {
"-".to_owned() ABSENT.to_owned()
} else { } else {
format!("{:.2}/s", count as f64 / secs) format!("{:.2}/s", count as f64 / secs)
} }
} }
fn format_count(value: u64) -> String {
value.to_string()
}
fn truncate(value: &str, max_chars: usize) -> String { fn truncate(value: &str, max_chars: usize) -> String {
if value.chars().count() <= max_chars { if value.chars().count() <= max_chars {
return value.to_owned(); return value.to_owned();
@@ -2026,14 +1995,10 @@ fn format_seq(value: u32) -> String {
value.to_string() value.to_string()
} }
fn format_optional_u64(value: Option<u64>) -> String {
value.map(format_count).unwrap_or_else(|| "-".to_owned())
}
fn format_optional_hex(value: Option<u64>) -> String { fn format_optional_hex(value: Option<u64>) -> String {
value value
.map(|value| format!("0x{value:x}")) .map(|value| format!("0x{value:x}"))
.unwrap_or_else(|| "-".to_owned()) .unwrap_or_else(|| ABSENT.to_owned())
} }
fn duration_ns(value: Duration) -> i128 { fn duration_ns(value: Duration) -> i128 {
@@ -2177,12 +2142,23 @@ mod tests {
} }
} }
#[test]
fn spans_stay_coarse_above_a_second_and_share_scalar_units_below_it() {
assert_eq!(format_span(Duration::ZERO), "0s");
assert_eq!(format_span(Duration::from_nanos(750)), "750ns");
assert_eq!(format_span(Duration::from_micros(500)), "500.0µs");
assert_eq!(format_span(Duration::from_millis(25)), "25.0ms");
assert_eq!(format_span(Duration::from_millis(1_500)), "1.5s");
assert_eq!(format_span(Duration::from_secs(90)), "1m30s");
}
#[test] #[test]
fn formats_signed_durations_and_missing_values() { fn formats_signed_durations_and_missing_values() {
assert_eq!(format_ns_i128(Some(-1_500_000)), "-1.5ms"); assert_eq!(format_optional_ns_i128(Some(-1_500_000)), "-1.5ms");
assert_eq!(format_ns_i128(Some(750)), "750ns"); assert_eq!(format_optional_ns_i128(Some(750)), "750ns");
assert_eq!(format_ns_i128(None), "-"); assert_eq!(format_optional_ns_i128(None), "-");
assert_eq!(format_optional_duration(None), "-"); assert_eq!(format_optional_ns_i128(Some(1_500)), "1.5µs");
assert_eq!(format_optional_span(None), "-");
assert_eq!(format_duration(Duration::from_millis(25)), "25.0ms"); assert_eq!(format_duration(Duration::from_millis(25)), "25.0ms");
assert_eq!(format_percent_ratio(1, 4), "25.00%"); assert_eq!(format_percent_ratio(1, 4), "25.00%");
assert_eq!(format_optional_hex(Some(0x1f)), "0x1f"); assert_eq!(format_optional_hex(Some(0x1f)), "0x1f");
@@ -2748,7 +2724,7 @@ mod tests {
assert_eq!( assert_eq!(
rendered, rendered,
vec!["-1.00ms", "-500us", "0.0us", "500us", "1.00ms"] vec!["-1.00ms", "-500µs", "0.0µs", "500µs", "1.00ms"]
); );
} }