use crate::config::MAX_HOPS;
use parking_lot::RwLock;
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::Arc;
use std::time::Duration;
use trippy::tracing::{Probe, ProbeStatus, Tracer, TracerChannel, TracerConfig, TracerRound};
#[derive(Debug, Clone)]
pub struct Trace {
max_samples: usize,
lowest_ttl: u8,
highest_ttl: u8,
highest_ttl_for_round: u8,
round: Option<usize>,
hops: Vec<Hop>,
error: Option<String>,
}
impl Trace {
pub fn new(max_samples: usize) -> Self {
Self {
max_samples,
lowest_ttl: 0,
highest_ttl: 0,
highest_ttl_for_round: 0,
round: None,
hops: (0..MAX_HOPS).map(|_| Hop::default()).collect(),
error: None,
}
}
pub fn round(&self) -> Option<usize> {
self.round
}
pub fn hops(&self) -> &[Hop] {
if self.lowest_ttl == 0 || self.highest_ttl == 0 {
&[]
} else {
let start = (self.lowest_ttl as usize) - 1;
let end = self.highest_ttl as usize;
&self.hops[start..end]
}
}
pub fn is_target(&self, hop: &Hop) -> bool {
self.highest_ttl == hop.ttl
}
pub fn is_in_round(&self, hop: &Hop) -> bool {
hop.ttl <= self.highest_ttl_for_round
}
pub fn target_hop(&self) -> &Hop {
if self.highest_ttl > 0 {
&self.hops[usize::from(self.highest_ttl) - 1]
} else {
&self.hops[0]
}
}
pub fn error(&self) -> Option<&str> {
self.error.as_deref()
}
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;
for probe in round.probes {
self.update_from_probe(probe);
}
}
fn update_from_probe(&mut self, probe: &Probe) {
self.update_lowest_ttl(probe);
self.update_round(probe);
match probe.status {
ProbeStatus::Complete => {
let index = usize::from(probe.ttl.0) - 1;
let hop = &mut self.hops[index];
hop.ttl = probe.ttl.0;
hop.total_sent += 1;
hop.total_recv += 1;
let dur = probe.duration();
let dur_ms = dur.as_secs_f64() * 1000_f64;
hop.total_time += dur;
hop.last = Some(dur);
hop.samples.insert(0, dur);
hop.best = hop.best.map_or(Some(dur), |d| Some(d.min(dur)));
hop.worst = hop.worst.map_or(Some(dur), |d| Some(d.max(dur)));
hop.mean += (dur_ms - hop.mean) / hop.total_recv as f64;
hop.m2 += (dur_ms - hop.mean) * (dur_ms - hop.mean);
if hop.samples.len() > self.max_samples {
hop.samples.pop();
}
let host = probe.host.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
*hop.addrs.entry(host).or_default() += 1;
}
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();
}
}
ProbeStatus::NotSent => {}
}
}
fn update_lowest_ttl(&mut self, probe: &Probe) {
if matches!(probe.status, ProbeStatus::Awaited | ProbeStatus::Complete) {
if self.lowest_ttl == 0 {
self.lowest_ttl = probe.ttl.0;
} else {
self.lowest_ttl = self.lowest_ttl.min(probe.ttl.0);
}
}
}
fn update_round(&mut self, probe: &Probe) {
if matches!(probe.status, ProbeStatus::Awaited | ProbeStatus::Complete) {
self.round = match self.round {
None => Some(probe.round.0),
Some(r) => Some(r.max(probe.round.0)),
}
}
}
}
#[derive(Debug, Clone)]
pub struct Hop {
ttl: u8,
addrs: HashMap<IpAddr, usize>,
total_sent: usize,
total_recv: usize,
total_time: Duration,
last: Option<Duration>,
best: Option<Duration>,
worst: Option<Duration>,
mean: f64,
m2: f64,
samples: Vec<Duration>,
}
impl Hop {
pub fn ttl(&self) -> u8 {
self.ttl
}
pub fn addrs(&self) -> impl Iterator<Item = &IpAddr> {
self.addrs.keys()
}
pub fn addrs_with_counts(&self) -> impl Iterator<Item = (&IpAddr, &usize)> {
self.addrs.iter()
}
pub fn addr_count(&self) -> usize {
self.addrs.len()
}
pub fn total_sent(&self) -> usize {
self.total_sent
}
pub fn total_recv(&self) -> usize {
self.total_recv
}
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
} else {
0_f64
}
}
pub fn last_ms(&self) -> Option<f64> {
self.last.map(|last| last.as_secs_f64() * 1000_f64)
}
pub fn best_ms(&self) -> Option<f64> {
self.best.map(|last| last.as_secs_f64() * 1000_f64)
}
pub fn worst_ms(&self) -> Option<f64> {
self.worst.map(|last| last.as_secs_f64() * 1000_f64)
}
pub fn avg_ms(&self) -> f64 {
if self.total_recv() > 0 {
(self.total_time.as_secs_f64() * 1000_f64) / self.total_recv as f64
} else {
0_f64
}
}
pub fn stddev_ms(&self) -> f64 {
if self.total_recv > 1 {
(self.m2 / (self.total_recv - 1) as f64).sqrt()
} else {
0_f64
}
}
pub fn samples(&self) -> &[Duration] {
&self.samples
}
}
impl Default for Hop {
fn default() -> Self {
Self {
ttl: 0,
addrs: HashMap::default(),
total_sent: 0,
total_recv: 0,
total_time: Duration::default(),
last: None,
best: None,
worst: None,
mean: 0f64,
m2: 0f64,
samples: Vec::default(),
}
}
}
pub fn run_backend(config: &TracerConfig, channel: TracerChannel, trace_data: Arc<RwLock<Trace>>) {
let td = trace_data.clone();
let tracer = Tracer::new(config, move |round| {
trace_data.write().update_from_round(round);
});
match tracer.trace(channel) {
Ok(_) => {}
Err(err) => {
td.write().error = Some(err.to_string());
}
};
}