1use std::collections::HashMap;
32
33#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct LatencyBucket {
40 pub lower_bound_us: u64,
42 pub upper_bound_us: u64,
44 pub count: u64,
46}
47
48#[derive(Debug, Clone)]
54pub struct PeerLatency {
55 pub peer_id: String,
57 pub samples: Vec<u64>,
59 pub max_samples: usize,
61 pub min_us: u64,
63 pub max_us: u64,
65 pub sum_us: u64,
67 pub count: u64,
69 write_pos: usize,
71 wrapped: bool,
73}
74
75impl PeerLatency {
76 fn new(peer_id: impl Into<String>, max_samples: usize) -> Self {
78 Self {
79 peer_id: peer_id.into(),
80 samples: Vec::with_capacity(max_samples.min(1024)),
81 max_samples,
82 min_us: u64::MAX,
83 max_us: 0,
84 sum_us: 0,
85 count: 0,
86 write_pos: 0,
87 wrapped: false,
88 }
89 }
90
91 fn add(&mut self, latency_us: u64) {
93 if self.max_samples == 0 {
94 self.count += 1;
96 self.sum_us = self.sum_us.saturating_add(latency_us);
97 if latency_us < self.min_us {
98 self.min_us = latency_us;
99 }
100 if latency_us > self.max_us {
101 self.max_us = latency_us;
102 }
103 return;
104 }
105
106 if self.samples.len() < self.max_samples {
107 self.samples.push(latency_us);
109 } else {
110 self.samples[self.write_pos] = latency_us;
112 self.wrapped = true;
113 }
114 self.write_pos = (self.write_pos + 1) % self.max_samples;
115
116 self.count += 1;
117 self.sum_us = self.sum_us.saturating_add(latency_us);
118 if latency_us < self.min_us {
119 self.min_us = latency_us;
120 }
121 if latency_us > self.max_us {
122 self.max_us = latency_us;
123 }
124 }
125
126 fn sorted_samples(&self) -> Vec<u64> {
128 let mut sorted = self.samples.clone();
129 sorted.sort_unstable();
130 sorted
131 }
132}
133
134#[derive(Debug, Clone, PartialEq)]
140pub struct LatencyTrackerStats {
141 pub tracked_peers: usize,
143 pub global_samples: u64,
145 pub global_mean_us: Option<f64>,
147}
148
149#[derive(Debug)]
159pub struct PeerLatencyTracker {
160 peers: HashMap<String, PeerLatency>,
162 max_samples_per_peer: usize,
164 global_count: u64,
166 global_sum_us: u64,
168}
169
170impl PeerLatencyTracker {
171 pub fn new(max_samples_per_peer: usize) -> Self {
173 Self {
174 peers: HashMap::new(),
175 max_samples_per_peer,
176 global_count: 0,
177 global_sum_us: 0,
178 }
179 }
180
181 pub fn record(&mut self, peer_id: &str, latency_us: u64) {
186 let max = self.max_samples_per_peer;
187 let entry = self
188 .peers
189 .entry(peer_id.to_string())
190 .or_insert_with(|| PeerLatency::new(peer_id, max));
191 entry.add(latency_us);
192
193 self.global_count += 1;
194 self.global_sum_us = self.global_sum_us.saturating_add(latency_us);
195 }
196
197 pub fn percentile(&self, peer_id: &str, p: f64) -> Option<u64> {
202 let entry = self.peers.get(peer_id)?;
203 if entry.samples.is_empty() {
204 return None;
205 }
206
207 let p = p.clamp(0.0, 1.0);
208 let sorted = entry.sorted_samples();
209 let n = sorted.len();
210
211 if n == 1 {
212 return Some(sorted[0]);
213 }
214
215 let rank = p * (n - 1) as f64;
217 let lower_idx = rank.floor() as usize;
218 let upper_idx = rank.ceil() as usize;
219
220 if lower_idx == upper_idx {
221 return Some(sorted[lower_idx]);
222 }
223
224 let frac = rank - lower_idx as f64;
225 let lower_val = sorted[lower_idx] as f64;
226 let upper_val = sorted[upper_idx] as f64;
227 let interpolated = lower_val + frac * (upper_val - lower_val);
228
229 Some(interpolated.round() as u64)
230 }
231
232 pub fn mean(&self, peer_id: &str) -> Option<f64> {
237 let entry = self.peers.get(peer_id)?;
238 if entry.count == 0 {
239 return None;
240 }
241 Some(entry.sum_us as f64 / entry.count as f64)
242 }
243
244 pub fn median(&self, peer_id: &str) -> Option<u64> {
248 self.percentile(peer_id, 0.5)
249 }
250
251 pub fn histogram(&self, peer_id: &str, bucket_count: usize) -> Option<Vec<LatencyBucket>> {
256 if bucket_count == 0 {
257 return None;
258 }
259
260 let entry = self.peers.get(peer_id)?;
261 if entry.samples.is_empty() {
262 return None;
263 }
264
265 let sorted = entry.sorted_samples();
266 let min_val = sorted[0];
267 let max_val = sorted[sorted.len() - 1];
268
269 if min_val == max_val {
271 let mut buckets = Vec::with_capacity(bucket_count);
272 for i in 0..bucket_count {
273 let count = if i == 0 { sorted.len() as u64 } else { 0 };
274 buckets.push(LatencyBucket {
275 lower_bound_us: min_val,
276 upper_bound_us: max_val,
277 count,
278 });
279 }
280 return Some(buckets);
281 }
282
283 let range = max_val - min_val;
284 let bucket_width = range as f64 / bucket_count as f64;
285 let mut buckets = Vec::with_capacity(bucket_count);
286
287 for i in 0..bucket_count {
288 let lower = min_val as f64 + i as f64 * bucket_width;
289 let upper = if i == bucket_count - 1 {
290 max_val as f64 + 1.0 } else {
292 min_val as f64 + (i + 1) as f64 * bucket_width
293 };
294
295 let count = sorted
296 .iter()
297 .filter(|&&v| {
298 let vf = v as f64;
299 if i == bucket_count - 1 {
300 vf >= lower && vf <= max_val as f64
301 } else {
302 vf >= lower && vf < upper
303 }
304 })
305 .count() as u64;
306
307 buckets.push(LatencyBucket {
308 lower_bound_us: lower.floor() as u64,
309 upper_bound_us: if i == bucket_count - 1 {
310 max_val
311 } else {
312 upper.ceil() as u64
313 },
314 count,
315 });
316 }
317
318 Some(buckets)
319 }
320
321 pub fn fastest_peers(&self, n: usize) -> Vec<(String, f64)> {
325 let mut means: Vec<(String, f64)> = self
326 .peers
327 .iter()
328 .filter(|(_, v)| v.count > 0)
329 .map(|(k, v)| (k.clone(), v.sum_us as f64 / v.count as f64))
330 .collect();
331
332 means.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
333 means.truncate(n);
334 means
335 }
336
337 pub fn slowest_peers(&self, n: usize) -> Vec<(String, f64)> {
341 let mut means: Vec<(String, f64)> = self
342 .peers
343 .iter()
344 .filter(|(_, v)| v.count > 0)
345 .map(|(k, v)| (k.clone(), v.sum_us as f64 / v.count as f64))
346 .collect();
347
348 means.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
349 means.truncate(n);
350 means
351 }
352
353 pub fn remove_peer(&mut self, peer_id: &str) -> bool {
357 self.peers.remove(peer_id).is_some()
358 }
359
360 pub fn peer_count(&self) -> usize {
362 self.peers.len()
363 }
364
365 pub fn global_mean(&self) -> Option<f64> {
369 if self.global_count == 0 {
370 return None;
371 }
372 Some(self.global_sum_us as f64 / self.global_count as f64)
373 }
374
375 pub fn stats(&self) -> LatencyTrackerStats {
377 LatencyTrackerStats {
378 tracked_peers: self.peers.len(),
379 global_samples: self.global_count,
380 global_mean_us: self.global_mean(),
381 }
382 }
383}
384
385#[cfg(test)]
390mod tests {
391 use super::*;
392
393 fn make_tracker() -> PeerLatencyTracker {
394 PeerLatencyTracker::new(1000)
395 }
396
397 #[test]
402 fn record_creates_peer_entry() {
403 let mut t = make_tracker();
404 t.record("p1", 200);
405 assert_eq!(t.peer_count(), 1);
406 }
407
408 #[test]
409 fn record_multiple_samples_same_peer() {
410 let mut t = make_tracker();
411 t.record("p1", 100);
412 t.record("p1", 200);
413 t.record("p1", 300);
414 let entry = t.peers.get("p1").expect("peer should exist");
415 assert_eq!(entry.samples.len(), 3);
416 assert_eq!(entry.count, 3);
417 assert_eq!(entry.sum_us, 600);
418 }
419
420 #[test]
421 fn record_multiple_peers() {
422 let mut t = make_tracker();
423 t.record("p1", 100);
424 t.record("p2", 200);
425 t.record("p3", 300);
426 assert_eq!(t.peer_count(), 3);
427 }
428
429 #[test]
430 fn record_updates_min_max() {
431 let mut t = make_tracker();
432 t.record("p1", 500);
433 t.record("p1", 100);
434 t.record("p1", 900);
435 let entry = t.peers.get("p1").expect("peer should exist");
436 assert_eq!(entry.min_us, 100);
437 assert_eq!(entry.max_us, 900);
438 }
439
440 #[test]
445 fn ring_buffer_eviction() {
446 let mut t = PeerLatencyTracker::new(3);
447 t.record("p1", 10);
448 t.record("p1", 20);
449 t.record("p1", 30);
450 t.record("p1", 40); let entry = t.peers.get("p1").expect("peer should exist");
453 assert_eq!(entry.samples.len(), 3);
454 assert_eq!(entry.count, 4);
455 let mut sorted = entry.samples.clone();
457 sorted.sort_unstable();
458 assert_eq!(sorted, vec![20, 30, 40]);
459 }
460
461 #[test]
462 fn ring_buffer_size_one() {
463 let mut t = PeerLatencyTracker::new(1);
464 t.record("p1", 100);
465 t.record("p1", 200);
466 let entry = t.peers.get("p1").expect("peer should exist");
467 assert_eq!(entry.samples.len(), 1);
468 assert_eq!(entry.samples[0], 200);
469 assert_eq!(entry.count, 2);
470 }
471
472 #[test]
473 fn ring_buffer_full_cycle() {
474 let mut t = PeerLatencyTracker::new(5);
475 for i in 1..=10u64 {
476 t.record("p1", i * 100);
477 }
478 let entry = t.peers.get("p1").expect("peer should exist");
479 assert_eq!(entry.samples.len(), 5);
480 assert_eq!(entry.count, 10);
481 let mut sorted = entry.samples.clone();
483 sorted.sort_unstable();
484 assert_eq!(sorted, vec![600, 700, 800, 900, 1000]);
485 }
486
487 #[test]
492 fn percentile_none_for_unknown_peer() {
493 let t = make_tracker();
494 assert!(t.percentile("nobody", 0.5).is_none());
495 }
496
497 #[test]
498 fn percentile_single_sample() {
499 let mut t = make_tracker();
500 t.record("p1", 500);
501 assert_eq!(t.percentile("p1", 0.0), Some(500));
502 assert_eq!(t.percentile("p1", 0.5), Some(500));
503 assert_eq!(t.percentile("p1", 1.0), Some(500));
504 }
505
506 #[test]
507 fn percentile_p50_even_count() {
508 let mut t = make_tracker();
509 for i in 1..=10u64 {
511 t.record("p1", i * 100);
512 }
513 let p50 = t.percentile("p1", 0.5).expect("should have data");
516 assert_eq!(p50, 550);
517 }
518
519 #[test]
520 fn percentile_p95() {
521 let mut t = make_tracker();
522 for i in 1..=100u64 {
523 t.record("p1", i);
524 }
525 let p95 = t.percentile("p1", 0.95).expect("should have data");
529 assert_eq!(p95, 95);
530 }
531
532 #[test]
533 fn percentile_p99() {
534 let mut t = make_tracker();
535 for i in 1..=100u64 {
536 t.record("p1", i);
537 }
538 let p99 = t.percentile("p1", 0.99).expect("should have data");
542 assert_eq!(p99, 99);
543 }
544
545 #[test]
546 fn percentile_clamped() {
547 let mut t = make_tracker();
548 t.record("p1", 100);
549 t.record("p1", 200);
550 assert_eq!(t.percentile("p1", -1.0), Some(100));
552 assert_eq!(t.percentile("p1", 2.0), Some(200));
554 }
555
556 #[test]
557 fn percentile_p0_returns_min() {
558 let mut t = make_tracker();
559 t.record("p1", 100);
560 t.record("p1", 200);
561 t.record("p1", 300);
562 assert_eq!(t.percentile("p1", 0.0), Some(100));
563 }
564
565 #[test]
566 fn percentile_p100_returns_max() {
567 let mut t = make_tracker();
568 t.record("p1", 100);
569 t.record("p1", 200);
570 t.record("p1", 300);
571 assert_eq!(t.percentile("p1", 1.0), Some(300));
572 }
573
574 #[test]
579 fn mean_none_for_unknown_peer() {
580 let t = make_tracker();
581 assert!(t.mean("nobody").is_none());
582 }
583
584 #[test]
585 fn mean_single_sample() {
586 let mut t = make_tracker();
587 t.record("p1", 500);
588 assert!((t.mean("p1").expect("should have data") - 500.0).abs() < f64::EPSILON);
589 }
590
591 #[test]
592 fn mean_multiple_samples() {
593 let mut t = make_tracker();
594 t.record("p1", 100);
595 t.record("p1", 200);
596 t.record("p1", 300);
597 let m = t.mean("p1").expect("should have data");
598 assert!((m - 200.0).abs() < f64::EPSILON);
599 }
600
601 #[test]
602 fn median_none_for_unknown_peer() {
603 let t = make_tracker();
604 assert!(t.median("nobody").is_none());
605 }
606
607 #[test]
608 fn median_returns_p50() {
609 let mut t = make_tracker();
610 t.record("p1", 100);
611 t.record("p1", 200);
612 t.record("p1", 300);
613 assert_eq!(t.median("p1"), Some(200));
615 }
616
617 #[test]
622 fn histogram_none_for_unknown_peer() {
623 let t = make_tracker();
624 assert!(t.histogram("nobody", 5).is_none());
625 }
626
627 #[test]
628 fn histogram_none_for_zero_buckets() {
629 let mut t = make_tracker();
630 t.record("p1", 100);
631 assert!(t.histogram("p1", 0).is_none());
632 }
633
634 #[test]
635 fn histogram_single_bucket() {
636 let mut t = make_tracker();
637 t.record("p1", 100);
638 t.record("p1", 200);
639 t.record("p1", 300);
640 let hist = t.histogram("p1", 1).expect("should have histogram");
641 assert_eq!(hist.len(), 1);
642 assert_eq!(hist[0].count, 3);
643 }
644
645 #[test]
646 fn histogram_bucket_count_matches() {
647 let mut t = make_tracker();
648 for i in 1..=100u64 {
649 t.record("p1", i);
650 }
651 let hist = t.histogram("p1", 10).expect("should have histogram");
652 assert_eq!(hist.len(), 10);
653 let total: u64 = hist.iter().map(|b| b.count).sum();
655 assert_eq!(total, 100);
656 }
657
658 #[test]
659 fn histogram_all_same_values() {
660 let mut t = make_tracker();
661 for _ in 0..5 {
662 t.record("p1", 42);
663 }
664 let hist = t.histogram("p1", 3).expect("should have histogram");
665 assert_eq!(hist.len(), 3);
666 assert_eq!(hist[0].count, 5);
668 assert_eq!(hist[1].count, 0);
669 assert_eq!(hist[2].count, 0);
670 }
671
672 #[test]
677 fn fastest_peers_ordering() {
678 let mut t = make_tracker();
679 t.record("fast", 100);
680 t.record("medium", 500);
681 t.record("slow", 1000);
682 let fastest = t.fastest_peers(3);
683 assert_eq!(fastest.len(), 3);
684 assert_eq!(fastest[0].0, "fast");
685 assert_eq!(fastest[1].0, "medium");
686 assert_eq!(fastest[2].0, "slow");
687 }
688
689 #[test]
690 fn fastest_peers_truncates() {
691 let mut t = make_tracker();
692 t.record("a", 100);
693 t.record("b", 200);
694 t.record("c", 300);
695 let fastest = t.fastest_peers(2);
696 assert_eq!(fastest.len(), 2);
697 }
698
699 #[test]
700 fn fastest_peers_empty() {
701 let t = make_tracker();
702 let fastest = t.fastest_peers(5);
703 assert!(fastest.is_empty());
704 }
705
706 #[test]
707 fn slowest_peers_ordering() {
708 let mut t = make_tracker();
709 t.record("fast", 100);
710 t.record("medium", 500);
711 t.record("slow", 1000);
712 let slowest = t.slowest_peers(3);
713 assert_eq!(slowest.len(), 3);
714 assert_eq!(slowest[0].0, "slow");
715 assert_eq!(slowest[1].0, "medium");
716 assert_eq!(slowest[2].0, "fast");
717 }
718
719 #[test]
720 fn slowest_peers_truncates() {
721 let mut t = make_tracker();
722 t.record("a", 100);
723 t.record("b", 200);
724 t.record("c", 300);
725 let slowest = t.slowest_peers(1);
726 assert_eq!(slowest.len(), 1);
727 assert_eq!(slowest[0].0, "c");
728 }
729
730 #[test]
735 fn remove_peer_returns_true_when_present() {
736 let mut t = make_tracker();
737 t.record("p1", 100);
738 assert!(t.remove_peer("p1"));
739 assert_eq!(t.peer_count(), 0);
740 }
741
742 #[test]
743 fn remove_peer_returns_false_when_absent() {
744 let mut t = make_tracker();
745 assert!(!t.remove_peer("nobody"));
746 }
747
748 #[test]
749 fn remove_peer_then_reinsert() {
750 let mut t = make_tracker();
751 t.record("p1", 100);
752 t.remove_peer("p1");
753 t.record("p1", 500);
754 let entry = t.peers.get("p1").expect("peer should exist");
755 assert_eq!(entry.count, 1);
756 assert_eq!(entry.samples, vec![500]);
757 }
758
759 #[test]
764 fn global_mean_none_when_empty() {
765 let t = make_tracker();
766 assert!(t.global_mean().is_none());
767 }
768
769 #[test]
770 fn global_mean_single_peer() {
771 let mut t = make_tracker();
772 t.record("p1", 100);
773 t.record("p1", 200);
774 assert!((t.global_mean().expect("should exist") - 150.0).abs() < f64::EPSILON);
775 }
776
777 #[test]
778 fn global_mean_multiple_peers() {
779 let mut t = make_tracker();
780 t.record("p1", 100);
781 t.record("p2", 300);
782 assert!((t.global_mean().expect("should exist") - 200.0).abs() < f64::EPSILON);
784 }
785
786 #[test]
787 fn stats_empty_tracker() {
788 let t = make_tracker();
789 let s = t.stats();
790 assert_eq!(s.tracked_peers, 0);
791 assert_eq!(s.global_samples, 0);
792 assert!(s.global_mean_us.is_none());
793 }
794
795 #[test]
796 fn stats_populated_tracker() {
797 let mut t = make_tracker();
798 t.record("p1", 100);
799 t.record("p1", 200);
800 t.record("p2", 300);
801 let s = t.stats();
802 assert_eq!(s.tracked_peers, 2);
803 assert_eq!(s.global_samples, 3);
804 let mean = s.global_mean_us.expect("should have mean");
805 assert!((mean - 200.0).abs() < f64::EPSILON);
806 }
807
808 #[test]
809 fn stats_after_remove_peer() {
810 let mut t = make_tracker();
811 t.record("p1", 100);
812 t.record("p2", 200);
813 t.remove_peer("p1");
814 let s = t.stats();
815 assert_eq!(s.tracked_peers, 1);
816 assert_eq!(s.global_samples, 2);
818 }
819
820 #[test]
825 fn single_sample_edge_case() {
826 let mut t = make_tracker();
827 t.record("p1", 42);
828 assert_eq!(t.percentile("p1", 0.0), Some(42));
829 assert_eq!(t.percentile("p1", 0.5), Some(42));
830 assert_eq!(t.percentile("p1", 1.0), Some(42));
831 assert_eq!(t.median("p1"), Some(42));
832 assert!((t.mean("p1").expect("mean") - 42.0).abs() < f64::EPSILON);
833 }
834
835 #[test]
836 fn empty_peer_returns_none() {
837 let t = make_tracker();
838 assert!(t.percentile("ghost", 0.5).is_none());
839 assert!(t.mean("ghost").is_none());
840 assert!(t.median("ghost").is_none());
841 assert!(t.histogram("ghost", 5).is_none());
842 }
843
844 #[test]
845 fn peer_count_after_operations() {
846 let mut t = make_tracker();
847 assert_eq!(t.peer_count(), 0);
848 t.record("p1", 100);
849 assert_eq!(t.peer_count(), 1);
850 t.record("p2", 200);
851 assert_eq!(t.peer_count(), 2);
852 t.remove_peer("p1");
853 assert_eq!(t.peer_count(), 1);
854 }
855
856 #[test]
857 fn large_sample_count() {
858 let mut t = PeerLatencyTracker::new(100);
859 for i in 0..500u64 {
860 t.record("p1", i + 1);
861 }
862 let entry = t.peers.get("p1").expect("peer should exist");
863 assert_eq!(entry.samples.len(), 100);
864 assert_eq!(entry.count, 500);
865 let m = t.mean("p1").expect("mean");
867 assert!((m - 250.5).abs() < f64::EPSILON);
868 }
869
870 #[test]
871 fn histogram_two_samples_two_buckets() {
872 let mut t = make_tracker();
873 t.record("p1", 0);
874 t.record("p1", 100);
875 let hist = t.histogram("p1", 2).expect("should have histogram");
876 assert_eq!(hist.len(), 2);
877 let total: u64 = hist.iter().map(|b| b.count).sum();
878 assert_eq!(total, 2);
879 }
880
881 #[test]
882 fn fastest_slowest_with_varied_means() {
883 let mut t = make_tracker();
884 t.record("alpha", 100);
886 t.record("alpha", 200); t.record("beta", 400);
888 t.record("beta", 600); t.record("gamma", 50);
890 t.record("gamma", 50); let fastest = t.fastest_peers(2);
893 assert_eq!(fastest[0].0, "gamma");
894 assert_eq!(fastest[1].0, "alpha");
895
896 let slowest = t.slowest_peers(2);
897 assert_eq!(slowest[0].0, "beta");
898 assert_eq!(slowest[1].0, "alpha");
899 }
900}