Skip to content

Commit b31680f

Browse files
bartlomiejuclaude
andauthored
fix(runtime): reduce memory retention after web worker termination (#32617)
## Summary Addresses #26058 — Web Workers use significantly more RSS than Chrome, and terminating them doesn't release the memory back to the OS. Two targeted changes: - **Call `malloc_trim(0)` on Linux after each worker thread exits.** When a worker's V8 isolate and tokio runtime are dropped, glibc's allocator holds onto the fragmented heap pages rather than returning them to the OS. This explicitly asks glibc to release them. Follows the same pattern already used in the SIGUSR2 memory trim handler (`runtime/worker.rs:81`). - **Remove the delayed termination hack entirely.** The 2-second timer that spawned threads/tasks to force-terminate workers is no longer needed — the upstream V8 issue that required it has been fixed. Workers now terminate cooperatively via the termination signal and event loop wakeup, which also eliminates the ~100 lingering OS threads during rapid worker churn. ### What this doesn't fix - The ~7-8MB per-isolate overhead from V8's lack of shared read-only heap (upstream V8 issue) - macOS/Windows RSS behavior (`malloc_trim` is Linux-only) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7698bcc commit b31680f

File tree

2 files changed

+18
-33
lines changed

2 files changed

+18
-33
lines changed

runtime/ops/worker_host.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,20 @@ fn op_create_worker(
269269
.await
270270
};
271271

272-
create_and_run_current_thread(fut)
272+
let _ = create_and_run_current_thread(fut);
273+
274+
// After the worker's tokio runtime and JsRuntime/V8 isolate have been
275+
// dropped, ask the system allocator to release freed memory back to the
276+
// OS. Without this, glibc in particular holds onto the fragmented heap
277+
// pages, causing RSS to remain high after many workers are created and
278+
// destroyed (https://github.com/denoland/deno/issues/26058).
279+
#[cfg(target_os = "linux")]
280+
{
281+
// SAFETY: calling libc function with no preconditions.
282+
unsafe {
283+
libc::malloc_trim(0);
284+
}
285+
}
273286
})?;
274287

275288
// Receive WebWorkerHandle from newly created worker

runtime/web_worker.rs

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -269,9 +269,7 @@ pub struct SendableWebWorkerHandle {
269269
port: MessagePort,
270270
receiver: mpsc::Receiver<WorkerControlEvent>,
271271
termination_signal: Arc<AtomicBool>,
272-
has_terminated: Arc<AtomicBool>,
273272
terminate_waker: Arc<AtomicWaker>,
274-
isolate_handle: v8::IsolateHandle,
275273
}
276274

277275
impl From<SendableWebWorkerHandle> for WebWorkerHandle {
@@ -280,9 +278,7 @@ impl From<SendableWebWorkerHandle> for WebWorkerHandle {
280278
receiver: Rc::new(RefCell::new(handle.receiver)),
281279
port: Rc::new(handle.port),
282280
termination_signal: handle.termination_signal,
283-
has_terminated: handle.has_terminated,
284281
terminate_waker: handle.terminate_waker,
285-
isolate_handle: handle.isolate_handle,
286282
}
287283
}
288284
}
@@ -299,9 +295,7 @@ pub struct WebWorkerHandle {
299295
pub port: Rc<MessagePort>,
300296
receiver: Rc<RefCell<mpsc::Receiver<WorkerControlEvent>>>,
301297
termination_signal: Arc<AtomicBool>,
302-
has_terminated: Arc<AtomicBool>,
303298
terminate_waker: Arc<AtomicWaker>,
304-
isolate_handle: v8::IsolateHandle,
305299
}
306300

307301
impl WebWorkerHandle {
@@ -315,36 +309,16 @@ impl WebWorkerHandle {
315309

316310
/// Terminate the worker
317311
/// This function will set the termination signal, close the message channel,
318-
/// and schedule to terminate the isolate after two seconds.
312+
/// and wake the worker's event loop so it can terminate.
319313
pub fn terminate(self) {
320-
use std::thread::sleep;
321-
use std::thread::spawn;
322-
use std::time::Duration;
323-
324314
let schedule_termination =
325315
!self.termination_signal.swap(true, Ordering::SeqCst);
326316

327317
self.port.disentangle();
328318

329-
if schedule_termination && !self.has_terminated.load(Ordering::SeqCst) {
319+
if schedule_termination {
330320
// Wake up the worker's event loop so it can terminate.
331321
self.terminate_waker.wake();
332-
333-
let has_terminated = self.has_terminated.clone();
334-
335-
// Schedule to terminate the isolate's execution.
336-
spawn(move || {
337-
sleep(Duration::from_secs(2));
338-
339-
// A worker's isolate can only be terminated once, so we need a guard
340-
// here.
341-
let already_terminated = has_terminated.swap(true, Ordering::SeqCst);
342-
343-
if !already_terminated {
344-
// Stop javascript execution
345-
self.isolate_handle.terminate_execution();
346-
}
347-
});
348322
}
349323
}
350324
}
@@ -363,9 +337,9 @@ fn create_handles(
363337
name,
364338
port: Rc::new(parent_port),
365339
termination_signal: termination_signal.clone(),
366-
has_terminated: has_terminated.clone(),
340+
has_terminated,
367341
terminate_waker: terminate_waker.clone(),
368-
isolate_handle: isolate_handle.clone(),
342+
isolate_handle,
369343
cancel: CancelHandle::new_rc(),
370344
sender: ctrl_tx,
371345
worker_type,
@@ -374,9 +348,7 @@ fn create_handles(
374348
receiver: ctrl_rx,
375349
port: worker_port,
376350
termination_signal,
377-
has_terminated,
378351
terminate_waker,
379-
isolate_handle,
380352
};
381353
(internal_handle, external_handle)
382354
}

0 commit comments

Comments
 (0)