Skip to main content

nodedb_mem/
metrics.rs

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