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, get, 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 current amount of live memory (bytes allocated minus freed)
89    /// tracked across all threads.
90    ///
91    /// For efficiency reasons every thread only synchronizes with this counter after ~100K bytes of
92    /// allocations or deallocations.  So this could be off by as much as 100K*number of thread in
93    /// either direction.
94    pub fn memory_usage() -> usize {
95        get()
96    }
97
98    pub fn thread_stop() {
99        flush();
100    }
101
102    pub fn thread_park() {
103        Self::collect(false);
104    }
105
106    /// When using mimalloc triggers some cleanup
107    /// force=false: process threadlocal free lists and other threadlocal deferred work
108    ///    only operates on thread local data and should be fast
109    /// force=true: do all the work of `process=false` and then process global shared structures and
110    /// return memory to the OS if possible, this is much slower and should only be done rarely.
111    pub fn collect(force: bool) {
112        #[cfg(all(feature = "custom_allocator", not(target_family = "wasm")))]
113        unsafe {
114            libmimalloc_sys::mi_collect(force);
115        }
116        #[cfg(not(all(feature = "custom_allocator", not(target_family = "wasm"))))]
117        {
118            let _ = force;
119        }
120    }
121
122    pub fn allocation_counters() -> AllocationCounters {
123        self::counter::allocation_counters()
124    }
125
126    pub fn reset_allocation_counters(start: AllocationCounters) {
127        self::counter::reset_allocation_counters(start);
128    }
129
130    /// Returns a memory pressure value in the range `0..=100`, or `None` when
131    /// the current platform does not expose a memory pressure signal or a
132    /// query for it failed.
133    ///
134    /// `0` means no memory pressure, `100` means maximum pressure.
135    ///
136    /// - On Linux this is derived from `/proc/pressure/memory` (the `some` `avg10` stall
137    ///   percentage), falling back to `(MemTotal - MemAvailable) / MemTotal` from `/proc/meminfo`
138    ///   when PSI is not available (older kernels, no `CONFIG_PSI`, or containers without access).
139    /// - On macOS this is derived from the `kern.memorystatus_level` sysctl (`100 -
140    ///   free_memory_percentage`).
141    /// - On Windows this is `MEMORYSTATUSEX::dwMemoryLoad` (percentage of physical memory in use).
142    /// - On other platforms this returns `None`.
143    pub fn memory_pressure() -> Option<u8> {
144        memory_pressure::memory_pressure()
145    }
146}
147
148/// Get the allocator for this platform that we should wrap with TurboMalloc.
149#[inline]
150fn base_alloc() -> &'static impl GlobalAlloc {
151    #[cfg(all(feature = "custom_allocator", not(target_family = "wasm")))]
152    return &mimalloc::MiMalloc;
153    #[cfg(not(all(feature = "custom_allocator", not(target_family = "wasm"))))]
154    return &std::alloc::System;
155}
156
157#[allow(unused_variables)]
158unsafe fn base_alloc_size(ptr: *const u8, layout: Layout) -> usize {
159    #[cfg(all(feature = "custom_allocator", not(target_family = "wasm")))]
160    return unsafe { mimalloc::MiMalloc.usable_size(ptr) };
161    #[cfg(not(all(feature = "custom_allocator", not(target_family = "wasm"))))]
162    return layout.size();
163}
164
165unsafe impl GlobalAlloc for TurboMalloc {
166    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
167        let ret = unsafe { base_alloc().alloc(layout) };
168        if !ret.is_null() {
169            let size = unsafe { base_alloc_size(ret, layout) };
170            add(size);
171        }
172        ret
173    }
174
175    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
176        let size = unsafe { base_alloc_size(ptr, layout) };
177        unsafe { base_alloc().dealloc(ptr, layout) };
178        remove(size);
179    }
180
181    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
182        let ret = unsafe { base_alloc().alloc_zeroed(layout) };
183        if !ret.is_null() {
184            let size = unsafe { base_alloc_size(ret, layout) };
185            add(size);
186        }
187        ret
188    }
189
190    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
191        let old_size = unsafe { base_alloc_size(ptr, layout) };
192        let ret = unsafe { base_alloc().realloc(ptr, layout, new_size) };
193        if !ret.is_null() {
194            // SAFETY: the caller must ensure that the `new_size` does not overflow.
195            // `layout.align()` comes from a `Layout` and is thus guaranteed to be valid.
196            let new_layout = unsafe { Layout::from_size_align_unchecked(new_size, layout.align()) };
197            let new_size = unsafe { base_alloc_size(ret, new_layout) };
198            update(old_size, new_size);
199        }
200        ret
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::TurboMalloc;
207
208    #[test]
209    fn memory_pressure_is_in_range() {
210        let value = TurboMalloc::memory_pressure();
211
212        // On all supported platforms the value must be reported.
213        #[cfg(any(
214            all(target_os = "linux", not(target_family = "wasm")),
215            target_os = "macos",
216            windows,
217        ))]
218        let value = value.expect("memory_pressure() should return Some on this platform");
219
220        // On unsupported platforms we expect None and have nothing further to assert.
221        #[cfg(not(any(
222            all(target_os = "linux", not(target_family = "wasm")),
223            target_os = "macos",
224            windows,
225        )))]
226        let Some(value) = value else {
227            return;
228        };
229
230        assert!(
231            value <= 100,
232            "memory_pressure() returned {value}, expected a value in 0..=100"
233        );
234    }
235}