Skip to main content

turbo_tasks_malloc/
lib.rs

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