use crate::commands::cli_command::{CliCommand, PRINT_TARGET};
use anyhow::Context;
use async_trait::async_trait;
use iggy_common::Client;
use std::fmt::{Display, Formatter, Result};
use std::time::Duration;
use tokio::time::{Instant, sleep};
use tracing::{Level, event};
pub struct PingCmd {
count: u32,
}
impl PingCmd {
pub fn new(count: u32) -> Self {
Self { count }
}
}
struct PingStats {
samples: Vec<u128>,
}
impl PingStats {
fn new() -> Self {
Self { samples: vec![] }
}
fn add(&mut self, ping_duration: &Duration) {
self.samples.push(ping_duration.as_nanos());
}
fn count(&self) -> usize {
self.samples.len()
}
fn get_min_avg_max(&self) -> (u128, u128, u128) {
let (min, max, sum) = self
.samples
.iter()
.fold((u128::MAX, u128::MIN, 0), |(min, max, sum), value| {
(min.min(*value), max.max(*value), sum + value)
});
let avg = sum / self.count() as u128;
(min, avg, max)
}
fn get_stats(&self) -> (u128, u128, u128, u128) {
let (min, avg, max) = self.get_min_avg_max();
let variance = self
.samples
.iter()
.map(|value| {
let diff = avg as f64 - (*value as f64);
diff * diff
})
.sum::<f64>()
/ self.count() as f64;
let std_dev = variance.sqrt() as u128;
(min, avg, max, std_dev)
}
}
fn nano_to_ms(nanoseconds: u128) -> f64 {
nanoseconds as f64 / 1_000_000.0
}
impl Display for PingStats {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let (min, avg, max, std_dev) = self.get_stats();
write!(
f,
"min/avg/max/mdev = {:.3}/{:.3}/{:.3}/{:.3} ms",
nano_to_ms(min),
nano_to_ms(avg),
nano_to_ms(max),
nano_to_ms(std_dev)
)
}
}
#[async_trait]
impl CliCommand for PingCmd {
fn explain(&self) -> String {
"ping command".to_owned()
}
fn login_required(&self) -> bool {
false
}
async fn execute_cmd(&mut self, client: &dyn Client) -> anyhow::Result<(), anyhow::Error> {
let print_width = (self.count.ilog10() + 1) as usize;
let mut ping_stats = PingStats::new();
for i in 1..=self.count {
let time_start = Instant::now();
client
.ping()
.await
.with_context(|| "Problem sending ping command".to_owned())?;
let ping_duration = time_start.elapsed();
ping_stats.add(&ping_duration);
event!(target: PRINT_TARGET, Level::INFO, "Ping sequence id: {:width$} time: {:.2} ms", i, nano_to_ms(ping_duration.as_nanos()), width = print_width);
sleep(Duration::from_secs(1)).await;
}
event!(target: PRINT_TARGET, Level::INFO, "");
event!(target: PRINT_TARGET, Level::INFO, "Ping statistics for {} ping commands", ping_stats.count());
event!(target: PRINT_TARGET, Level::INFO, "{ping_stats}");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_add_samples() {
let mut ping_stats = PingStats::new();
ping_stats.add(&Duration::from_millis(1));
ping_stats.add(&Duration::from_millis(2));
ping_stats.add(&Duration::from_millis(3));
ping_stats.add(&Duration::from_millis(4));
ping_stats.add(&Duration::from_millis(5));
ping_stats.add(&Duration::from_millis(6));
assert_eq!(ping_stats.count(), 6);
}
#[test]
fn should_get_min_avg_max() {
let mut ping_stats = PingStats::new();
ping_stats.add(&Duration::from_millis(1));
ping_stats.add(&Duration::from_millis(9));
assert_eq!(ping_stats.count(), 2);
assert_eq!(ping_stats.get_min_avg_max(), (1000000, 5000000, 9000000));
}
#[test]
fn should_return_stats() {
let mut ping_stats = PingStats::new();
ping_stats.add(&Duration::from_nanos(1));
ping_stats.add(&Duration::from_nanos(3));
ping_stats.add(&Duration::from_nanos(3));
ping_stats.add(&Duration::from_nanos(3));
ping_stats.add(&Duration::from_nanos(5));
assert_eq!(ping_stats.count(), 5);
assert_eq!(ping_stats.get_stats(), (1, 3, 5, 1));
}
#[test]
fn should_format_stats() {
let mut ping_stats = PingStats::new();
ping_stats.add(&Duration::from_nanos(1322444));
ping_stats.add(&Duration::from_nanos(3457432));
ping_stats.add(&Duration::from_nanos(5343270));
ping_stats.add(&Duration::from_nanos(7837541));
assert_eq!(
format!("{ping_stats}"),
"min/avg/max/mdev = 1.322/4.490/7.838/2.400 ms"
);
}
}