Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 7 additions & 25 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2433,6 +2433,10 @@ impl VirtualMachine {
if self.is_watcher_enabled() {
// accessed here (no overlapping `&mut EventLoop`).
self.event_loop_mut().perform_gc();
// See `EventLoop::ref_loop_scoped` — this drive loop keeps
// ticking until the module promise settles, so ref the loop so
// `auto_tick` parks on timer deadlines instead of spinning.
let _loop_ref = self.event_loop_shared().ref_loop_scoped();
Comment thread
claude[bot] marked this conversation as resolved.
loop {
let Some(p) = self.pending_internal_promise else {
break;
Expand Down Expand Up @@ -3615,31 +3619,6 @@ impl VirtualMachine {
self.event_loop_mut().enqueue_task_concurrent(task);
}

/// `cond` is `&Cell<bool>` (not `&mut bool`): the re-entrant
/// `tick()/auto_tick()` calls run JS that flips the flag through an
/// independently-captured handle, so the read must not be `noalias`.
/// `Cell` is `!Freeze`, which suppresses the LLVM `noalias`/`readonly`
/// attributes and forces a real reload on every `.get()` — no raw-pointer
/// laundering needed for the condition.
pub fn wait_for(&mut self, cond: &core::cell::Cell<bool>) {
// R-2 noalias mitigation (PORT_NOTES_PLAN R-2; precedent
// `b818e70e1c57` NodeHTTPResponse::cork): `&mut self` is
// LLVM-`noalias`, but `tick()/auto_tick()` re-enter JS which reaches
// `self` again via `VirtualMachine::get()`. Launder `self` so each
// access goes through an opaque address.
let this: *mut Self = core::hint::black_box(core::ptr::from_mut(self));
while !cond.get() {
// SAFETY: `this` is the unique live VM; each deref is a momentary
// access only (no borrow held across the re-entrant call).
unsafe { (*this).event_loop_mut().tick() };
if !cond.get() {
// SAFETY: as above — momentary deref of the unique live VM,
// no borrow held across the re-entrant call.
unsafe { (*this).auto_tick() };
}
}
}

/// Ticks the event loop until no tasks keep it alive.
pub fn wait_for_tasks(&mut self) {
while self.is_event_loop_alive() {
Expand Down Expand Up @@ -4620,6 +4599,9 @@ impl VirtualMachine {
// pending_internal_promise can change if hot module reloading is enabled
if self.is_watcher_enabled() {
self.event_loop_mut().perform_gc();
// See `EventLoop::ref_loop_scoped` — park instead of spin while
// waiting on the module promise.
let _loop_ref = self.event_loop_shared().ref_loop_scoped();
loop {
let Some(p) = self.pending_internal_promise else {
break;
Expand Down
69 changes: 69 additions & 0 deletions src/jsc/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,20 @@ impl Drop for EventLoopEnterGuard {
}
}

/// Keeps the uSockets loop ref'd for the guard's lifetime. Construct via
/// [`EventLoop::ref_loop_scoped`], whose docs carry the full contract.
#[must_use = "dropping immediately releases the loop ref, reintroducing the busy-spin"]
pub struct LoopRefGuard(*mut uws::Loop);

impl Drop for LoopRefGuard {
#[inline]
fn drop(&mut self) {
// SAFETY: the loop is the per-thread singleton, live for the VM
// lifetime; balances the `ref_()` in `ref_loop_scoped`.
unsafe { (*self.0).unref() };
}
}

impl EventLoop {
/// Before your code enters JavaScript at the top of the event loop, call
/// `loop.enter()`. If running a single callback, prefer `runCallback` instead.
Expand Down Expand Up @@ -953,13 +967,65 @@ impl EventLoop {
self.vm_ref().as_mut().auto_tick_active();
}

/// Hold a uSockets-loop ref for the lifetime of the returned guard.
///
/// Condition-gated drivers (`wait_for_promise`, the `bun:test` drive
/// loop, the entry-point/preload loaders) tick `auto_tick` until a
/// JS-visible condition is satisfied, independently of whether JS has
/// anything refing the loop. Without this ref `auto_tick` takes its
/// `!is_active()` branch — a non-blocking pump that busy-spins the driver
/// on POSIX and on Windows never runs due timers (`uv_run` skips them
/// when the loop has no ref'd handles). With the ref, `auto_tick`'s
/// active branch parks in epoll/kqueue (`uv_run(UV_RUN_ONCE)` on Windows)
/// until the next timer-heap deadline — or **indefinitely** when the heap
/// is empty, which is the normal state while awaiting an external event.
///
/// # Contract
/// While the guard is held, every exit condition of the guarded loop must
/// be observable through something that wakes the uws loop: a task
/// enqueued on this `EventLoop`, a heap timer, an I/O event, or an
/// explicit [`Self::wakeup`] paired with the flag store. A bare
/// cross-thread or signal-handler flag flip is NOT sufficient — before
/// this ref existed the non-blocking pump observed such flips within
/// microseconds (see `sigint_handler` in `cli/repl.rs`, which wakes the
/// loop after setting `execution_forbidden`).
///
/// The ref also makes [`VirtualMachine::is_event_loop_alive`] and
/// `is_event_loop_alive_excluding_immediates` report true for the guard's
/// scope. That is intentional and has one JS-visible consequence: an
/// unref'd `setImmediate` is only cleared-without-running when the loop
/// looks dead, so one scheduled inside a guarded driver (a `bun:test`
/// test body, a preload, a synchronously-required ESM module) now runs.
/// This matches Node, whose `node:test` runner refs the loop for the same
/// reason.
///
/// Liveness-gated drivers (the main run loop via `auto_tick_active`,
/// `wait_for_tasks`) must NOT use this — their exit condition is
/// precisely that nothing refs the loop. A driver that checks liveness
/// inside the loop but is otherwise condition-gated (the Worker entry
/// loader, `wait_for_promise_with_termination`) must scope the guard to
/// `auto_tick` only, after the liveness check, so that check reads the
/// real ref state.
pub fn ref_loop_scoped(&self) -> LoopRefGuard {
let loop_ = self.usockets_loop();
// SAFETY: `usockets_loop()` returns the live per-thread uws loop;
// `ref_()` bumps `num_polls` + `active` (POSIX) / `active_handles`
// (Windows).
unsafe { (*loop_).ref_() };
Comment thread
robobun marked this conversation as resolved.
LoopRefGuard(loop_)
}
Comment thread
claude[bot] marked this conversation as resolved.

/// `eventLoop().waitForPromise(promise)` — spin tick/auto_tick until
/// `promise` settles or execution is forbidden.
///
/// Holds a loop ref, so `execution_forbidden` must be paired with a loop
/// wakeup to be observed — see [`Self::ref_loop_scoped`]'s contract.
pub fn wait_for_promise(&mut self, promise: jsc::AnyPromise) {
let jsc_vm = self.vm_ref().jsc_vm();
if promise.status() != PromiseStatus::Pending {
return;
}
let _loop_ref = self.ref_loop_scoped();
Comment thread
robobun marked this conversation as resolved.
while promise.status() == PromiseStatus::Pending {
if jsc_vm.execution_forbidden() {
break;
Expand Down Expand Up @@ -1172,6 +1238,9 @@ impl EventLoop {
if !self.vm_ref().is_event_loop_alive() {
break;
}
// Scoped to auto_tick only, so the check above reads the
// real ref state — see ref_loop_scoped's contract.
let _loop_ref = self.ref_loop_scoped();
self.auto_tick();
}
}
Expand Down
21 changes: 21 additions & 0 deletions src/runtime/cli/repl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,11 @@ impl<'a> Repl<'a> {
// Cleared in disable_signals_during_wait; Release pairs with the
// Acquire load in `sigint_handler`.
SIGINT_VM.store(vm.jsc_vm, core::sync::atomic::Ordering::Release);
// `wait_for_promise` refs the loop (see `EventLoop::ref_loop_scoped`),
// so `auto_tick` parks in epoll/kqueue instead of polling. Setting
// `execution_forbidden` from the handler is therefore not observable
// on its own — the handler must also wake the loop.
SIGINT_LOOP.store(vm.uws_loop(), core::sync::atomic::Ordering::Release);
}

#[cfg(unix)]
Expand All @@ -990,6 +995,7 @@ impl<'a> Repl<'a> {
/// Restore raw terminal mode after promise wait
fn disable_signals_during_wait(&mut self) {
SIGINT_VM.store(core::ptr::null_mut(), core::sync::atomic::Ordering::Release);
SIGINT_LOOP.store(core::ptr::null_mut(), core::sync::atomic::Ordering::Release);

#[cfg(unix)]
{
Expand Down Expand Up @@ -2436,6 +2442,11 @@ impl<'a> Drop for Repl<'a> {
static SIGINT_VM: core::sync::atomic::AtomicPtr<jsc::VM> =
core::sync::atomic::AtomicPtr::new(core::ptr::null_mut());

/// The uws loop the interrupted `wait_for_promise` is parked in. See
/// `enable_signals_during_wait`.
static SIGINT_LOOP: core::sync::atomic::AtomicPtr<bun_uws::Loop> =
core::sync::atomic::AtomicPtr::new(core::ptr::null_mut());

#[cfg(unix)]
extern "C" fn sigint_handler(_: c_int) {
let vm = SIGINT_VM.load(core::sync::atomic::Ordering::Acquire);
Expand All @@ -2444,6 +2455,16 @@ extern "C" fn sigint_handler(_: c_int) {
// blocked in wait while the handler runs, so it stays valid).
jsc::VM::opaque_ref(vm).set_execution_forbidden(true);
}
let loop_ = SIGINT_LOOP.load(core::sync::atomic::Ordering::Acquire);
if !loop_.is_null() {
// Wake the parked `auto_tick` so `wait_for_promise` re-checks
// `execution_forbidden`. `wakeup_raw` takes a raw pointer (the parked
// thread holds `&mut Loop`) and bottoms out in an atomic increment
// plus a `write()` to the wakeup eventfd — async-signal-safe.
// SAFETY: `loop_` is the VM's loop, live while the JS thread is
// parked in the wait this handler interrupts.
unsafe { bun_uws::Loop::wakeup_raw(loop_) };
}
}

fn is_incomplete_code(code: &[u8]) -> bool {
Expand Down
6 changes: 6 additions & 0 deletions src/runtime/cli/test_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3228,6 +3228,12 @@ impl TestCommand {
// Process event loop while bun_test tests are running
vm.event_loop_ref().tick();

// This drive loop keeps ticking until `Phase::Done` (a
// JS-visible condition) regardless of whether JS has anything
// refing the loop. Hold a loop ref for the duration so
// `auto_tick` parks on the next timer-heap deadline rather
// than busy-spinning when a test awaits an unref'd timer.
let _loop_ref = vm.event_loop_shared().ref_loop_scoped();
let mut prev_unhandled_count = vm.unhandled_error_counter;
while buntest.phase != bun_test::Phase::Done {
if buntest.wants_wakeup {
Expand Down
52 changes: 29 additions & 23 deletions src/runtime/jsc_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,12 @@ unsafe fn load_preloads(vm: *mut VirtualMachine) -> bun_jsc::CrateResult<*mut JS
let el = unsafe { &*vm }.event_loop();
// SAFETY: `el` is the live per-thread event loop.
unsafe { (*el).perform_gc() };
// See `EventLoop::ref_loop_scoped` — this drive loop keeps
// ticking until the preload's module promise settles, so
// ref the loop so `auto_tick` parks on timer deadlines
// instead of spinning (mirrors the entry-point loaders).
// SAFETY: `el` is the live per-thread event loop.
let _loop_ref = unsafe { &*el }.ref_loop_scoped();
loop {
// SAFETY: `pending_internal_promise` was set just above (or
// swapped by HMR to another live cell); `status()` is a
Expand Down Expand Up @@ -983,23 +989,21 @@ unsafe fn auto_tick(vm: *mut VirtualMachine) {
// SAFETY: `el` is the live per-thread event loop.
unsafe { (*el).process_gc_timer() };
// Note (§Forbidden aliased-&mut): `get_timeout` may fire a
// `WTFTimer` JS callback.
// A re-entrant `setTimeout`/`clearTimeout` reaches
// `timer::All::insert`/`remove` via `runtime_state()` and would
// mint a second `&mut timer` if we held `&mut (*state).timer`
// across the call. Pass the raw `*mut Self` instead;
// `timer::All::get_timeout` forms short-lived `&mut` only around
// heap ops that cannot re-enter JS, releasing the borrow before
// invoking `fire()`.
// `WTFTimer` JS callback. A re-entrant `setTimeout`/
// `clearTimeout` reaches `timer::All::insert`/`remove` via
// `runtime_state()` and would mint a second `&mut timer` if a
// `&mut (*state).timer` were live across the call, so the
// receiver is a raw `*mut All`.
// `get_timeout` reads CLOCK_MONOTONIC to compare against the timer heap; hand that
// same reading to the tick for the park hook's idle-sweep rate limit. It is lazy,
// and so is the hook: NOW_NS_UNKNOWN means it took none.
let mut now: Option<bun_core::Timespec> = None;
// SAFETY: `state` is the live per-thread `RuntimeState`; the
// `timer` field address is stable for the VM lifetime.
// SAFETY: `state` is the live per-thread `RuntimeState` (JS
// thread, no outstanding `&mut All`); the `timer` field address
// is stable for the VM lifetime.
let have_timeout = unsafe {
timer::All::get_timeout(
&mut (*state).timer,
core::ptr::addr_of_mut!((*state).timer),
&mut timespec,
has_pending_immediate,
quic_next_tick_us,
Expand All @@ -1023,12 +1027,13 @@ unsafe fn auto_tick(vm: *mut VirtualMachine) {
{
// Note (§Forbidden aliased-&mut): `drain_timers` fires user
// `setTimeout` callbacks which may re-enter `timer::All::insert`/
// `remove` via `runtime_state()`. Pass raw `*mut Self` so no
// long-lived `&mut (*state).timer` is held across `fire()`;
// `drain_timers` forms short-lived `&mut` only around heap pop/peek.
// SAFETY: `state` is the live per-thread `RuntimeState`; the `timer`
// field address is stable for the VM lifetime.
unsafe { timer::All::drain_timers(&mut (*state).timer, vm.cast()) };
// `remove` via `runtime_state()`. The receiver is a raw `*mut All` so
// no `&mut (*state).timer` is held across `fire()`; `drain_timers`
// forms short-lived `&mut` only around heap pop/peek.
// SAFETY: `state` is the live per-thread `RuntimeState` (JS thread,
// no outstanding `&mut All`); the `timer` field address is stable for
// the VM lifetime.
unsafe { timer::All::drain_timers(core::ptr::addr_of_mut!((*state).timer), vm.cast()) };
}
#[cfg(not(unix))]
let _ = state;
Expand Down Expand Up @@ -1119,11 +1124,12 @@ unsafe fn auto_tick_active(vm: *mut VirtualMachine) {
// same reading to the tick for the park hook's idle-sweep rate limit. It is lazy,
// and so is the hook: NOW_NS_UNKNOWN means it took none.
let mut now: Option<bun_core::Timespec> = None;
// SAFETY: `state` is the live per-thread `RuntimeState`; see
// Note on `auto_tick` re: aliased-&mut across `fire()`.
// SAFETY: `state` is the live per-thread `RuntimeState` (JS
// thread, no outstanding `&mut All`); see Note on `auto_tick`
// re: aliased-&mut across `fire()`.
let have_timeout = unsafe {
timer::All::get_timeout(
&mut (*state).timer,
core::ptr::addr_of_mut!((*state).timer),
&mut timespec,
has_pending_immediate,
quic_next_tick_us,
Expand All @@ -1145,9 +1151,9 @@ unsafe fn auto_tick_active(vm: *mut VirtualMachine) {

#[cfg(unix)]
{
// SAFETY: `state` is the live per-thread `RuntimeState`; see Note
// on `auto_tick` re: aliased-&mut across `fire()`.
unsafe { timer::All::drain_timers(&mut (*state).timer, vm.cast()) };
// SAFETY: `state` is the live per-thread `RuntimeState` (JS thread,
// no outstanding `&mut All`); see Note on `auto_tick`.
unsafe { timer::All::drain_timers(core::ptr::addr_of_mut!((*state).timer), vm.cast()) };
}
#[cfg(not(unix))]
let _ = state;
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/timer/Timer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,9 +505,9 @@ pub fn drain_timers_export(vm: *mut VirtualMachine) {
if all.is_null() {
return;
}
// SAFETY: `all` is the live per-thread `All`; `vm` is the erased VM pointer
// (mod.rs::All::drain_timers takes `*mut ()`).
unsafe { (*all).drain_timers(vm.cast::<()>()) };
// SAFETY: `all` is the live per-thread `All` with no outstanding `&mut`;
// `vm` is the erased VM pointer (mod.rs::All::drain_timers takes `*mut ()`).
unsafe { All::drain_timers(all, vm.cast::<()>()) };
}

// `generate-host-exports.ts`
Expand Down
Loading
Loading