1use std::collections::HashMap;
7
8#[derive(Clone, Debug, PartialEq)]
10pub enum GossipEvent {
11 Received {
13 msg_id: u64,
15 from_peer: String,
17 hop_count: u32,
19 },
20 Forwarded {
22 msg_id: u64,
24 to_peers: Vec<String>,
26 },
27 Duplicate {
29 msg_id: u64,
31 from_peer: String,
33 },
34 Expired {
36 msg_id: u64,
38 },
39}
40
41#[derive(Clone, Debug, PartialEq)]
43pub struct MessageTrace {
44 pub msg_id: u64,
46 pub first_seen_tick: u64,
48 pub received_count: u32,
50 pub forward_count: u32,
52 pub duplicate_count: u32,
54 pub min_hop_count: u32,
56}
57
58impl MessageTrace {
59 pub fn redundancy_ratio(&self) -> f64 {
63 self.duplicate_count as f64 / self.received_count.max(1) as f64
64 }
65}
66
67#[derive(Clone, Debug, PartialEq)]
69pub struct GossipMetricsSnapshot {
70 pub total_messages: u64,
72 pub total_duplicates: u64,
74 pub total_forwarded: u64,
76 pub avg_hop_count: f64,
78 pub duplicate_rate: f64,
80}
81
82impl GossipMetricsSnapshot {
83 pub fn estimated_coverage(&self) -> f64 {
88 1.0 - self.duplicate_rate
89 }
90}
91
92#[derive(Debug)]
94pub struct PeerGossipMetrics {
95 pub traces: HashMap<u64, MessageTrace>,
97 pub current_tick: u64,
99}
100
101impl PeerGossipMetrics {
102 pub fn new() -> Self {
104 Self {
105 traces: HashMap::new(),
106 current_tick: 0,
107 }
108 }
109
110 pub fn record_event(&mut self, event: GossipEvent) {
124 match event {
125 GossipEvent::Received {
126 msg_id,
127 from_peer: _,
128 hop_count,
129 } => {
130 let tick = self.current_tick;
131 let trace = self.traces.entry(msg_id).or_insert_with(|| MessageTrace {
132 msg_id,
133 first_seen_tick: tick,
134 received_count: 0,
135 forward_count: 0,
136 duplicate_count: 0,
137 min_hop_count: hop_count,
138 });
139
140 if trace.received_count >= 1 {
142 trace.duplicate_count += 1;
143 }
144
145 trace.received_count += 1;
146
147 if hop_count < trace.min_hop_count {
148 trace.min_hop_count = hop_count;
149 }
150 }
151
152 GossipEvent::Forwarded { msg_id, to_peers } => {
153 if let Some(trace) = self.traces.get_mut(&msg_id) {
154 trace.forward_count += to_peers.len() as u32;
155 }
156 }
157
158 GossipEvent::Duplicate {
159 msg_id,
160 from_peer: _,
161 } => {
162 if let Some(trace) = self.traces.get_mut(&msg_id) {
163 trace.duplicate_count += 1;
164 }
165 }
166
167 GossipEvent::Expired { msg_id: _ } => {
168 }
170 }
171 }
172
173 pub fn advance_tick(&mut self) {
175 self.current_tick += 1;
176 }
177
178 pub fn snapshot(&self) -> GossipMetricsSnapshot {
180 let total_messages = self.traces.len() as u64;
181 let total_duplicates: u64 = self.traces.values().map(|t| t.duplicate_count as u64).sum();
182 let total_forwarded: u64 = self.traces.values().map(|t| t.forward_count as u64).sum();
183
184 let avg_hop_count = if self.traces.is_empty() {
185 0.0
186 } else {
187 let sum: u64 = self.traces.values().map(|t| t.min_hop_count as u64).sum();
188 sum as f64 / total_messages as f64
189 };
190
191 let duplicate_rate = total_duplicates as f64 / total_messages.max(1) as f64;
192
193 GossipMetricsSnapshot {
194 total_messages,
195 total_duplicates,
196 total_forwarded,
197 avg_hop_count,
198 duplicate_rate,
199 }
200 }
201
202 pub fn top_messages_by_redundancy(&self, n: usize) -> Vec<&MessageTrace> {
204 let mut traces: Vec<&MessageTrace> = self.traces.values().collect();
205 traces.sort_by(|a, b| {
206 b.redundancy_ratio()
207 .partial_cmp(&a.redundancy_ratio())
208 .unwrap_or(std::cmp::Ordering::Equal)
209 });
210 traces.truncate(n);
211 traces
212 }
213
214 pub fn message_trace(&self, msg_id: u64) -> Option<&MessageTrace> {
216 self.traces.get(&msg_id)
217 }
218}
219
220impl Default for PeerGossipMetrics {
221 fn default() -> Self {
222 Self::new()
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 fn recv(msg_id: u64, from: &str, hop: u32) -> GossipEvent {
233 GossipEvent::Received {
234 msg_id,
235 from_peer: from.to_string(),
236 hop_count: hop,
237 }
238 }
239
240 fn fwd(msg_id: u64, peers: &[&str]) -> GossipEvent {
241 GossipEvent::Forwarded {
242 msg_id,
243 to_peers: peers.iter().map(|s| s.to_string()).collect(),
244 }
245 }
246
247 fn dup(msg_id: u64, from: &str) -> GossipEvent {
248 GossipEvent::Duplicate {
249 msg_id,
250 from_peer: from.to_string(),
251 }
252 }
253
254 fn exp(msg_id: u64) -> GossipEvent {
255 GossipEvent::Expired { msg_id }
256 }
257
258 #[test]
261 fn received_creates_trace() {
262 let mut m = PeerGossipMetrics::new();
263 m.record_event(recv(1, "peer-A", 2));
264
265 let trace = m.message_trace(1).expect("trace should exist");
266 assert_eq!(trace.msg_id, 1);
267 assert_eq!(trace.received_count, 1);
268 assert_eq!(trace.duplicate_count, 0);
269 assert_eq!(trace.min_hop_count, 2);
270 assert_eq!(trace.first_seen_tick, 0);
271 }
272
273 #[test]
276 fn second_received_increments_duplicate_count() {
277 let mut m = PeerGossipMetrics::new();
278 m.record_event(recv(1, "peer-A", 2));
279 m.record_event(recv(1, "peer-B", 3));
280
281 let trace = m.message_trace(1).expect("trace should exist");
282 assert_eq!(trace.received_count, 2);
283 assert_eq!(trace.duplicate_count, 1);
284 }
285
286 #[test]
289 fn third_received_increments_duplicate_count_again() {
290 let mut m = PeerGossipMetrics::new();
291 m.record_event(recv(1, "peer-A", 2));
292 m.record_event(recv(1, "peer-B", 3));
293 m.record_event(recv(1, "peer-C", 1));
294
295 let trace = m.message_trace(1).expect("trace should exist");
296 assert_eq!(trace.received_count, 3);
297 assert_eq!(trace.duplicate_count, 2);
298 }
299
300 #[test]
303 fn forwarded_updates_forward_count() {
304 let mut m = PeerGossipMetrics::new();
305 m.record_event(recv(2, "peer-A", 1));
306 m.record_event(fwd(2, &["peer-B", "peer-C"]));
307
308 let trace = m.message_trace(2).expect("trace should exist");
309 assert_eq!(trace.forward_count, 2);
310 }
311
312 #[test]
315 fn forwarded_single_peer() {
316 let mut m = PeerGossipMetrics::new();
317 m.record_event(recv(3, "peer-A", 0));
318 m.record_event(fwd(3, &["peer-B"]));
319
320 let trace = m.message_trace(3).expect("trace should exist");
321 assert_eq!(trace.forward_count, 1);
322 }
323
324 #[test]
327 fn explicit_duplicate_increments_count() {
328 let mut m = PeerGossipMetrics::new();
329 m.record_event(recv(4, "peer-A", 1));
330 m.record_event(dup(4, "peer-B"));
331
332 let trace = m.message_trace(4).expect("trace should exist");
333 assert_eq!(trace.duplicate_count, 1);
334 }
335
336 #[test]
339 fn multiple_explicit_duplicates() {
340 let mut m = PeerGossipMetrics::new();
341 m.record_event(recv(5, "peer-A", 2));
342 m.record_event(dup(5, "peer-B"));
343 m.record_event(dup(5, "peer-C"));
344
345 let trace = m.message_trace(5).expect("trace should exist");
346 assert_eq!(trace.duplicate_count, 2);
347 }
348
349 #[test]
352 fn min_hop_count_tracks_minimum() {
353 let mut m = PeerGossipMetrics::new();
354 m.record_event(recv(6, "peer-A", 5));
355 m.record_event(recv(6, "peer-B", 3));
356 m.record_event(recv(6, "peer-C", 7));
357
358 let trace = m.message_trace(6).expect("trace should exist");
359 assert_eq!(trace.min_hop_count, 3);
360 }
361
362 #[test]
365 fn min_hop_count_initial_value() {
366 let mut m = PeerGossipMetrics::new();
367 m.record_event(recv(7, "peer-A", 4));
368
369 let trace = m.message_trace(7).expect("trace should exist");
370 assert_eq!(trace.min_hop_count, 4);
371 }
372
373 #[test]
376 fn redundancy_ratio_correct() {
377 let mut m = PeerGossipMetrics::new();
378 m.record_event(recv(8, "peer-A", 1));
379 m.record_event(recv(8, "peer-B", 1));
380 m.record_event(recv(8, "peer-C", 1));
381 let trace = m.message_trace(8).expect("trace should exist");
383 let expected = 2.0 / 3.0;
384 assert!((trace.redundancy_ratio() - expected).abs() < 1e-10);
385 }
386
387 #[test]
390 fn redundancy_ratio_zero_no_duplicates() {
391 let mut m = PeerGossipMetrics::new();
392 m.record_event(recv(9, "peer-A", 1));
393
394 let trace = m.message_trace(9).expect("trace should exist");
395 assert_eq!(trace.redundancy_ratio(), 0.0);
396 }
397
398 #[test]
401 fn snapshot_totals() {
402 let mut m = PeerGossipMetrics::new();
403 m.record_event(recv(10, "peer-A", 1));
405 m.record_event(recv(10, "peer-B", 2));
406 m.record_event(fwd(10, &["peer-C", "peer-D", "peer-E"]));
407 m.record_event(recv(11, "peer-A", 2));
409 m.record_event(fwd(11, &["peer-F"]));
410
411 let snap = m.snapshot();
412 assert_eq!(snap.total_messages, 2);
413 assert_eq!(snap.total_duplicates, 1);
414 assert_eq!(snap.total_forwarded, 4);
415 }
416
417 #[test]
420 fn snapshot_duplicate_rate() {
421 let mut m = PeerGossipMetrics::new();
422 m.record_event(recv(12, "peer-A", 1));
423 m.record_event(recv(12, "peer-B", 1));
424 m.record_event(recv(13, "peer-A", 1));
425
426 let snap = m.snapshot();
427 let expected = 1.0 / 2.0;
429 assert!((snap.duplicate_rate - expected).abs() < 1e-10);
430 }
431
432 #[test]
435 fn estimated_coverage() {
436 let mut m = PeerGossipMetrics::new();
437 m.record_event(recv(14, "peer-A", 1));
438 m.record_event(recv(14, "peer-B", 1)); let snap = m.snapshot();
441 assert!((snap.estimated_coverage() - 0.0).abs() < 1e-10);
443 }
444
445 #[test]
448 fn estimated_coverage_no_duplicates() {
449 let mut m = PeerGossipMetrics::new();
450 m.record_event(recv(15, "peer-A", 1));
451 m.record_event(recv(16, "peer-B", 1));
452
453 let snap = m.snapshot();
454 assert!((snap.estimated_coverage() - 1.0).abs() < 1e-10);
455 }
456
457 #[test]
460 fn top_messages_by_redundancy_ordered() {
461 let mut m = PeerGossipMetrics::new();
462 m.record_event(recv(20, "peer-A", 1));
464 m.record_event(recv(21, "peer-A", 1));
466 m.record_event(recv(21, "peer-B", 1));
467 m.record_event(recv(22, "peer-A", 1));
469 m.record_event(recv(22, "peer-B", 1));
470 m.record_event(recv(22, "peer-C", 1));
471
472 let top = m.top_messages_by_redundancy(3);
473 assert_eq!(top.len(), 3);
474 assert_eq!(top[0].msg_id, 22);
475 assert_eq!(top[1].msg_id, 21);
476 assert_eq!(top[2].msg_id, 20);
477 }
478
479 #[test]
482 fn top_messages_by_redundancy_truncates() {
483 let mut m = PeerGossipMetrics::new();
484 for id in 30..36_u64 {
485 m.record_event(recv(id, "peer-A", 1));
486 }
487
488 let top = m.top_messages_by_redundancy(3);
489 assert_eq!(top.len(), 3);
490 }
491
492 #[test]
495 fn message_trace_some() {
496 let mut m = PeerGossipMetrics::new();
497 m.record_event(recv(40, "peer-A", 1));
498 assert!(m.message_trace(40).is_some());
499 }
500
501 #[test]
504 fn message_trace_none() {
505 let m = PeerGossipMetrics::new();
506 assert!(m.message_trace(999).is_none());
507 }
508
509 #[test]
512 fn expired_is_noop_trace_survives() {
513 let mut m = PeerGossipMetrics::new();
514 m.record_event(recv(50, "peer-A", 1));
515 m.record_event(exp(50));
516
517 let trace = m.message_trace(50).expect("trace should survive expiry");
518 assert_eq!(trace.received_count, 1);
519 assert_eq!(trace.duplicate_count, 0);
520 }
521
522 #[test]
525 fn advance_tick_increments() {
526 let mut m = PeerGossipMetrics::new();
527 assert_eq!(m.current_tick, 0);
528 m.advance_tick();
529 assert_eq!(m.current_tick, 1);
530 m.advance_tick();
531 assert_eq!(m.current_tick, 2);
532 }
533
534 #[test]
537 fn first_seen_tick_correct() {
538 let mut m = PeerGossipMetrics::new();
539 m.advance_tick(); m.advance_tick(); m.record_event(recv(60, "peer-A", 1));
542
543 let trace = m.message_trace(60).expect("trace should exist");
544 assert_eq!(trace.first_seen_tick, 2);
545 }
546
547 #[test]
550 fn snapshot_avg_hop_count() {
551 let mut m = PeerGossipMetrics::new();
552 m.record_event(recv(70, "peer-A", 2));
553 m.record_event(recv(71, "peer-A", 4));
554
555 let snap = m.snapshot();
556 assert!((snap.avg_hop_count - 3.0).abs() < 1e-10);
558 }
559
560 #[test]
563 fn empty_snapshot() {
564 let m = PeerGossipMetrics::new();
565 let snap = m.snapshot();
566 assert_eq!(snap.total_messages, 0);
567 assert_eq!(snap.total_duplicates, 0);
568 assert_eq!(snap.total_forwarded, 0);
569 assert_eq!(snap.avg_hop_count, 0.0);
570 assert_eq!(snap.duplicate_rate, 0.0);
571 assert_eq!(snap.estimated_coverage(), 1.0);
572 }
573}