From fdb4ee9ed7f996b80a055e78778984cbcffb1150 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Tue, 14 Apr 2026 22:50:07 +0000 Subject: [PATCH 1/6] Add TurboMalloc::thread_park() to flush counters and trigger mi_collect on thread park mimalloc defers freeing memory until N more allocations happen. Parked tokio worker threads don't allocate, so deferred frees accumulate. Calling mi_collect(true) on park forces mimalloc to process deferred frees and return memory to the OS. Co-Authored-By: Claude --- Cargo.lock | 1 + crates/next-build-test/src/main.rs | 1 + crates/next-napi-bindings/src/lib.rs | 1 + turbopack/crates/turbo-tasks-backend/fuzz/src/graph.rs | 3 +++ turbopack/crates/turbo-tasks-malloc/Cargo.toml | 8 +++++++- turbopack/crates/turbo-tasks-malloc/src/lib.rs | 8 ++++++++ turbopack/crates/turbopack-cli/benches/small_apps.rs | 10 +++++++--- turbopack/crates/turbopack-cli/src/main.rs | 1 + 8 files changed, 29 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c67eed0ac8f..849372703271 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10133,6 +10133,7 @@ dependencies = [ name = "turbo-tasks-malloc" version = "0.1.0" dependencies = [ + "libmimalloc-sys", "mimalloc", ] diff --git a/crates/next-build-test/src/main.rs b/crates/next-build-test/src/main.rs index af1aca33ff7e..980b6234c140 100644 --- a/crates/next-build-test/src/main.rs +++ b/crates/next-build-test/src/main.rs @@ -86,6 +86,7 @@ fn main() { tracing::debug!("threads stopped"); }) .on_thread_park(|| { + TurboMalloc::thread_park(); LAST_SWC_ATOM_GC_TIME.with_borrow_mut(|cell| { if cell.is_none_or(|t| t.elapsed() > Duration::from_secs(2)) { swc_core::ecma::atoms::hstr::global_atom_store_gc(); diff --git a/crates/next-napi-bindings/src/lib.rs b/crates/next-napi-bindings/src/lib.rs index 66a884881b97..3d7312f9e7bd 100644 --- a/crates/next-napi-bindings/src/lib.rs +++ b/crates/next-napi-bindings/src/lib.rs @@ -100,6 +100,7 @@ fn init() { TurboMalloc::thread_stop(); }) .on_thread_park(|| { + TurboMalloc::thread_park(); LAST_SWC_ATOM_GC_TIME.with_borrow_mut(|cell| { if cell.is_none_or(|t| t.elapsed() > Duration::from_secs(2)) { swc_core::ecma::atoms::hstr::global_atom_store_gc(); diff --git a/turbopack/crates/turbo-tasks-backend/fuzz/src/graph.rs b/turbopack/crates/turbo-tasks-backend/fuzz/src/graph.rs index 928b4d082637..2cd238eb3104 100644 --- a/turbopack/crates/turbo-tasks-backend/fuzz/src/graph.rs +++ b/turbopack/crates/turbo-tasks-backend/fuzz/src/graph.rs @@ -87,6 +87,9 @@ static RUNTIME: Lazy = Lazy::new(|| { .on_thread_stop(|| { TurboMalloc::thread_stop(); }) + .on_thread_park(|| { + TurboMalloc::thread_park(); + }) .build() .unwrap() }); diff --git a/turbopack/crates/turbo-tasks-malloc/Cargo.toml b/turbopack/crates/turbo-tasks-malloc/Cargo.toml index 2933ebad133d..4294fe1a2f2c 100644 --- a/turbopack/crates/turbo-tasks-malloc/Cargo.toml +++ b/turbopack/crates/turbo-tasks-malloc/Cargo.toml @@ -17,6 +17,9 @@ mimalloc = { version = "0.1.48", features = [ "v3", "extended", ], optional = true } +libmimalloc-sys = { version = "0.1.44", features = [ + "extended", +], optional = true } [target.'cfg(all(target_os = "linux", not(target_family = "wasm")))'.dependencies] mimalloc = { version = "0.1.48", features = [ @@ -24,7 +27,10 @@ mimalloc = { version = "0.1.48", features = [ "extended", "local_dynamic_tls", ], optional = true } +libmimalloc-sys = { version = "0.1.44", features = [ + "extended", +], optional = true } [features] -custom_allocator = ["dep:mimalloc"] +custom_allocator = ["dep:mimalloc", "dep:libmimalloc-sys"] default = ["custom_allocator"] diff --git a/turbopack/crates/turbo-tasks-malloc/src/lib.rs b/turbopack/crates/turbo-tasks-malloc/src/lib.rs index 47410b0e11a1..6c9099fa3353 100644 --- a/turbopack/crates/turbo-tasks-malloc/src/lib.rs +++ b/turbopack/crates/turbo-tasks-malloc/src/lib.rs @@ -93,6 +93,14 @@ impl TurboMalloc { flush(); } + pub fn thread_park() { + flush(); + #[cfg(all(feature = "custom_allocator", not(target_family = "wasm")))] + unsafe { + libmimalloc_sys::mi_collect(true); + } + } + pub fn allocation_counters() -> AllocationCounters { self::counter::allocation_counters() } diff --git a/turbopack/crates/turbopack-cli/benches/small_apps.rs b/turbopack/crates/turbopack-cli/benches/small_apps.rs index c8a2b747b2d2..9e455ecaee87 100644 --- a/turbopack/crates/turbopack-cli/benches/small_apps.rs +++ b/turbopack/crates/turbopack-cli/benches/small_apps.rs @@ -50,9 +50,13 @@ fn bench_small_apps(c: &mut Criterion) { b.iter(move || { let mut rt = tokio::runtime::Builder::new_multi_thread(); - rt.enable_all().on_thread_stop(|| { - TurboMalloc::thread_stop(); - }); + rt.enable_all() + .on_thread_stop(|| { + TurboMalloc::thread_stop(); + }) + .on_thread_park(|| { + TurboMalloc::thread_park(); + }); let rt = rt.build().unwrap(); let apps_dir = apps_dir.clone(); diff --git a/turbopack/crates/turbopack-cli/src/main.rs b/turbopack/crates/turbopack-cli/src/main.rs index 014ed07c93f8..222dd8b4d169 100644 --- a/turbopack/crates/turbopack-cli/src/main.rs +++ b/turbopack/crates/turbopack-cli/src/main.rs @@ -29,6 +29,7 @@ fn main() { TurboMalloc::thread_stop(); }) .on_thread_park(|| { + TurboMalloc::thread_park(); LAST_SWC_ATOM_GC_TIME.with_borrow_mut(|cell| { use std::time::Duration; From f024aaf31612248084e37eb6e3b4f62934ec3b6a Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Tue, 14 Apr 2026 23:08:14 +0000 Subject: [PATCH 2/6] Move libmimalloc-sys to top-level [dependencies] section Co-Authored-By: Claude --- turbopack/crates/turbo-tasks-malloc/Cargo.toml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/turbopack/crates/turbo-tasks-malloc/Cargo.toml b/turbopack/crates/turbo-tasks-malloc/Cargo.toml index 4294fe1a2f2c..2be0c1a7b1a6 100644 --- a/turbopack/crates/turbo-tasks-malloc/Cargo.toml +++ b/turbopack/crates/turbo-tasks-malloc/Cargo.toml @@ -10,16 +10,15 @@ autobenches = false bench = false [dependencies] - +libmimalloc-sys = { version = "0.1.44", features = [ + "extended", +], optional = true } [target.'cfg(not(any(target_os = "linux", target_family = "wasm")))'.dependencies] mimalloc = { version = "0.1.48", features = [ "v3", "extended", ], optional = true } -libmimalloc-sys = { version = "0.1.44", features = [ - "extended", -], optional = true } [target.'cfg(all(target_os = "linux", not(target_family = "wasm")))'.dependencies] mimalloc = { version = "0.1.48", features = [ @@ -27,9 +26,6 @@ mimalloc = { version = "0.1.48", features = [ "extended", "local_dynamic_tls", ], optional = true } -libmimalloc-sys = { version = "0.1.44", features = [ - "extended", -], optional = true } [features] custom_allocator = ["dep:mimalloc", "dep:libmimalloc-sys"] From 8c756726a997a1bb65d7681ff39cb0cfc3a3b72c Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Tue, 14 Apr 2026 23:09:24 +0000 Subject: [PATCH 3/6] Move libmimalloc-sys to cfg(not(target_family = "wasm")) section Co-Authored-By: Claude --- turbopack/crates/turbo-tasks-malloc/Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/turbopack/crates/turbo-tasks-malloc/Cargo.toml b/turbopack/crates/turbo-tasks-malloc/Cargo.toml index 2be0c1a7b1a6..4a5a971554d6 100644 --- a/turbopack/crates/turbo-tasks-malloc/Cargo.toml +++ b/turbopack/crates/turbo-tasks-malloc/Cargo.toml @@ -10,6 +10,8 @@ autobenches = false bench = false [dependencies] + +[target.'cfg(not(target_family = "wasm"))'.dependencies] libmimalloc-sys = { version = "0.1.44", features = [ "extended", ], optional = true } From 30dfd110e4b8b647ec455d92d0a52b0bb52ae0d6 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Tue, 14 Apr 2026 23:20:49 +0000 Subject: [PATCH 4/6] Call TurboMalloc::thread_park() after SWC atom store GC on thread park Per review feedback: collect deferred mimalloc frees after the SWC atom store purge so that the atoms freed by GC are also reclaimed in the same collection pass. Co-Authored-By: Claude --- crates/next-build-test/src/main.rs | 2 +- crates/next-napi-bindings/src/lib.rs | 2 +- turbopack/crates/turbopack-cli/src/main.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/next-build-test/src/main.rs b/crates/next-build-test/src/main.rs index 980b6234c140..2c223031f47c 100644 --- a/crates/next-build-test/src/main.rs +++ b/crates/next-build-test/src/main.rs @@ -86,13 +86,13 @@ fn main() { tracing::debug!("threads stopped"); }) .on_thread_park(|| { - TurboMalloc::thread_park(); LAST_SWC_ATOM_GC_TIME.with_borrow_mut(|cell| { if cell.is_none_or(|t| t.elapsed() > Duration::from_secs(2)) { swc_core::ecma::atoms::hstr::global_atom_store_gc(); *cell = Some(Instant::now()); } }); + TurboMalloc::thread_park(); }) .build() .unwrap() diff --git a/crates/next-napi-bindings/src/lib.rs b/crates/next-napi-bindings/src/lib.rs index 3d7312f9e7bd..5107b983c65a 100644 --- a/crates/next-napi-bindings/src/lib.rs +++ b/crates/next-napi-bindings/src/lib.rs @@ -100,13 +100,13 @@ fn init() { TurboMalloc::thread_stop(); }) .on_thread_park(|| { - TurboMalloc::thread_park(); LAST_SWC_ATOM_GC_TIME.with_borrow_mut(|cell| { if cell.is_none_or(|t| t.elapsed() > Duration::from_secs(2)) { swc_core::ecma::atoms::hstr::global_atom_store_gc(); *cell = Some(Instant::now()); } }); + TurboMalloc::thread_park(); }) .worker_threads(worker_threads) // Avoid a limit on threads to avoid deadlocks due to usage of block_in_place diff --git a/turbopack/crates/turbopack-cli/src/main.rs b/turbopack/crates/turbopack-cli/src/main.rs index 222dd8b4d169..a540e5b0223c 100644 --- a/turbopack/crates/turbopack-cli/src/main.rs +++ b/turbopack/crates/turbopack-cli/src/main.rs @@ -29,7 +29,6 @@ fn main() { TurboMalloc::thread_stop(); }) .on_thread_park(|| { - TurboMalloc::thread_park(); LAST_SWC_ATOM_GC_TIME.with_borrow_mut(|cell| { use std::time::Duration; @@ -38,6 +37,7 @@ fn main() { *cell = Some(Instant::now()); } }); + TurboMalloc::thread_park(); }); let args = Arguments::parse(); From c8989c8cf4b2cb078eecbf0d51cfae07236cdfb1 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Tue, 14 Apr 2026 23:21:14 +0000 Subject: [PATCH 5/6] Use mi_collect(false) on thread park to only process lazy free lists Per review: mi_collect(false) processes deferred frees without triggering syscalls or globally synchronized operations, which is sufficient for reclaiming memory freed by parked threads and less expensive than force=true. Co-Authored-By: Claude --- turbopack/crates/turbo-tasks-malloc/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/turbopack/crates/turbo-tasks-malloc/src/lib.rs b/turbopack/crates/turbo-tasks-malloc/src/lib.rs index 6c9099fa3353..b1e353a56975 100644 --- a/turbopack/crates/turbo-tasks-malloc/src/lib.rs +++ b/turbopack/crates/turbo-tasks-malloc/src/lib.rs @@ -97,7 +97,7 @@ impl TurboMalloc { flush(); #[cfg(all(feature = "custom_allocator", not(target_family = "wasm")))] unsafe { - libmimalloc_sys::mi_collect(true); + libmimalloc_sys::mi_collect(false); } } From 0a97949673be1d54cf593acf80e46297d6b13349 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Tue, 14 Apr 2026 23:43:17 +0000 Subject: [PATCH 6/6] Remove flush() from thread_park() to preserve monotonic counter invariant The trace server assumes thread allocation counters only go up. flush() resets the thread-local buffer to zero (transferring the value to the global atomic), which violates that assumption. thread_park() only needs mi_collect() to process deferred frees. Co-Authored-By: Claude --- turbopack/crates/turbo-tasks-malloc/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/turbopack/crates/turbo-tasks-malloc/src/lib.rs b/turbopack/crates/turbo-tasks-malloc/src/lib.rs index b1e353a56975..edf251ab604d 100644 --- a/turbopack/crates/turbo-tasks-malloc/src/lib.rs +++ b/turbopack/crates/turbo-tasks-malloc/src/lib.rs @@ -94,7 +94,6 @@ impl TurboMalloc { } pub fn thread_park() { - flush(); #[cfg(all(feature = "custom_allocator", not(target_family = "wasm")))] unsafe { libmimalloc_sys::mi_collect(false);