Files
b9cd44892c Add TurboMalloc::thread_park() to flush and collect on thread park (#92804)
### What?

Adds `TurboMalloc::thread_park()` — a new method on `TurboMalloc` that
calls `libmimalloc_sys::mi_collect(false)` to process mimalloc's lazy
free lists when a Tokio worker thread parks.

Registers it as an `on_thread_park` callback in every Tokio runtime that
already registers `on_thread_stop`:
- `turbopack-cli/src/main.rs`
- `turbopack-cli/benches/small_apps.rs`
- `next-napi-bindings/src/lib.rs`
- `next-build-test/src/main.rs`
- `turbo-tasks-backend/fuzz/src/graph.rs`

### Why?

mimalloc uses deferred freeing: memory freed by one thread is not
reclaimed immediately but is queued until that thread performs N more
allocations. Tokio worker threads that are parked (waiting for work)
don't allocate, so deferred frees accumulate indefinitely — memory is
never returned to the OS until the thread wakes up and does real work.

Calling `mi_collect(false)` on park processes the lazy free lists,
allowing mimalloc to reclaim that memory, while avoiding the syscalls
and globally synchronized operations that `mi_collect(true)` would
trigger.

### How?

- `thread_park()` only calls `mi_collect(false)` — it intentionally does
**not** call `flush()`. `flush()` transfers the thread-local allocation
counter to the global atomic and resets it to zero, which would violate
the trace server's assumption that per-thread counters are monotonically
increasing. Counter sync remains exclusively in `thread_stop()`.
- `thread_park()` is feature-gated behind `cfg(all(feature =
"custom_allocator", not(target_family = "wasm")))` and is a no-op
otherwise.
- `libmimalloc-sys` is added as a direct optional dependency under
`[target.'cfg(not(target_family = "wasm"))'.dependencies]` (it was
already a transitive dep via `mimalloc`) to access `mi_collect`
directly, since `mimalloc`'s `use ffi::*` is not a public re-export.
- In each `on_thread_park` callback, `TurboMalloc::thread_park()` is
called **after** the existing SWC atom store GC, so atoms freed by that
GC pass are also eligible for reclamation in the same collection.

<!-- NEXT_JS_LLM_PR -->

---------

Co-authored-by: Tobias Koppers <[email protected]>
Co-authored-by: Claude <[email protected]>
2026-04-15 03:43:39 +02:00

102 lines
3.4 KiB
Rust

#![cfg_attr(not(codspeed), allow(unused))]
#[global_allocator]
static ALLOC: turbo_tasks_malloc::TurboMalloc = turbo_tasks_malloc::TurboMalloc;
use std::path::{Path, PathBuf};
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use turbopack_cli::arguments::{BuildArguments, CommonArguments};
fn list_apps() -> (PathBuf, Vec<PathBuf>) {
// We need to rely on `CARGO_MANIFEST_DIR` because we are running it via `cargo codspeed`
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let apps_dir = Path::new(&manifest_dir)
.join("../../benchmark-apps")
.canonicalize()
.expect("failed to canonicalize path");
let mut apps = Vec::new();
for entry in std::fs::read_dir(&apps_dir).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.is_dir() {
// Exclude node_modules
if path.file_name().unwrap_or_default() == "node_modules" {
continue;
}
apps.push(path);
}
}
(apps_dir, apps)
}
fn bench_small_apps(c: &mut Criterion) {
use turbo_tasks_malloc::TurboMalloc;
let (apps_dir, apps) = list_apps();
let mut g = c.benchmark_group("turbopack/build/apps");
for app in apps {
g.bench_function(
BenchmarkId::new("build", app.file_name().unwrap().to_string_lossy()),
|b| {
let apps_dir = apps_dir.clone();
let app = app.clone();
b.iter(move || {
let mut rt = tokio::runtime::Builder::new_multi_thread();
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();
let app = app.clone();
let app_name = app.file_name().unwrap().to_string_lossy().to_string();
rt.block_on(async move {
turbopack_cli::build::build(&BuildArguments {
common: CommonArguments {
entries: Some(vec![format!("{app_name}/index.tsx")]),
dir: Some(app.clone()),
root: Some(apps_dir),
log_level: None,
show_all: false,
log_detail: false,
full_stats: false,
target: None,
worker_threads: None,
persistent_caching: false,
cache_dir: None,
},
no_sourcemap: false,
no_minify: false,
force_memory_cleanup: true,
no_scope_hoist: false,
})
.await
})
.unwrap();
});
},
);
}
}
criterion_group!(
name = benches;
config = Criterion::default().sample_size(10);
targets = bench_small_apps
);
criterion_main!(benches);