use chrono::prelude::*;
use core::option::Option;
use core::option::Option::{None, Some};
use core::time::Duration;
use itertools::Itertools;
use tui::style::Style;
use tui::symbols;
use tui::widgets::{Dataset, GraphType, Paragraph};
pub struct PlotData {
pub display: String,
pub data: Vec<(f64, f64)>,
pub style: Style,
buffer: chrono::Duration,
simple_graphics: bool,
}
impl PlotData {
pub fn new(display: String, buffer: u64, style: Style, simple_graphics: bool) -> PlotData {
PlotData {
display,
data: Vec::with_capacity(150), style,
buffer: chrono::Duration::seconds(buffer as i64),
simple_graphics,
}
}
pub fn update(&mut self, item: Option<Duration>) {
let now = Local::now();
let idx = now.timestamp_millis() as f64 / 1_000f64;
match item {
Some(dur) => self.data.push((idx, dur.as_micros() as f64)),
None => self.data.push((idx, f64::NAN)),
}
let earliest_timestamp = (now - self.buffer).timestamp_millis() as f64 / 1_000f64;
let last_idx = self
.data
.iter()
.enumerate()
.filter(|(_, (timestamp, _))| *timestamp < earliest_timestamp)
.map(|(idx, _)| idx)
.last();
if let Some(idx) = last_idx {
self.data.drain(0..idx).for_each(drop)
}
}
pub fn header_stats(&self) -> Vec<Paragraph> {
let ping_header = Paragraph::new(self.display.clone()).style(self.style);
let items: Vec<&f64> = self
.data
.iter()
.filter(|(_, x)| !x.is_nan())
.map(|(_, v)| v)
.sorted_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.collect();
if items.is_empty() {
return vec![ping_header];
}
let min = **items.first().unwrap();
let max = **items.last().unwrap();
let avg = items.iter().copied().sum::<f64>() / items.len() as f64;
let jtr = items
.iter()
.zip(items.iter().skip(1))
.map(|(&prev, &curr)| (curr - prev).abs())
.sum::<f64>()
/ (items.len() - 1) as f64;
let percentile_position = 0.95 * items.len() as f32;
let rounded_position = percentile_position.round() as usize;
let p95 = items.get(rounded_position).map(|i| **i).unwrap_or(0f64);
let to = self.data.iter().filter(|(_, x)| x.is_nan()).count();
let last = self.data.last().unwrap_or(&(0f64, 0f64)).1;
vec![
ping_header,
Paragraph::new(format!("last {:?}", Duration::from_micros(last as u64)))
.style(self.style),
Paragraph::new(format!("min {:?}", Duration::from_micros(min as u64)))
.style(self.style),
Paragraph::new(format!("max {:?}", Duration::from_micros(max as u64)))
.style(self.style),
Paragraph::new(format!("avg {:?}", Duration::from_micros(avg as u64)))
.style(self.style),
Paragraph::new(format!("jtr {:?}", Duration::from_micros(jtr as u64)))
.style(self.style),
Paragraph::new(format!("p95 {:?}", Duration::from_micros(p95 as u64)))
.style(self.style),
Paragraph::new(format!("t/o {to:?}")).style(self.style),
]
}
}
impl<'a> From<&'a PlotData> for Dataset<'a> {
fn from(plot: &'a PlotData) -> Self {
let slice = plot.data.as_slice();
Dataset::default()
.marker(if plot.simple_graphics {
symbols::Marker::Dot
} else {
symbols::Marker::Braille
})
.style(plot.style)
.graph_type(GraphType::Line)
.data(slice)
}
}