diff --git a/src/backend.rs b/src/backend.rs index 06e0805c9..4a80eeb13 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -1,8 +1,11 @@ +use crate::backend::flow::FlowId; use crate::config::MAX_HOPS; use crate::platform::Platform; use indexmap::IndexMap; use parking_lot::RwLock; +use std::collections::HashMap; use std::net::{IpAddr, Ipv4Addr}; +use std::ops::Range; use std::sync::Arc; use std::time::Duration; use tracing::instrument; @@ -15,23 +18,19 @@ use trippy::tracing::{ #[derive(Debug, Clone)] pub struct Trace { max_samples: usize, - lowest_ttl: u8, - highest_ttl: u8, - highest_ttl_for_round: u8, round: Option, - hops: Vec, + flows: HashMap, error: Option, } impl Trace { pub fn new(max_samples: usize) -> Self { + let mut flows = HashMap::new(); + flows.insert(FlowId(0), Flow::new()); Self { max_samples, - lowest_ttl: 0, - highest_ttl: 0, - highest_ttl_for_round: 0, round: None, - hops: (0..MAX_HOPS).map(|_| Hop::default()).collect(), + flows, error: None, } } @@ -41,40 +40,78 @@ impl Trace { self.round } - /// Information about each hop in the trace. - pub fn hops(&self) -> &[Hop] { - if self.lowest_ttl == 0 || self.highest_ttl == 0 { + // TODO try to remove, never make public again + fn hops(&self) -> &[Hop] { + let flow = self.flows.get(&FlowId(0)).unwrap(); + if flow.lowest_ttl == 0 || flow.highest_ttl == 0 { &[] } else { - let start = (self.lowest_ttl as usize) - 1; - let end = self.highest_ttl as usize; - &self.hops[start..end] + let start = (flow.lowest_ttl as usize) - 1; + let end = flow.highest_ttl as usize; + &flow.hops[start..end] + } + } + + // TODO no need to access hops here? + pub fn hop_count(&self) -> usize { + let flow = self.flows.get(&FlowId(0)).unwrap(); + if flow.lowest_ttl == 0 || flow.highest_ttl == 0 { + 0 + } else { + let start = (flow.lowest_ttl as usize) - 1; + let end = flow.highest_ttl as usize; + flow.hops[start..end].len() + } + } + + /// TODO still buggy for `first_ttl` config + pub fn hop_range(&self) -> Range { + let flow = self.flows.get(&FlowId(0)).unwrap(); + if flow.lowest_ttl == 0 || flow.highest_ttl == 0 { + Range { start: 0, end: 0 } + } else { + let start = (flow.lowest_ttl as usize) - 1; + let end = flow.highest_ttl as usize; + Range { start, end } } } + /// The maximum number of hosts per hop. + pub fn max_addr_count(&self) -> u8 { + self.hops() + .iter() + .map(|h| h.addrs.len()) + .max() + .and_then(|i| u8::try_from(i).ok()) + .unwrap_or_default() + } + /// Is a given `Hop` the target hop? /// /// A `Hop` is considered to be the target if it has the highest `ttl` value observed. /// /// Note that if the target host does not respond to probes then the the highest `ttl` observed /// will be one greater than the `ttl` of the last host which did respond. - pub fn is_target(&self, hop: &Hop) -> bool { - self.highest_ttl == hop.ttl + pub fn is_target(&self, index: usize) -> bool { + let flow = self.flows.get(&FlowId(0)).unwrap(); + let hop = &flow.hops[index]; + flow.highest_ttl == hop.ttl } /// Is a given `Hop` in the current round? - pub fn is_in_round(&self, hop: &Hop) -> bool { - hop.ttl <= self.highest_ttl_for_round + pub fn is_in_round(&self, index: usize) -> bool { + let flow = self.flows.get(&FlowId(0)).unwrap(); + let hop = &flow.hops[index]; + hop.ttl <= flow.highest_ttl_for_round } - /// Return the target `Hop`. - /// - /// TODO Do we guarantee there is always a target hop? - pub fn target_hop(&self) -> &Hop { - if self.highest_ttl > 0 { - &self.hops[usize::from(self.highest_ttl) - 1] + /// Get the index of the target `Hop`. + pub fn target_index(&self) -> usize { + let flow = self.flows.get(&FlowId(0)).unwrap(); + if flow.highest_ttl > 0 { + usize::from(flow.highest_ttl - 1) } else { - &self.hops[0] + 0 } } @@ -84,8 +121,13 @@ impl Trace { /// Update the tracing state from a `TracerRound`. pub fn update_from_round(&mut self, round: &TracerRound<'_>) { - self.highest_ttl = std::cmp::max(self.highest_ttl, round.largest_ttl.0); - self.highest_ttl_for_round = round.largest_ttl.0; + let _flow_id = FlowId::from_addrs(round.probes.iter().map(|p| (p.ttl.0, p.host))).flow_id(); + // println!("flow_id: {}", flow_id.flow_id()); + // let flow = self.flows.entry(flow_id).or_insert_with(|| Flow::new()); + + let flow = self.flows.get_mut(&FlowId(0)).unwrap(); + flow.highest_ttl = std::cmp::max(flow.highest_ttl, round.largest_ttl.0); + flow.highest_ttl_for_round = round.largest_ttl.0; for probe in round.probes { self.update_from_probe(probe); } @@ -94,10 +136,11 @@ impl Trace { fn update_from_probe(&mut self, probe: &Probe) { self.update_lowest_ttl(probe); self.update_round(probe); + let flow = self.flows.get_mut(&FlowId(0)).unwrap(); match probe.status { ProbeStatus::Complete => { let index = usize::from(probe.ttl.0) - 1; - let hop = &mut self.hops[index]; + let hop = &mut flow.hops[index]; hop.ttl = probe.ttl.0; hop.total_sent += 1; hop.total_recv += 1; @@ -118,11 +161,11 @@ impl Trace { } ProbeStatus::Awaited => { let index = usize::from(probe.ttl.0) - 1; - self.hops[index].total_sent += 1; - self.hops[index].ttl = probe.ttl.0; - self.hops[index].samples.insert(0, Duration::default()); - if self.hops[index].samples.len() > self.max_samples { - self.hops[index].samples.pop(); + flow.hops[index].total_sent += 1; + flow.hops[index].ttl = probe.ttl.0; + flow.hops[index].samples.insert(0, Duration::default()); + if flow.hops[index].samples.len() > self.max_samples { + flow.hops[index].samples.pop(); } } ProbeStatus::NotSent => {} @@ -131,11 +174,12 @@ impl Trace { /// Update `lowest_ttl` for valid probes. fn update_lowest_ttl(&mut self, probe: &Probe) { + let flow = self.flows.get_mut(&FlowId(0)).unwrap(); if matches!(probe.status, ProbeStatus::Awaited | ProbeStatus::Complete) { - if self.lowest_ttl == 0 { - self.lowest_ttl = probe.ttl.0; + if flow.lowest_ttl == 0 { + flow.lowest_ttl = probe.ttl.0; } else { - self.lowest_ttl = self.lowest_ttl.min(probe.ttl.0); + flow.lowest_ttl = flow.lowest_ttl.min(probe.ttl.0); } } } @@ -149,103 +193,150 @@ impl Trace { } } } -} - -/// Information about a single `Hop` within a `Trace`. -#[derive(Debug, Clone)] -pub struct Hop { - ttl: u8, - addrs: IndexMap, - total_sent: usize, - total_recv: usize, - total_time: Duration, - last: Option, - best: Option, - worst: Option, - mean: f64, - m2: f64, - samples: Vec, -} -impl Hop { /// The time-to-live of this hop. - pub fn ttl(&self) -> u8 { - self.ttl + pub fn ttl(&self, index: usize) -> u8 { + let flow = self.flows.get(&FlowId(0)).unwrap(); + let hop = &flow.hops[index]; + hop.ttl } /// The set of addresses that have responded for this time-to-live. - pub fn addrs(&self) -> impl Iterator { - self.addrs.keys() + pub fn addrs(&self, index: usize) -> impl Iterator { + let flow = self.flows.get(&FlowId(0)).unwrap(); + let hop = &flow.hops[index]; + hop.addrs.keys() } - pub fn addrs_with_counts(&self) -> impl Iterator { - self.addrs.iter() + pub fn addrs_with_counts(&self, index: usize) -> impl Iterator { + let flow = self.flows.get(&FlowId(0)).unwrap(); + let hop = &flow.hops[index]; + hop.addrs.iter() } /// The number of unique address observed for this time-to-live. - pub fn addr_count(&self) -> usize { - self.addrs.len() + pub fn addr_count(&self, index: usize) -> usize { + let flow = self.flows.get(&FlowId(0)).unwrap(); + let hop = &flow.hops[index]; + hop.addrs.len() } /// The total number of probes sent. - pub fn total_sent(&self) -> usize { - self.total_sent + pub fn total_sent(&self, index: usize) -> usize { + let flow = self.flows.get(&FlowId(0)).unwrap(); + let hop = &flow.hops[index]; + hop.total_sent } /// The total number of probes responses received. - pub fn total_recv(&self) -> usize { - self.total_recv + pub fn total_recv(&self, index: usize) -> usize { + let flow = self.flows.get(&FlowId(0)).unwrap(); + let hop = &flow.hops[index]; + hop.total_recv } /// The % of packets that are lost. - pub fn loss_pct(&self) -> f64 { - if self.total_sent > 0 { - let lost = self.total_sent - self.total_recv; - lost as f64 / self.total_sent as f64 * 100f64 + pub fn loss_pct(&self, index: usize) -> f64 { + let flow = self.flows.get(&FlowId(0)).unwrap(); + let hop = &flow.hops[index]; + if hop.total_sent > 0 { + let lost = hop.total_sent - hop.total_recv; + lost as f64 / hop.total_sent as f64 * 100f64 } else { 0_f64 } } /// The duration of the last probe. - pub fn last_ms(&self) -> Option { - self.last.map(|last| last.as_secs_f64() * 1000_f64) + pub fn last_ms(&self, index: usize) -> Option { + let flow = self.flows.get(&FlowId(0)).unwrap(); + let hop = &flow.hops[index]; + hop.last.map(|last| last.as_secs_f64() * 1000_f64) } /// The duration of the best probe observed. - pub fn best_ms(&self) -> Option { - self.best.map(|last| last.as_secs_f64() * 1000_f64) + pub fn best_ms(&self, index: usize) -> Option { + let flow = self.flows.get(&FlowId(0)).unwrap(); + let hop = &flow.hops[index]; + hop.best.map(|last| last.as_secs_f64() * 1000_f64) } /// The duration of the worst probe observed. - pub fn worst_ms(&self) -> Option { - self.worst.map(|last| last.as_secs_f64() * 1000_f64) + pub fn worst_ms(&self, index: usize) -> Option { + let flow = self.flows.get(&FlowId(0)).unwrap(); + let hop = &flow.hops[index]; + hop.worst.map(|last| last.as_secs_f64() * 1000_f64) } /// The average duration of all probes. - pub fn avg_ms(&self) -> f64 { - if self.total_recv() > 0 { - (self.total_time.as_secs_f64() * 1000_f64) / self.total_recv as f64 + pub fn avg_ms(&self, index: usize) -> f64 { + let flow = self.flows.get(&FlowId(0)).unwrap(); + let hop = &flow.hops[index]; + if hop.total_recv > 0 { + (hop.total_time.as_secs_f64() * 1000_f64) / hop.total_recv as f64 } else { 0_f64 } } /// The standard deviation of all probes. - pub fn stddev_ms(&self) -> f64 { - if self.total_recv > 1 { - (self.m2 / (self.total_recv - 1) as f64).sqrt() + pub fn stddev_ms(&self, index: usize) -> f64 { + let flow = self.flows.get(&FlowId(0)).unwrap(); + let hop = &flow.hops[index]; + if hop.total_recv > 1 { + (hop.m2 / (hop.total_recv - 1) as f64).sqrt() } else { 0_f64 } } /// The last N samples. - pub fn samples(&self) -> &[Duration] { - &self.samples + pub fn samples(&self, index: usize) -> &[Duration] { + let flow = self.flows.get(&FlowId(0)).unwrap(); + let hop = &flow.hops[index]; + &hop.samples + } +} + +// /// An identifier for a flow. +// pub type FlowId = u64; + +/// A Flow holds data a unique tracing flow. +#[derive(Debug, Clone)] +struct Flow { + lowest_ttl: u8, + highest_ttl: u8, + highest_ttl_for_round: u8, + hops: Vec, +} + +impl Flow { + pub fn new() -> Self { + Self { + lowest_ttl: 0, + highest_ttl: 0, + highest_ttl_for_round: 0, + hops: (0..MAX_HOPS).map(|_| Hop::default()).collect(), + } } } +/// Information about a single `Hop` within a `Flow`. +#[derive(Debug, Clone)] +struct Hop { + ttl: u8, + addrs: IndexMap, + total_sent: usize, + total_recv: usize, + total_time: Duration, + last: Option, + best: Option, + worst: Option, + mean: f64, + m2: f64, + samples: Vec, +} + impl Default for Hop { fn default() -> Self { Self { @@ -264,6 +355,69 @@ impl Default for Hop { } } +mod flow { + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + use std::net::IpAddr; + + /// TODO + #[derive(Debug, Clone, Eq, PartialEq, Hash)] + pub struct FlowId(pub(super) u64); + + impl FlowId { + /// TODO + pub fn from_addrs(addrs: impl Iterator)>) -> Self { + let hasher = addrs.fold(DefaultHasher::new(), |mut hasher, hop| { + hop.hash(&mut hasher); + hasher + }); + Self(hasher.finish()) + } + + /// TODO + pub fn flow_id(&self) -> u64 { + self.0 + } + } + + #[cfg(test)] + mod tests { + use super::*; + use std::net::Ipv4Addr; + use std::str::FromStr; + + #[test] + fn test_flow_id() { + let hops = [ + (1, Some(addr("192.168.1.1"))), + (2, Some(addr("10.193.232.14"))), + (3, Some(addr("10.193.232.21"))), + (4, Some(addr("218.102.40.26"))), + (5, Some(addr("10.195.41.17"))), + (6, Some(addr("63.218.1.105"))), + (7, Some(addr("63.223.60.126"))), + (8, Some(addr("213.248.97.220"))), + (9, Some(addr("62.115.118.110"))), + (10, None), + (11, Some(addr("62.115.140.43"))), + (12, Some(addr("62.115.115.173"))), + (13, Some(addr("62.115.45.195"))), + (14, Some(addr("185.74.76.23"))), + (15, Some(addr("89.18.162.17"))), + (16, Some(addr("213.189.4.73"))), + ]; + assert_eq!( + 2_435_116_302_937_406_375, + FlowId::from_addrs(hops.into_iter()).flow_id() + ); + } + + fn addr(addr: &str) -> IpAddr { + IpAddr::V4(Ipv4Addr::from_str(addr).unwrap()) + } + } +} + /// Run the tracing backend. /// /// Note that this implementation blocks the tracer on the `RwLock` and so any delays in the the TUI diff --git a/src/frontend/render/body.rs b/src/frontend/render/body.rs index 817580a10..85b5c87fc 100644 --- a/src/frontend/render/body.rs +++ b/src/frontend/render/body.rs @@ -10,7 +10,7 @@ use ratatui::Frame; pub fn render(f: &mut Frame<'_>, rec: Rect, app: &mut TuiApp) { if let Some(err) = app.selected_tracer_data.error() { bsod::render(f, rec, err); - } else if app.tracer_data().hops().is_empty() { + } else if app.tracer_data().hop_count() == 0 { splash::render(f, app, rec); } else if app.show_chart { chart::render(f, app, rec); diff --git a/src/frontend/render/chart.rs b/src/frontend/render/chart.rs index 745c76075..31b41239d 100644 --- a/src/frontend/render/chart.rs +++ b/src/frontend/render/chart.rs @@ -8,20 +8,19 @@ use ratatui::Frame; /// Render the ping history for all hops as a chart. pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { - let selected_hop = app.table_state.selected().map_or_else( - || app.tracer_data().target_hop(), - |s| &app.tracer_data().hops()[s], - ); - let samples = app.tui_config.max_samples / app.zoom_factor; - let series_data = app - .selected_tracer_data - .hops() - .iter() - .map(|hop| { - hop.samples() + let selected_index = app + .table_state + .selected() + .unwrap_or_else(|| app.tracer_data().target_index()); + let sample_count = app.tui_config.max_samples / app.zoom_factor; + let hop_range = app.tracer_data().hop_range(); + let series_data = hop_range + .map(|hindex| { + let samples = app.tracer_data().samples(hindex); + samples .iter() .enumerate() - .take(samples) + .take(sample_count) .map(|(i, s)| (i as f64, (s.as_secs_f64() * 1000_f64))) .collect::>() }) @@ -43,9 +42,7 @@ pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { .marker(Marker::Braille) .style(Style::default().fg({ match i { - i if i + 1 == selected_hop.ttl() as usize => { - app.tui_config.theme.hops_chart_selected_color - } + i if i == selected_index => app.tui_config.theme.hops_chart_selected_color, _ => app.tui_config.theme.hops_chart_unselected_color, } })) @@ -56,13 +53,16 @@ pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { .x_axis( Axis::default() .title("Samples") - .bounds([0_f64, samples as f64]) + .bounds([0_f64, sample_count as f64]) .labels_alignment(Alignment::Right) .labels( - ["0".to_string(), format!("{samples} ({}x)", app.zoom_factor)] - .into_iter() - .map(Span::from) - .collect(), + [ + "0".to_string(), + format!("{sample_count} ({}x)", app.zoom_factor), + ] + .into_iter() + .map(Span::from) + .collect(), ) .style(Style::default().fg(app.tui_config.theme.hops_chart_axis_color)), ) diff --git a/src/frontend/render/header.rs b/src/frontend/render/header.rs index 8ece2b4dc..5add566cf 100644 --- a/src/frontend/render/header.rs +++ b/src/frontend/render/header.rs @@ -94,7 +94,7 @@ pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { Span::raw(render_status(app)), Span::raw(format!( ", discovered {} hops", - app.tracer_data().hops().len() + app.tracer_data().hop_count() )), ]), ]; diff --git a/src/frontend/render/histogram.rs b/src/frontend/render/histogram.rs index 26781b53a..f4568bbde 100644 --- a/src/frontend/render/histogram.rs +++ b/src/frontend/render/histogram.rs @@ -8,16 +8,18 @@ use std::time::Duration; /// Render a histogram of ping frequencies. pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { - let target_hop = app.table_state.selected().map_or_else( - || app.tracer_data().target_hop(), - |s| &app.tracer_data().hops()[s], - ); - let freq_data = sample_frequency(target_hop.samples()); + let hindex = app + .table_state + .selected() + .unwrap_or_else(|| app.tracer_data().target_index()); + let ttl = app.tracer_data().ttl(hindex); + let samples = app.tracer_data().samples(hindex); + let freq_data = sample_frequency(samples); let freq_data_ref: Vec<_> = freq_data.iter().map(|(b, c)| (b.as_str(), *c)).collect(); let barchart = BarChart::default() .block( Block::default() - .title(format!("Frequency #{}", target_hop.ttl())) + .title(format!("Frequency #{ttl}")) .style( Style::default() .bg(app.tui_config.theme.bg_color) diff --git a/src/frontend/render/history.rs b/src/frontend/render/history.rs index 1a4b3d3f9..13d6b6a7d 100644 --- a/src/frontend/render/history.rs +++ b/src/frontend/render/history.rs @@ -6,9 +6,10 @@ use ratatui::Frame; /// Render the ping history for the final hop which is typically the target. pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { - let selected_hop = app.selected_hop_or_target(); - let data = selected_hop - .samples() + let hindex = app.selected_hop_or_target_index(); + let samples = app.tracer_data().samples(hindex); + let ttl = app.tracer_data().ttl(hindex); + let data = samples .iter() .take(rect.width as usize) .map(|s| (s.as_secs_f64() * 1000_f64) as u64) @@ -16,7 +17,7 @@ pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { let history = Sparkline::default() .block( Block::default() - .title(format!("Samples #{}", selected_hop.ttl())) + .title(format!("Samples #{ttl}")) .style( Style::default() .bg(app.tui_config.theme.bg_color) diff --git a/src/frontend/render/table.rs b/src/frontend/render/table.rs index 9756aa8da..26487d56c 100644 --- a/src/frontend/render/table.rs +++ b/src/frontend/render/table.rs @@ -1,4 +1,3 @@ -use crate::backend::Hop; use crate::config::{AddressMode, AsMode, GeoIpMode}; use crate::frontend::config::TuiConfig; use crate::frontend::theme::Theme; @@ -31,10 +30,16 @@ use trippy::dns::{AsInfo, DnsEntry, DnsResolver, Resolved, Resolver, Unresolved} pub fn render(f: &mut Frame<'_>, app: &mut TuiApp, rect: Rect) { let header = render_table_header(app.tui_config.theme); let selected_style = Style::default().add_modifier(Modifier::REVERSED); - let rows = - app.tracer_data().hops().iter().map(|hop| { - render_table_row(app, hop, &app.resolver, &app.geoip_lookup, &app.tui_config) - }); + let hop_range = app.tracer_data().hop_range(); + let rows = hop_range.into_iter().map(|hindex| { + render_table_row( + app, + hindex, + &app.resolver, + &app.geoip_lookup, + &app.tui_config, + ) + }); let table = Table::new(rows) .header(header) .block( @@ -68,32 +73,38 @@ fn render_table_header(theme: Theme) -> Row<'static> { /// Render a single row in the table of hops. fn render_table_row( app: &TuiApp, - hop: &Hop, + hindex: usize, dns: &DnsResolver, geoip_lookup: &GeoIpLookup, config: &TuiConfig, ) -> Row<'static> { - let is_selected_hop = app - .selected_hop() - .map(|h| h.ttl() == hop.ttl()) - .unwrap_or_default(); - let is_target = app.tracer_data().is_target(hop); - let is_in_round = app.tracer_data().is_in_round(hop); - let ttl_cell = render_ttl_cell(hop); + let is_selected_hop = app.table_state.selected() == Some(hindex); + let is_target = app.tracer_data().is_target(hindex); + let is_in_round = app.tracer_data().is_in_round(hindex); + let ttl = app.tracer_data().ttl(hindex); + let loss_pct = app.tracer_data().loss_pct(hindex); + let total_sent = app.tracer_data().total_sent(hindex); + let total_recv = app.tracer_data().total_recv(hindex); + let last = app.tracer_data().last_ms(hindex); + let avg = app.tracer_data().avg_ms(hindex); + let best = app.tracer_data().best_ms(hindex); + let worst = app.tracer_data().worst_ms(hindex); + let stddev = app.tracer_data().stddev_ms(hindex); + let ttl_cell = render_ttl_cell(ttl); let (hostname_cell, row_height) = if is_selected_hop && app.show_hop_details { - render_hostname_with_details(app, hop, dns, geoip_lookup, config) + render_hostname_with_details(app, hindex, dns, geoip_lookup, config) } else { - render_hostname(hop, dns, geoip_lookup, config) + render_hostname(app, hindex, dns, geoip_lookup, config) }; - let loss_pct_cell = render_loss_pct_cell(hop); - let total_sent_cell = render_total_sent_cell(hop); - let total_recv_cell = render_total_recv_cell(hop); - let last_cell = render_last_cell(hop); - let avg_cell = render_avg_cell(hop); - let best_cell = render_best_cell(hop); - let worst_cell = render_worst_cell(hop); - let stddev_cell = render_stddev_cell(hop); - let status_cell = render_status_cell(hop, is_target); + let loss_pct_cell = render_loss_pct_cell(loss_pct); + let total_sent_cell = render_total_sent_cell(total_sent); + let total_recv_cell = render_total_recv_cell(total_recv); + let last_cell = render_last_cell(last); + let avg_cell = render_avg_cell(total_recv, avg); + let best_cell = render_best_cell(best); + let worst_cell = render_worst_cell(worst); + let stddev_cell = render_stddev_cell(total_recv, stddev); + let status_cell = render_status_cell(total_sent, total_recv, is_target); let cells = [ ttl_cell, hostname_cell, @@ -118,68 +129,56 @@ fn render_table_row( .style(Style::default().fg(row_color)) } -fn render_ttl_cell(hop: &Hop) -> Cell<'static> { - Cell::from(format!("{}", hop.ttl())) +fn render_ttl_cell(ttl: u8) -> Cell<'static> { + Cell::from(format!("{ttl}")) } -fn render_loss_pct_cell(hop: &Hop) -> Cell<'static> { - Cell::from(format!("{:.1}%", hop.loss_pct())) +fn render_loss_pct_cell(loss_pct: f64) -> Cell<'static> { + Cell::from(format!("{loss_pct:.1}%")) } -fn render_total_sent_cell(hop: &Hop) -> Cell<'static> { - Cell::from(format!("{}", hop.total_sent())) +fn render_total_sent_cell(total_sent: usize) -> Cell<'static> { + Cell::from(format!("{total_sent}")) } -fn render_total_recv_cell(hop: &Hop) -> Cell<'static> { - Cell::from(format!("{}", hop.total_recv())) +fn render_total_recv_cell(total_recv: usize) -> Cell<'static> { + Cell::from(format!("{total_recv}")) } -fn render_avg_cell(hop: &Hop) -> Cell<'static> { - Cell::from(if hop.total_recv() > 0 { - format!("{:.1}", hop.avg_ms()) +fn render_avg_cell(total_recv: usize, avg: f64) -> Cell<'static> { + Cell::from(if total_recv > 0 { + format!("{avg:.1}") } else { String::default() }) } -fn render_last_cell(hop: &Hop) -> Cell<'static> { - Cell::from( - hop.last_ms() - .map(|last| format!("{last:.1}")) - .unwrap_or_default(), - ) +fn render_last_cell(last: Option) -> Cell<'static> { + Cell::from(last.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_best_cell(best: Option) -> Cell<'static> { + Cell::from(best.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_worst_cell(worst: Option) -> Cell<'static> { + Cell::from(worst.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()) +fn render_stddev_cell(total_recv: usize, stddev: f64) -> Cell<'static> { + Cell::from(if total_recv > 1 { + format!("{stddev:.1}") } else { String::default() }) } -fn render_status_cell(hop: &Hop, is_target: bool) -> Cell<'static> { - let lost = hop.total_sent() - hop.total_recv(); +fn render_status_cell(total_sent: usize, total_recv: usize, is_target: bool) -> Cell<'static> { + let lost = total_sent - total_recv; Cell::from(match (lost, is_target) { - (lost, target) if target && lost == hop.total_sent() => "🔴", + (lost, target) if target && lost == total_sent => "🔴", (lost, target) if target && lost > 0 => "🟡", - (lost, target) if !target && lost == hop.total_sent() => "🟤", + (lost, target) if !target && lost == total_sent => "🟤", (lost, target) if !target && lost > 0 => "🔵", _ => "🟢", }) @@ -187,30 +186,37 @@ fn render_status_cell(hop: &Hop, is_target: bool) -> Cell<'static> { /// Render hostname table cell (normal mode). fn render_hostname( - hop: &Hop, + app: &TuiApp, + hindex: usize, dns: &DnsResolver, geoip_lookup: &GeoIpLookup, config: &TuiConfig, ) -> (Cell<'static>, u16) { - let (hostname, count) = if hop.total_recv() > 0 { + let total_recv = app.tracer_data().total_recv(hindex); + let addrs_with_counts = app.tracer_data().addrs_with_counts(hindex); + let addr_count = app.tracer_data().addr_count(hindex); + + let (hostname, count) = if total_recv > 0 { match config.max_addrs { None => { - let hostnames = hop - .addrs_with_counts() - .map(|(addr, &freq)| format_address(addr, freq, hop, dns, geoip_lookup, config)) + let hostnames = addrs_with_counts + .map(|(addr, &freq)| { + format_address(app, addr, freq, hindex, dns, geoip_lookup, config) + }) .join("\n"); - let count = hop.addr_count().clamp(1, u8::MAX as usize); + let count = addr_count.clamp(1, u8::MAX as usize); (hostnames, count as u16) } Some(max_addr) => { - let hostnames = hop - .addrs_with_counts() + let hostnames = addrs_with_counts .sorted_unstable_by_key(|(_, &cnt)| cnt) .rev() .take(max_addr as usize) - .map(|(addr, &freq)| format_address(addr, freq, hop, dns, geoip_lookup, config)) + .map(|(addr, &freq)| { + format_address(app, addr, freq, hindex, dns, geoip_lookup, config) + }) .join("\n"); - let count = hop.addr_count().clamp(1, max_addr as usize); + let count = addr_count.clamp(1, max_addr as usize); (hostnames, count as u16) } } @@ -222,9 +228,10 @@ fn render_hostname( /// Perform a reverse DNS lookup for an address and format the result. fn format_address( + app: &TuiApp, addr: &IpAddr, freq: usize, - hop: &Hop, + hindex: usize, dns: &DnsResolver, geoip_lookup: &GeoIpLookup, config: &TuiConfig, @@ -266,23 +273,27 @@ fn format_address( .unwrap_or_default() .map(|geo| geo.location()), }; + + let addr_count = app.tracer_data().addr_count(hindex); + let total_recv = app.tracer_data().total_recv(hindex); + match geo_fmt { - Some(geo) if hop.addr_count() > 1 => { + Some(geo) if addr_count > 1 => { format!( "{} [{}] [{:.1}%]", addr_fmt, geo, - (freq as f64 / hop.total_recv() as f64) * 100_f64 + (freq as f64 / total_recv as f64) * 100_f64 ) } Some(geo) => { format!("{addr_fmt} [{geo}]") } - None if hop.addr_count() > 1 => { + None if addr_count > 1 => { format!( "{} [{:.1}%]", addr_fmt, - (freq as f64 / hop.total_recv() as f64) * 100_f64 + (freq as f64 / total_recv as f64) * 100_f64 ) } None => addr_fmt, @@ -328,14 +339,15 @@ fn format_asinfo(asinfo: &AsInfo, as_mode: AsMode) -> String { /// Render hostname table cell (detailed mode). fn render_hostname_with_details( app: &TuiApp, - hop: &Hop, + hindex: usize, dns: &DnsResolver, geoip_lookup: &GeoIpLookup, config: &TuiConfig, ) -> (Cell<'static>, u16) { - let (rendered, count) = if hop.total_recv() > 0 { - let index = app.selected_hop_address; - format_details(hop, index, dns, geoip_lookup, config) + let total_recv = app.tracer_data().total_recv(hindex); + let (rendered, count) = if total_recv > 0 { + let offset = app.selected_hop_address; + format_details(app, hindex, offset, dns, geoip_lookup, config) } else { (String::from("No response"), 1) }; @@ -345,34 +357,43 @@ fn render_hostname_with_details( /// Format hop details. fn format_details( - hop: &Hop, + app: &TuiApp, + hindex: usize, offset: usize, dns: &DnsResolver, geoip_lookup: &GeoIpLookup, config: &TuiConfig, ) -> (String, u16) { - let Some(addr) = hop.addrs().nth(offset) else { + let mut addrs = app.tracer_data().addrs(hindex); + let count = app.tracer_data().addr_count(hindex); + + let Some(addr) = addrs.nth(offset) else { return (format!("Error: no addr for index {offset}"), 1); }; - let count = hop.addr_count(); - let index = offset + 1; + let addr_index = offset + 1; let geoip = geoip_lookup.lookup(*addr).unwrap_or_default(); if config.lookup_as_info { let dns_entry = dns.lazy_reverse_lookup_with_asinfo(*addr); match dns_entry { DnsEntry::Pending(addr) => { - let details = fmt_details_with_asn(addr, index, count, None, None, geoip); + let details = fmt_details_with_asn(addr, addr_index, count, None, None, geoip); (details, 6) } DnsEntry::Resolved(Resolved::WithAsInfo(addr, hosts, asinfo)) => { let details = - fmt_details_with_asn(addr, index, count, Some(hosts), Some(asinfo), geoip); + fmt_details_with_asn(addr, addr_index, count, Some(hosts), Some(asinfo), geoip); (details, 6) } DnsEntry::NotFound(Unresolved::WithAsInfo(addr, asinfo)) => { - let details = - fmt_details_with_asn(addr, index, count, Some(vec![]), Some(asinfo), geoip); + let details = fmt_details_with_asn( + addr, + addr_index, + count, + Some(vec![]), + Some(asinfo), + geoip, + ); (details, 6) } DnsEntry::Failed(ip) => { @@ -390,15 +411,15 @@ fn format_details( let dns_entry = dns.lazy_reverse_lookup(*addr); match dns_entry { DnsEntry::Pending(addr) => { - let details = fmt_details_no_asn(addr, index, count, None, geoip); + let details = fmt_details_no_asn(addr, addr_index, count, None, geoip); (details, 3) } DnsEntry::Resolved(Resolved::Normal(addr, hosts)) => { - let details = fmt_details_no_asn(addr, index, count, Some(hosts), geoip); + let details = fmt_details_no_asn(addr, addr_index, count, Some(hosts), geoip); (details, 3) } DnsEntry::NotFound(Unresolved::Normal(addr)) => { - let details = fmt_details_no_asn(addr, index, count, Some(vec![]), geoip); + let details = fmt_details_no_asn(addr, addr_index, count, Some(vec![]), geoip); (details, 3) } DnsEntry::Failed(ip) => { diff --git a/src/frontend/render/world.rs b/src/frontend/render/world.rs index 1ddd8eb3d..a4e2fa34c 100644 --- a/src/frontend/render/world.rs +++ b/src/frontend/render/world.rs @@ -1,4 +1,3 @@ -use crate::backend::Hop; use crate::frontend::tui_app::TuiApp; use itertools::Itertools; use ratatui::layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}; @@ -45,14 +44,11 @@ fn render_map_canvas(f: &mut Frame<'_>, app: &TuiApp, rect: Rect, entries: &[Map render_map_canvas_world(ctx, theme.map_world_color); ctx.layer(); for entry in entries { + let hindex = app.selected_hop_or_target_index(); + let ttl = app.tracer_data().ttl(hindex); render_map_canvas_pin(ctx, entry); render_map_canvas_radius(ctx, entry, theme.map_radius_color); - render_map_canvas_selected( - ctx, - entry, - app.selected_hop_or_target(), - theme.map_selected_color, - ); + render_map_canvas_selected(ctx, entry, ttl, theme.map_selected_color); } }) .marker(Marker::Braille) @@ -101,19 +97,14 @@ fn render_map_canvas_radius(ctx: &mut Context<'_>, entry: &MapEntry, color: Colo } /// Render the map canvas selected item box. -fn render_map_canvas_selected( - ctx: &mut Context<'_>, - entry: &MapEntry, - selected_hop: &Hop, - color: Color, -) { +fn render_map_canvas_selected(ctx: &mut Context<'_>, entry: &MapEntry, ttl: u8, color: Color) { let MapEntry { latitude, longitude, hops, .. } = entry; - if hops.contains(&selected_hop.ttl()) { + if hops.contains(&ttl) { ctx.draw(&Rectangle { x: longitude - 5.0_f64, y: latitude - 5.0_f64, @@ -127,11 +118,16 @@ fn render_map_canvas_selected( /// Render the map info panel. fn render_map_info_panel(f: &mut Frame<'_>, app: &TuiApp, rect: Rect, entries: &[MapEntry]) { let theme = app.tui_config.theme; - let selected_hop = app.selected_hop_or_target(); + let hindex = app.selected_hop_or_target_index(); + + let ttl = app.tracer_data().ttl(hindex); + let addr_count = app.tracer_data().addr_count(hindex); + let mut addrs = app.tracer_data().addrs(hindex); + let locations = entries .iter() .filter_map(|entry| { - if entry.hops.contains(&selected_hop.ttl()) { + if entry.hops.contains(&ttl) { Some(format!("{} [{}]", entry.long_name, entry.location)) } else { None @@ -140,19 +136,15 @@ fn render_map_info_panel(f: &mut Frame<'_>, app: &TuiApp, rect: Rect, entries: & .collect::>(); let info = match locations.as_slice() { _ if app.tracer_config().geoip_mmdb_file.is_none() => "GeoIp not enabled".to_string(), - [] if selected_hop.addr_count() > 0 => format!( - "No GeoIp data for hop {} ({})", - selected_hop.ttl(), - selected_hop.addrs().join(", ") - ), - [] => format!("No GeoIp data for hop {}", selected_hop.ttl()), + [] if addr_count > 0 => format!("No GeoIp data for hop {} ({})", ttl, addrs.join(", ")), + [] => format!("No GeoIp data for hop {ttl}"), [loc] => loc.to_string(), - _ => format!("Multiple GeoIp locations for hop {}", selected_hop.ttl()), + _ => format!("Multiple GeoIp locations for hop {ttl}"), }; let info_panel = Paragraph::new(info) .block( Block::default() - .title(format!("Hop {}", selected_hop.ttl())) + .title(format!("Hop {ttl}")) .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(theme.map_info_panel_border_color)) @@ -182,8 +174,11 @@ struct MapEntry { /// Each entry represent a single `GeoIp` location, which may be associated with multiple hops. fn build_map_entries(app: &TuiApp) -> Vec { let mut geo_map: HashMap = HashMap::new(); - for hop in app.tracer_data().hops() { - for addr in hop.addrs() { + let hop_range = app.tracer_data().hop_range(); + for hindex in hop_range { + let addrs = app.tracer_data().addrs(hindex); + let ttl = app.tracer_data().ttl(hindex); + for addr in addrs { if let Some(geo) = app.geoip_lookup.lookup(*addr).unwrap_or_default() { if let Some((latitude, longitude, radius)) = geo.coordinates() { let entry = geo_map.entry(geo.long_name()).or_insert(MapEntry { @@ -194,7 +189,7 @@ fn build_map_entries(app: &TuiApp) -> Vec { radius, hops: vec![], }); - entry.hops.push(hop.ttl()); + entry.hops.push(ttl); } }; } diff --git a/src/frontend/tui_app.rs b/src/frontend/tui_app.rs index 092a1e0c0..541e004dd 100644 --- a/src/frontend/tui_app.rs +++ b/src/frontend/tui_app.rs @@ -1,4 +1,4 @@ -use crate::backend::{Hop, Trace}; +use crate::backend::Trace; use crate::frontend::config::TuiConfig; use crate::frontend::render::settings::SETTINGS_TABS; use crate::geoip::GeoIpLookup; @@ -75,17 +75,14 @@ impl TuiApp { Trace::new(self.tui_config.max_samples); } - pub fn selected_hop_or_target(&self) -> &Hop { - self.table_state.selected().map_or_else( - || self.tracer_data().target_hop(), - |s| &self.tracer_data().hops()[s], - ) - } - - pub fn selected_hop(&self) -> Option<&Hop> { + pub fn selected_hop_or_target_index(&self) -> usize { self.table_state .selected() - .map(|s| &self.tracer_data().hops()[s]) + .unwrap_or_else(|| self.tracer_data().target_index()) + } + + pub fn selected_hop_index(&self) -> Option { + self.table_state.selected() } pub fn tracer_config(&self) -> &TraceInfo { @@ -93,7 +90,7 @@ impl TuiApp { } pub fn clamp_selected_hop(&mut self) { - let hop_count = self.tracer_data().hops().len(); + let hop_count = self.tracer_data().hop_count(); if let Some(selected) = self.table_state.selected() { if selected > hop_count - 1 { self.table_state.select(Some(hop_count - 1)); @@ -102,7 +99,7 @@ impl TuiApp { } pub fn next_hop(&mut self) { - let hop_count = self.tracer_data().hops().len(); + let hop_count = self.tracer_data().hop_count(); if hop_count == 0 { return; } @@ -122,7 +119,7 @@ impl TuiApp { } pub fn previous_hop(&mut self) { - let hop_count = self.tracer_data().hops().len(); + let hop_count = self.tracer_data().hop_count(); if hop_count == 0 { return; } @@ -153,15 +150,15 @@ impl TuiApp { } pub fn next_hop_address(&mut self) { - if let Some(hop) = self.selected_hop() { - if self.selected_hop_address < hop.addr_count() - 1 { + if let Some(hindex) = self.selected_hop_index() { + if self.selected_hop_address < self.tracer_data().addr_count(hindex) - 1 { self.selected_hop_address += 1; } } } pub fn previous_hop_address(&mut self) { - if self.selected_hop().is_some() && self.selected_hop_address > 0 { + if self.selected_hop_index().is_some() && self.selected_hop_address > 0 { self.selected_hop_address -= 1; } } @@ -297,13 +294,15 @@ impl TuiApp { /// The maximum number of hosts per hop for the currently selected trace. pub fn max_hosts(&self) -> u8 { - self.selected_tracer_data - .hops() - .iter() - .map(|h| h.addrs().count()) - .max() - .and_then(|i| u8::try_from(i).ok()) - .unwrap_or_default() + self.selected_tracer_data.max_addr_count() + + // .apply(|hops| { + // hops.iter() + // .map(|h| h.addrs().count()) + // .max() + // .and_then(|i| u8::try_from(i).ok()) + // .unwrap_or_default() + // }) } } diff --git a/src/report.rs b/src/report.rs index 67a239480..0f8052991 100644 --- a/src/report.rs +++ b/src/report.rs @@ -18,34 +18,37 @@ pub fn run_report_csv( ) -> anyhow::Result<()> { let trace = wait_for_round(&info.data, report_cycles)?; println!("Target,TargetIp,Hop,IPs,Addrs,Loss%,Snt,Recv,Last,Avg,Best,Wrst,StdDev,"); - for hop in trace.hops() { - let ttl = hop.ttl(); - let ips = hop.addrs().join(":"); + for hindex in trace.hop_range() { + let ttl = trace.ttl(hindex); + let ips = trace.addrs(hindex).join(":"); let ip = if ips.is_empty() { String::from("???") } else { ips }; - let hosts = hop.addrs().map(|ip| resolver.reverse_lookup(*ip)).join(":"); + let hosts = trace + .addrs(hindex) + .map(|ip| resolver.reverse_lookup(*ip)) + .join(":"); let host = if hosts.is_empty() { String::from("???") } else { hosts }; - let sent = hop.total_sent(); - let recv = hop.total_recv(); - let last = hop - .last_ms() + let sent = trace.total_sent(hindex); + let recv = trace.total_recv(hindex); + let last = trace + .last_ms(hindex) .map_or_else(|| String::from("???"), |last| format!("{last:.1}")); - let best = hop - .best_ms() + let best = trace + .best_ms(hindex) .map_or_else(|| String::from("???"), |best| format!("{best:.1}")); - let worst = hop - .worst_ms() + let worst = trace + .worst_ms(hindex) .map_or_else(|| String::from("???"), |worst| format!("{worst:.1}")); - let stddev = hop.stddev_ms(); - let avg = hop.avg_ms(); - let loss_pct = hop.loss_pct(); + let stddev = trace.stddev_ms(hindex); + let avg = trace.avg_ms(hindex); + let loss_pct = trace.loss_pct(hindex); println!( "{},{},{},{},{},{:.1}%,{},{},{},{:.1},{},{},{:.1}", info.target_hostname, @@ -119,31 +122,29 @@ pub fn run_report_json( ) -> anyhow::Result<()> { let trace = wait_for_round(&info.data, report_cycles)?; let hops: Vec = trace - .hops() - .iter() - .map(|hop| { - let hosts: Vec<_> = hop - .addrs() + .hop_range() + .map(|hindex| { + let hosts: Vec<_> = trace + .addrs(hindex) .map(|ip| Host { ip: ip.to_string(), hostname: resolver.reverse_lookup(*ip).to_string(), }) .collect(); ReportHop { - ttl: hop.ttl(), + ttl: trace.ttl(hindex), hosts, - loss_pct: hop.loss_pct(), - sent: hop.total_sent(), - last: hop.last_ms().unwrap_or_default(), - recv: hop.total_recv(), - avg: hop.avg_ms(), - best: hop.best_ms().unwrap_or_default(), - worst: hop.worst_ms().unwrap_or_default(), - stddev: hop.stddev_ms(), + loss_pct: trace.loss_pct(hindex), + sent: trace.total_sent(hindex), + last: trace.last_ms(hindex).unwrap_or_default(), + recv: trace.total_recv(hindex), + avg: trace.avg_ms(hindex), + best: trace.best_ms(hindex).unwrap_or_default(), + worst: trace.worst_ms(hindex).unwrap_or_default(), + stddev: trace.stddev_ms(hindex), } }) .collect(); - let report = Report { info: ReportInfo { target: Host { @@ -190,16 +191,16 @@ fn run_report_table( .load_preset(preset) .set_content_arrangement(ContentArrangement::Dynamic) .set_header(columns); - for hop in trace.hops() { - let ttl = hop.ttl().to_string(); - let ips = hop.addrs().join("\n"); + for hindex in trace.hop_range() { + let ttl = trace.ttl(hindex).to_string(); + let ips = trace.addrs(hindex).join("\n"); let ip = if ips.is_empty() { String::from("???") } else { ips }; - let hosts = hop - .addrs() + let hosts = trace + .addrs(hindex) .map(|ip| resolver.reverse_lookup(*ip).to_string()) .join("\n"); let host = if hosts.is_empty() { @@ -207,20 +208,20 @@ fn run_report_table( } else { hosts }; - let sent = hop.total_sent().to_string(); - let recv = hop.total_recv().to_string(); - let last = hop - .last_ms() + let sent = trace.total_sent(hindex).to_string(); + let recv = trace.total_recv(hindex).to_string(); + let last = trace + .last_ms(hindex) .map_or_else(|| String::from("???"), |last| format!("{last:.1}")); - let best = hop - .best_ms() + let best = trace + .best_ms(hindex) .map_or_else(|| String::from("???"), |best| format!("{best:.1}")); - let worst = hop - .worst_ms() + let worst = trace + .worst_ms(hindex) .map_or_else(|| String::from("???"), |worst| format!("{worst:.1}")); - let stddev = format!("{:.1}", hop.stddev_ms()); - let avg = format!("{:.1}", hop.avg_ms()); - let loss_pct = format!("{:.1}", hop.loss_pct()); + let stddev = format!("{:.1}", trace.stddev_ms(hindex)); + let avg = format!("{:.1}", trace.avg_ms(hindex)); + let loss_pct = format!("{:.1}", trace.loss_pct(hindex)); table.add_row(vec![ &ttl, &ip, &host, &loss_pct, &sent, &recv, &last, &avg, &best, &worst, &stddev, ]); @@ -237,26 +238,26 @@ pub fn run_report_stream(info: &TraceInfo) -> anyhow::Result<()> { if let Some(err) = trace_data.error() { return Err(anyhow!("error: {}", err)); } - for hop in trace_data.hops() { - let ttl = hop.ttl(); - let addrs = hop.addrs().collect::>(); - let sent = hop.total_sent(); - let recv = hop.total_recv(); - let last = hop - .last_ms() + for hindex in trace_data.hop_range() { + let ttl = trace_data.ttl(hindex); + let addrs = trace_data.addrs(hindex).collect::>(); + let sent = trace_data.total_sent(hindex); + let recv = trace_data.total_recv(hindex); + let last = trace_data + .last_ms(hindex) .map(|last| format!("{last:.1}")) .unwrap_or_default(); - let best = hop - .best_ms() + let best = trace_data + .best_ms(hindex) .map(|best| format!("{best:.1}")) .unwrap_or_default(); - let worst = hop - .worst_ms() + let worst = trace_data + .worst_ms(hindex) .map(|worst| format!("{worst:.1}")) .unwrap_or_default(); - let stddev = hop.stddev_ms(); - let avg = hop.avg_ms(); - let loss_pct = hop.loss_pct(); + let stddev = trace_data.stddev_ms(hindex); + let avg = trace_data.avg_ms(hindex); + let loss_pct = trace_data.loss_pct(hindex); println!( "ttl={ttl} addrs={addrs:?} loss_pct={loss_pct:.1}, sent={sent} recv={recv} last={last} best={best} worst={worst} avg={avg:.1} stddev={stddev:.1}" );