nodedb_mem/metrics.rs
1//! Metrics export for the memory governor.
2//!
3//! Provides jemalloc introspection to report actual RSS, mapped memory,
4//! and arena statistics alongside the governor's logical budget tracking.
5
6/// System memory statistics from jemalloc.
7#[derive(Debug, Clone)]
8pub struct SystemMemoryStats {
9 /// Resident Set Size — actual physical memory used.
10 pub rss_bytes: usize,
11
12 /// Total bytes allocated by the application.
13 pub allocated_bytes: usize,
14
15 /// Total bytes in active pages (mapped and potentially dirty).
16 pub active_bytes: usize,
17
18 /// Total bytes mapped by the allocator (may exceed active).
19 pub mapped_bytes: usize,
20
21 /// Total bytes retained in the allocator's caches.
22 pub retained_bytes: usize,
23
24 /// Fragmentation ratio: `(active - allocated) / active`.
25 ///
26 /// Measures how much memory the allocator has claimed from the OS
27 /// (active pages) but isn't actually used by application objects.
28 /// This gap comes from jemalloc internal metadata, free-list
29 /// fragmentation, and thread cache overhead.
30 ///
31 /// Healthy: < 0.15 (15%). Warning: 0.15–0.25. Critical: > 0.25.
32 /// A sustained ratio above 0.25 indicates severe fragmentation —
33 /// the process is consuming significantly more RSS than its live
34 /// data warrants, and OOM risk increases under memory pressure.
35 pub fragmentation_ratio: f64,
36}
37
38impl SystemMemoryStats {
39 /// Query jemalloc for current system memory statistics.
40 ///
41 /// Returns `None` if jemalloc introspection is unavailable.
42 pub fn query() -> Option<Self> {
43 // Trigger a stats epoch refresh.
44 let _ = tikv_jemalloc_ctl::epoch::advance();
45
46 let allocated = tikv_jemalloc_ctl::stats::allocated::read().ok()?;
47 let active = tikv_jemalloc_ctl::stats::active::read().ok()?;
48 let mapped = tikv_jemalloc_ctl::stats::mapped::read().ok()?;
49 let retained = tikv_jemalloc_ctl::stats::retained::read().ok()?;
50 let resident = tikv_jemalloc_ctl::stats::resident::read().ok()?;
51
52 // Fragmentation: how much of jemalloc's active memory is wasted.
53 // active = pages the allocator has obtained from the OS.
54 // allocated = bytes the application is actually using.
55 // The difference is internal fragmentation + free-list overhead.
56 let fragmentation_ratio = if active > 0 {
57 (active.saturating_sub(allocated)) as f64 / active as f64
58 } else {
59 0.0
60 };
61
62 Some(Self {
63 rss_bytes: resident,
64 allocated_bytes: allocated,
65 active_bytes: active,
66 mapped_bytes: mapped,
67 retained_bytes: retained,
68 fragmentation_ratio,
69 })
70 }
71
72 /// Returns `true` if fragmentation exceeds the warning threshold (25%).
73 pub fn is_fragmentation_critical(&self) -> bool {
74 self.fragmentation_ratio > 0.25
75 }
76}