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