Skip to main content

irontide_session/
stats.rs

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
6//! Session statistics metric registry and atomic counter array.
7//!
8//! Provides 99 atomic metrics across 8 categories (network, disk, DHT, peers,
9//! protocol, bandwidth, session, operational diagnostics). [`MetricKind`] distinguishes monotonic
10//! counters from point-in-time gauges. [`SessionStatsMetric`] provides static
11//! metadata for each metric, and [`SessionCounters`] holds the atomic values.
12
13use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
14use std::time::Instant;
15
16// ---------------------------------------------------------------------------
17// MetricKind
18// ---------------------------------------------------------------------------
19
20/// Whether a metric is a monotonically increasing counter or a point-in-time gauge.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
22pub enum MetricKind {
23    /// Monotonically increasing (bytes sent, pieces downloaded, etc.).
24    Counter,
25    /// Current value that can go up or down (connections, DHT nodes, etc.).
26    Gauge,
27}
28
29// ---------------------------------------------------------------------------
30// SessionStatsMetric
31// ---------------------------------------------------------------------------
32
33/// Static metadata for a single session metric.
34#[derive(Debug, Clone, Copy)]
35pub struct SessionStatsMetric {
36    /// Human-readable dotted name (e.g. `"net.bytes_sent"`).
37    pub name: &'static str,
38    /// Whether this metric is a counter or gauge.
39    pub kind: MetricKind,
40}
41
42// ---------------------------------------------------------------------------
43// Metric index constants (74 total)
44// ---------------------------------------------------------------------------
45
46// -- Network (0..11) --
47
48/// Metric index: total bytes sent across all connections (counter).
49pub const NET_BYTES_SENT: usize = 0;
50/// Metric index: total bytes received across all connections (counter).
51pub const NET_BYTES_RECV: usize = 1;
52/// Metric index: current number of established connections (gauge).
53pub const NET_NUM_CONNECTIONS: usize = 2;
54/// Metric index: current number of half-open (connecting) sockets (gauge).
55pub const NET_NUM_HALF_OPEN: usize = 3;
56/// Metric index: current number of TCP peers (gauge).
57pub const NET_NUM_TCP_PEERS: usize = 4;
58/// Metric index: current number of uTP peers (gauge).
59pub const NET_NUM_UTP_PEERS: usize = 5;
60/// Metric index: current number of TCP connections (gauge).
61pub const NET_NUM_TCP_CONNECTIONS: usize = 6;
62/// Metric index: current number of uTP connections (gauge).
63pub const NET_NUM_UTP_CONNECTIONS: usize = 7;
64/// Metric index: total bytes sent over TCP (counter).
65pub const NET_TCP_BYTES_SENT: usize = 8;
66/// Metric index: total bytes received over TCP (counter).
67pub const NET_TCP_BYTES_RECV: usize = 9;
68/// Metric index: total bytes sent over uTP (counter).
69pub const NET_UTP_BYTES_SENT: usize = 10;
70/// Metric index: total bytes received over uTP (counter).
71pub const NET_UTP_BYTES_RECV: usize = 11;
72
73// -- Disk (12..21) --
74
75/// Metric index: total disk read operations (counter).
76pub const DISK_READ_COUNT: usize = 12;
77/// Metric index: total disk write operations (counter).
78pub const DISK_WRITE_COUNT: usize = 13;
79/// Metric index: total bytes read from disk (counter).
80pub const DISK_READ_BYTES: usize = 14;
81/// Metric index: total bytes written to disk (counter).
82pub const DISK_WRITE_BYTES: usize = 15;
83/// Metric index: total disk cache hits (counter).
84pub const DISK_CACHE_HITS: usize = 16;
85/// Metric index: total disk cache misses (counter).
86pub const DISK_CACHE_MISSES: usize = 17;
87/// Metric index: current disk job queue depth (gauge).
88pub const DISK_QUEUE_DEPTH: usize = 18;
89/// Metric index: cumulative disk job time in microseconds (counter).
90pub const DISK_JOB_TIME_US: usize = 19;
91/// Metric index: current write buffer size in bytes (gauge).
92pub const DISK_WRITE_BUFFER_BYTES: usize = 20;
93/// Metric index: total piece hash operations (counter).
94pub const DISK_HASH_COUNT: usize = 21;
95
96// -- DHT (22..28) --
97
98/// Metric index: current number of DHT routing table nodes (gauge).
99pub const DHT_NODES: usize = 22;
100/// Metric index: total DHT lookup operations (counter).
101pub const DHT_LOOKUPS: usize = 23;
102/// Metric index: total bytes received via DHT (counter).
103pub const DHT_BYTES_IN: usize = 24;
104/// Metric index: total bytes sent via DHT (counter).
105pub const DHT_BYTES_OUT: usize = 25;
106/// Metric index: current number of IPv4 DHT nodes (gauge).
107pub const DHT_NODES_V4: usize = 26;
108/// Metric index: current number of IPv6 DHT nodes (gauge).
109pub const DHT_NODES_V6: usize = 27;
110/// Metric index: total DHT announce operations (counter).
111pub const DHT_ANNOUNCE_COUNT: usize = 28;
112
113// -- Peers (29..40) --
114
115/// Metric index: current number of unchoked peers (gauge).
116pub const PEER_NUM_UNCHOKED: usize = 29;
117/// Metric index: current number of interested peers (gauge).
118pub const PEER_NUM_INTERESTED: usize = 30;
119/// Metric index: current number of peers we are uploading to (gauge).
120pub const PEER_NUM_UPLOADING: usize = 31;
121/// Metric index: current number of peers we are downloading from (gauge).
122pub const PEER_NUM_DOWNLOADING: usize = 32;
123/// Metric index: current number of torrents in seeding state (gauge).
124pub const PEER_NUM_SEEDING_TORRENTS: usize = 33;
125/// Metric index: current number of torrents in downloading state (gauge).
126pub const PEER_NUM_DOWNLOADING_TORRENTS: usize = 34;
127/// Metric index: current number of torrents being checked (gauge).
128pub const PEER_NUM_CHECKING_TORRENTS: usize = 35;
129/// Metric index: current number of paused torrents (gauge).
130pub const PEER_NUM_PAUSED_TORRENTS: usize = 36;
131/// Metric index: current total number of connected peers (gauge).
132pub const PEER_PEERS_CONNECTED: usize = 37;
133/// Metric index: current total number of known available peers (gauge).
134pub const PEER_PEERS_AVAILABLE: usize = 38;
135/// Metric index: current number of active web seeds (gauge).
136pub const PEER_NUM_WEB_SEEDS: usize = 39;
137/// Metric index: current number of banned peers (gauge).
138pub const PEER_NUM_BANNED: usize = 40;
139
140// -- Protocol (41..54) --
141
142/// Metric index: total pieces downloaded across all torrents (counter).
143pub const PROTO_PIECES_DOWNLOADED: usize = 41;
144/// Metric index: total pieces uploaded across all torrents (counter).
145pub const PROTO_PIECES_UPLOADED: usize = 42;
146/// Metric index: total piece hash verification failures (counter).
147pub const PROTO_HASHFAILS: usize = 43;
148/// Metric index: total wasted bytes (duplicate/rejected data) (counter).
149pub const PROTO_WASTE_BYTES: usize = 44;
150/// Metric index: total piece request messages sent (counter).
151pub const PROTO_PIECE_REQUESTS: usize = 45;
152/// Metric index: total piece reject messages received (counter).
153pub const PROTO_PIECE_REJECTS: usize = 46;
154/// Metric index: total incoming handshakes received (counter).
155pub const PROTO_HANDSHAKES_IN: usize = 47;
156/// Metric index: total outgoing handshakes sent (counter).
157pub const PROTO_HANDSHAKES_OUT: usize = 48;
158/// Metric index: total PEX messages received (counter).
159pub const PROTO_PEX_MESSAGES_IN: usize = 49;
160/// Metric index: total PEX messages sent (counter).
161pub const PROTO_PEX_MESSAGES_OUT: usize = 50;
162/// Metric index: total tracker announce requests (counter).
163pub const PROTO_TRACKER_ANNOUNCES: usize = 51;
164/// Metric index: total tracker announce errors (counter).
165pub const PROTO_TRACKER_ERRORS: usize = 52;
166/// Metric index: total BEP 9 metadata requests sent (counter).
167pub const PROTO_METADATA_REQUESTS: usize = 53;
168/// Metric index: total BEP 9 metadata pieces received (counter).
169pub const PROTO_METADATA_RECEIVES: usize = 54;
170
171// -- Bandwidth (55..64) --
172
173/// Metric index: current aggregate upload rate in bytes/sec (gauge).
174pub const BW_UPLOAD_RATE: usize = 55;
175/// Metric index: current aggregate download rate in bytes/sec (gauge).
176pub const BW_DOWNLOAD_RATE: usize = 56;
177/// Metric index: current TCP upload rate in bytes/sec (gauge).
178pub const BW_UPLOAD_RATE_TCP: usize = 57;
179/// Metric index: current TCP download rate in bytes/sec (gauge).
180pub const BW_DOWNLOAD_RATE_TCP: usize = 58;
181/// Metric index: current uTP upload rate in bytes/sec (gauge).
182pub const BW_UPLOAD_RATE_UTP: usize = 59;
183/// Metric index: current uTP download rate in bytes/sec (gauge).
184pub const BW_DOWNLOAD_RATE_UTP: usize = 60;
185/// Metric index: current payload-only upload rate in bytes/sec (gauge).
186pub const BW_PAYLOAD_UPLOAD_RATE: usize = 61;
187/// Metric index: current payload-only download rate in bytes/sec (gauge).
188pub const BW_PAYLOAD_DOWNLOAD_RATE: usize = 62;
189/// Metric index: total bytes uploaded since session start (counter).
190pub const BW_TOTAL_UPLOADED: usize = 63;
191/// Metric index: total bytes downloaded since session start (counter).
192pub const BW_TOTAL_DOWNLOADED: usize = 64;
193
194// -- Session (65..69) --
195
196/// Metric index: current number of active (non-paused) torrents (gauge).
197pub const SES_ACTIVE_TORRENTS: usize = 65;
198/// Metric index: total number of torrents in the session (gauge).
199pub const SES_NUM_TORRENTS: usize = 66;
200/// Metric index: session uptime in seconds (gauge).
201pub const SES_UPTIME_SECS: usize = 67;
202/// Metric index: total connections blocked by the IP filter (counter).
203pub const SES_IP_FILTER_BLOCKED: usize = 68;
204/// Metric index: total torrents paused by auto-management (counter).
205pub const SES_QUEUE_PAUSED_BY_AUTO: usize = 69;
206
207/// First diagnostic counter index. Counters at or above this index are
208/// only incremented when `SessionCounters::diagnostics` is enabled.
209pub const DIAGNOSTIC_COUNTERS_START: usize = 70;
210
211// -- Sim-perf engine surface (70..73) --
212//
213// Four counters added for the sim-perf harness so regression scenarios can
214// gate on internal queue pressure and per-peer drain wakes. Increment sites:
215//   - EVENT_TX_HIGH_WATER / DISPATCH_TX_HIGH_WATER: peer_tasks.rs at the
216//     four `BackpressureQueue::enqueue_or_send` sites (set_max).
217//   - PEER_WAKE_EVENTS_TOTAL / PEER_DRAIN_ITEMS_TOTAL: peer_tasks.rs ARM
218//     5 + ARM 6 (`dispatch_drain_notify` / `event_drain_notify`).
219//     PEER_DRAIN_ITEMS_TOTAL increments by drained-batch size, NOT per
220//     item, so contention stays manageable across ~200 reader_loops.
221
222/// Metric index: peer event-channel max queue depth (M182 surface; gauge).
223pub const EVENT_TX_HIGH_WATER: usize = 70;
224/// Metric index: peer dispatch-channel max queue depth (M182 surface; gauge).
225pub const DISPATCH_TX_HIGH_WATER: usize = 71;
226/// Metric index: total drain-notify arm fires (counter).
227pub const PEER_WAKE_EVENTS_TOTAL: usize = 72;
228/// Metric index: total items drained from peer backpressure queues (counter).
229pub const PEER_DRAIN_ITEMS_TOTAL: usize = 73;
230
231// -- Dispatch diagnostics (74..81) --
232
233/// Metric index: total `AcquirePiece` requests handled (counter).
234pub const DISPATCH_ACQUIRE_TOTAL: usize = 74;
235/// Metric index: total `AcquirePiece` requests returning `NoneAvailable` (counter).
236pub const DISPATCH_ACQUIRE_NONE_TOTAL: usize = 75;
237/// Metric index: cumulative microseconds spent in `AcquirePiece` handling (counter).
238pub const DISPATCH_ACQUIRE_US: usize = 76;
239/// Metric index: total `reservation_notify` wakeups fired (counter).
240pub const DISPATCH_NOTIFY_WAKEUP_TOTAL: usize = 77;
241/// Metric index: total peer connections completing handshake (counter).
242pub const DISPATCH_PEER_CONNECT_TOTAL: usize = 78;
243/// Metric index: total peer disconnections (counter).
244pub const DISPATCH_PEER_DISCONNECT_TOTAL: usize = 79;
245/// Metric index: cumulative `AcquirePiece` round-trip microseconds (counter).
246pub const DISPATCH_ACQUIRE_RTT_US: usize = 80;
247/// Metric index: cumulative microseconds peers spent waiting on `piece_notify` (counter).
248pub const DISPATCH_NOTIFY_WAIT_US: usize = 81;
249/// Metric index: pipeline-tick wakes that were skipped because the dispatch
250/// state (queue size + in-flight count) was unchanged since the previous tick
251/// (counter). High values relative to `DISPATCH_NOTIFY_WAKEUP_TOTAL` indicate
252/// the state-gated tick is working — peers are not being spuriously woken when
253/// nothing has changed since the last tick.
254pub const DISPATCH_TICK_WAKE_SKIPPED: usize = 82;
255/// Metric index: `acquire_piece` calls whose Phase 2 linear walk over
256/// `order_map.order` was short-circuited because the peer's bitfield does
257/// not intersect `queue_pieces` (counter). Ratio against
258/// `DISPATCH_ACQUIRE_TOTAL` reveals how often peers ask for work they
259/// have no eligible piece for — the bitfield-intersection guard short-
260/// circuits these in `O(num_pieces / 8)` bytes instead of `O(num_pieces)`.
261pub const DISPATCH_WALK_SKIPPED: usize = 83;
262/// Metric index: `acquire_piece` calls where the per-peer cursor started
263/// from a non-zero position, indicating the walk resumed from a previous
264/// call rather than scanning from the front of `order_map.order` (counter).
265pub const DISPATCH_CURSOR_RESUMED: usize = 84;
266
267// ── Hypothesis validation telemetry (85..94) ──
268// Added 2026-05-13 to validate the target_depth feedback loop hypothesis
269// before committing to architectural changes. See
270// docs/investigations/2026-05-13-peer-pipeline-comparison-rqbit-9.0.md §13.
271
272/// Times a remote peer unchoked us (counter).
273pub const REMOTE_UNCHOKE_TOTAL: usize = 85;
274/// Times a remote peer re-choked us after having unchoked (counter).
275pub const REMOTE_RECHOKE_TOTAL: usize = 86;
276/// Cumulative milliseconds peers spent unchoked before re-choking (counter).
277/// Divide by `REMOTE_RECHOKE_TOTAL` for mean unchoke duration.
278pub const REMOTE_UNCHOKE_DURATION_SUM_MS: usize = 87;
279/// Deprecated: dynamic depth gate removed in v0.186.4. Slot retained to
280/// preserve counter index stability for existing benchmark CSVs.
281pub const TARGET_DEPTH_SUM: usize = 88;
282/// Deprecated: see `TARGET_DEPTH_SUM`.
283pub const TARGET_DEPTH_SAMPLES: usize = 89;
284/// Deprecated: see `TARGET_DEPTH_SUM`.
285pub const TARGET_DEPTH_BELOW_32: usize = 90;
286/// Cumulative microseconds from remote-unchoke to first Piece block received (counter).
287/// Divide by `FIRST_BLOCK_LATENCY_COUNT` for mean unchoke-to-first-block latency.
288pub const FIRST_BLOCK_LATENCY_SUM_US: usize = 91;
289/// Number of first-block-after-unchoke measurements (counter).
290pub const FIRST_BLOCK_LATENCY_COUNT: usize = 92;
291/// Cumulative milliseconds of peer connection lifetime at disconnect (counter).
292/// Divide by `PEER_LIFETIME_COUNT` for mean connection duration.
293pub const PEER_LIFETIME_SUM_MS: usize = 93;
294/// Number of peer disconnects contributing to `PEER_LIFETIME_SUM_MS` (counter).
295pub const PEER_LIFETIME_COUNT: usize = 94;
296
297// -- Operational diagnostics (95..98) --
298
299/// Metric index: total piece-level steals from slow peers (counter).
300pub const PIECE_STEALS_TOTAL: usize = 95;
301/// Metric index: total choke-rotation evictions (counter).
302pub const CHOKE_ROTATION_EVICTIONS_TOTAL: usize = 96;
303/// Metric index: total TCP/uTP connect failures before BT handshake (counter).
304pub const CONNECT_FAILURES_TOTAL: usize = 97;
305/// Metric index: total data-contribution timeout evictions (counter).
306pub const DATA_TIMEOUT_EVICTIONS_TOTAL: usize = 98;
307
308/// Total number of metrics tracked by the session.
309pub const NUM_METRICS: usize = 99;
310
311// ---------------------------------------------------------------------------
312// session_stats_metrics()
313// ---------------------------------------------------------------------------
314
315/// Return static metadata for all session metrics.
316///
317/// The returned slice is indexed by metric constant (e.g. [`NET_BYTES_SENT`]).
318#[must_use]
319pub fn session_stats_metrics() -> &'static [SessionStatsMetric] {
320    use MetricKind::{Counter, Gauge};
321    static METRICS: [SessionStatsMetric; NUM_METRICS] = [
322        // Network (0..11)
323        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        // Disk (12..21)
372        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        // DHT (22..28)
413        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        // Peers (29..40)
442        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        // Protocol (41..54)
491        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        // Bandwidth (55..64)
548        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        // Session (65..69)
589        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        // Sim-perf engine surface (70..73)
610        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        // Dispatch diagnostics (74..81)
627        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        // Hypothesis validation telemetry (85..94)
672        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        // Operational diagnostics (95..98)
713        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
733// ---------------------------------------------------------------------------
734// SessionCounters
735// ---------------------------------------------------------------------------
736
737/// Atomic counter array shared between session and torrent actors.
738///
739/// All values are [`AtomicI64`] — counters are incremented, gauges are set.
740/// The struct is `Send + Sync` (auto-derived from `AtomicI64`).
741pub 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    /// Create a new counter array with all values initialised to zero
751    /// and diagnostic counters disabled.
752    #[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    /// Create a new counter array with diagnostic counters enabled.
764    #[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    /// Whether diagnostic counters (indices >= [`DIAGNOSTIC_COUNTERS_START`])
776    /// are being incremented.
777    #[must_use]
778    pub fn diagnostics_enabled(&self) -> bool {
779        self.diagnostics.load(Ordering::Relaxed)
780    }
781
782    /// Atomically add `delta` to a counter metric.
783    #[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    /// Like [`Self::inc`] but only increments when diagnostic counters are
790    /// enabled. Use for indices >= [`DIAGNOSTIC_COUNTERS_START`].
791    #[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    /// Atomically set a gauge metric to `value`.
800    #[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    /// Like [`Self::set_max`] but only updates when diagnostic counters are
807    /// enabled. Use for indices >= [`DIAGNOSTIC_COUNTERS_START`].
808    #[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    /// Atomically update a high-water gauge: store `value` only when it
817    /// exceeds the current value. Used by the sim-perf surface to track
818    /// the **peak** depth observed for `EVENT_TX_HIGH_WATER` and
819    /// `DISPATCH_TX_HIGH_WATER`. Race-tolerant — the worst case is a
820    /// missed update on a tied write.
821    #[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    /// Read the current value of a metric.
835    #[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    /// Take a consistent snapshot of all metric values.
842    ///
843    /// Also updates the uptime gauge and computes bandwidth rate deltas
844    /// (upload/download rate = bytes since last snapshot).
845    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        // Update uptime gauge.
853        vals[SES_UPTIME_SECS] = self.started_at.elapsed().as_secs() as i64;
854
855        // Compute bandwidth rate deltas.
856        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    /// Number of metrics tracked.
867    pub fn len(&self) -> usize {
868        NUM_METRICS
869    }
870
871    /// Always returns `false` — there are always metrics.
872    pub fn is_empty(&self) -> bool {
873        false
874    }
875
876    /// Seconds elapsed since the session was created.
877    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// ---------------------------------------------------------------------------
889// Tests
890// ---------------------------------------------------------------------------
891
892#[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        // Even without sleeping, uptime should be >= 0.
954        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; // uptime is computed dynamically
1094            }
1095            // BW_UPLOAD_RATE and BW_DOWNLOAD_RATE are computed from deltas
1096            // and will be 0 on first snapshot (prev == 0, cur == 0).
1097            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); // lower — ignored
1135        assert_eq!(c.get(EVENT_TX_HIGH_WATER), 5);
1136        c.set_max(EVENT_TX_HIGH_WATER, 8); // higher — applied
1137        assert_eq!(c.get(EVENT_TX_HIGH_WATER), 8);
1138    }
1139}