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 OUTBOUND_UNCHOKE_FLIPS_TOTAL: usize = 99;
317pub const OUTBOUND_CHOKE_FLIPS_TOTAL: usize = 100;
320
321pub const BUDGET_PERMITS_ABSORBED_TOTAL: usize = 101;
327pub const BUDGET_REALLOCS_TOTAL: usize = 102;
330
331pub const DISPATCH_QUEUE_PIECES: usize = 103;
342pub const DISPATCH_INFLIGHT_COUNT: usize = 104;
344pub const DISPATCH_INFLIGHT_CHOKED: usize = 105;
346pub const DISPATCH_PIECE_AVG_US: usize = 106;
348pub const DISPATCH_STEAL_AGE_SUM_US: usize = 107;
351pub const DISPATCH_CHOKE_PARK_TOTAL: usize = 108;
354pub const DISPATCH_WASTED_BYTES: usize = 109;
358pub const DISPATCH_PARK_DUR_SUM_US: usize = 110;
361pub const DISPATCH_STEALS_TOTAL: usize = 111;
365
366pub const NUM_METRICS: usize = 112;
368
369#[must_use]
377pub fn session_stats_metrics() -> &'static [SessionStatsMetric] {
378 use MetricKind::{Counter, Gauge};
379 static METRICS: [SessionStatsMetric; NUM_METRICS] = [
380 SessionStatsMetric {
382 name: "net.bytes_sent",
383 kind: Counter,
384 },
385 SessionStatsMetric {
386 name: "net.bytes_recv",
387 kind: Counter,
388 },
389 SessionStatsMetric {
390 name: "net.num_connections",
391 kind: Gauge,
392 },
393 SessionStatsMetric {
394 name: "net.num_half_open",
395 kind: Gauge,
396 },
397 SessionStatsMetric {
398 name: "net.num_tcp_peers",
399 kind: Gauge,
400 },
401 SessionStatsMetric {
402 name: "net.num_utp_peers",
403 kind: Gauge,
404 },
405 SessionStatsMetric {
406 name: "net.num_tcp_connections",
407 kind: Gauge,
408 },
409 SessionStatsMetric {
410 name: "net.num_utp_connections",
411 kind: Gauge,
412 },
413 SessionStatsMetric {
414 name: "net.tcp_bytes_sent",
415 kind: Counter,
416 },
417 SessionStatsMetric {
418 name: "net.tcp_bytes_recv",
419 kind: Counter,
420 },
421 SessionStatsMetric {
422 name: "net.utp_bytes_sent",
423 kind: Counter,
424 },
425 SessionStatsMetric {
426 name: "net.utp_bytes_recv",
427 kind: Counter,
428 },
429 SessionStatsMetric {
431 name: "disk.read_count",
432 kind: Counter,
433 },
434 SessionStatsMetric {
435 name: "disk.write_count",
436 kind: Counter,
437 },
438 SessionStatsMetric {
439 name: "disk.read_bytes",
440 kind: Counter,
441 },
442 SessionStatsMetric {
443 name: "disk.write_bytes",
444 kind: Counter,
445 },
446 SessionStatsMetric {
447 name: "disk.cache_hits",
448 kind: Counter,
449 },
450 SessionStatsMetric {
451 name: "disk.cache_misses",
452 kind: Counter,
453 },
454 SessionStatsMetric {
455 name: "disk.queue_depth",
456 kind: Gauge,
457 },
458 SessionStatsMetric {
459 name: "disk.job_time_us",
460 kind: Counter,
461 },
462 SessionStatsMetric {
463 name: "disk.write_buffer_bytes",
464 kind: Gauge,
465 },
466 SessionStatsMetric {
467 name: "disk.hash_count",
468 kind: Counter,
469 },
470 SessionStatsMetric {
472 name: "dht.nodes",
473 kind: Gauge,
474 },
475 SessionStatsMetric {
476 name: "dht.lookups",
477 kind: Counter,
478 },
479 SessionStatsMetric {
480 name: "dht.bytes_in",
481 kind: Counter,
482 },
483 SessionStatsMetric {
484 name: "dht.bytes_out",
485 kind: Counter,
486 },
487 SessionStatsMetric {
488 name: "dht.nodes_v4",
489 kind: Gauge,
490 },
491 SessionStatsMetric {
492 name: "dht.nodes_v6",
493 kind: Gauge,
494 },
495 SessionStatsMetric {
496 name: "dht.announce_count",
497 kind: Counter,
498 },
499 SessionStatsMetric {
501 name: "peer.num_unchoked",
502 kind: Gauge,
503 },
504 SessionStatsMetric {
505 name: "peer.num_interested",
506 kind: Gauge,
507 },
508 SessionStatsMetric {
509 name: "peer.num_uploading",
510 kind: Gauge,
511 },
512 SessionStatsMetric {
513 name: "peer.num_downloading",
514 kind: Gauge,
515 },
516 SessionStatsMetric {
517 name: "peer.num_seeding_torrents",
518 kind: Gauge,
519 },
520 SessionStatsMetric {
521 name: "peer.num_downloading_torrents",
522 kind: Gauge,
523 },
524 SessionStatsMetric {
525 name: "peer.num_checking_torrents",
526 kind: Gauge,
527 },
528 SessionStatsMetric {
529 name: "peer.num_paused_torrents",
530 kind: Gauge,
531 },
532 SessionStatsMetric {
533 name: "peer.peers_connected",
534 kind: Gauge,
535 },
536 SessionStatsMetric {
537 name: "peer.peers_available",
538 kind: Gauge,
539 },
540 SessionStatsMetric {
541 name: "peer.num_web_seeds",
542 kind: Gauge,
543 },
544 SessionStatsMetric {
545 name: "peer.num_banned",
546 kind: Gauge,
547 },
548 SessionStatsMetric {
550 name: "proto.pieces_downloaded",
551 kind: Counter,
552 },
553 SessionStatsMetric {
554 name: "proto.pieces_uploaded",
555 kind: Counter,
556 },
557 SessionStatsMetric {
558 name: "proto.hashfails",
559 kind: Counter,
560 },
561 SessionStatsMetric {
562 name: "proto.waste_bytes",
563 kind: Counter,
564 },
565 SessionStatsMetric {
566 name: "proto.piece_requests",
567 kind: Counter,
568 },
569 SessionStatsMetric {
570 name: "proto.piece_rejects",
571 kind: Counter,
572 },
573 SessionStatsMetric {
574 name: "proto.handshakes_in",
575 kind: Counter,
576 },
577 SessionStatsMetric {
578 name: "proto.handshakes_out",
579 kind: Counter,
580 },
581 SessionStatsMetric {
582 name: "proto.pex_messages_in",
583 kind: Counter,
584 },
585 SessionStatsMetric {
586 name: "proto.pex_messages_out",
587 kind: Counter,
588 },
589 SessionStatsMetric {
590 name: "proto.tracker_announces",
591 kind: Counter,
592 },
593 SessionStatsMetric {
594 name: "proto.tracker_errors",
595 kind: Counter,
596 },
597 SessionStatsMetric {
598 name: "proto.metadata_requests",
599 kind: Counter,
600 },
601 SessionStatsMetric {
602 name: "proto.metadata_receives",
603 kind: Counter,
604 },
605 SessionStatsMetric {
607 name: "bw.upload_rate",
608 kind: Gauge,
609 },
610 SessionStatsMetric {
611 name: "bw.download_rate",
612 kind: Gauge,
613 },
614 SessionStatsMetric {
615 name: "bw.upload_rate_tcp",
616 kind: Gauge,
617 },
618 SessionStatsMetric {
619 name: "bw.download_rate_tcp",
620 kind: Gauge,
621 },
622 SessionStatsMetric {
623 name: "bw.upload_rate_utp",
624 kind: Gauge,
625 },
626 SessionStatsMetric {
627 name: "bw.download_rate_utp",
628 kind: Gauge,
629 },
630 SessionStatsMetric {
631 name: "bw.payload_upload_rate",
632 kind: Gauge,
633 },
634 SessionStatsMetric {
635 name: "bw.payload_download_rate",
636 kind: Gauge,
637 },
638 SessionStatsMetric {
639 name: "bw.total_uploaded",
640 kind: Counter,
641 },
642 SessionStatsMetric {
643 name: "bw.total_downloaded",
644 kind: Counter,
645 },
646 SessionStatsMetric {
648 name: "ses.active_torrents",
649 kind: Gauge,
650 },
651 SessionStatsMetric {
652 name: "ses.num_torrents",
653 kind: Gauge,
654 },
655 SessionStatsMetric {
656 name: "ses.uptime_secs",
657 kind: Gauge,
658 },
659 SessionStatsMetric {
660 name: "ses.ip_filter_blocked",
661 kind: Counter,
662 },
663 SessionStatsMetric {
664 name: "ses.queue_paused_by_auto",
665 kind: Counter,
666 },
667 SessionStatsMetric {
669 name: "perf.event_tx_high_water",
670 kind: Gauge,
671 },
672 SessionStatsMetric {
673 name: "perf.dispatch_tx_high_water",
674 kind: Gauge,
675 },
676 SessionStatsMetric {
677 name: "perf.peer_wake_events_total",
678 kind: Counter,
679 },
680 SessionStatsMetric {
681 name: "perf.peer_drain_items_total",
682 kind: Counter,
683 },
684 SessionStatsMetric {
686 name: "dispatch.acquire_total",
687 kind: Counter,
688 },
689 SessionStatsMetric {
690 name: "dispatch.acquire_none_total",
691 kind: Counter,
692 },
693 SessionStatsMetric {
694 name: "dispatch.acquire_us",
695 kind: Counter,
696 },
697 SessionStatsMetric {
698 name: "dispatch.notify_wakeup_total",
699 kind: Counter,
700 },
701 SessionStatsMetric {
702 name: "dispatch.peer_connect_total",
703 kind: Counter,
704 },
705 SessionStatsMetric {
706 name: "dispatch.peer_disconnect_total",
707 kind: Counter,
708 },
709 SessionStatsMetric {
710 name: "dispatch.acquire_rtt_us",
711 kind: Counter,
712 },
713 SessionStatsMetric {
714 name: "dispatch.notify_wait_us",
715 kind: Counter,
716 },
717 SessionStatsMetric {
718 name: "dispatch.tick_wake_skipped",
719 kind: Counter,
720 },
721 SessionStatsMetric {
722 name: "dispatch.walk_skipped",
723 kind: Counter,
724 },
725 SessionStatsMetric {
726 name: "dispatch.cursor_resumed",
727 kind: Counter,
728 },
729 SessionStatsMetric {
731 name: "peer.remote_unchoke_total",
732 kind: Counter,
733 },
734 SessionStatsMetric {
735 name: "peer.remote_rechoke_total",
736 kind: Counter,
737 },
738 SessionStatsMetric {
739 name: "peer.remote_unchoke_duration_sum_ms",
740 kind: Counter,
741 },
742 SessionStatsMetric {
743 name: "peer.target_depth_sum",
744 kind: Counter,
745 },
746 SessionStatsMetric {
747 name: "peer.target_depth_samples",
748 kind: Counter,
749 },
750 SessionStatsMetric {
751 name: "peer.target_depth_below_32",
752 kind: Counter,
753 },
754 SessionStatsMetric {
755 name: "peer.first_block_latency_sum_us",
756 kind: Counter,
757 },
758 SessionStatsMetric {
759 name: "peer.first_block_latency_count",
760 kind: Counter,
761 },
762 SessionStatsMetric {
763 name: "peer.lifetime_sum_ms",
764 kind: Counter,
765 },
766 SessionStatsMetric {
767 name: "peer.lifetime_count",
768 kind: Counter,
769 },
770 SessionStatsMetric {
772 name: "peer.piece_steals_total",
773 kind: Counter,
774 },
775 SessionStatsMetric {
776 name: "peer.choke_rotation_evictions_total",
777 kind: Counter,
778 },
779 SessionStatsMetric {
780 name: "peer.connect_failures_total",
781 kind: Counter,
782 },
783 SessionStatsMetric {
784 name: "peer.data_timeout_evictions_total",
785 kind: Counter,
786 },
787 SessionStatsMetric {
789 name: "peer.outbound_unchoke_flips_total",
790 kind: Counter,
791 },
792 SessionStatsMetric {
793 name: "peer.outbound_choke_flips_total",
794 kind: Counter,
795 },
796 SessionStatsMetric {
798 name: "peer.budget_permits_absorbed_total",
799 kind: Counter,
800 },
801 SessionStatsMetric {
802 name: "peer.budget_reallocs_total",
803 kind: Counter,
804 },
805 SessionStatsMetric {
807 name: "dispatch.queue_pieces",
808 kind: Gauge,
809 },
810 SessionStatsMetric {
811 name: "dispatch.inflight_count",
812 kind: Gauge,
813 },
814 SessionStatsMetric {
815 name: "dispatch.inflight_choked",
816 kind: Gauge,
817 },
818 SessionStatsMetric {
819 name: "dispatch.piece_avg_us",
820 kind: Gauge,
821 },
822 SessionStatsMetric {
823 name: "dispatch.steal_age_sum_us",
824 kind: Counter,
825 },
826 SessionStatsMetric {
827 name: "dispatch.choke_park_total",
828 kind: Counter,
829 },
830 SessionStatsMetric {
831 name: "dispatch.wasted_bytes",
832 kind: Counter,
833 },
834 SessionStatsMetric {
835 name: "dispatch.park_dur_sum_us",
836 kind: Counter,
837 },
838 SessionStatsMetric {
839 name: "dispatch.steals_total",
840 kind: Counter,
841 },
842 ];
843 &METRICS
844}
845
846pub struct SessionCounters {
855 values: [AtomicI64; NUM_METRICS],
856 started_at: Instant,
857 prev_bytes_sent: AtomicI64,
858 prev_bytes_recv: AtomicI64,
859 diagnostics: AtomicBool,
860}
861
862impl SessionCounters {
863 #[must_use]
866 pub fn new() -> Self {
867 Self {
868 values: std::array::from_fn(|_| AtomicI64::new(0)),
869 started_at: Instant::now(),
870 prev_bytes_sent: AtomicI64::new(0),
871 prev_bytes_recv: AtomicI64::new(0),
872 diagnostics: AtomicBool::new(false),
873 }
874 }
875
876 #[must_use]
878 pub fn new_with_diagnostics(enabled: bool) -> Self {
879 Self {
880 values: std::array::from_fn(|_| AtomicI64::new(0)),
881 started_at: Instant::now(),
882 prev_bytes_sent: AtomicI64::new(0),
883 prev_bytes_recv: AtomicI64::new(0),
884 diagnostics: AtomicBool::new(enabled),
885 }
886 }
887
888 #[must_use]
891 pub fn diagnostics_enabled(&self) -> bool {
892 self.diagnostics.load(Ordering::Relaxed)
893 }
894
895 #[inline]
897 pub fn inc(&self, metric: usize, delta: i64) {
898 debug_assert!(metric < NUM_METRICS);
899 self.values[metric].fetch_add(delta, Ordering::Relaxed);
900 }
901
902 #[inline]
905 pub fn inc_diag(&self, metric: usize, delta: i64) {
906 debug_assert!(metric >= DIAGNOSTIC_COUNTERS_START);
907 if self.diagnostics.load(Ordering::Relaxed) {
908 self.values[metric].fetch_add(delta, Ordering::Relaxed);
909 }
910 }
911
912 #[inline]
914 pub fn set(&self, metric: usize, value: i64) {
915 debug_assert!(metric < NUM_METRICS);
916 self.values[metric].store(value, Ordering::Relaxed);
917 }
918
919 #[inline]
922 pub fn set_max_diag(&self, metric: usize, value: i64) {
923 debug_assert!(metric >= DIAGNOSTIC_COUNTERS_START);
924 if self.diagnostics.load(Ordering::Relaxed) {
925 self.set_max(metric, value);
926 }
927 }
928
929 #[inline]
935 pub fn set_max(&self, metric: usize, value: i64) {
936 debug_assert!(metric < NUM_METRICS);
937 let cell = &self.values[metric];
938 let mut cur = cell.load(Ordering::Relaxed);
939 while value > cur {
940 match cell.compare_exchange_weak(cur, value, Ordering::Relaxed, Ordering::Relaxed) {
941 Ok(_) => return,
942 Err(observed) => cur = observed,
943 }
944 }
945 }
946
947 #[inline]
949 pub fn get(&self, metric: usize) -> i64 {
950 debug_assert!(metric < NUM_METRICS);
951 self.values[metric].load(Ordering::Relaxed)
952 }
953
954 pub fn snapshot(&self) -> Vec<i64> {
959 let mut vals: Vec<i64> = self
960 .values
961 .iter()
962 .map(|a| a.load(Ordering::Relaxed))
963 .collect();
964
965 vals[SES_UPTIME_SECS] = self.started_at.elapsed().as_secs() as i64;
967
968 let cur_sent = vals[NET_BYTES_SENT];
970 let cur_recv = vals[NET_BYTES_RECV];
971 let prev_sent = self.prev_bytes_sent.swap(cur_sent, Ordering::Relaxed);
972 let prev_recv = self.prev_bytes_recv.swap(cur_recv, Ordering::Relaxed);
973 vals[BW_UPLOAD_RATE] = cur_sent.saturating_sub(prev_sent);
974 vals[BW_DOWNLOAD_RATE] = cur_recv.saturating_sub(prev_recv);
975
976 vals
977 }
978
979 pub fn len(&self) -> usize {
981 NUM_METRICS
982 }
983
984 pub fn is_empty(&self) -> bool {
986 false
987 }
988
989 pub fn uptime_secs(&self) -> u64 {
991 self.started_at.elapsed().as_secs()
992 }
993}
994
995impl Default for SessionCounters {
996 fn default() -> Self {
997 Self::new()
998 }
999}
1000
1001#[cfg(test)]
1006mod tests {
1007 use super::*;
1008 use std::collections::HashSet;
1009
1010 #[test]
1011 fn metrics_registry_has_correct_count() {
1012 assert_eq!(session_stats_metrics().len(), NUM_METRICS);
1013 }
1014
1015 #[test]
1016 fn all_metric_names_are_unique() {
1017 let names: HashSet<&str> = session_stats_metrics().iter().map(|m| m.name).collect();
1018 assert_eq!(names.len(), NUM_METRICS);
1019 }
1020
1021 #[test]
1022 fn all_metric_names_have_category_prefix() {
1023 for m in session_stats_metrics() {
1024 assert!(
1025 m.name.contains('.'),
1026 "metric name {:?} has no category prefix",
1027 m.name
1028 );
1029 }
1030 }
1031
1032 #[test]
1033 fn counter_inc_and_get() {
1034 let c = SessionCounters::new();
1035 c.inc(NET_BYTES_SENT, 5);
1036 assert_eq!(c.get(NET_BYTES_SENT), 5);
1037 c.inc(NET_BYTES_SENT, 3);
1038 assert_eq!(c.get(NET_BYTES_SENT), 8);
1039 }
1040
1041 #[test]
1042 fn m257b_outbound_flip_counters_respect_diag_gating() {
1043 let on = SessionCounters::new_with_diagnostics(true);
1044 on.inc_diag(OUTBOUND_UNCHOKE_FLIPS_TOTAL, 3);
1045 on.inc_diag(OUTBOUND_CHOKE_FLIPS_TOTAL, 2);
1046 let snap = on.snapshot();
1047 assert_eq!(snap.len(), NUM_METRICS);
1048 assert_eq!(snap[OUTBOUND_UNCHOKE_FLIPS_TOTAL], 3);
1049 assert_eq!(snap[OUTBOUND_CHOKE_FLIPS_TOTAL], 2);
1050
1051 let off = SessionCounters::new_with_diagnostics(false);
1052 off.inc_diag(OUTBOUND_UNCHOKE_FLIPS_TOTAL, 1);
1053 assert_eq!(off.get(OUTBOUND_UNCHOKE_FLIPS_TOTAL), 0);
1054 }
1055
1056 #[test]
1057 fn gauge_set_and_get() {
1058 let c = SessionCounters::new();
1059 c.set(NET_NUM_CONNECTIONS, 42);
1060 assert_eq!(c.get(NET_NUM_CONNECTIONS), 42);
1061 c.set(NET_NUM_CONNECTIONS, 0);
1062 assert_eq!(c.get(NET_NUM_CONNECTIONS), 0);
1063 }
1064
1065 #[test]
1066 fn snapshot_returns_all_values() {
1067 let c = SessionCounters::new();
1068 c.inc(NET_BYTES_SENT, 100);
1069 c.set(DHT_NODES, 50);
1070 c.inc(PROTO_HASHFAILS, 3);
1071 let snap = c.snapshot();
1072 assert_eq!(snap.len(), NUM_METRICS);
1073 assert_eq!(snap[NET_BYTES_SENT], 100);
1074 assert_eq!(snap[DHT_NODES], 50);
1075 assert_eq!(snap[PROTO_HASHFAILS], 3);
1076 }
1077
1078 #[test]
1079 fn snapshot_includes_uptime() {
1080 let c = SessionCounters::new();
1081 let snap = c.snapshot();
1083 assert!(snap[SES_UPTIME_SECS] >= 0);
1084 }
1085
1086 #[test]
1087 fn counters_are_send_and_sync() {
1088 fn assert_send_sync<T: Send + Sync>() {}
1089 assert_send_sync::<SessionCounters>();
1090 }
1091
1092 #[test]
1093 fn metric_kind_serializes() {
1094 let counter_json = serde_json::to_string(&MetricKind::Counter).unwrap();
1095 let gauge_json = serde_json::to_string(&MetricKind::Gauge).unwrap();
1096 assert_eq!(
1097 serde_json::from_str::<MetricKind>(&counter_json).unwrap(),
1098 MetricKind::Counter
1099 );
1100 assert_eq!(
1101 serde_json::from_str::<MetricKind>(&gauge_json).unwrap(),
1102 MetricKind::Gauge
1103 );
1104 }
1105
1106 #[test]
1107 fn metric_index_constants_in_range() {
1108 let indices = [
1109 NET_BYTES_SENT,
1110 NET_BYTES_RECV,
1111 NET_NUM_CONNECTIONS,
1112 NET_NUM_HALF_OPEN,
1113 NET_NUM_TCP_PEERS,
1114 NET_NUM_UTP_PEERS,
1115 NET_NUM_TCP_CONNECTIONS,
1116 NET_NUM_UTP_CONNECTIONS,
1117 NET_TCP_BYTES_SENT,
1118 NET_TCP_BYTES_RECV,
1119 NET_UTP_BYTES_SENT,
1120 NET_UTP_BYTES_RECV,
1121 DISK_READ_COUNT,
1122 DISK_WRITE_COUNT,
1123 DISK_READ_BYTES,
1124 DISK_WRITE_BYTES,
1125 DISK_CACHE_HITS,
1126 DISK_CACHE_MISSES,
1127 DISK_QUEUE_DEPTH,
1128 DISK_JOB_TIME_US,
1129 DISK_WRITE_BUFFER_BYTES,
1130 DISK_HASH_COUNT,
1131 DHT_NODES,
1132 DHT_LOOKUPS,
1133 DHT_BYTES_IN,
1134 DHT_BYTES_OUT,
1135 DHT_NODES_V4,
1136 DHT_NODES_V6,
1137 DHT_ANNOUNCE_COUNT,
1138 PEER_NUM_UNCHOKED,
1139 PEER_NUM_INTERESTED,
1140 PEER_NUM_UPLOADING,
1141 PEER_NUM_DOWNLOADING,
1142 PEER_NUM_SEEDING_TORRENTS,
1143 PEER_NUM_DOWNLOADING_TORRENTS,
1144 PEER_NUM_CHECKING_TORRENTS,
1145 PEER_NUM_PAUSED_TORRENTS,
1146 PEER_PEERS_CONNECTED,
1147 PEER_PEERS_AVAILABLE,
1148 PEER_NUM_WEB_SEEDS,
1149 PEER_NUM_BANNED,
1150 PROTO_PIECES_DOWNLOADED,
1151 PROTO_PIECES_UPLOADED,
1152 PROTO_HASHFAILS,
1153 PROTO_WASTE_BYTES,
1154 PROTO_PIECE_REQUESTS,
1155 PROTO_PIECE_REJECTS,
1156 PROTO_HANDSHAKES_IN,
1157 PROTO_HANDSHAKES_OUT,
1158 PROTO_PEX_MESSAGES_IN,
1159 PROTO_PEX_MESSAGES_OUT,
1160 PROTO_TRACKER_ANNOUNCES,
1161 PROTO_TRACKER_ERRORS,
1162 PROTO_METADATA_REQUESTS,
1163 PROTO_METADATA_RECEIVES,
1164 BW_UPLOAD_RATE,
1165 BW_DOWNLOAD_RATE,
1166 BW_UPLOAD_RATE_TCP,
1167 BW_DOWNLOAD_RATE_TCP,
1168 BW_UPLOAD_RATE_UTP,
1169 BW_DOWNLOAD_RATE_UTP,
1170 BW_PAYLOAD_UPLOAD_RATE,
1171 BW_PAYLOAD_DOWNLOAD_RATE,
1172 BW_TOTAL_UPLOADED,
1173 BW_TOTAL_DOWNLOADED,
1174 SES_ACTIVE_TORRENTS,
1175 SES_NUM_TORRENTS,
1176 SES_UPTIME_SECS,
1177 SES_IP_FILTER_BLOCKED,
1178 SES_QUEUE_PAUSED_BY_AUTO,
1179 EVENT_TX_HIGH_WATER,
1180 DISPATCH_TX_HIGH_WATER,
1181 PEER_WAKE_EVENTS_TOTAL,
1182 PEER_DRAIN_ITEMS_TOTAL,
1183 DISPATCH_ACQUIRE_TOTAL,
1184 DISPATCH_ACQUIRE_NONE_TOTAL,
1185 DISPATCH_ACQUIRE_US,
1186 DISPATCH_NOTIFY_WAKEUP_TOTAL,
1187 DISPATCH_PEER_CONNECT_TOTAL,
1188 DISPATCH_PEER_DISCONNECT_TOTAL,
1189 DISPATCH_ACQUIRE_RTT_US,
1190 DISPATCH_NOTIFY_WAIT_US,
1191 DISPATCH_TICK_WAKE_SKIPPED,
1192 DISPATCH_WALK_SKIPPED,
1193 DISPATCH_CURSOR_RESUMED,
1194 REMOTE_UNCHOKE_TOTAL,
1195 REMOTE_RECHOKE_TOTAL,
1196 REMOTE_UNCHOKE_DURATION_SUM_MS,
1197 TARGET_DEPTH_SUM,
1198 TARGET_DEPTH_SAMPLES,
1199 TARGET_DEPTH_BELOW_32,
1200 FIRST_BLOCK_LATENCY_SUM_US,
1201 FIRST_BLOCK_LATENCY_COUNT,
1202 PEER_LIFETIME_SUM_MS,
1203 PEER_LIFETIME_COUNT,
1204 PIECE_STEALS_TOTAL,
1205 CHOKE_ROTATION_EVICTIONS_TOTAL,
1206 CONNECT_FAILURES_TOTAL,
1207 DATA_TIMEOUT_EVICTIONS_TOTAL,
1208 OUTBOUND_UNCHOKE_FLIPS_TOTAL,
1209 OUTBOUND_CHOKE_FLIPS_TOTAL,
1210 BUDGET_PERMITS_ABSORBED_TOTAL,
1211 BUDGET_REALLOCS_TOTAL,
1212 DISPATCH_QUEUE_PIECES,
1213 DISPATCH_INFLIGHT_COUNT,
1214 DISPATCH_INFLIGHT_CHOKED,
1215 DISPATCH_PIECE_AVG_US,
1216 DISPATCH_STEAL_AGE_SUM_US,
1217 DISPATCH_CHOKE_PARK_TOTAL,
1218 DISPATCH_WASTED_BYTES,
1219 DISPATCH_PARK_DUR_SUM_US,
1220 DISPATCH_STEALS_TOTAL,
1221 ];
1222 assert_eq!(indices.len(), NUM_METRICS);
1223 for &idx in &indices {
1224 assert!(idx < NUM_METRICS, "index {idx} >= NUM_METRICS");
1225 }
1226 }
1227
1228 #[test]
1229 fn default_counters_all_zero() {
1230 let c = SessionCounters::default();
1231 let snap = c.snapshot();
1232 for (i, &val) in snap.iter().enumerate() {
1233 if i == SES_UPTIME_SECS {
1234 continue; }
1236 assert_eq!(val, 0, "metric index {i} should be 0 but was {val}");
1239 }
1240 }
1241
1242 #[test]
1243 fn concurrent_inc_from_multiple_threads() {
1244 use std::sync::Arc;
1245
1246 let c = Arc::new(SessionCounters::new());
1247 let threads: Vec<_> = (0..4)
1248 .map(|_| {
1249 let c = Arc::clone(&c);
1250 std::thread::spawn(move || {
1251 for _ in 0..1000 {
1252 c.inc(NET_BYTES_SENT, 1);
1253 }
1254 })
1255 })
1256 .collect();
1257 for t in threads {
1258 t.join().unwrap();
1259 }
1260 assert_eq!(c.get(NET_BYTES_SENT), 4000);
1261 }
1262
1263 #[test]
1264 fn len_and_is_empty() {
1265 let c = SessionCounters::new();
1266 assert_eq!(c.len(), NUM_METRICS);
1267 assert!(!c.is_empty());
1268 }
1269
1270 #[test]
1271 fn set_max_records_peak() {
1272 let c = SessionCounters::new();
1273 c.set_max(EVENT_TX_HIGH_WATER, 5);
1274 assert_eq!(c.get(EVENT_TX_HIGH_WATER), 5);
1275 c.set_max(EVENT_TX_HIGH_WATER, 3); assert_eq!(c.get(EVENT_TX_HIGH_WATER), 5);
1277 c.set_max(EVENT_TX_HIGH_WATER, 8); assert_eq!(c.get(EVENT_TX_HIGH_WATER), 8);
1279 }
1280}