Skip to main content

irontide_engine_support/
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// -- Outbound choke-flip churn (99..100) --
309
310/// M257b: outbound unchoke flips applied by the local choker —
311/// `am_choking: true → false` transitions committed in `run_choker`.
312/// Paired with [`OUTBOUND_CHOKE_FLIPS_TOTAL`], this measures
313/// rotation churn from OUR side (gap review §3 — the M257d
314/// hysteresis target); the `REMOTE_*` counters (85–87) measure the
315/// same axis as seen from peers choking us.
316pub const OUTBOUND_UNCHOKE_FLIPS_TOTAL: usize = 99;
317/// M257b: outbound choke flips (`am_choking: false → true`) committed
318/// in `run_choker`.
319pub const OUTBOUND_CHOKE_FLIPS_TOTAL: usize = 100;
320
321// -- Request-budget convergence (101..102) --
322
323/// M257c: permit returns ABSORBED by the request budget — the reader's
324/// return site measured granted depth above `target_depth` and withheld
325/// the permit, shrinking the peer's pipeline by one. Diagnostic-gated.
326pub const BUDGET_PERMITS_ABSORBED_TOTAL: usize = 101;
327/// M257c: allocator runs (`pipeline_tick`) that changed at least one
328/// peer's `target_depth`. Diagnostic-gated.
329pub const BUDGET_REALLOCS_TOTAL: usize = 102;
330
331// -- M257g contended-collapse diagnostics (103..110) --
332//
333// Instrument the metastable bimodal collapse before fixing it (the
334// M257d/f/e "fix on a hypothesis" mistake). These expose the choke-hold
335// pool-drain + steal-feedback-runaway mechanism: queue depth, inflight
336// reservations and how many are locked by choked conns, the ballooning
337// steal-threshold input, steal latency, choke parks, and the recovery
338// re-download cost. All diagnostic-gated (indices >= 70).
339
340/// Metric index: current `queue_pieces` depth on the leecher (gauge).
341pub const DISPATCH_QUEUE_PIECES: usize = 103;
342/// Metric index: current `inflight` reservation count (gauge).
343pub const DISPATCH_INFLIGHT_COUNT: usize = 104;
344/// Metric index: `inflight` pieces whose owning peer is choking us (gauge).
345pub const DISPATCH_INFLIGHT_CHOKED: usize = 105;
346/// Metric index: current `peer_avg_time` (steal-threshold input) in µs (gauge).
347pub const DISPATCH_PIECE_AVG_US: usize = 106;
348/// Metric index: cumulative age (µs) of pieces at the moment they were
349/// stolen — divide by [`PIECE_STEALS_TOTAL`] for mean steal latency (counter).
350pub const DISPATCH_STEAL_AGE_SUM_US: usize = 107;
351/// Metric index: requester parks on `unchoke_notify` at dispatch step 2a
352/// (counter). Park-reason histogram vs [`DISPATCH_ACQUIRE_NONE_TOTAL`].
353pub const DISPATCH_CHOKE_PARK_TOTAL: usize = 108;
354/// Metric index: cumulative full length of stolen pieces (counter) — the
355/// new owner re-downloads from block 0, so this upper-bounds the steal
356/// re-download cost (prior owner's partial progress is task-local).
357pub const DISPATCH_WASTED_BYTES: usize = 109;
358/// Metric index: cumulative µs spent parked on `unchoke_notify` — divide by
359/// [`DISPATCH_CHOKE_PARK_TOTAL`] for mean park duration ≈ `T_choke` (counter).
360pub const DISPATCH_PARK_DUR_SUM_US: usize = 110;
361/// Metric index: acquire-path `try_steal` recoveries (counter). Distinct
362/// from [`PIECE_STEALS_TOTAL`] (the M149 block-level steal scan); divide
363/// [`DISPATCH_STEAL_AGE_SUM_US`] by THIS for mean steal latency.
364pub const DISPATCH_STEALS_TOTAL: usize = 111;
365
366/// Total number of metrics tracked by the session.
367pub const NUM_METRICS: usize = 112;
368
369// ---------------------------------------------------------------------------
370// session_stats_metrics()
371// ---------------------------------------------------------------------------
372
373/// Return static metadata for all session metrics.
374///
375/// The returned slice is indexed by metric constant (e.g. [`NET_BYTES_SENT`]).
376#[must_use]
377pub fn session_stats_metrics() -> &'static [SessionStatsMetric] {
378    use MetricKind::{Counter, Gauge};
379    static METRICS: [SessionStatsMetric; NUM_METRICS] = [
380        // Network (0..11)
381        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        // Disk (12..21)
430        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        // DHT (22..28)
471        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        // Peers (29..40)
500        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        // Protocol (41..54)
549        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        // Bandwidth (55..64)
606        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        // Session (65..69)
647        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        // Sim-perf engine surface (70..73)
668        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        // Dispatch diagnostics (74..81)
685        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        // Hypothesis validation telemetry (85..94)
730        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        // Operational diagnostics (95..98)
771        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        // M257b outbound choke-flip churn (99..100)
788        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        // M257c request-budget convergence (101..102)
797        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        // M257g contended-collapse diagnostics (103..110)
806        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
846// ---------------------------------------------------------------------------
847// SessionCounters
848// ---------------------------------------------------------------------------
849
850/// Atomic counter array shared between session and torrent actors.
851///
852/// All values are [`AtomicI64`] — counters are incremented, gauges are set.
853/// The struct is `Send + Sync` (auto-derived from `AtomicI64`).
854pub 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    /// Create a new counter array with all values initialised to zero
864    /// and diagnostic counters disabled.
865    #[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    /// Create a new counter array with diagnostic counters enabled.
877    #[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    /// Whether diagnostic counters (indices >= [`DIAGNOSTIC_COUNTERS_START`])
889    /// are being incremented.
890    #[must_use]
891    pub fn diagnostics_enabled(&self) -> bool {
892        self.diagnostics.load(Ordering::Relaxed)
893    }
894
895    /// Atomically add `delta` to a counter metric.
896    #[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    /// Like [`Self::inc`] but only increments when diagnostic counters are
903    /// enabled. Use for indices >= [`DIAGNOSTIC_COUNTERS_START`].
904    #[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    /// Atomically set a gauge metric to `value`.
913    #[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    /// Like [`Self::set_max`] but only updates when diagnostic counters are
920    /// enabled. Use for indices >= [`DIAGNOSTIC_COUNTERS_START`].
921    #[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    /// Atomically update a high-water gauge: store `value` only when it
930    /// exceeds the current value. Used by the sim-perf surface to track
931    /// the **peak** depth observed for `EVENT_TX_HIGH_WATER` and
932    /// `DISPATCH_TX_HIGH_WATER`. Race-tolerant — the worst case is a
933    /// missed update on a tied write.
934    #[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    /// Read the current value of a metric.
948    #[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    /// Take a consistent snapshot of all metric values.
955    ///
956    /// Also updates the uptime gauge and computes bandwidth rate deltas
957    /// (upload/download rate = bytes since last snapshot).
958    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        // Update uptime gauge.
966        vals[SES_UPTIME_SECS] = self.started_at.elapsed().as_secs() as i64;
967
968        // Compute bandwidth rate deltas.
969        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    /// Number of metrics tracked.
980    pub fn len(&self) -> usize {
981        NUM_METRICS
982    }
983
984    /// Always returns `false` — there are always metrics.
985    pub fn is_empty(&self) -> bool {
986        false
987    }
988
989    /// Seconds elapsed since the session was created.
990    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// ---------------------------------------------------------------------------
1002// Tests
1003// ---------------------------------------------------------------------------
1004
1005#[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        // Even without sleeping, uptime should be >= 0.
1082        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; // uptime is computed dynamically
1235            }
1236            // BW_UPLOAD_RATE and BW_DOWNLOAD_RATE are computed from deltas
1237            // and will be 0 on first snapshot (prev == 0, cur == 0).
1238            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); // lower — ignored
1276        assert_eq!(c.get(EVENT_TX_HIGH_WATER), 5);
1277        c.set_max(EVENT_TX_HIGH_WATER, 8); // higher — applied
1278        assert_eq!(c.get(EVENT_TX_HIGH_WATER), 8);
1279    }
1280}