1#![allow(
2 clippy::cast_possible_wrap,
3 reason = "M175: session uptime — `started_at.elapsed().as_secs() as i64` wraps only after ~292 billion years"
4)]
5
6use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
14use std::time::Instant;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
22pub enum MetricKind {
23 Counter,
25 Gauge,
27}
28
29#[derive(Debug, Clone, Copy)]
35pub struct SessionStatsMetric {
36 pub name: &'static str,
38 pub kind: MetricKind,
40}
41
42pub const NET_BYTES_SENT: usize = 0;
50pub const NET_BYTES_RECV: usize = 1;
52pub const NET_NUM_CONNECTIONS: usize = 2;
54pub const NET_NUM_HALF_OPEN: usize = 3;
56pub const NET_NUM_TCP_PEERS: usize = 4;
58pub const NET_NUM_UTP_PEERS: usize = 5;
60pub const NET_NUM_TCP_CONNECTIONS: usize = 6;
62pub const NET_NUM_UTP_CONNECTIONS: usize = 7;
64pub const NET_TCP_BYTES_SENT: usize = 8;
66pub const NET_TCP_BYTES_RECV: usize = 9;
68pub const NET_UTP_BYTES_SENT: usize = 10;
70pub const NET_UTP_BYTES_RECV: usize = 11;
72
73pub const DISK_READ_COUNT: usize = 12;
77pub const DISK_WRITE_COUNT: usize = 13;
79pub const DISK_READ_BYTES: usize = 14;
81pub const DISK_WRITE_BYTES: usize = 15;
83pub const DISK_CACHE_HITS: usize = 16;
85pub const DISK_CACHE_MISSES: usize = 17;
87pub const DISK_QUEUE_DEPTH: usize = 18;
89pub const DISK_JOB_TIME_US: usize = 19;
91pub const DISK_WRITE_BUFFER_BYTES: usize = 20;
93pub const DISK_HASH_COUNT: usize = 21;
95
96pub const DHT_NODES: usize = 22;
100pub const DHT_LOOKUPS: usize = 23;
102pub const DHT_BYTES_IN: usize = 24;
104pub const DHT_BYTES_OUT: usize = 25;
106pub const DHT_NODES_V4: usize = 26;
108pub const DHT_NODES_V6: usize = 27;
110pub const DHT_ANNOUNCE_COUNT: usize = 28;
112
113pub const PEER_NUM_UNCHOKED: usize = 29;
117pub const PEER_NUM_INTERESTED: usize = 30;
119pub const PEER_NUM_UPLOADING: usize = 31;
121pub const PEER_NUM_DOWNLOADING: usize = 32;
123pub const PEER_NUM_SEEDING_TORRENTS: usize = 33;
125pub const PEER_NUM_DOWNLOADING_TORRENTS: usize = 34;
127pub const PEER_NUM_CHECKING_TORRENTS: usize = 35;
129pub const PEER_NUM_PAUSED_TORRENTS: usize = 36;
131pub const PEER_PEERS_CONNECTED: usize = 37;
133pub const PEER_PEERS_AVAILABLE: usize = 38;
135pub const PEER_NUM_WEB_SEEDS: usize = 39;
137pub const PEER_NUM_BANNED: usize = 40;
139
140pub const PROTO_PIECES_DOWNLOADED: usize = 41;
144pub const PROTO_PIECES_UPLOADED: usize = 42;
146pub const PROTO_HASHFAILS: usize = 43;
148pub const PROTO_WASTE_BYTES: usize = 44;
150pub const PROTO_PIECE_REQUESTS: usize = 45;
152pub const PROTO_PIECE_REJECTS: usize = 46;
154pub const PROTO_HANDSHAKES_IN: usize = 47;
156pub const PROTO_HANDSHAKES_OUT: usize = 48;
158pub const PROTO_PEX_MESSAGES_IN: usize = 49;
160pub const PROTO_PEX_MESSAGES_OUT: usize = 50;
162pub const PROTO_TRACKER_ANNOUNCES: usize = 51;
164pub const PROTO_TRACKER_ERRORS: usize = 52;
166pub const PROTO_METADATA_REQUESTS: usize = 53;
168pub const PROTO_METADATA_RECEIVES: usize = 54;
170
171pub const BW_UPLOAD_RATE: usize = 55;
175pub const BW_DOWNLOAD_RATE: usize = 56;
177pub const BW_UPLOAD_RATE_TCP: usize = 57;
179pub const BW_DOWNLOAD_RATE_TCP: usize = 58;
181pub const BW_UPLOAD_RATE_UTP: usize = 59;
183pub const BW_DOWNLOAD_RATE_UTP: usize = 60;
185pub const BW_PAYLOAD_UPLOAD_RATE: usize = 61;
187pub const BW_PAYLOAD_DOWNLOAD_RATE: usize = 62;
189pub const BW_TOTAL_UPLOADED: usize = 63;
191pub const BW_TOTAL_DOWNLOADED: usize = 64;
193
194pub const SES_ACTIVE_TORRENTS: usize = 65;
198pub const SES_NUM_TORRENTS: usize = 66;
200pub const SES_UPTIME_SECS: usize = 67;
202pub const SES_IP_FILTER_BLOCKED: usize = 68;
204pub const SES_QUEUE_PAUSED_BY_AUTO: usize = 69;
206
207pub const DIAGNOSTIC_COUNTERS_START: usize = 70;
210
211pub const EVENT_TX_HIGH_WATER: usize = 70;
224pub const DISPATCH_TX_HIGH_WATER: usize = 71;
226pub const PEER_WAKE_EVENTS_TOTAL: usize = 72;
228pub const PEER_DRAIN_ITEMS_TOTAL: usize = 73;
230
231pub const DISPATCH_ACQUIRE_TOTAL: usize = 74;
235pub const DISPATCH_ACQUIRE_NONE_TOTAL: usize = 75;
237pub const DISPATCH_ACQUIRE_US: usize = 76;
239pub const DISPATCH_NOTIFY_WAKEUP_TOTAL: usize = 77;
241pub const DISPATCH_PEER_CONNECT_TOTAL: usize = 78;
243pub const DISPATCH_PEER_DISCONNECT_TOTAL: usize = 79;
245pub const DISPATCH_ACQUIRE_RTT_US: usize = 80;
247pub const DISPATCH_NOTIFY_WAIT_US: usize = 81;
249pub const DISPATCH_TICK_WAKE_SKIPPED: usize = 82;
255pub const DISPATCH_WALK_SKIPPED: usize = 83;
262pub const DISPATCH_CURSOR_RESUMED: usize = 84;
266
267pub const REMOTE_UNCHOKE_TOTAL: usize = 85;
274pub const REMOTE_RECHOKE_TOTAL: usize = 86;
276pub const REMOTE_UNCHOKE_DURATION_SUM_MS: usize = 87;
279pub const TARGET_DEPTH_SUM: usize = 88;
282pub const TARGET_DEPTH_SAMPLES: usize = 89;
284pub const TARGET_DEPTH_BELOW_32: usize = 90;
286pub const FIRST_BLOCK_LATENCY_SUM_US: usize = 91;
289pub const FIRST_BLOCK_LATENCY_COUNT: usize = 92;
291pub const PEER_LIFETIME_SUM_MS: usize = 93;
294pub const PEER_LIFETIME_COUNT: usize = 94;
296
297pub const PIECE_STEALS_TOTAL: usize = 95;
301pub const CHOKE_ROTATION_EVICTIONS_TOTAL: usize = 96;
303pub const CONNECT_FAILURES_TOTAL: usize = 97;
305pub const DATA_TIMEOUT_EVICTIONS_TOTAL: usize = 98;
307
308pub const NUM_METRICS: usize = 99;
310
311#[must_use]
319pub fn session_stats_metrics() -> &'static [SessionStatsMetric] {
320 use MetricKind::{Counter, Gauge};
321 static METRICS: [SessionStatsMetric; NUM_METRICS] = [
322 SessionStatsMetric {
324 name: "net.bytes_sent",
325 kind: Counter,
326 },
327 SessionStatsMetric {
328 name: "net.bytes_recv",
329 kind: Counter,
330 },
331 SessionStatsMetric {
332 name: "net.num_connections",
333 kind: Gauge,
334 },
335 SessionStatsMetric {
336 name: "net.num_half_open",
337 kind: Gauge,
338 },
339 SessionStatsMetric {
340 name: "net.num_tcp_peers",
341 kind: Gauge,
342 },
343 SessionStatsMetric {
344 name: "net.num_utp_peers",
345 kind: Gauge,
346 },
347 SessionStatsMetric {
348 name: "net.num_tcp_connections",
349 kind: Gauge,
350 },
351 SessionStatsMetric {
352 name: "net.num_utp_connections",
353 kind: Gauge,
354 },
355 SessionStatsMetric {
356 name: "net.tcp_bytes_sent",
357 kind: Counter,
358 },
359 SessionStatsMetric {
360 name: "net.tcp_bytes_recv",
361 kind: Counter,
362 },
363 SessionStatsMetric {
364 name: "net.utp_bytes_sent",
365 kind: Counter,
366 },
367 SessionStatsMetric {
368 name: "net.utp_bytes_recv",
369 kind: Counter,
370 },
371 SessionStatsMetric {
373 name: "disk.read_count",
374 kind: Counter,
375 },
376 SessionStatsMetric {
377 name: "disk.write_count",
378 kind: Counter,
379 },
380 SessionStatsMetric {
381 name: "disk.read_bytes",
382 kind: Counter,
383 },
384 SessionStatsMetric {
385 name: "disk.write_bytes",
386 kind: Counter,
387 },
388 SessionStatsMetric {
389 name: "disk.cache_hits",
390 kind: Counter,
391 },
392 SessionStatsMetric {
393 name: "disk.cache_misses",
394 kind: Counter,
395 },
396 SessionStatsMetric {
397 name: "disk.queue_depth",
398 kind: Gauge,
399 },
400 SessionStatsMetric {
401 name: "disk.job_time_us",
402 kind: Counter,
403 },
404 SessionStatsMetric {
405 name: "disk.write_buffer_bytes",
406 kind: Gauge,
407 },
408 SessionStatsMetric {
409 name: "disk.hash_count",
410 kind: Counter,
411 },
412 SessionStatsMetric {
414 name: "dht.nodes",
415 kind: Gauge,
416 },
417 SessionStatsMetric {
418 name: "dht.lookups",
419 kind: Counter,
420 },
421 SessionStatsMetric {
422 name: "dht.bytes_in",
423 kind: Counter,
424 },
425 SessionStatsMetric {
426 name: "dht.bytes_out",
427 kind: Counter,
428 },
429 SessionStatsMetric {
430 name: "dht.nodes_v4",
431 kind: Gauge,
432 },
433 SessionStatsMetric {
434 name: "dht.nodes_v6",
435 kind: Gauge,
436 },
437 SessionStatsMetric {
438 name: "dht.announce_count",
439 kind: Counter,
440 },
441 SessionStatsMetric {
443 name: "peer.num_unchoked",
444 kind: Gauge,
445 },
446 SessionStatsMetric {
447 name: "peer.num_interested",
448 kind: Gauge,
449 },
450 SessionStatsMetric {
451 name: "peer.num_uploading",
452 kind: Gauge,
453 },
454 SessionStatsMetric {
455 name: "peer.num_downloading",
456 kind: Gauge,
457 },
458 SessionStatsMetric {
459 name: "peer.num_seeding_torrents",
460 kind: Gauge,
461 },
462 SessionStatsMetric {
463 name: "peer.num_downloading_torrents",
464 kind: Gauge,
465 },
466 SessionStatsMetric {
467 name: "peer.num_checking_torrents",
468 kind: Gauge,
469 },
470 SessionStatsMetric {
471 name: "peer.num_paused_torrents",
472 kind: Gauge,
473 },
474 SessionStatsMetric {
475 name: "peer.peers_connected",
476 kind: Gauge,
477 },
478 SessionStatsMetric {
479 name: "peer.peers_available",
480 kind: Gauge,
481 },
482 SessionStatsMetric {
483 name: "peer.num_web_seeds",
484 kind: Gauge,
485 },
486 SessionStatsMetric {
487 name: "peer.num_banned",
488 kind: Gauge,
489 },
490 SessionStatsMetric {
492 name: "proto.pieces_downloaded",
493 kind: Counter,
494 },
495 SessionStatsMetric {
496 name: "proto.pieces_uploaded",
497 kind: Counter,
498 },
499 SessionStatsMetric {
500 name: "proto.hashfails",
501 kind: Counter,
502 },
503 SessionStatsMetric {
504 name: "proto.waste_bytes",
505 kind: Counter,
506 },
507 SessionStatsMetric {
508 name: "proto.piece_requests",
509 kind: Counter,
510 },
511 SessionStatsMetric {
512 name: "proto.piece_rejects",
513 kind: Counter,
514 },
515 SessionStatsMetric {
516 name: "proto.handshakes_in",
517 kind: Counter,
518 },
519 SessionStatsMetric {
520 name: "proto.handshakes_out",
521 kind: Counter,
522 },
523 SessionStatsMetric {
524 name: "proto.pex_messages_in",
525 kind: Counter,
526 },
527 SessionStatsMetric {
528 name: "proto.pex_messages_out",
529 kind: Counter,
530 },
531 SessionStatsMetric {
532 name: "proto.tracker_announces",
533 kind: Counter,
534 },
535 SessionStatsMetric {
536 name: "proto.tracker_errors",
537 kind: Counter,
538 },
539 SessionStatsMetric {
540 name: "proto.metadata_requests",
541 kind: Counter,
542 },
543 SessionStatsMetric {
544 name: "proto.metadata_receives",
545 kind: Counter,
546 },
547 SessionStatsMetric {
549 name: "bw.upload_rate",
550 kind: Gauge,
551 },
552 SessionStatsMetric {
553 name: "bw.download_rate",
554 kind: Gauge,
555 },
556 SessionStatsMetric {
557 name: "bw.upload_rate_tcp",
558 kind: Gauge,
559 },
560 SessionStatsMetric {
561 name: "bw.download_rate_tcp",
562 kind: Gauge,
563 },
564 SessionStatsMetric {
565 name: "bw.upload_rate_utp",
566 kind: Gauge,
567 },
568 SessionStatsMetric {
569 name: "bw.download_rate_utp",
570 kind: Gauge,
571 },
572 SessionStatsMetric {
573 name: "bw.payload_upload_rate",
574 kind: Gauge,
575 },
576 SessionStatsMetric {
577 name: "bw.payload_download_rate",
578 kind: Gauge,
579 },
580 SessionStatsMetric {
581 name: "bw.total_uploaded",
582 kind: Counter,
583 },
584 SessionStatsMetric {
585 name: "bw.total_downloaded",
586 kind: Counter,
587 },
588 SessionStatsMetric {
590 name: "ses.active_torrents",
591 kind: Gauge,
592 },
593 SessionStatsMetric {
594 name: "ses.num_torrents",
595 kind: Gauge,
596 },
597 SessionStatsMetric {
598 name: "ses.uptime_secs",
599 kind: Gauge,
600 },
601 SessionStatsMetric {
602 name: "ses.ip_filter_blocked",
603 kind: Counter,
604 },
605 SessionStatsMetric {
606 name: "ses.queue_paused_by_auto",
607 kind: Counter,
608 },
609 SessionStatsMetric {
611 name: "perf.event_tx_high_water",
612 kind: Gauge,
613 },
614 SessionStatsMetric {
615 name: "perf.dispatch_tx_high_water",
616 kind: Gauge,
617 },
618 SessionStatsMetric {
619 name: "perf.peer_wake_events_total",
620 kind: Counter,
621 },
622 SessionStatsMetric {
623 name: "perf.peer_drain_items_total",
624 kind: Counter,
625 },
626 SessionStatsMetric {
628 name: "dispatch.acquire_total",
629 kind: Counter,
630 },
631 SessionStatsMetric {
632 name: "dispatch.acquire_none_total",
633 kind: Counter,
634 },
635 SessionStatsMetric {
636 name: "dispatch.acquire_us",
637 kind: Counter,
638 },
639 SessionStatsMetric {
640 name: "dispatch.notify_wakeup_total",
641 kind: Counter,
642 },
643 SessionStatsMetric {
644 name: "dispatch.peer_connect_total",
645 kind: Counter,
646 },
647 SessionStatsMetric {
648 name: "dispatch.peer_disconnect_total",
649 kind: Counter,
650 },
651 SessionStatsMetric {
652 name: "dispatch.acquire_rtt_us",
653 kind: Counter,
654 },
655 SessionStatsMetric {
656 name: "dispatch.notify_wait_us",
657 kind: Counter,
658 },
659 SessionStatsMetric {
660 name: "dispatch.tick_wake_skipped",
661 kind: Counter,
662 },
663 SessionStatsMetric {
664 name: "dispatch.walk_skipped",
665 kind: Counter,
666 },
667 SessionStatsMetric {
668 name: "dispatch.cursor_resumed",
669 kind: Counter,
670 },
671 SessionStatsMetric {
673 name: "peer.remote_unchoke_total",
674 kind: Counter,
675 },
676 SessionStatsMetric {
677 name: "peer.remote_rechoke_total",
678 kind: Counter,
679 },
680 SessionStatsMetric {
681 name: "peer.remote_unchoke_duration_sum_ms",
682 kind: Counter,
683 },
684 SessionStatsMetric {
685 name: "peer.target_depth_sum",
686 kind: Counter,
687 },
688 SessionStatsMetric {
689 name: "peer.target_depth_samples",
690 kind: Counter,
691 },
692 SessionStatsMetric {
693 name: "peer.target_depth_below_32",
694 kind: Counter,
695 },
696 SessionStatsMetric {
697 name: "peer.first_block_latency_sum_us",
698 kind: Counter,
699 },
700 SessionStatsMetric {
701 name: "peer.first_block_latency_count",
702 kind: Counter,
703 },
704 SessionStatsMetric {
705 name: "peer.lifetime_sum_ms",
706 kind: Counter,
707 },
708 SessionStatsMetric {
709 name: "peer.lifetime_count",
710 kind: Counter,
711 },
712 SessionStatsMetric {
714 name: "peer.piece_steals_total",
715 kind: Counter,
716 },
717 SessionStatsMetric {
718 name: "peer.choke_rotation_evictions_total",
719 kind: Counter,
720 },
721 SessionStatsMetric {
722 name: "peer.connect_failures_total",
723 kind: Counter,
724 },
725 SessionStatsMetric {
726 name: "peer.data_timeout_evictions_total",
727 kind: Counter,
728 },
729 ];
730 &METRICS
731}
732
733pub struct SessionCounters {
742 values: [AtomicI64; NUM_METRICS],
743 started_at: Instant,
744 prev_bytes_sent: AtomicI64,
745 prev_bytes_recv: AtomicI64,
746 diagnostics: AtomicBool,
747}
748
749impl SessionCounters {
750 #[must_use]
753 pub fn new() -> Self {
754 Self {
755 values: std::array::from_fn(|_| AtomicI64::new(0)),
756 started_at: Instant::now(),
757 prev_bytes_sent: AtomicI64::new(0),
758 prev_bytes_recv: AtomicI64::new(0),
759 diagnostics: AtomicBool::new(false),
760 }
761 }
762
763 #[must_use]
765 pub fn new_with_diagnostics(enabled: bool) -> Self {
766 Self {
767 values: std::array::from_fn(|_| AtomicI64::new(0)),
768 started_at: Instant::now(),
769 prev_bytes_sent: AtomicI64::new(0),
770 prev_bytes_recv: AtomicI64::new(0),
771 diagnostics: AtomicBool::new(enabled),
772 }
773 }
774
775 #[must_use]
778 pub fn diagnostics_enabled(&self) -> bool {
779 self.diagnostics.load(Ordering::Relaxed)
780 }
781
782 #[inline]
784 pub fn inc(&self, metric: usize, delta: i64) {
785 debug_assert!(metric < NUM_METRICS);
786 self.values[metric].fetch_add(delta, Ordering::Relaxed);
787 }
788
789 #[inline]
792 pub fn inc_diag(&self, metric: usize, delta: i64) {
793 debug_assert!(metric >= DIAGNOSTIC_COUNTERS_START);
794 if self.diagnostics.load(Ordering::Relaxed) {
795 self.values[metric].fetch_add(delta, Ordering::Relaxed);
796 }
797 }
798
799 #[inline]
801 pub fn set(&self, metric: usize, value: i64) {
802 debug_assert!(metric < NUM_METRICS);
803 self.values[metric].store(value, Ordering::Relaxed);
804 }
805
806 #[inline]
809 pub fn set_max_diag(&self, metric: usize, value: i64) {
810 debug_assert!(metric >= DIAGNOSTIC_COUNTERS_START);
811 if self.diagnostics.load(Ordering::Relaxed) {
812 self.set_max(metric, value);
813 }
814 }
815
816 #[inline]
822 pub fn set_max(&self, metric: usize, value: i64) {
823 debug_assert!(metric < NUM_METRICS);
824 let cell = &self.values[metric];
825 let mut cur = cell.load(Ordering::Relaxed);
826 while value > cur {
827 match cell.compare_exchange_weak(cur, value, Ordering::Relaxed, Ordering::Relaxed) {
828 Ok(_) => return,
829 Err(observed) => cur = observed,
830 }
831 }
832 }
833
834 #[inline]
836 pub fn get(&self, metric: usize) -> i64 {
837 debug_assert!(metric < NUM_METRICS);
838 self.values[metric].load(Ordering::Relaxed)
839 }
840
841 pub fn snapshot(&self) -> Vec<i64> {
846 let mut vals: Vec<i64> = self
847 .values
848 .iter()
849 .map(|a| a.load(Ordering::Relaxed))
850 .collect();
851
852 vals[SES_UPTIME_SECS] = self.started_at.elapsed().as_secs() as i64;
854
855 let cur_sent = vals[NET_BYTES_SENT];
857 let cur_recv = vals[NET_BYTES_RECV];
858 let prev_sent = self.prev_bytes_sent.swap(cur_sent, Ordering::Relaxed);
859 let prev_recv = self.prev_bytes_recv.swap(cur_recv, Ordering::Relaxed);
860 vals[BW_UPLOAD_RATE] = cur_sent.saturating_sub(prev_sent);
861 vals[BW_DOWNLOAD_RATE] = cur_recv.saturating_sub(prev_recv);
862
863 vals
864 }
865
866 pub fn len(&self) -> usize {
868 NUM_METRICS
869 }
870
871 pub fn is_empty(&self) -> bool {
873 false
874 }
875
876 pub fn uptime_secs(&self) -> u64 {
878 self.started_at.elapsed().as_secs()
879 }
880}
881
882impl Default for SessionCounters {
883 fn default() -> Self {
884 Self::new()
885 }
886}
887
888#[cfg(test)]
893mod tests {
894 use super::*;
895 use std::collections::HashSet;
896
897 #[test]
898 fn metrics_registry_has_correct_count() {
899 assert_eq!(session_stats_metrics().len(), NUM_METRICS);
900 }
901
902 #[test]
903 fn all_metric_names_are_unique() {
904 let names: HashSet<&str> = session_stats_metrics().iter().map(|m| m.name).collect();
905 assert_eq!(names.len(), NUM_METRICS);
906 }
907
908 #[test]
909 fn all_metric_names_have_category_prefix() {
910 for m in session_stats_metrics() {
911 assert!(
912 m.name.contains('.'),
913 "metric name {:?} has no category prefix",
914 m.name
915 );
916 }
917 }
918
919 #[test]
920 fn counter_inc_and_get() {
921 let c = SessionCounters::new();
922 c.inc(NET_BYTES_SENT, 5);
923 assert_eq!(c.get(NET_BYTES_SENT), 5);
924 c.inc(NET_BYTES_SENT, 3);
925 assert_eq!(c.get(NET_BYTES_SENT), 8);
926 }
927
928 #[test]
929 fn gauge_set_and_get() {
930 let c = SessionCounters::new();
931 c.set(NET_NUM_CONNECTIONS, 42);
932 assert_eq!(c.get(NET_NUM_CONNECTIONS), 42);
933 c.set(NET_NUM_CONNECTIONS, 0);
934 assert_eq!(c.get(NET_NUM_CONNECTIONS), 0);
935 }
936
937 #[test]
938 fn snapshot_returns_all_values() {
939 let c = SessionCounters::new();
940 c.inc(NET_BYTES_SENT, 100);
941 c.set(DHT_NODES, 50);
942 c.inc(PROTO_HASHFAILS, 3);
943 let snap = c.snapshot();
944 assert_eq!(snap.len(), NUM_METRICS);
945 assert_eq!(snap[NET_BYTES_SENT], 100);
946 assert_eq!(snap[DHT_NODES], 50);
947 assert_eq!(snap[PROTO_HASHFAILS], 3);
948 }
949
950 #[test]
951 fn snapshot_includes_uptime() {
952 let c = SessionCounters::new();
953 let snap = c.snapshot();
955 assert!(snap[SES_UPTIME_SECS] >= 0);
956 }
957
958 #[test]
959 fn counters_are_send_and_sync() {
960 fn assert_send_sync<T: Send + Sync>() {}
961 assert_send_sync::<SessionCounters>();
962 }
963
964 #[test]
965 fn metric_kind_serializes() {
966 let counter_json = serde_json::to_string(&MetricKind::Counter).unwrap();
967 let gauge_json = serde_json::to_string(&MetricKind::Gauge).unwrap();
968 assert_eq!(
969 serde_json::from_str::<MetricKind>(&counter_json).unwrap(),
970 MetricKind::Counter
971 );
972 assert_eq!(
973 serde_json::from_str::<MetricKind>(&gauge_json).unwrap(),
974 MetricKind::Gauge
975 );
976 }
977
978 #[test]
979 fn metric_index_constants_in_range() {
980 let indices = [
981 NET_BYTES_SENT,
982 NET_BYTES_RECV,
983 NET_NUM_CONNECTIONS,
984 NET_NUM_HALF_OPEN,
985 NET_NUM_TCP_PEERS,
986 NET_NUM_UTP_PEERS,
987 NET_NUM_TCP_CONNECTIONS,
988 NET_NUM_UTP_CONNECTIONS,
989 NET_TCP_BYTES_SENT,
990 NET_TCP_BYTES_RECV,
991 NET_UTP_BYTES_SENT,
992 NET_UTP_BYTES_RECV,
993 DISK_READ_COUNT,
994 DISK_WRITE_COUNT,
995 DISK_READ_BYTES,
996 DISK_WRITE_BYTES,
997 DISK_CACHE_HITS,
998 DISK_CACHE_MISSES,
999 DISK_QUEUE_DEPTH,
1000 DISK_JOB_TIME_US,
1001 DISK_WRITE_BUFFER_BYTES,
1002 DISK_HASH_COUNT,
1003 DHT_NODES,
1004 DHT_LOOKUPS,
1005 DHT_BYTES_IN,
1006 DHT_BYTES_OUT,
1007 DHT_NODES_V4,
1008 DHT_NODES_V6,
1009 DHT_ANNOUNCE_COUNT,
1010 PEER_NUM_UNCHOKED,
1011 PEER_NUM_INTERESTED,
1012 PEER_NUM_UPLOADING,
1013 PEER_NUM_DOWNLOADING,
1014 PEER_NUM_SEEDING_TORRENTS,
1015 PEER_NUM_DOWNLOADING_TORRENTS,
1016 PEER_NUM_CHECKING_TORRENTS,
1017 PEER_NUM_PAUSED_TORRENTS,
1018 PEER_PEERS_CONNECTED,
1019 PEER_PEERS_AVAILABLE,
1020 PEER_NUM_WEB_SEEDS,
1021 PEER_NUM_BANNED,
1022 PROTO_PIECES_DOWNLOADED,
1023 PROTO_PIECES_UPLOADED,
1024 PROTO_HASHFAILS,
1025 PROTO_WASTE_BYTES,
1026 PROTO_PIECE_REQUESTS,
1027 PROTO_PIECE_REJECTS,
1028 PROTO_HANDSHAKES_IN,
1029 PROTO_HANDSHAKES_OUT,
1030 PROTO_PEX_MESSAGES_IN,
1031 PROTO_PEX_MESSAGES_OUT,
1032 PROTO_TRACKER_ANNOUNCES,
1033 PROTO_TRACKER_ERRORS,
1034 PROTO_METADATA_REQUESTS,
1035 PROTO_METADATA_RECEIVES,
1036 BW_UPLOAD_RATE,
1037 BW_DOWNLOAD_RATE,
1038 BW_UPLOAD_RATE_TCP,
1039 BW_DOWNLOAD_RATE_TCP,
1040 BW_UPLOAD_RATE_UTP,
1041 BW_DOWNLOAD_RATE_UTP,
1042 BW_PAYLOAD_UPLOAD_RATE,
1043 BW_PAYLOAD_DOWNLOAD_RATE,
1044 BW_TOTAL_UPLOADED,
1045 BW_TOTAL_DOWNLOADED,
1046 SES_ACTIVE_TORRENTS,
1047 SES_NUM_TORRENTS,
1048 SES_UPTIME_SECS,
1049 SES_IP_FILTER_BLOCKED,
1050 SES_QUEUE_PAUSED_BY_AUTO,
1051 EVENT_TX_HIGH_WATER,
1052 DISPATCH_TX_HIGH_WATER,
1053 PEER_WAKE_EVENTS_TOTAL,
1054 PEER_DRAIN_ITEMS_TOTAL,
1055 DISPATCH_ACQUIRE_TOTAL,
1056 DISPATCH_ACQUIRE_NONE_TOTAL,
1057 DISPATCH_ACQUIRE_US,
1058 DISPATCH_NOTIFY_WAKEUP_TOTAL,
1059 DISPATCH_PEER_CONNECT_TOTAL,
1060 DISPATCH_PEER_DISCONNECT_TOTAL,
1061 DISPATCH_ACQUIRE_RTT_US,
1062 DISPATCH_NOTIFY_WAIT_US,
1063 DISPATCH_TICK_WAKE_SKIPPED,
1064 DISPATCH_WALK_SKIPPED,
1065 DISPATCH_CURSOR_RESUMED,
1066 REMOTE_UNCHOKE_TOTAL,
1067 REMOTE_RECHOKE_TOTAL,
1068 REMOTE_UNCHOKE_DURATION_SUM_MS,
1069 TARGET_DEPTH_SUM,
1070 TARGET_DEPTH_SAMPLES,
1071 TARGET_DEPTH_BELOW_32,
1072 FIRST_BLOCK_LATENCY_SUM_US,
1073 FIRST_BLOCK_LATENCY_COUNT,
1074 PEER_LIFETIME_SUM_MS,
1075 PEER_LIFETIME_COUNT,
1076 PIECE_STEALS_TOTAL,
1077 CHOKE_ROTATION_EVICTIONS_TOTAL,
1078 CONNECT_FAILURES_TOTAL,
1079 DATA_TIMEOUT_EVICTIONS_TOTAL,
1080 ];
1081 assert_eq!(indices.len(), NUM_METRICS);
1082 for &idx in &indices {
1083 assert!(idx < NUM_METRICS, "index {idx} >= NUM_METRICS");
1084 }
1085 }
1086
1087 #[test]
1088 fn default_counters_all_zero() {
1089 let c = SessionCounters::default();
1090 let snap = c.snapshot();
1091 for (i, &val) in snap.iter().enumerate() {
1092 if i == SES_UPTIME_SECS {
1093 continue; }
1095 assert_eq!(val, 0, "metric index {i} should be 0 but was {val}");
1098 }
1099 }
1100
1101 #[test]
1102 fn concurrent_inc_from_multiple_threads() {
1103 use std::sync::Arc;
1104
1105 let c = Arc::new(SessionCounters::new());
1106 let threads: Vec<_> = (0..4)
1107 .map(|_| {
1108 let c = Arc::clone(&c);
1109 std::thread::spawn(move || {
1110 for _ in 0..1000 {
1111 c.inc(NET_BYTES_SENT, 1);
1112 }
1113 })
1114 })
1115 .collect();
1116 for t in threads {
1117 t.join().unwrap();
1118 }
1119 assert_eq!(c.get(NET_BYTES_SENT), 4000);
1120 }
1121
1122 #[test]
1123 fn len_and_is_empty() {
1124 let c = SessionCounters::new();
1125 assert_eq!(c.len(), NUM_METRICS);
1126 assert!(!c.is_empty());
1127 }
1128
1129 #[test]
1130 fn set_max_records_peak() {
1131 let c = SessionCounters::new();
1132 c.set_max(EVENT_TX_HIGH_WATER, 5);
1133 assert_eq!(c.get(EVENT_TX_HIGH_WATER), 5);
1134 c.set_max(EVENT_TX_HIGH_WATER, 3); assert_eq!(c.get(EVENT_TX_HIGH_WATER), 5);
1136 c.set_max(EVENT_TX_HIGH_WATER, 8); assert_eq!(c.get(EVENT_TX_HIGH_WATER), 8);
1138 }
1139}