From 0d75c2eb99233181d8677e390d3a8925b6e6a13a Mon Sep 17 00:00:00 2001 From: trkelly23 <69165022+trkelly23@users.noreply.github.com> Date: Fri, 22 Dec 2023 10:54:20 -0500 Subject: [PATCH] feat display jitter(WIP) feat display jitter(WIP) feat display jitter(WIP) --- src/backend/trace.rs | 71 ++++++++++++++++++++++++++++++++++++ src/config/columns.rs | 18 ++++++++- src/frontend/columns.rs | 50 ++++++++++++++++++++++++- src/frontend/render/table.rs | 60 +++++++++--------------------- src/report/types.rs | 13 ++++++- 5 files changed, 166 insertions(+), 46 deletions(-) diff --git a/src/backend/trace.rs b/src/backend/trace.rs index 619ffbafe..5ca98b0cb 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,21 @@ 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 = 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); + //Max following logic of hop.worst + 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))); @@ -381,3 +433,22 @@ impl TraceData { } } } +#[cfg(test)] +mod tests { + //use chrono::Duration; + use crate::backend::trace::{Hop, TraceData}; + use trippy::tracing::Probe; + + #[test] + #[ignore = "Possible integration test between TraceData,Hop & Probe."] + fn test_jitter() { + //let hop = Hop::default(); + let probe = Probe::default(); + let mut td = TraceData::new(2); + td.hops.push(Hop::default()); + td.hops.push(Hop::default()); + // + td.update_from_probe(&probe); + assert_eq!(td.hops[0].jitter, None); + } +} 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 7cd1f1820..6dc9d4b81 100644 --- a/src/frontend/columns.rs +++ b/src/frontend/columns.rs @@ -44,6 +44,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, } //Output a char for each column type @@ -61,6 +69,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', } } } @@ -79,6 +91,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, } } } @@ -97,6 +113,10 @@ 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"), } } } @@ -108,6 +128,26 @@ impl Column { Self::Ttl => 3, Self::Host => 42, Self::LossPct => 5, + Self::Sent => 4, + Self::Received => 4, + Self::Last => 4, + Self::Average => 4, + Self::Best => 4, + Self::Worst => 4, + Self::StdDev => 4, + Self::Status => 4, + Self::Jitter => 4, + Self::Javg => 4, + Self::Jmax => 4, + Self::Jinta => 4, + } + } + pub fn width_min(self) -> u16 { + #[allow(clippy::match_same_arms)] + match self { + Self::Ttl => 4, + Self::Host => 42, + Self::LossPct => 5, Self::Sent => 5, Self::Received => 5, Self::Last => 5, @@ -116,6 +156,10 @@ impl Column { Self::Worst => 5, Self::StdDev => 5, Self::Status => 5, + Self::Jitter => 5, + Self::Javg => 5, + Self::Jmax => 5, + Self::Jinta => 5, } } } @@ -208,8 +252,12 @@ mod tests { Column::Worst, Column::StdDev, Column::Status, + Column::Jitter, + Column::Javg, + Column::Jmax, + Column::Jinta, ]); - assert_eq!("holsravbwdt", format!("{cols}")); + assert_eq!("holsravbwdtjgxi", format!("{cols}")); } ///Reverse subset test for subset of colummns diff --git a/src/frontend/render/table.rs b/src/frontend/render/table.rs index 9368607f6..d6a71e497 100644 --- a/src/frontend/render/table.rs +++ b/src/frontend/render/table.rs @@ -59,8 +59,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 +131,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,30 +141,26 @@ 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_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_usize_cell(value: usize) -> Cell<'static> { + Cell::from(format!("{value}")) } fn render_avg_cell(hop: &Hop) -> Cell<'static> { @@ -176,30 +171,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()) @@ -207,6 +178,9 @@ fn render_stddev_cell(hop: &Hop) -> Cell<'static> { String::default() }) } +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(); @@ -612,6 +586,6 @@ fn get_column_widths(columns: &Columns) -> Vec { columns .0 .iter() - .map(|c| Constraint::Percentage(c.width_pct())) + .map(|c| Constraint::Min(c.width_min())) .collect() } diff --git a/src/report/types.rs b/src/report/types.rs index 7cde0733e..9f7921d19 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(), } } } @@ -72,7 +84,6 @@ impl<'a, R: Resolver, I: Iterator> From<(I, &R)> for Hosts { ) } } - impl Display for Hosts { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0.iter().format(", "))