diff --git a/Cargo.lock b/Cargo.lock index 22478f0..22e798f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -302,6 +302,12 @@ dependencies = [ "fslock", ] +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + [[package]] name = "byteorder" version = "1.5.0" @@ -368,6 +374,7 @@ dependencies = [ "humansize", "hyper", "hyper-util", + "indicatif", "pin-project-lite", "rand", "rustls", @@ -475,6 +482,18 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "console" +version = "0.16.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96a4956774c13c126a8b5af4daa79384f4d826534c95a02d76afb39e2ab64e3" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width", + "windows-sys 0.61.2", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -683,6 +702,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "equivalent" version = "1.0.1" @@ -1089,6 +1114,19 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indicatif" +version = "0.18.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" +dependencies = [ + "console", + "portable-atomic", + "unicode-width", + "unit-prefix", + "web-time", +] + [[package]] name = "inout" version = "0.1.4" @@ -1146,6 +1184,17 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1479,9 +1528,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "powerfmt" @@ -1752,6 +1801,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + [[package]] name = "ryu" version = "1.0.18" @@ -2296,6 +2351,18 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + [[package]] name = "universal-hash" version = "0.5.1" @@ -2384,6 +2451,61 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.5", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webrtc" version = "0.12.0" diff --git a/Cargo.toml b/Cargo.toml index 93bea2f..ae35850 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ http-body-util = "0.1.2" humansize = "2.1.3" hyper = { version = "1.0", features = ["client", "http1", "http2"] } hyper-util = { version = "0.1", features = ["tokio"] } +indicatif = "0.18" pin-project-lite = "0.2" rand = "0.8" rustls = { version = "0.23.10", default-features = false, features = ["std", "ring"] } diff --git a/src/main.rs b/src/main.rs index 00f8dcc..89254c5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,8 @@ mod saturate; mod up_down; mod util; +use std::io::IsTerminal; + use cf_mach::{nq_core, nq_latency, nq_load_generator, nq_packetloss, nq_rpm, nq_tokio_network}; use clap::error::ErrorKind; @@ -39,6 +41,10 @@ async fn main() -> anyhow::Result<()> { return Ok(()); } + let show_progress = std::io::stderr().is_terminal() + && std::env::var_os("RUST_LOG").is_none() + && args.verbosity.log_level_filter() <= LevelFilter::Error; + setup_logging(args.verbosity)?; // default to RPM @@ -47,7 +53,7 @@ async fn main() -> anyhow::Result<()> { .unwrap_or_else(|| Command::Rpm(RpmArgs::default())); match command { - Command::Rpm(config) => rpm::run(config).await?, + Command::Rpm(config) => rpm::run(config, show_progress).await?, Command::Download(config) => up_down::download(config).await?, Command::Upload(config) => up_down::upload(config).await?, Command::Rtt { url, runs } => latency::run(url, runs).await?, diff --git a/src/rpm.rs b/src/rpm.rs index ea95f51..d5da057 100644 --- a/src/rpm.rs +++ b/src/rpm.rs @@ -11,6 +11,7 @@ use crate::nq_rpm::{Responsiveness, ResponsivenessConfig, ResponsivenessResult}; use crate::nq_tokio_network::TokioNetwork; use anyhow::{Context, bail}; use http_body_util::BodyExt; +use indicatif::{ProgressBar, ProgressFinish, ProgressStyle}; use serde::Deserialize; use tokio::time::timeout; use tokio_util::sync::CancellationToken; @@ -48,8 +49,34 @@ fn warn_on_degraded_result(leg: &str, failed_connections: usize, upload_bytes_pe } } +fn start_phase(show: bool, msg: &'static str) -> ProgressBar { + if !show { + return ProgressBar::hidden(); + } + + let pb = ProgressBar::new_spinner() + .with_style( + ProgressStyle::with_template("{spinner:.cyan} {msg} {elapsed:.dim}") + .expect("valid spinner template"), + ) + .with_message(msg) + .with_finish(ProgressFinish::AndClear); + pb.enable_steady_tick(Duration::from_millis(100)); + pb +} + +/// Replace a phase's spinner with a permanent `✓` line. +fn finish_phase(pb: ProgressBar, msg: String) { + pb.set_style( + ProgressStyle::with_template("{prefix:.green} {msg} {elapsed:.dim}") + .expect("valid finished template"), + ); + pb.set_prefix("✓"); + pb.finish_with_message(msg); +} + /// Run a responsiveness test. -pub async fn run(cli_config: RpmArgs) -> anyhow::Result<()> { +pub async fn run(cli_config: RpmArgs, show_progress: bool) -> anyhow::Result<()> { info!("running responsiveness test"); let scoped_headers = crate::access::cf_access_scoped_headers()?; @@ -65,7 +92,9 @@ pub async fn run(cli_config: RpmArgs) -> anyhow::Result<()> { let rpm_urls = match cli_config.config.clone() { Some(endpoint) => { info!("fetching configuration from {endpoint}"); + let pb = start_phase(show_progress, "Fetching configuration"); let urls = get_rpm_config(endpoint, scoped_headers.clone()).await?; + finish_phase(pb, "Fetched configuration".to_string()); info!("retrieved configuration urls: {urls:?}"); urls @@ -84,23 +113,23 @@ pub async fn run(cli_config: RpmArgs) -> anyhow::Result<()> { // first get unloaded RTT measurements info!("determining unloaded latency"); + let pb = start_phase(show_progress, "Measuring unloaded latency"); let rtt_result = crate::latency::run_test(&LatencyConfig { url: rpm_urls.small_download_url.parse()?, runs: 20, scoped_headers: scoped_headers.clone(), }) .await?; - info!( - "unloaded latency: {} ms. jitter: {} ms", - rtt_result - .median() - .map(pretty_secs_to_ms) - .unwrap_or_default(), - rtt_result - .jitter() - .map(pretty_secs_to_ms) - .unwrap_or_default(), - ); + let median_ms = rtt_result + .median() + .map(pretty_secs_to_ms) + .unwrap_or_default(); + let jitter_ms = rtt_result + .jitter() + .map(pretty_secs_to_ms) + .unwrap_or_default(); + finish_phase(pb, format!("Unloaded latency: {median_ms} ms")); + info!("unloaded latency: {median_ms} ms. jitter: {jitter_ms} ms"); let config = ResponsivenessConfig { large_download_url: rpm_urls.large_download_url.parse()?, @@ -129,11 +158,15 @@ pub async fn run(cli_config: RpmArgs) -> anyhow::Result<()> { } info!("running download test"); + let pb = start_phase(show_progress, "Running download test"); let download_result = run_test(&config, true).await?; + finish_phase(pb, "Download test".to_string()); debug!("download result={download_result:?}"); info!("running upload test"); + let pb = start_phase(show_progress, "Running upload test"); let upload_result = run_test(&config, false).await?; + finish_phase(pb, "Upload test".to_string()); debug!("upload result={upload_result:?}"); warn_on_degraded_result(