1#[derive(Debug, Clone, Copy)]
11pub struct NetworkMetrics {
12 pub loss_ratio: f64,
14 pub late_frame_rate: f64,
16 pub jitter_ms: Option<f64>,
18}
19
20pub struct NetworkConditions {
22 last_sequence: Option<u64>,
23 total_expected: u64,
24 observed_frames: u64,
25 lost_frames: u64,
26 late_frames: u64,
27 last_arrival: Option<u64>,
28 last_interval: Option<u64>,
29 total_jitter_ns: u128,
30 jitter_samples: u64,
31 max_loss_gap: u64,
32}
33
34impl NetworkConditions {
35 pub fn new() -> Self {
37 Self {
38 last_sequence: None,
39 total_expected: 0,
40 observed_frames: 0,
41 lost_frames: 0,
42 late_frames: 0,
43 last_arrival: None,
44 last_interval: None,
45 total_jitter_ns: 0,
46 jitter_samples: 0,
47 max_loss_gap: 0,
48 }
49 }
50
51 pub fn record_frame(&mut self, sequence: u64, arrival_us: u64, deadline_us: u64) {
58 if let Some(last_seq) = self.last_sequence {
59 if sequence <= last_seq {
60 return;
62 }
63 let delta = sequence - last_seq;
64 self.total_expected = self.total_expected.saturating_add(delta);
65 if delta > 1 {
66 self.lost_frames = self.lost_frames.saturating_add(delta - 1);
67 self.max_loss_gap = self.max_loss_gap.max(delta - 1);
68 }
69 } else {
70 self.total_expected = self.total_expected.saturating_add(1);
71 }
72
73 self.last_sequence = Some(sequence);
74 self.observed_frames = self.observed_frames.saturating_add(1);
75
76 if arrival_us > deadline_us {
77 self.late_frames = self.late_frames.saturating_add(1);
78 }
79
80 if let Some(last) = self.last_arrival {
81 let interval = arrival_us.saturating_sub(last);
82 if let Some(prev_interval) = self.last_interval {
83 let jitter = if interval > prev_interval {
84 interval - prev_interval
85 } else {
86 prev_interval - interval
87 };
88 self.total_jitter_ns = self.total_jitter_ns.saturating_add(jitter as u128);
89 self.jitter_samples = self.jitter_samples.saturating_add(1);
90 }
91 self.last_interval = Some(interval);
92 }
93 self.last_arrival = Some(arrival_us);
94 }
95
96 pub fn metrics(&self) -> NetworkMetrics {
98 let total_expected = self.total_expected.max(self.observed_frames);
99 let loss_ratio = if total_expected == 0 {
100 0.0
101 } else {
102 self.lost_frames as f64 / total_expected as f64
103 };
104
105 let late_frame_rate = if self.observed_frames == 0 {
106 0.0
107 } else {
108 self.late_frames as f64 / self.observed_frames as f64
109 };
110
111 let jitter_ms = if self.jitter_samples == 0 {
112 None
113 } else {
114 Some(self.total_jitter_ns as f64 / self.jitter_samples as f64 / 1000.0)
115 };
116
117 NetworkMetrics {
118 loss_ratio,
119 late_frame_rate,
120 jitter_ms,
121 }
122 }
123
124 pub fn max_loss_gap(&self) -> u64 {
126 self.max_loss_gap
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn loss_ratio_accounts_for_missing_sequences() {
136 let mut net = NetworkConditions::new();
137 net.record_frame(1, 0, 1);
138 net.record_frame(2, 1_000, 2_000);
139 net.record_frame(4, 3_000, 4_000);
140 let metrics = net.metrics();
141 assert!((metrics.loss_ratio - (1.0 / 4.0)).abs() < f64::EPSILON);
142 }
143
144 #[test]
145 fn late_frame_rate_counts_deadlines() {
146 let mut net = NetworkConditions::new();
147 net.record_frame(1, 0, 0);
148 net.record_frame(2, 5_000, 3_000);
149 net.record_frame(3, 6_000, 6_000);
150 let metrics = net.metrics();
151 assert!((metrics.late_frame_rate - (1.0 / 3.0)).abs() < f64::EPSILON);
152 }
153
154 #[test]
155 fn jitter_ms_average() {
156 let mut net = NetworkConditions::new();
157 net.record_frame(1, 0, 0);
158 net.record_frame(2, 1_000, 2_000);
159 net.record_frame(3, 2_500, 4_000);
160 net.record_frame(4, 3_900, 5_000);
161 let metrics = net.metrics();
162 assert_eq!(metrics.jitter_ms, Some(0.3));
164 }
165}