Skip to main content

memory_budget/
jemalloc.rs

1//! jemalloc stats split for the [`crate::BudgetSnapshot`] non-cache bucket.
2//!
3//! `rss − cache_bytes − lease_bytes` lumps every byte the budget cannot
4//! resize into a single number. When that number grows monotonically
5//! across a long-running workload it is impossible to tell whether the
6//! application is retaining live objects or jemalloc is sitting on
7//! retained-but-decommitted pages
8//! (work for allocator tuning). This module surfaces jemalloc's own
9//! introspection so the tick log can name the bucket directly.
10//!
11//! [`SystemJemallocStats`] is feature-gated behind `jemalloc` so the
12//! memory-budget crate stays allocator-agnostic; binaries that install
13//! `tikv-jemallocator` as `#[global_allocator]` enable the feature and
14//! wire it into the budget via [`crate::MemoryBudget::with_jemalloc_stats`].
15//! Everyone else falls back to [`NoJemallocStats`] and the budget tick
16//! reports zeros for these columns.
17
18/// Snapshot of jemalloc's process-wide accounting at one tick. All
19/// values are bytes. Definitions match the `stats.*` mallctl namespace
20/// (see [jemalloc(3)](https://jemalloc.net/jemalloc.3.html#stats)).
21#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
22pub struct JemallocStats {
23    /// Bytes currently held by live application allocations
24    /// (`stats.allocated`). The closest thing to "what is the live heap
25    /// from the program's point of view". If this grows monotonically
26    /// across runs while caches stay flat, the program is holding more
27    /// memory.
28    pub allocated_bytes: u64,
29    /// Bytes in active jemalloc extents (`stats.active`). Always
30    /// `>= allocated_bytes`; the delta is fragmentation within active
31    /// extents.
32    pub active_bytes: u64,
33    /// Bytes used by jemalloc's own metadata structures
34    /// (`stats.metadata`).
35    pub metadata_bytes: u64,
36    /// jemalloc's resident-set estimate (`stats.resident`): active +
37    /// retained-but-resident + metadata, in pages.
38    pub resident_bytes: u64,
39    /// Total virtual memory mapped by jemalloc (`stats.mapped`).
40    pub mapped_bytes: u64,
41    /// Virtual memory retained by jemalloc after decommit
42    /// (`stats.retained`). This is the bucket that explains "process
43    /// RSS stays high after a workload finishes" — pages were
44    /// returned to jemalloc but jemalloc has not yet returned them to
45    /// the kernel. Controlled by `dirty_decay_ms` / `muzzy_decay_ms`
46    /// in `MALLOC_CONF`.
47    pub retained_bytes: u64,
48}
49
50impl JemallocStats {
51    /// Conservatively reconcile process-wide and allocator measurements.
52    ///
53    /// These are overlapping views, so take their maximum rather than adding
54    /// them or allowing a smaller allocator reading to hide other memory.
55    #[must_use]
56    pub fn observed_usage_bytes(self, process_bytes: u64) -> u64 {
57        process_bytes
58            .max(self.resident_bytes)
59            .max(self.allocated_bytes)
60    }
61}
62
63/// "What does jemalloc say about our memory usage right now?"
64///
65/// Implementations must be cheap (the budget calls
66/// [`JemallocStatsSource::read`] once per tick). Returning all-zeros
67/// is a valid answer for builds that do not use jemalloc.
68pub trait JemallocStatsSource: Send + Sync {
69    /// Read the current stats. Implementations are free to cache the
70    /// epoch handle, but should call `epoch.advance` (or equivalent)
71    /// before reading so the returned values reflect the current
72    /// state and not the values from the previous epoch.
73    fn read(&self) -> JemallocStats;
74}
75
76/// No-op source used when the binary does not install jemalloc as
77/// the global allocator. All readings are zero, which the tick log
78/// distinguishes from real-but-tiny readings via the surrounding
79/// `cache_bytes_current` / `rss_bytes` context.
80pub struct NoJemallocStats;
81
82impl JemallocStatsSource for NoJemallocStats {
83    fn read(&self) -> JemallocStats {
84        JemallocStats::default()
85    }
86}
87
88#[cfg(feature = "jemalloc")]
89pub use system::SystemJemallocStats;
90
91#[cfg(feature = "jemalloc")]
92mod system {
93    use tikv_jemalloc_ctl::{epoch, epoch_mib, stats};
94
95    use super::{JemallocStats, JemallocStatsSource};
96
97    /// Production stats source backed by `tikv-jemalloc-ctl`'s mallctl
98    /// wrapper. Caches the per-stat MIB handles so each tick is just
99    /// six word-sized reads after an `epoch.advance`.
100    ///
101    /// Construct via [`SystemJemallocStats::new`]. The constructor
102    /// resolves the mallctl handles once; a failure here means jemalloc
103    /// was compiled without stats support and the budget has no useful
104    /// information to surface, so the caller should fall back to
105    /// [`super::NoJemallocStats`] instead.
106    pub struct SystemJemallocStats {
107        epoch: epoch_mib,
108        allocated: stats::allocated_mib,
109        active: stats::active_mib,
110        metadata: stats::metadata_mib,
111        resident: stats::resident_mib,
112        mapped: stats::mapped_mib,
113        retained: stats::retained_mib,
114    }
115
116    impl SystemJemallocStats {
117        /// Resolve the mallctl MIB handles. Returns `None` if jemalloc
118        /// was built without stats support — caller should fall back
119        /// to [`super::NoJemallocStats`].
120        #[must_use]
121        pub fn new() -> Option<Self> {
122            Some(Self {
123                epoch: epoch::mib().ok()?,
124                allocated: stats::allocated::mib().ok()?,
125                active: stats::active::mib().ok()?,
126                metadata: stats::metadata::mib().ok()?,
127                resident: stats::resident::mib().ok()?,
128                mapped: stats::mapped::mib().ok()?,
129                retained: stats::retained::mib().ok()?,
130            })
131        }
132    }
133
134    impl JemallocStatsSource for SystemJemallocStats {
135        fn read(&self) -> JemallocStats {
136            // jemalloc's stats are sampled into a snapshot on each
137            // `epoch.advance` call; without this every read returns the
138            // values frozen at process start.
139            if self.epoch.advance().is_err() {
140                return JemallocStats::default();
141            }
142            JemallocStats {
143                allocated_bytes: self.allocated.read().unwrap_or(0) as u64,
144                active_bytes: self.active.read().unwrap_or(0) as u64,
145                metadata_bytes: self.metadata.read().unwrap_or(0) as u64,
146                resident_bytes: self.resident.read().unwrap_or(0) as u64,
147                mapped_bytes: self.mapped.read().unwrap_or(0) as u64,
148                retained_bytes: self.retained.read().unwrap_or(0) as u64,
149            }
150        }
151    }
152}