From de864ad0fcccdf2f356fb72911ec02453b2e9312 Mon Sep 17 00:00:00 2001 From: trkelly23 <69165022+trkelly23@users.noreply.github.com> Date: Thu, 18 Jan 2024 11:46:47 -0500 Subject: [PATCH] feat display jitter values Fixed clippy warnings to pass PR tests --- src/backend/trace.rs | 51 ++++++++++++++++++++++++++ src/config/columns.rs | 18 +++++++++- src/frontend/columns.rs | 65 +++++++++++++++++++-------------- src/frontend/render/table.rs | 69 ++++++++++++++---------------------- src/lib.rs | 2 +- src/main.rs | 2 +- src/report/types.rs | 12 +++++++ 7 files changed, 146 insertions(+), 73 deletions(-) diff --git a/src/backend/trace.rs b/src/backend/trace.rs index 619ffbafe..50d3ad907 100644 --- a/src/backend/trace.rs +++ b/src/backend/trace.rs @@ -144,6 +144,14 @@ pub struct Hop { extensions: Option, mean: f64, m2: f64, + /// The ABS(RTTx - RTTx-n) + jitter: Option, + /// The Sequential jitter average calculated for each + javg: Option, + /// The worst jitter reading recorded + jmax: Option, + /// The interval calculation i.e smooth + jinta: f64, } impl Hop { @@ -219,6 +227,31 @@ impl Hop { } } + /// The duration of the jitter probe observed. + pub fn jitter_ms(&self) -> Option { + self.jitter.map(|j| j.as_secs_f64() * 1000_f64) + } + /// The duration of the jworst probe observed. + pub fn jmax_ms(&self) -> Option { + self.jmax.map(|x| x.as_secs_f64() * 1000_f64) + } + /// The jitter average duration of all probes. + pub fn javg_ms(&self) -> Option { + if self.total_recv() > 0 { + self.javg + } else { + None + } + } + /// The jitter interval of all probes. + pub fn jinta(&self) -> Option { + if self.total_recv() > 0 { + Some(self.jinta) + } else { + None + } + } + /// The last N samples. pub fn samples(&self) -> &[Duration] { &self.samples @@ -244,6 +277,10 @@ impl Default for Hop { m2: 0f64, samples: Vec::default(), extensions: None, + jitter: None, + javg: None, + jmax: None, + jinta: 0f64, } } } @@ -336,6 +373,20 @@ impl TraceData { let dur = probe.duration(); let dur_ms = dur.as_secs_f64() * 1000_f64; hop.total_time += dur; + //Before last is set use it to calc jitter + let last_ms = hop.last_ms().unwrap_or_default(); + let jitter_ms = (last_ms - dur_ms).abs(); + let jitter_dur = Duration::from_secs_f64(jitter_ms / 1000_f64); + hop.jitter = hop.last.and(Some(jitter_dur)); + let mut javg_ms = hop.javg_ms().unwrap_or_default(); + //Welfords online algorithm avg without dataset values. + javg_ms += (jitter_ms - javg_ms) / hop.total_recv as f64; + hop.javg = Some(javg_ms); + // algorithm is from rfc1889, A.8 or rfc3550 + hop.jinta += jitter_ms - ((hop.jinta + 8.0) / 16.0); + hop.jmax = hop + .jmax + .map_or(Some(jitter_dur), |d| Some(d.max(jitter_dur))); hop.last = Some(dur); hop.samples.insert(0, dur); hop.best = hop.best.map_or(Some(dur), |d| Some(d.min(dur))); diff --git a/src/config/columns.rs b/src/config/columns.rs index ac69c9a35..84cacf7d4 100644 --- a/src/config/columns.rs +++ b/src/config/columns.rs @@ -71,6 +71,14 @@ pub enum TuiColumn { StdDev, /// The status of a hop. Status, + /// The jitter abs(RTTx-RTTx-1) + Jitter, + /// The jitter total average + Javg, + /// The worst or max jitter recorded. + Jmax, + /// The smoothed jitter reading + Jinta, } impl TryFrom for TuiColumn { @@ -89,6 +97,10 @@ impl TryFrom for TuiColumn { 'w' => Ok(Self::Worst), 'd' => Ok(Self::StdDev), 't' => Ok(Self::Status), + 'j' => Ok(Self::Jitter), + 'g' => Ok(Self::Javg), + 'x' => Ok(Self::Jmax), + 'i' => Ok(Self::Jinta), c => Err(anyhow!(format!("unknown column code: {c}"))), } } @@ -108,6 +120,10 @@ impl Display for TuiColumn { Self::Worst => write!(f, "w"), Self::StdDev => write!(f, "d"), Self::Status => write!(f, "t"), + Self::Jitter => write!(f, "j"), + Self::Javg => write!(f, "g"), + Self::Jmax => write!(f, "x"), + Self::Jinta => write!(f, "i"), } } } @@ -134,7 +150,7 @@ mod tests { } ///Negative test for invalid characters - #[test_case('x' ; "invalid x")] + #[test_case('k' ; "invalid x")] #[test_case('z' ; "invalid z")] fn test_try_invalid_char_for_tui_column(c: char) { // Negative test for an unknown character diff --git a/src/frontend/columns.rs b/src/frontend/columns.rs index ea1e81e35..5151a21e9 100644 --- a/src/frontend/columns.rs +++ b/src/frontend/columns.rs @@ -8,13 +8,6 @@ pub struct Columns(pub Vec); impl Columns { /// Column width constraints. - /// - /// All columns are returned as `Constraint::Min(width)`. - /// - /// For `Fixed(n)` columns the width is as specified in `n`. - /// For `Variable` columns the width is calculated by subtracting the total - /// size of all `Fixed` columns from the width of the containing `Rect` and - /// dividing by the number of `Variable` columns. pub fn constraints(&self, rect: Rect) -> Vec { let total_fixed_width = self .0 @@ -24,18 +17,12 @@ impl Columns { ColumnWidth::Variable => 0, }) .sum(); - let variable_width_count = self - .0 - .iter() - .filter(|c| matches!(c.width(), ColumnWidth::Variable)) - .count() as u16; - let variable_width = - rect.width.saturating_sub(total_fixed_width) / variable_width_count.max(1); + let total_variable_width = rect.width.saturating_sub(total_fixed_width); self.0 .iter() .map(|c| match c.width() { ColumnWidth::Fixed(width) => Constraint::Min(width), - ColumnWidth::Variable => Constraint::Min(variable_width), + ColumnWidth::Variable => Constraint::Min(total_variable_width), }) .collect() } @@ -79,6 +66,14 @@ pub enum Column { StdDev, /// The status of a hop. Status, + /// The jitter of a hop(RTTx-RTTx-1). + Jitter, + /// The Average Jitter + Javg, + /// The worst or max jitter hop RTT + Jmax, + /// The smoothed jitter reading for a hop + Jinta, } impl From for char { @@ -95,6 +90,10 @@ impl From for char { Column::Worst => 'w', Column::StdDev => 'd', Column::Status => 't', + Column::Jitter => 'j', + Column::Javg => 'g', + Column::Jmax => 'x', + Column::Jinta => 'i', } } } @@ -113,6 +112,10 @@ impl From for Column { TuiColumn::Worst => Self::Worst, TuiColumn::StdDev => Self::StdDev, TuiColumn::Status => Self::Status, + TuiColumn::Jitter => Self::Jitter, + TuiColumn::Javg => Self::Javg, + TuiColumn::Jmax => Self::Jmax, + TuiColumn::Jinta => Self::Jinta, } } } @@ -131,10 +134,23 @@ impl Display for Column { Self::Worst => write!(f, "Wrst"), Self::StdDev => write!(f, "StDev"), Self::Status => write!(f, "Sts"), + Self::Jitter => write!(f, "Jttr"), + Self::Javg => write!(f, "Javg"), + Self::Jmax => write!(f, "Jmax"), + Self::Jinta => write!(f, "Jint"), } } } +/// Table column layout constraints. +#[derive(Debug, PartialEq)] +enum ColumnWidth { + /// A fixed size column. + Fixed(u16), + /// A column that will use the remaining space. + Variable, +} + impl Column { /// The width of the column. pub(self) fn width(self) -> ColumnWidth { @@ -150,20 +166,15 @@ impl Column { Self::Best => ColumnWidth::Fixed(7), Self::Worst => ColumnWidth::Fixed(7), Self::StdDev => ColumnWidth::Fixed(8), - Self::Status => ColumnWidth::Fixed(7), + Self::Status => ColumnWidth::Fixed(4), + Self::Jitter => ColumnWidth::Fixed(7), + Self::Javg => ColumnWidth::Fixed(7), + Self::Jmax => ColumnWidth::Fixed(7), + Self::Jinta => ColumnWidth::Fixed(7), } } } -/// Table column layout constraints. -#[derive(Debug, PartialEq)] -enum ColumnWidth { - /// A fixed size column. - Fixed(u16), - /// A column that will use the remaining space. - Variable, -} - #[cfg(test)] mod tests { use super::*; @@ -229,7 +240,7 @@ mod tests { assert_eq!( vec![ Min(4), - Min(11), + Min(14), Min(8), Min(7), Min(7), @@ -238,7 +249,7 @@ mod tests { Min(7), Min(7), Min(8), - Min(7) + Min(4) ], constraints ); diff --git a/src/frontend/render/table.rs b/src/frontend/render/table.rs index 425e87bc2..bbf9e5ad7 100644 --- a/src/frontend/render/table.rs +++ b/src/frontend/render/table.rs @@ -17,7 +17,7 @@ use trippy::tracing::{Extension, Extensions, MplsLabelStackMember, UnknownExtens /// Render the table of data about the hops. /// -/// For each hop, we show: +/// For each hop, we show by default: /// /// - The time-to-live (indexed from 1) at this hop (`#`) /// - The host(s) reported at this hop (`Host`) @@ -30,6 +30,13 @@ use trippy::tracing::{Extension, Extensions, MplsLabelStackMember, UnknownExtens /// - The worst round-trip time for all probes at this hop (`Wrst`) /// - The standard deviation round-trip time for all probes at this hop (`StDev`) /// - The status of this hop (`Sts`) +/// +/// - Optional columns +/// - The current jitter i.e. round-trip difference with the last round-trip ('Jttr') +/// - The average jitter time for all probes at this hop ('Javg') +/// - The best round-trip jitter tim for all probes at this hop ('Jmax') +/// - The smoothed jitter value for all probes at this hop ('Jinta') +/// - pub fn render(f: &mut Frame<'_>, app: &mut TuiApp, rect: Rect) { let config = &app.tui_config; let widths = config.tui_columns.constraints(rect); @@ -59,8 +66,7 @@ pub fn render(f: &mut Frame<'_>, app: &mut TuiApp, rect: Rect) { .bg(app.tui_config.theme.bg_color) .fg(app.tui_config.theme.text_color), ) - .highlight_style(selected_style) - .column_spacing(1); + .highlight_style(selected_style); f.render_stateful_widget(table, rect, &mut app.table_state); } @@ -132,7 +138,7 @@ fn new_cell( ) -> Cell<'static> { let is_target = app.tracer_data().is_target(hop, app.selected_flow); match column { - Column::Ttl => render_ttl_cell(hop), + Column::Ttl => render_usize_cell(hop.ttl().into()), Column::Host => { let (host_cell, _) = if is_selected_hop && app.show_hop_details { render_hostname_with_details(app, hop, dns, geoip_lookup, config) @@ -142,32 +148,29 @@ fn new_cell( host_cell } Column::LossPct => render_loss_pct_cell(hop), - Column::Sent => render_total_sent_cell(hop), - Column::Received => render_total_recv_cell(hop), - Column::Last => render_last_cell(hop), + Column::Sent => render_usize_cell(hop.total_sent()), + Column::Received => render_usize_cell(hop.total_recv()), + Column::Last => render_float_cell(hop.last_ms(), 1), Column::Average => render_avg_cell(hop), - Column::Best => render_best_cell(hop), - Column::Worst => render_worst_cell(hop), + Column::Best => render_float_cell(hop.best_ms(), 1), + Column::Worst => render_float_cell(hop.worst_ms(), 1), Column::StdDev => render_stddev_cell(hop), Column::Status => render_status_cell(hop, is_target), + Column::Jitter => render_float_cell(hop.jitter_ms(), 1), + Column::Javg => render_float_cell(hop.javg_ms(), 1), + Column::Jmax => render_float_cell(hop.jmax_ms(), 1), + Column::Jinta => render_float_cell(hop.jinta(), 1), } } -fn render_ttl_cell(hop: &Hop) -> Cell<'static> { - Cell::from(format!("{}", hop.ttl())) + +fn render_usize_cell(value: usize) -> Cell<'static> { + Cell::from(format!("{value}")) } fn render_loss_pct_cell(hop: &Hop) -> Cell<'static> { Cell::from(format!("{:.1}%", hop.loss_pct())) } -fn render_total_sent_cell(hop: &Hop) -> Cell<'static> { - Cell::from(format!("{}", hop.total_sent())) -} - -fn render_total_recv_cell(hop: &Hop) -> Cell<'static> { - Cell::from(format!("{}", hop.total_recv())) -} - fn render_avg_cell(hop: &Hop) -> Cell<'static> { Cell::from(if hop.total_recv() > 0 { format!("{:.1}", hop.avg_ms()) @@ -176,30 +179,6 @@ fn render_avg_cell(hop: &Hop) -> Cell<'static> { }) } -fn render_last_cell(hop: &Hop) -> Cell<'static> { - Cell::from( - hop.last_ms() - .map(|last| format!("{last:.1}")) - .unwrap_or_default(), - ) -} - -fn render_best_cell(hop: &Hop) -> Cell<'static> { - Cell::from( - hop.best_ms() - .map(|best| format!("{best:.1}")) - .unwrap_or_default(), - ) -} - -fn render_worst_cell(hop: &Hop) -> Cell<'static> { - Cell::from( - hop.worst_ms() - .map(|worst| format!("{worst:.1}")) - .unwrap_or_default(), - ) -} - fn render_stddev_cell(hop: &Hop) -> Cell<'static> { Cell::from(if hop.total_recv() > 1 { format!("{:.1}", hop.stddev_ms()) @@ -208,6 +187,10 @@ fn render_stddev_cell(hop: &Hop) -> Cell<'static> { }) } +fn render_float_cell(value: Option, places: usize) -> Cell<'static> { + Cell::from(value.map_or(String::new(), |v| format!("{v:.places$}"))) +} + fn render_status_cell(hop: &Hop, is_target: bool) -> Cell<'static> { let lost = hop.total_sent() - hop.total_recv(); Cell::from(match (lost, is_target) { diff --git a/src/lib.rs b/src/lib.rs index 45d32763f..05d81e861 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ #![warn(clippy::all, clippy::pedantic, clippy::nursery, rust_2018_idioms)] #![allow( clippy::module_name_repetitions, - clippy::struct_field_names, + clippy::redundant_field_names, clippy::option_if_let_else, clippy::missing_const_for_fn, clippy::cast_possible_truncation, diff --git a/src/main.rs b/src/main.rs index 40186940c..722c1bdc1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ #![warn(clippy::all, clippy::pedantic, clippy::nursery, rust_2018_idioms)] #![allow( clippy::module_name_repetitions, - clippy::struct_field_names, + clippy::redundant_field_names, clippy::option_if_let_else, clippy::missing_const_for_fn, clippy::cast_precision_loss, diff --git a/src/report/types.rs b/src/report/types.rs index 7cde0733e..89ad8d787 100644 --- a/src/report/types.rs +++ b/src/report/types.rs @@ -35,6 +35,14 @@ pub struct Hop { pub worst: f64, #[serde(serialize_with = "fixed_width")] pub stddev: f64, + #[serde(serialize_with = "fixed_width")] + pub jitter: f64, + #[serde(serialize_with = "fixed_width")] + pub javg: f64, + #[serde(serialize_with = "fixed_width")] + pub jmax: f64, + #[serde(serialize_with = "fixed_width")] + pub jinta: f64, } impl From<(&backend::trace::Hop, &R)> for Hop { @@ -53,6 +61,10 @@ impl From<(&backend::trace::Hop, &R)> for Hop { best: value.best_ms().unwrap_or_default(), worst: value.worst_ms().unwrap_or_default(), stddev: value.stddev_ms(), + jitter: value.jitter_ms().unwrap_or_default(), + javg: value.javg_ms().unwrap_or_default(), + jmax: value.jmax_ms().unwrap_or_default(), + jinta: value.jinta().unwrap_or_default(), } } }