mirror of
https://github.com/hanzoai/next.js.git
synced 2026-10-30 14:46:38 +00:00
### 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]>
102 lines
3.4 KiB
Rust
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);
|