iggy_cli/commands/binary_system/
ping.rs1use crate::commands::cli_command::{CliCommand, PRINT_TARGET};
20use anyhow::Context;
21use async_trait::async_trait;
22use iggy_common::Client;
23use std::fmt::{Display, Formatter, Result};
24use std::time::Duration;
25use tokio::time::{Instant, sleep};
26use tracing::{Level, event};
27
28pub struct PingCmd {
29 count: u32,
30}
31
32impl PingCmd {
33 pub fn new(count: u32) -> Self {
34 Self { count }
35 }
36}
37
38struct PingStats {
39 samples: Vec<u128>,
40}
41
42impl PingStats {
43 fn new() -> Self {
44 Self { samples: vec![] }
45 }
46
47 fn add(&mut self, ping_duration: &Duration) {
48 self.samples.push(ping_duration.as_nanos());
49 }
50
51 fn count(&self) -> usize {
52 self.samples.len()
53 }
54
55 fn get_min_avg_max(&self) -> (u128, u128, u128) {
56 let (min, max, sum) = self
57 .samples
58 .iter()
59 .fold((u128::MAX, u128::MIN, 0), |(min, max, sum), value| {
60 (min.min(*value), max.max(*value), sum + value)
61 });
62 let avg = sum / self.count() as u128;
63
64 (min, avg, max)
65 }
66
67 fn get_stats(&self) -> (u128, u128, u128, u128) {
68 let (min, avg, max) = self.get_min_avg_max();
69
70 let variance = self
71 .samples
72 .iter()
73 .map(|value| {
74 let diff = avg as f64 - (*value as f64);
75
76 diff * diff
77 })
78 .sum::<f64>()
79 / self.count() as f64;
80 let std_dev = variance.sqrt() as u128;
81
82 (min, avg, max, std_dev)
83 }
84}
85
86fn nano_to_ms(nanoseconds: u128) -> f64 {
87 nanoseconds as f64 / 1_000_000.0
88}
89
90impl Display for PingStats {
91 fn fmt(&self, f: &mut Formatter<'_>) -> Result {
92 let (min, avg, max, std_dev) = self.get_stats();
93 write!(
94 f,
95 "min/avg/max/mdev = {:.3}/{:.3}/{:.3}/{:.3} ms",
96 nano_to_ms(min),
97 nano_to_ms(avg),
98 nano_to_ms(max),
99 nano_to_ms(std_dev)
100 )
101 }
102}
103
104#[async_trait]
105impl CliCommand for PingCmd {
106 fn explain(&self) -> String {
107 "ping command".to_owned()
108 }
109
110 fn login_required(&self) -> bool {
111 false
112 }
113
114 async fn execute_cmd(&mut self, client: &dyn Client) -> anyhow::Result<(), anyhow::Error> {
115 let print_width = (self.count.ilog10() + 1) as usize;
116 let mut ping_stats = PingStats::new();
117
118 for i in 1..=self.count {
119 let time_start = Instant::now();
120 client
121 .ping()
122 .await
123 .with_context(|| "Problem sending ping command".to_owned())?;
124 let ping_duration = time_start.elapsed();
125 ping_stats.add(&ping_duration);
126 event!(target: PRINT_TARGET, Level::INFO, "Ping sequence id: {:width$} time: {:.2} ms", i, nano_to_ms(ping_duration.as_nanos()), width = print_width);
127 sleep(Duration::from_secs(1)).await;
128 }
129
130 event!(target: PRINT_TARGET, Level::INFO, "");
131 event!(target: PRINT_TARGET, Level::INFO, "Ping statistics for {} ping commands", ping_stats.count());
132 event!(target: PRINT_TARGET, Level::INFO, "{ping_stats}");
133
134 Ok(())
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn should_add_samples() {
144 let mut ping_stats = PingStats::new();
145
146 ping_stats.add(&Duration::from_millis(1));
147 ping_stats.add(&Duration::from_millis(2));
148 ping_stats.add(&Duration::from_millis(3));
149 ping_stats.add(&Duration::from_millis(4));
150 ping_stats.add(&Duration::from_millis(5));
151 ping_stats.add(&Duration::from_millis(6));
152
153 assert_eq!(ping_stats.count(), 6);
154 }
155
156 #[test]
157 fn should_get_min_avg_max() {
158 let mut ping_stats = PingStats::new();
159
160 ping_stats.add(&Duration::from_millis(1));
161 ping_stats.add(&Duration::from_millis(9));
162
163 assert_eq!(ping_stats.count(), 2);
164 assert_eq!(ping_stats.get_min_avg_max(), (1000000, 5000000, 9000000));
165 }
166
167 #[test]
168 fn should_return_stats() {
169 let mut ping_stats = PingStats::new();
170
171 ping_stats.add(&Duration::from_nanos(1));
172 ping_stats.add(&Duration::from_nanos(3));
173 ping_stats.add(&Duration::from_nanos(3));
174 ping_stats.add(&Duration::from_nanos(3));
175 ping_stats.add(&Duration::from_nanos(5));
176
177 assert_eq!(ping_stats.count(), 5);
178 assert_eq!(ping_stats.get_stats(), (1, 3, 5, 1));
179 }
180
181 #[test]
182 fn should_format_stats() {
183 let mut ping_stats = PingStats::new();
184
185 ping_stats.add(&Duration::from_nanos(1322444));
186 ping_stats.add(&Duration::from_nanos(3457432));
187 ping_stats.add(&Duration::from_nanos(5343270));
188 ping_stats.add(&Duration::from_nanos(7837541));
189
190 assert_eq!(
191 format!("{ping_stats}"),
192 "min/avg/max/mdev = 1.322/4.490/7.838/2.400 ms"
193 );
194 }
195}