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 += curr.abs_diff(prev);
112 prev = curr;
113 }
114
115 Some(total_diff / (self.rtt_history.len() - 1) as u32)
116 }
117
118 pub fn merge(&mut self, mut other: HostTelemetry) {
124 if other.max_samples > self.max_samples {
125 self.max_samples = other.max_samples;
126 }
127
128 if self.max_samples == 0 {
129 return;
130 }
131
132 let mut combined: Vec<_> = self
134 .rtt_history
135 .drain(..)
136 .chain(other.rtt_history.drain(..))
137 .collect();
138
139 combined.sort_by_key(|&(time, _)| time);
140
141 let start_idx = combined.len().saturating_sub(self.max_samples);
142 self.rtt_history
143 .extend(combined.into_iter().skip(start_idx));
144
145 if self.ttl.is_none() {
146 self.ttl = other.ttl;
147 }
148 if self.distance_hops.is_none() {
149 self.distance_hops = other.distance_hops;
150 }
151 }
152}
153
154impl std::fmt::Display for HostTelemetry {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 match self.average_rtt() {
157 Some(avg) => write!(
158 f,
159 "avg={:?}, jitter={:?}",
160 avg,
161 self.jitter().unwrap_or(Duration::ZERO)
162 ),
163 None => write!(f, "no telemetry"),
164 }
165 }
166}
167
168impl Default for HostTelemetry {
169 fn default() -> Self {
170 Self::new(10)
171 }
172}
173
174#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn telemetry_math_safety() {
189 let t = HostTelemetry::new(10);
190 assert_eq!(t.average_rtt(), None);
191 assert_eq!(t.jitter(), None);
192 assert_eq!(t.lartt(), None);
193 }
194
195 #[test]
196 fn telemetry_averaging_logic() {
197 let mut t = HostTelemetry::new(5);
198 t.add_rtt(Duration::from_millis(10));
199 t.add_rtt(Duration::from_millis(20));
200 assert_eq!(t.average_rtt(), Some(Duration::from_millis(15)));
201 }
202
203 #[test]
204 fn jitter_calculation_consistency() {
205 let mut t = HostTelemetry::new(5);
206 t.add_rtt(Duration::from_millis(100)); t.add_rtt(Duration::from_millis(110)); t.add_rtt(Duration::from_millis(105)); assert_eq!(
211 t.jitter(),
212 Some(Duration::from_millis(7) + Duration::from_micros(500))
213 );
214 }
215
216 #[test]
217 fn merge_capacity_upgrade() {
218 let mut t1 = HostTelemetry::new(3);
219 let t2 = HostTelemetry::new(10);
220 t1.merge(t2);
221 assert_eq!(t1.max_samples, 10);
222 }
223
224 #[test]
225 fn merge_zero_capacity_safety() {
226 let mut t1 = HostTelemetry::new(0);
227 let t2 = HostTelemetry::new(0);
228 t1.merge(t2);
229 assert_eq!(t1.rtt_history.len(), 0);
230 }
231}