1#![cfg_attr(
9 not(test),
10 deny(
11 clippy::unwrap_used,
12 clippy::unreachable,
13 clippy::expect_used,
14 clippy::panic,
15 clippy::unimplemented,
16 clippy::todo
17 )
18)]
19
20#[cfg(not(feature = "jemalloc"))]
21use std::alloc::{GlobalAlloc, Layout, System};
22#[cfg(not(feature = "jemalloc"))]
23use std::sync::atomic::{AtomicUsize, Ordering};
24
25#[cfg(not(feature = "jemalloc"))]
27pub static ALLOCATED: AtomicUsize = AtomicUsize::new(0);
28#[cfg(not(feature = "jemalloc"))]
30pub static PEAK: AtomicUsize = AtomicUsize::new(0);
31
32#[cfg(not(feature = "jemalloc"))]
34pub struct TrackingAlloc;
35
36#[cfg(not(feature = "jemalloc"))]
37#[inline]
38fn record_alloc(size: usize) {
39 let current = ALLOCATED.fetch_add(size, Ordering::Relaxed) + size;
40 PEAK.fetch_max(current, Ordering::Relaxed);
41}
42
43#[cfg(not(feature = "jemalloc"))]
44#[inline]
45fn record_dealloc(size: usize) {
46 ALLOCATED.fetch_sub(size, Ordering::Relaxed);
47}
48
49#[cfg(not(feature = "jemalloc"))]
50unsafe impl GlobalAlloc for TrackingAlloc {
54 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
55 let ptr = unsafe { System.alloc(layout) };
58 if !ptr.is_null() {
59 record_alloc(layout.size());
60 }
61 ptr
62 }
63
64 unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
65 let ptr = unsafe { System.alloc_zeroed(layout) };
68 if !ptr.is_null() {
69 record_alloc(layout.size());
70 }
71 ptr
72 }
73
74 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
75 record_dealloc(layout.size());
76 unsafe { System.dealloc(ptr, layout) };
79 }
80
81 unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
82 let new_ptr = unsafe { System.realloc(ptr, layout, new_size) };
85 if !new_ptr.is_null() {
86 match new_size.cmp(&layout.size()) {
87 std::cmp::Ordering::Greater => record_alloc(new_size - layout.size()),
88 std::cmp::Ordering::Less => record_dealloc(layout.size() - new_size),
89 std::cmp::Ordering::Equal => {}
90 }
91 }
92 new_ptr
93 }
94}
95
96#[cfg(not(feature = "jemalloc"))]
98#[expect(
99 clippy::cast_precision_loss,
100 reason = "Allocator metrics are approximate diagnostics reported as f64 MiB values."
101)]
102#[must_use]
103pub fn stats() -> (f64, f64) {
104 let allocated = ALLOCATED.load(Ordering::Relaxed) as f64 / 1_048_576.0;
105 let peak = PEAK.load(Ordering::Relaxed) as f64 / 1_048_576.0;
106 (allocated, peak)
107}
108
109#[cfg(feature = "jemalloc-stats")]
111#[expect(
112 clippy::cast_precision_loss,
113 reason = "Allocator metrics are approximate diagnostics reported as f64 MiB values."
114)]
115#[must_use]
116pub fn stats() -> (f64, f64) {
117 if let Err(error) = tikv_jemalloc_ctl::epoch::advance() {
118 tracing::warn!(error = %error, "failed to refresh jemalloc stats epoch");
119 }
120
121 let allocated = tikv_jemalloc_ctl::stats::allocated::read().unwrap_or(0) as f64 / 1_048_576.0;
122 let resident = tikv_jemalloc_ctl::stats::resident::read().unwrap_or(0) as f64 / 1_048_576.0;
123 (allocated, resident)
124}
125
126#[cfg(all(feature = "jemalloc", not(feature = "jemalloc-stats")))]
128pub fn stats() -> (f64, f64) {
129 (0.0, 0.0)
130}
131
132#[cfg(test)]
133mod tests {
134 #[cfg(not(feature = "jemalloc"))]
135 use std::{
136 alloc::{GlobalAlloc, Layout},
137 ptr,
138 sync::{Mutex, MutexGuard},
139 };
140
141 #[cfg(not(feature = "jemalloc"))]
142 static TEST_ALLOC_LOCK: Mutex<()> = Mutex::new(());
143
144 #[cfg(not(feature = "jemalloc"))]
145 fn reset_tracking_counters() -> MutexGuard<'static, ()> {
146 let guard = TEST_ALLOC_LOCK.lock().unwrap();
147 super::ALLOCATED.store(0, std::sync::atomic::Ordering::Relaxed);
148 super::PEAK.store(0, std::sync::atomic::Ordering::Relaxed);
149 guard
150 }
151
152 #[cfg(not(feature = "jemalloc"))]
153 fn assert_stats_bytes(allocated: usize, peak: usize) {
154 let (allocated_mib, peak_mib) = super::stats();
155 let mib_per_byte = 1.0 / 1_048_576.0;
156 let expected_allocated_mib =
157 f64::from(u32::try_from(allocated).expect("test allocation fits in u32"))
158 * mib_per_byte;
159 let expected_peak_mib =
160 f64::from(u32::try_from(peak).expect("test allocation fits in u32")) * mib_per_byte;
161
162 assert!((allocated_mib - expected_allocated_mib).abs() < f64::EPSILON);
163 assert!((peak_mib - expected_peak_mib).abs() < f64::EPSILON);
164 }
165
166 #[cfg(not(feature = "jemalloc"))]
167 #[test]
168 fn stats_returns_non_negative_counters() {
169 let (allocated, peak) = super::stats();
170
171 assert!(allocated >= 0.0);
172 assert!(peak >= 0.0);
173 }
174
175 #[cfg(not(feature = "jemalloc"))]
176 #[test]
177 fn record_alloc_updates_current_and_peak_bytes() {
178 let _guard = reset_tracking_counters();
179
180 super::record_alloc(256);
181 assert_stats_bytes(256, 256);
182
183 super::record_alloc(128);
184 assert_stats_bytes(384, 384);
185 }
186
187 #[cfg(not(feature = "jemalloc"))]
188 #[test]
189 fn record_dealloc_reduces_current_without_lowering_peak() {
190 let _guard = reset_tracking_counters();
191
192 super::record_alloc(512);
193 super::record_dealloc(128);
194
195 assert_stats_bytes(384, 512);
196 }
197
198 #[cfg(not(feature = "jemalloc"))]
199 #[test]
200 fn tracking_alloc_records_alloc_and_dealloc() {
201 let _guard = reset_tracking_counters();
202 let allocator = super::TrackingAlloc;
203 let layout = Layout::from_size_align(64, 8).unwrap();
204
205 let ptr = unsafe { allocator.alloc(layout) };
208 assert!(!ptr.is_null());
209 assert_stats_bytes(64, 64);
210
211 unsafe { allocator.dealloc(ptr, layout) };
214 assert_stats_bytes(0, 64);
215 }
216
217 #[cfg(not(feature = "jemalloc"))]
218 #[test]
219 fn tracking_alloc_zeroed_returns_zeroed_memory_and_records_size() {
220 let _guard = reset_tracking_counters();
221 let allocator = super::TrackingAlloc;
222 let layout = Layout::from_size_align(32, 8).unwrap();
223
224 let ptr = unsafe { allocator.alloc_zeroed(layout) };
227 assert!(!ptr.is_null());
228
229 let bytes = unsafe { std::slice::from_raw_parts(ptr, layout.size()) };
232 assert!(bytes.iter().all(|byte| *byte == 0));
233 assert_stats_bytes(32, 32);
234
235 unsafe { allocator.dealloc(ptr, layout) };
238 assert_stats_bytes(0, 32);
239 }
240
241 #[cfg(not(feature = "jemalloc"))]
242 #[test]
243 fn tracking_alloc_realloc_grow_and_shrink_adjusts_counters() {
244 let _guard = reset_tracking_counters();
245 let allocator = super::TrackingAlloc;
246 let initial_layout = Layout::from_size_align(16, 8).unwrap();
247
248 let ptr = unsafe { allocator.alloc(initial_layout) };
251 assert!(!ptr.is_null());
252
253 unsafe { ptr::write_bytes(ptr, 0xAB, initial_layout.size()) };
255
256 let grown_ptr = unsafe { allocator.realloc(ptr, initial_layout, 64) };
258 assert!(!grown_ptr.is_null());
259 assert_stats_bytes(64, 64);
260
261 let preserved = unsafe { std::slice::from_raw_parts(grown_ptr, initial_layout.size()) };
263 assert!(preserved.iter().all(|byte| *byte == 0xAB));
264
265 let grown_layout = Layout::from_size_align(64, 8).unwrap();
266 let shrunk_ptr = unsafe { allocator.realloc(grown_ptr, grown_layout, 24) };
269 assert!(!shrunk_ptr.is_null());
270 assert_stats_bytes(24, 64);
271
272 let shrunk_layout = Layout::from_size_align(24, 8).unwrap();
273 unsafe { allocator.dealloc(shrunk_ptr, shrunk_layout) };
276 assert_stats_bytes(0, 64);
277 }
278
279 #[cfg(not(feature = "jemalloc"))]
280 #[test]
281 fn tracking_alloc_realloc_same_size_leaves_counters_unchanged() {
282 let _guard = reset_tracking_counters();
283 let allocator = super::TrackingAlloc;
284 let layout = Layout::from_size_align(40, 8).unwrap();
285
286 let ptr = unsafe { allocator.alloc(layout) };
289 assert!(!ptr.is_null());
290 assert_stats_bytes(40, 40);
291
292 let same_size_ptr = unsafe { allocator.realloc(ptr, layout, layout.size()) };
295 assert!(!same_size_ptr.is_null());
296 assert_stats_bytes(40, 40);
297
298 unsafe { allocator.dealloc(same_size_ptr, layout) };
301 assert_stats_bytes(0, 40);
302 }
303
304 #[cfg(all(feature = "jemalloc", not(feature = "jemalloc-stats")))]
305 #[test]
306 fn jemalloc_without_stats_returns_zeroes() {
307 assert_eq!(super::stats(), (0.0, 0.0));
308 }
309}