Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: better display for ping stats #81

Merged
merged 9 commits into from
Dec 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 63 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dirs = "5.0.1"
futures = "0.3.31"
http = "1.2.0"
humantime = "2.1.0"
indicatif = "0.17.9"
miette = { version = "7.2.0", features = ["fancy"] }
rand = "0.8.5"
serde = { version = "1.0.214", features = ["derive"] }
Expand Down
146 changes: 110 additions & 36 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use clap::{builder::styling, Parser, Subcommand};
use colored::*;
use config::{config_path, create_config};
use error::{S2CliError, ServiceError, ServiceErrorContext};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use ping::{LatencyStats, PingResult, Pinger};
use rand::Rng;
use stream::{RecordStream, StreamService};
Expand Down Expand Up @@ -894,17 +895,70 @@ async fn run() -> Result<(), S2CliError> {
let interval = interval.max(Duration::from_millis(100));
let batch_bytes = batch_bytes.min(128 * 1024);

eprintln!("Preparing...");
let prepare_loader = ProgressBar::new_spinner()
.with_prefix("Preparing...")
.with_style(
ProgressStyle::default_spinner()
.template("{spinner} {prefix}")
.expect("valid template"),
);
prepare_loader.enable_steady_tick(Duration::from_millis(50));

let mut pinger = Pinger::init(&stream_client).await?;

prepare_loader.finish_and_clear();

let mut pings = Vec::new();

let stat_bars = MultiProgress::new();

let bytes_bar = ProgressBar::no_length().with_prefix("bytes").with_style(
ProgressStyle::default_bar()
.template("{pos:.bold} {prefix:.bold}")
.expect("valid template"),
);

let mut max_ack = 500;
let ack_bar = ProgressBar::new(max_ack).with_prefix("ack").with_style(
ProgressStyle::default_bar()
.template("{prefix:.bold} [{bar:40.blue/blue}] {pos:>4}/{len:<4} ms")
.expect("valid template"),
);

let mut max_e2e = 500;
let e2e_bar = ProgressBar::new(max_e2e).with_prefix("e2e").with_style(
ProgressStyle::default_bar()
.template("{prefix:.bold} [{bar:40.red/red}] {pos:>4}/{len:<4} ms")
.expect("valid template"),
);

// HACK: This bar basically has no purpose. It's just to clear all
// other bars since the very first bar in the set doesn't clear when
// `^C` signal is received.
let empty_line_bar = {
let bar = stat_bars.add(
ProgressBar::no_length().with_style(
ProgressStyle::default_bar()
.template("\n")
.expect("valid template"),
),
);
// Force render the bar.
bar.inc(1);
bar
};
let bytes_bar = stat_bars.add(bytes_bar);
let ack_bar = stat_bars.add(ack_bar);
let e2e_bar = stat_bars.add(e2e_bar);

async fn ping_next(
pinger: &mut Pinger,
pings: &mut Vec<PingResult>,
interval: Duration,
batch_bytes: u64,
bytes_bar: &ProgressBar,
ack_meter: (&ProgressBar, /* max_ack */ &mut u64),
e2e_meter: (&ProgressBar, /* max_e2e */ &mut u64),
) -> Result<(), S2CliError> {
let jitter_op = if rand::random() {
u64::saturating_add
Expand All @@ -921,12 +975,21 @@ async fn run() -> Result<(), S2CliError> {
return Ok(());
};

eprintln!(
"{:<5} bytes: ack = {:<7} e2e = {:<7}",
res.bytes.to_string().blue(),
format!("{} ms", res.ack.as_millis()).blue(),
format!("{} ms", res.e2e.as_millis()).blue(),
);
bytes_bar.set_position(record_bytes);

let (ack_bar, max_ack) = ack_meter;

let ack = res.ack.as_millis() as u64;
*max_ack = std::cmp::max(*max_ack, ack);
ack_bar.set_length(*max_ack);
ack_bar.set_position(ack);

let (e2e_bar, max_e2e) = e2e_meter;

let e2e = res.e2e.as_millis() as u64;
*max_e2e = std::cmp::max(*max_e2e, e2e);
e2e_bar.set_length(*max_e2e);
e2e_bar.set_position(e2e);

pings.push(res);

Expand All @@ -936,55 +999,66 @@ async fn run() -> Result<(), S2CliError> {

while Some(pings.len()) != num_batches {
select! {
_ = ping_next(&mut pinger, &mut pings, interval, batch_bytes) => (),
res = ping_next(
&mut pinger,
&mut pings,
interval,
batch_bytes,
&bytes_bar,
(&ack_bar, &mut max_ack),
(&e2e_bar, &mut max_e2e),
) => res?,
_ = signal::ctrl_c() => break,
}
}

// Close the pinger.
std::mem::drop(pinger);

bytes_bar.finish_and_clear();
ack_bar.finish_and_clear();
e2e_bar.finish_and_clear();
empty_line_bar.finish_and_clear();

let total_batches = pings.len();
let (bytes, (acks, e2es)): (Vec<_>, (Vec<_>, Vec<_>)) = pings
.into_iter()
.map(|PingResult { bytes, ack, e2e }| (bytes, (ack, e2e)))
.unzip();
let total_bytes = bytes.into_iter().sum::<u64>();

eprintln!(/* Empty line */);
eprintln!("Round-tripped {total_bytes} bytes in {total_batches} batches");

pub fn print_stats(stats: LatencyStats, name: &str) {
eprintln!(
"{:-^60}",
format!(" {name} Latency Statistics ").yellow().bold()
);

fn stat(key: &str, val: String) {
eprintln!("{:>9} {}", key, val.green());
eprintln!("{}", format!("{name} Latency Statistics ").yellow().bold());

fn stat_duration(key: &str, val: Duration, scale: f64) {
let bar = "⠸".repeat((val.as_millis() as f64 * scale).round() as usize);
eprintln!(
"{:7}: {:>7} │ {}",
key,
format!("{} ms", val.as_millis()).green().bold(),
bar
)
}

fn stat_duration(key: &str, val: Duration) {
stat(key, format!("{} ms", val.as_millis()));
}
let stats = stats.into_vec();
let max_val = stats
.iter()
.map(|(_, val)| val)
.max()
.unwrap_or(&Duration::ZERO);

let LatencyStats {
mean,
median,
p95,
p99,
max,
min,
stddev,
} = stats;

stat_duration("Mean", mean);
stat_duration("Median", median);
stat_duration("P95", p95);
stat_duration("P99", p99);
stat_duration("Max", max);
stat_duration("Min", min);
stat_duration("Std Dev", stddev);
let max_bar_len = 50;
let scale = if max_val.as_millis() > max_bar_len {
max_bar_len as f64 / max_val.as_millis() as f64
} else {
1.0
};

for (name, val) in stats {
stat_duration(&name, val, scale);
}
}

eprintln!(/* Empty line */);
Expand Down
Loading
Loading