Skip to main content

turbo_tasks_malloc/
lib.rs

1mod counter;
2mod memory_footprint;
3mod memory_pressure;
4
5use std::{
6    alloc::{GlobalAlloc, Layout},
7    marker::PhantomData,
8    ops::{Add, AddAssign},
9};
10
11use self::counter::{add, flush, remove, update};
12
13#[derive(Default, Clone, Debug)]
14pub struct AllocationInfo {
15    pub allocations: usize,
16    pub deallocations: usize,
17    pub allocation_count: usize,
18    pub deallocation_count: usize,
19}
20
21impl AllocationInfo {
22    pub const ZERO: Self = Self {
23        allocations: 0,
24        deallocations: 0,
25        allocation_count: 0,
26        deallocation_count: 0,
27    };
28
29    pub fn is_empty(&self) -> bool {
30        self.allocations == 0
31            && self.deallocations == 0
32            && self.allocation_count == 0
33            && self.deallocation_count == 0
34    }
35
36    pub fn memory_usage(&self) -> usize {
37        self.allocations.saturating_sub(self.deallocations)
38    }
39}
40
41impl Add<Self> for AllocationInfo {
42    type Output = Self;
43
44    fn add(self, other: Self) -> Self {
45        Self {
46            allocations: self.allocations + other.allocations,
47            deallocations: self.deallocations + other.deallocations,
48            allocation_count: self.allocation_count + other.allocation_count,
49            deallocation_count: self.deallocation_count + other.deallocation_count,
50        }
51    }
52}
53
54impl AddAssign<Self> for AllocationInfo {
55    fn add_assign(&mut self, other: Self) {
56        self.allocations += other.allocations;
57        self.deallocations += other.deallocations;
58        self.allocation_count += other.allocation_count;
59        self.deallocation_count += other.deallocation_count;
60    }
61}
62
63#[derive(Default, Clone, Debug)]
64pub struct AllocationCounters {
65    pub allocations: usize,
66    pub deallocations: usize,
67    pub allocation_count: usize,
68    pub deallocation_count: usize,
69    _not_send: PhantomData<*mut ()>,
70}
71
72impl AllocationCounters {
73    const fn new() -> Self {
74        Self {
75            allocation_count: 0,
76            deallocation_count: 0,
77            allocations: 0,
78            deallocations: 0,
79            _not_send: PhantomData {},
80        }
81    }
82}
83
84/// Turbo's preferred global allocator. This is a new type instead of a type
85/// alias because you can't use type aliases to instantiate unit types (E0423).
86pub struct TurboMalloc;
87
88impl TurboMalloc {
89    /// Returns the bytes mimalloc currently has committed from the OS. This measures what the
90    /// allocator holds rather than the process's total footprint, and it does not track frees in
91    /// lock step, since mimalloc reuses and purges pages on its own schedule.
92    ///
93    /// See `current_commit` in [`mi_process_info`], which documents each figure mimalloc reports.
94    ///
95    /// [`mi_process_info`]: https://docs.rs/libmimalloc-sys/latest/libmimalloc_sys/fn.mi_process_info.html
96    ///
97    /// Without the `custom_allocator` feature this is a process-wide live-bytes counter instead,
98    /// which is approximate because threads buffer their updates.
99    pub fn memory_usage() -> usize {
100        #[cfg(all(feature = "custom_allocator", not(target_family = "wasm"), not(miri)))]
101        {
102            // `current_commit` is a relaxed atomic load, but `mi_process_info` also calls
103            // `_mi_prim_process_info`, which is a `getrusage` (plus a `task_info` on macOS). All
104            // eight out-params are optional, so ask only for the one we use.
105            let mut current_commit = 0usize;
106            // Safety: every out-param is either null or a valid `usize` we own.
107            unsafe {
108                libmimalloc_sys::mi_process_info(
109                    /* elapsed_msecs */ std::ptr::null_mut(),
110                    /* user_msecs */ std::ptr::null_mut(),
111                    /* system_msecs */ std::ptr::null_mut(),
112                    /* current_rss */ std::ptr::null_mut(),
113                    /* peak_rss */ std::ptr::null_mut(),
114                    &mut current_commit,
115                    /* peak_commit */ std::ptr::null_mut(),
116                    /* page_faults */ std::ptr::null_mut(),
117                );
118            }
119            current_commit
120        }
121        #[cfg(not(all(feature = "custom_allocator", not(target_family = "wasm"), not(miri))))]
122        {
123            self::counter::get()
124        }
125    }
126
127    /// Clears the calling thread's allocation counters. Call this when a thread is about to stop,
128    /// so a thread that reuses its slot does not inherit the previous totals.
129    pub fn thread_stop() {
130        flush();
131    }
132
133    pub fn thread_park() {
134        Self::collect(false);
135    }
136
137    /// When using mimalloc triggers some cleanup
138    /// force=false: process threadlocal free lists and other threadlocal deferred work
139    ///    only operates on thread local data and should be fast
140    /// force=true: do all the work of `process=false` and then process global shared structures and
141    /// return memory to the OS if possible, this is much slower and should only be done rarely.
142    pub fn collect(force: bool) {
143        #[cfg(all(feature = "custom_allocator", not(target_family = "wasm"), not(miri)))]
144        unsafe {
145            libmimalloc_sys::mi_collect(force);
146        }
147        #[cfg(not(all(feature = "custom_allocator", not(target_family = "wasm"), not(miri))))]
148        {
149            let _ = force;
150        }
151    }
152
153    pub fn allocation_counters() -> AllocationCounters {
154        self::counter::allocation_counters()
155    }
156
157    pub fn reset_allocation_counters(start: AllocationCounters) {
158        self::counter::reset_allocation_counters(start);
159    }
160
161    /// Returns a memory pressure value in the range `0..=100`, or `None` when
162    /// the current platform does not expose a memory pressure signal or a
163    /// query for it failed.
164    ///
165    /// `0` means no memory pressure, `100` means maximum pressure.
166    ///
167    /// - On Linux this is derived from `/proc/pressure/memory` (the `some` `avg10` stall
168    ///   percentage), falling back to `(MemTotal - MemAvailable) / MemTotal` from `/proc/meminfo`
169    ///   when PSI is not available (older kernels, no `CONFIG_PSI`, or containers without access).
170    /// - On macOS this is derived from the `kern.memorystatus_level` sysctl (`100 -
171    ///   free_memory_percentage`).
172    /// - On Windows this is `MEMORYSTATUSEX::dwMemoryLoad` (percentage of physical memory in use).
173    /// - On other platforms this returns `None`.
174    pub fn memory_pressure() -> Option<u8> {
175        memory_pressure::memory_pressure()
176    }
177
178    /// Returns the resident memory size of the current process in bytes, or
179    /// `None` when the current platform does not expose a memory footprint
180    /// signal or a query for it failed.
181    ///
182    /// - On Linux this is the resident set size from `/proc/self/statm`.
183    /// - On macOS this is `rusage_info_v0::ri_phys_footprint` from `proc_pid_rusage` (what Apple
184    ///   calls "memory footprint" / Activity Monitor's "Memory" column).
185    /// - On Windows this is `PROCESS_MEMORY_COUNTERS::WorkingSetSize`.
186    /// - On other platforms this returns `None`.
187    pub fn memory_footprint() -> Option<usize> {
188        memory_footprint::memory_footprint()
189    }
190}
191
192/// Get the allocator for this platform that we should wrap with TurboMalloc.
193#[inline]
194fn base_alloc() -> &'static impl GlobalAlloc {
195    #[cfg(all(feature = "custom_allocator", not(target_family = "wasm"), not(miri)))]
196    return &mimalloc::MiMalloc;
197    #[cfg(not(all(feature = "custom_allocator", not(target_family = "wasm"), not(miri))))]
198    return &std::alloc::System;
199}
200
201#[allow(unused_variables)]
202unsafe fn base_alloc_size(ptr: *const u8, layout: Layout) -> usize {
203    #[cfg(all(feature = "custom_allocator", not(target_family = "wasm"), not(miri)))]
204    return unsafe { mimalloc::MiMalloc.usable_size(ptr) };
205    #[cfg(not(all(feature = "custom_allocator", not(target_family = "wasm"), not(miri))))]
206    return layout.size();
207}
208
209unsafe impl GlobalAlloc for TurboMalloc {
210    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
211        let ret = unsafe { base_alloc().alloc(layout) };
212        if !ret.is_null() {
213            let size = unsafe { base_alloc_size(ret, layout) };
214            add(size);
215        }
216        ret
217    }
218
219    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
220        let size = unsafe { base_alloc_size(ptr, layout) };
221        unsafe { base_alloc().dealloc(ptr, layout) };
222        remove(size);
223    }
224
225    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
226        let ret = unsafe { base_alloc().alloc_zeroed(layout) };
227        if !ret.is_null() {
228            let size = unsafe { base_alloc_size(ret, layout) };
229            add(size);
230        }
231        ret
232    }
233
234    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
235        let old_size = unsafe { base_alloc_size(ptr, layout) };
236        let ret = unsafe { base_alloc().realloc(ptr, layout, new_size) };
237        if !ret.is_null() {
238            // SAFETY: the caller must ensure that the `new_size` does not overflow.
239            // `layout.align()` comes from a `Layout` and is thus guaranteed to be valid.
240            let new_layout = unsafe { Layout::from_size_align_unchecked(new_size, layout.align()) };
241            let new_size = unsafe { base_alloc_size(ret, new_layout) };
242            update(old_size, new_size);
243        }
244        ret
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::TurboMalloc;
251
252    // `memory_usage` reports what *this* allocator has committed, so the test binary has to
253    // actually route its allocations through it. Without this the `vec!` below goes to the
254    // system allocator and mimalloc's counter never moves.
255    #[global_allocator]
256    static ALLOC: TurboMalloc = TurboMalloc;
257
258    /// Guards against the counter silently becoming unavailable.
259    #[test]
260    fn memory_usage_is_reported_and_tracks_a_large_allocation() {
261        let before = TurboMalloc::memory_usage();
262        assert!(before > 0, "a running process has live memory");
263
264        // Large enough to dwarf whatever else the test process does concurrently, and written to
265        // so the pages are actually committed.
266        const SIZE: usize = 256 * 1024 * 1024;
267        let mut buffer = vec![0u8; SIZE];
268        for chunk in buffer.chunks_mut(4096) {
269            chunk[0] = 1;
270        }
271        std::hint::black_box(&buffer);
272
273        let after = TurboMalloc::memory_usage();
274        assert!(
275            after >= before + SIZE / 2,
276            "expected a rise of at least {} bytes, got {before} -> {after}",
277            SIZE / 2
278        );
279        drop(buffer);
280    }
281
282    #[test]
283    fn memory_pressure_is_in_range() {
284        let value = TurboMalloc::memory_pressure();
285
286        // On all supported platforms the value must be reported.
287        #[cfg(any(
288            all(target_os = "linux", not(target_family = "wasm"), not(miri)),
289            target_os = "macos",
290            windows,
291        ))]
292        let value = value.expect("memory_pressure() should return Some on this platform");
293
294        // On unsupported platforms we expect None and have nothing further to assert.
295        #[cfg(not(any(
296            all(target_os = "linux", not(target_family = "wasm"), not(miri)),
297            target_os = "macos",
298            windows,
299        )))]
300        let Some(value) = value else {
301            return;
302        };
303
304        assert!(
305            value <= 100,
306            "memory_pressure() returned {value}, expected a value in 0..=100"
307        );
308    }
309}