zond_engine/core/models/host/
telemetry.rs1use std::{
13 collections::VecDeque,
14 time::{Duration, Instant},
15};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct HostTelemetry {
24 rtt_history: VecDeque<(Instant, Duration)>,
27
28 pub max_samples: usize,
31
32 pub ttl: Option<u8>,
34
35 pub distance_hops: Option<u8>,
37}
38
39impl HostTelemetry {
40 pub fn new(max_samples: usize) -> Self {
42 Self {
43 rtt_history: VecDeque::with_capacity(max_samples),
44 max_samples,
45 ttl: None,
46 distance_hops: None,
47 }
48 }
49
50 pub fn history(&self) -> &VecDeque<(Instant, Duration)> {
52 &self.rtt_history
53 }
54
55 pub fn add_rtt(&mut self, rtt: Duration) {
57 self.add_rtt_at(Instant::now(), rtt);
58 }
59
60 pub fn add_rtt_at(&mut self, time: Instant, rtt: Duration) {
62 if self.max_samples == 0 {
63 return;
64 }
65
66 self.rtt_history.push_back((time, rtt));
67
68 while self.rtt_history.len() > self.max_samples {
69 self.rtt_history.pop_front();
70 }
71 }
72
73 pub fn lartt(&self) -> Option<Duration> {
75 self.rtt_history.back().map(|&(_, rtt)| rtt)
76 }
77
78 pub fn min_rtt(&self) -> Option<Duration> {
80 self.rtt_history.iter().map(|(_, rtt)| *rtt).min()
81 }
82
83 pub fn max_rtt(&self) -> Option<Duration> {
85 self.rtt_history.iter().map(|(_, rtt)| *rtt).max()
86 }
87
88 pub fn average_rtt(&self) -> Option<Duration> {
90 if self.rtt_history.is_empty() {
91 return None;
92 }
93 let sum: Duration = self.rtt_history.iter().map(|(_, rtt)| *rtt).sum();
94 Some(sum / self.rtt_history.len() as u32)
95 }
96
97 pub fn jitter(&self) -> Option<Duration> {
103 if self.rtt_history.len() < 2 {
104 return None;
105 }
106
107 let mut total_diff = Duration::ZERO;
108 let mut prev = self.rtt_history[0].1;
109
110 for &(_, curr) in self.rtt_history.iter().skip(1) {
111 total_diff += if curr > prev {
112 curr - prev
113 } else {
114 prev - curr
115 };
116 prev = curr;
117 }
118
119 Some(total_diff / (self.rtt_history.len() - 1) as u32)
120 }
121
122 pub fn merge(&mut self, mut other: HostTelemetry) {
128 if other.max_samples > self.max_samples {
129 self.max_samples = other.max_samples;
130 }
131
132 if self.max_samples == 0 {
133 return;
134 }
135
136 let mut combined: Vec<_> = self
138 .rtt_history
139 .drain(..)
140 .chain(other.rtt_history.drain(..))
141 .collect();
142
143 combined.sort_by_key(|&(time, _)| time);
144
145 let start_idx = combined.len().saturating_sub(self.max_samples);
146 self.rtt_history
147 .extend(combined.into_iter().skip(start_idx));
148
149 if self.ttl.is_none() {
150 self.ttl = other.ttl;
151 }
152 if self.distance_hops.is_none() {
153 self.distance_hops = other.distance_hops;
154 }
155 }
156}
157
158impl std::fmt::Display for HostTelemetry {
159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 match self.average_rtt() {
161 Some(avg) => write!(
162 f,
163 "avg={:?}, jitter={:?}",
164 avg,
165 self.jitter().unwrap_or(Duration::ZERO)
166 ),
167 None => write!(f, "no telemetry"),
168 }
169 }
170}
171
172impl Default for HostTelemetry {
173 fn default() -> Self {
174 Self::new(10)
175 }
176}
177
178#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn telemetry_math_safety() {
193 let t = HostTelemetry::new(10);
194 assert_eq!(t.average_rtt(), None);
195 assert_eq!(t.jitter(), None);
196 assert_eq!(t.lartt(), None);
197 }
198
199 #[test]
200 fn telemetry_averaging_logic() {
201 let mut t = HostTelemetry::new(5);
202 t.add_rtt(Duration::from_millis(10));
203 t.add_rtt(Duration::from_millis(20));
204 assert_eq!(t.average_rtt(), Some(Duration::from_millis(15)));
205 }
206
207 #[test]
208 fn jitter_calculation_consistency() {
209 let mut t = HostTelemetry::new(5);
210 t.add_rtt(Duration::from_millis(100)); t.add_rtt(Duration::from_millis(110)); t.add_rtt(Duration::from_millis(105)); assert_eq!(t.jitter(), Some(Duration::from_millis(7) + Duration::from_micros(500)));
215 }
216
217 #[test]
218 fn merge_capacity_upgrade() {
219 let mut t1 = HostTelemetry::new(3);
220 let t2 = HostTelemetry::new(10);
221 t1.merge(t2);
222 assert_eq!(t1.max_samples, 10);
223 }
224
225 #[test]
226 fn merge_zero_capacity_safety() {
227 let mut t1 = HostTelemetry::new(0);
228 let t2 = HostTelemetry::new(0);
229 t1.merge(t2);
230 assert_eq!(t1.rtt_history.len(), 0);
231 }
232}