diff --git a/cli-output/src/cli_output.rs b/cli-output/src/cli_output.rs index 20651265b..e7810da2c 100644 --- a/cli-output/src/cli_output.rs +++ b/cli-output/src/cli_output.rs @@ -46,6 +46,8 @@ use { }, }; +static CHECK_MARK: Emoji = Emoji("✅ ", ""); +static CROSS_MARK: Emoji = Emoji("❌ ", ""); static WARNING: Emoji = Emoji("⚠️", "!"); #[derive(PartialEq, Debug)] @@ -2524,6 +2526,172 @@ impl fmt::Display for CliGossipNodes { impl QuietDisplay for CliGossipNodes {} impl VerboseDisplay for CliGossipNodes {} +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliPing { + pub source_pubkey: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub fixed_blockhash: Option, + #[serde(skip_serializing)] + pub blockhash_from_cluster: bool, + pub pings: Vec, + pub transaction_stats: CliPingTxStats, + #[serde(skip_serializing_if = "Option::is_none")] + pub confirmation_stats: Option, +} + +impl fmt::Display for CliPing { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f)?; + writeln_name_value(f, "Source Account:", &self.source_pubkey)?; + if let Some(fixed_blockhash) = &self.fixed_blockhash { + let blockhash_origin = if self.blockhash_from_cluster { + "fetched from cluster" + } else { + "supplied from cli arguments" + }; + writeln!( + f, + "Fixed blockhash is used: {} ({})", + fixed_blockhash, blockhash_origin + )?; + } + writeln!(f)?; + for ping in &self.pings { + write!(f, "{}", ping)?; + } + writeln!(f)?; + writeln!(f, "--- transaction statistics ---")?; + write!(f, "{}", self.transaction_stats)?; + if let Some(confirmation_stats) = &self.confirmation_stats { + write!(f, "{}", confirmation_stats)?; + } + Ok(()) + } +} + +impl QuietDisplay for CliPing {} +impl VerboseDisplay for CliPing {} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliPingData { + pub success: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub signature: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ms: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + #[serde(skip_serializing)] + pub print_timestamp: bool, + pub timestamp: String, + pub sequence: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub lamports: Option, +} +impl fmt::Display for CliPingData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let (mark, msg) = if let Some(signature) = &self.signature { + if self.success { + ( + CHECK_MARK, + format!( + "{} lamport(s) transferred: seq={:<3} time={:>4}ms signature={}", + self.lamports.unwrap(), + self.sequence, + self.ms.unwrap(), + signature + ), + ) + } else if let Some(error) = &self.error { + ( + CROSS_MARK, + format!( + "Transaction failed: seq={:<3} error={:?} signature={}", + self.sequence, error, signature + ), + ) + } else { + ( + CROSS_MARK, + format!( + "Confirmation timeout: seq={:<3} signature={}", + self.sequence, signature + ), + ) + } + } else { + ( + CROSS_MARK, + format!( + "Submit failed: seq={:<3} error={:?}", + self.sequence, + self.error.as_ref().unwrap(), + ), + ) + }; + + writeln!( + f, + "{}{}{}", + if self.print_timestamp { + &self.timestamp + } else { + "" + }, + mark, + msg + ) + } +} + +impl QuietDisplay for CliPingData {} +impl VerboseDisplay for CliPingData {} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliPingTxStats { + pub num_transactions: u32, + pub num_transaction_confirmed: u32, +} +impl fmt::Display for CliPingTxStats { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!( + f, + "{} transactions submitted, {} transactions confirmed, {:.1}% transaction loss", + self.num_transactions, + self.num_transaction_confirmed, + (100. + - f64::from(self.num_transaction_confirmed) / f64::from(self.num_transactions) + * 100.) + ) + } +} + +impl QuietDisplay for CliPingTxStats {} +impl VerboseDisplay for CliPingTxStats {} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliPingConfirmationStats { + pub min: f64, + pub mean: f64, + pub max: f64, + pub std_dev: f64, +} +impl fmt::Display for CliPingConfirmationStats { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!( + f, + "confirmation min/mean/max/stddev = {:.0}/{:.0}/{:.0}/{:.0} ms", + self.min, self.mean, self.max, self.std_dev, + ) + } +} +impl QuietDisplay for CliPingConfirmationStats {} +impl VerboseDisplay for CliPingConfirmationStats {} + #[cfg(test)] mod tests { use { diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index 3d828046c..a0b5d83a8 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -4,7 +4,7 @@ use { spend_utils::{resolve_spend_tx_and_check_account_balance, SpendAmount}, }, clap::{value_t, value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand}, - console::{style, Emoji}, + console::style, crossbeam_channel::unbounded, serde::{Deserialize, Serialize}, solana_clap_utils::{ @@ -16,7 +16,7 @@ use { solana_cli_output::{ display::{ build_balance_message, format_labeled_address, new_spinner_progress_bar, - println_name_value, println_transaction, unix_timestamp_to_string, writeln_name_value, + println_transaction, unix_timestamp_to_string, writeln_name_value, }, *, }, @@ -75,9 +75,6 @@ use { thiserror::Error, }; -static CHECK_MARK: Emoji = Emoji("✅ ", ""); -static CROSS_MARK: Emoji = Emoji("❌ ", ""); - pub trait ClusterQuerySubCommands { fn cluster_query_subcommands(self) -> Self; } @@ -1354,15 +1351,14 @@ pub fn process_ping( fixed_blockhash: &Option, print_timestamp: bool, ) -> ProcessResult { - println_name_value("Source Account:", &config.signers[0].pubkey().to_string()); - println!(); - let (signal_sender, signal_receiver) = unbounded(); ctrlc::set_handler(move || { let _ = signal_sender.send(()); }) .expect("Error setting Ctrl-C handler"); + let mut cli_pings = vec![]; + let mut submit_count = 0; let mut confirmed_count = 0; let mut confirmation_time: VecDeque = VecDeque::with_capacity(1024); @@ -1370,17 +1366,13 @@ pub fn process_ping( let mut blockhash = rpc_client.get_latest_blockhash()?; let mut lamports = 0; let mut blockhash_acquired = Instant::now(); + let mut blockhash_from_cluster = false; if let Some(fixed_blockhash) = fixed_blockhash { - let blockhash_origin = if *fixed_blockhash != Hash::default() { + if *fixed_blockhash != Hash::default() { blockhash = *fixed_blockhash; - "supplied from cli arguments" } else { - "fetched from cluster" - }; - println!( - "Fixed blockhash is used: {} ({})", - blockhash, blockhash_origin - ); + blockhash_from_cluster = true; + } } 'mainloop: for seq in 0..count.unwrap_or(std::u64::MAX) { let now = Instant::now(); @@ -1416,11 +1408,7 @@ pub fn process_ping( .duration_since(UNIX_EPOCH) .unwrap() .as_micros(); - if print_timestamp { - format!("[{}.{:06}] ", micros / 1_000_000, micros % 1_000_000) - } else { - String::new() - } + format!("[{}.{:06}] ", micros / 1_000_000, micros % 1_000_000) }; match rpc_client.send_transaction(&tx) { @@ -1434,35 +1422,51 @@ pub fn process_ping( Ok(()) => { let elapsed_time_millis = elapsed_time.as_millis() as u64; confirmation_time.push_back(elapsed_time_millis); - println!( - "{}{}{} lamport(s) transferred: seq={:<3} time={:>4}ms signature={}", - timestamp(), - CHECK_MARK, lamports, seq, elapsed_time_millis, signature - ); + let cli_ping_data = CliPingData { + success: true, + signature: Some(signature.to_string()), + ms: Some(elapsed_time_millis), + error: None, + timestamp: timestamp(), + print_timestamp, + sequence: seq, + lamports: Some(lamports), + }; + eprint!("{}", cli_ping_data); + cli_pings.push(cli_ping_data); confirmed_count += 1; } Err(err) => { - println!( - "{}{}Transaction failed: seq={:<3} error={:?} signature={}", - timestamp(), - CROSS_MARK, - seq, - err, - signature - ); + let cli_ping_data = CliPingData { + success: false, + signature: Some(signature.to_string()), + ms: None, + error: Some(err.to_string()), + timestamp: timestamp(), + print_timestamp, + sequence: seq, + lamports: None, + }; + eprint!("{}", cli_ping_data); + cli_pings.push(cli_ping_data); } } break; } if elapsed_time >= *timeout { - println!( - "{}{}Confirmation timeout: seq={:<3} signature={}", - timestamp(), - CROSS_MARK, - seq, - signature - ); + let cli_ping_data = CliPingData { + success: false, + signature: Some(signature.to_string()), + ms: None, + error: None, + timestamp: timestamp(), + print_timestamp, + sequence: seq, + lamports: None, + }; + eprint!("{}", cli_ping_data); + cli_pings.push(cli_ping_data); break; } @@ -1476,13 +1480,18 @@ pub fn process_ping( } } Err(err) => { - println!( - "{}{}Submit failed: seq={:<3} error={:?}", - timestamp(), - CROSS_MARK, - seq, - err - ); + let cli_ping_data = CliPingData { + success: false, + signature: None, + ms: None, + error: Some(err.to_string()), + timestamp: timestamp(), + print_timestamp, + sequence: seq, + lamports: None, + }; + eprint!("{}", cli_ping_data); + cli_pings.push(cli_ping_data); } } submit_count += 1; @@ -1492,28 +1501,34 @@ pub fn process_ping( } } - println!(); - println!("--- transaction statistics ---"); - println!( - "{} transactions submitted, {} transactions confirmed, {:.1}% transaction loss", - submit_count, - confirmed_count, - (100. - f64::from(confirmed_count) / f64::from(submit_count) * 100.) - ); - if !confirmation_time.is_empty() { + let transaction_stats = CliPingTxStats { + num_transactions: submit_count, + num_transaction_confirmed: confirmed_count, + }; + let confirmation_stats = if !confirmation_time.is_empty() { let samples: Vec = confirmation_time.iter().map(|t| *t as f64).collect(); let dist = criterion_stats::Distribution::from(samples.into_boxed_slice()); let mean = dist.mean(); - println!( - "confirmation min/mean/max/stddev = {:.0}/{:.0}/{:.0}/{:.0} ms", - dist.min(), + Some(CliPingConfirmationStats { + min: dist.min(), mean, - dist.max(), - dist.std_dev(Some(mean)) - ); - } + max: dist.max(), + std_dev: dist.std_dev(Some(mean)), + }) + } else { + None + }; - Ok("".to_string()) + let cli_ping = CliPing { + source_pubkey: config.signers[0].pubkey().to_string(), + fixed_blockhash: fixed_blockhash.map(|_| blockhash.to_string()), + blockhash_from_cluster, + pings: cli_pings, + transaction_stats, + confirmation_stats, + }; + + Ok(config.output_format.formatted_string(&cli_ping)) } pub fn parse_logs(