From 280497cef8374922094b6f55bdb4e774d7780205 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 08:11:11 +0000 Subject: [PATCH 1/8] timer: remove unsafe from the timer module TimeoutObject/ImmediateObject/WTFTimer/DateHeaderTimer/EventLoopDelayMonitor and timer::All no longer contain any unsafe code. The intrusive timer heap is reached through TimerRef (a handle to a TimerOwner's EventLoopTimer slot) and TimerHeap in bun_event_loop, which own the links and heap membership; bun_io::heap::Intrusive keeps its nodes behind Cell links and never forms a &mut. timer::All is &self-only with interior mutability, so timer callbacks that re-enter it no longer alias a &mut. The heap's ref on a scheduled JS timer is a typed RefPtr slot instead of bare ref/deref counting, and the tag->owner recovery for firing/cancelling timers lives in dispatch.rs. --- src/codegen/generate-classes.ts | 27 +- src/event_loop/EventLoopTimer.rs | 322 ++++- src/install/PackageManager.rs | 13 +- src/install/lifecycle_script_runner.rs | 27 +- src/io/heap.rs | 334 +++-- src/jsc/AbortSignal.rs | 15 +- src/jsc/host_fn.rs | 61 +- src/jsc_macros/lib.rs | 17 +- src/libuv_sys/libuv.rs | 14 + src/ptr/js_cell.rs | 7 + src/runtime/api/bun/js_bun_spawn_bindings.rs | 2 +- src/runtime/api/bun/subprocess.rs | 16 +- src/runtime/api/cron.rs | 25 +- src/runtime/bake/DevServer.rs | 17 +- src/runtime/bake/dev_server/hmr_socket.rs | 28 +- .../bake/dev_server/source_map_store.rs | 15 +- src/runtime/dispatch.rs | 283 +++-- src/runtime/dns_jsc/dns.rs | 32 +- src/runtime/dns_jsc/dns_sd.rs | 21 +- src/runtime/hw_exports.rs | 18 +- src/runtime/jsc_hooks.rs | 176 +-- src/runtime/node/node_fs_stat_watcher.rs | 10 +- src/runtime/node/quic/endpoint.rs | 18 +- src/runtime/socket/UpgradedDuplex.rs | 16 +- src/runtime/socket/WindowsNamedPipe.rs | 16 +- src/runtime/test_runner/bun_test.rs | 12 +- src/runtime/test_runner/jest.rs | 2 +- src/runtime/test_runner/timers/FakeTimers.rs | 198 +-- src/runtime/timer/DateHeaderTimer.rs | 21 +- src/runtime/timer/EventLoopDelayMonitor.rs | 34 +- src/runtime/timer/ImmediateObject.rs | 66 +- src/runtime/timer/TimeoutObject.rs | 80 +- src/runtime/timer/Timer.rs | 179 +-- src/runtime/timer/WTFTimer.rs | 303 ++--- src/runtime/timer/mod.rs | 1130 +++++------------ src/runtime/timer/timer_object_internals.rs | 988 +++++--------- 36 files changed, 1972 insertions(+), 2571 deletions(-) diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index 2ea66b64116a..10b00e63e8ba 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -2048,13 +2048,26 @@ function generateRust( ` ${T}::${fastId}(this, global${args.length ? ", " + argFwd : ""})`, ); } - thunk( - names.fn, - `(this: ${recv}, global: &JSGlobalObject, callframe: &CallFrame${passThis ? ", js_this_value: JSValue" : ""}) -> JSValue`, - passThis - ? ` ${helper("host_fn_this_value")}(this, global, callframe, js_this_value, |t, g, c, v| ${T}::${id}(t, g, c, v))` - : ` ${helper("host_fn_this")}(this, global, callframe, |t, g, c| ${T}::${id}(t, g, c))`, - ); + if (sharedThis) { + // `*mut T` + receiver-generic helper: the method takes `&self` or + // `this: ThisPtr` and inference picks (see `HostReceiver`). + thunk( + names.fn, + `(this: *mut ${T}, global: &JSGlobalObject, callframe: &CallFrame${passThis ? ", js_this_value: JSValue" : ""}) -> JSValue`, + ` // SAFETY: C++ passes the wrapper's live \`m_ctx\`.\n ` + + (passThis + ? ` unsafe { host_fn::host_fn_this_value_ptr(this, global, callframe, js_this_value, |t, g, c, v| ${T}::${id}(t, g, c, v)) }` + : ` unsafe { host_fn::host_fn_this_ptr(this, global, callframe, |t, g, c| ${T}::${id}(t, g, c)) }`), + ); + } else { + thunk( + names.fn, + `(this: ${recv}, global: &JSGlobalObject, callframe: &CallFrame${passThis ? ", js_this_value: JSValue" : ""}) -> JSValue`, + passThis + ? ` ${helper("host_fn_this_value")}(this, global, callframe, js_this_value, |t, g, c, v| ${T}::${id}(t, g, c, v))` + : ` ${helper("host_fn_this")}(this, global, callframe, |t, g, c| ${T}::${id}(t, g, c))`, + ); + } } } } diff --git a/src/event_loop/EventLoopTimer.rs b/src/event_loop/EventLoopTimer.rs index e69dfb58dc63..7aceae54d7bf 100644 --- a/src/event_loop/EventLoopTimer.rs +++ b/src/event_loop/EventLoopTimer.rs @@ -5,6 +5,10 @@ use Timespec as timespec; pub use bun_core::Timespec; +use core::ptr::NonNull; + +use bun_ptr::JsCell; + // Re-export so higher tiers see the *same* type they pass to // `bun_io::heap::Intrusive` (a zero-sized local stub // would make the real pairing-heap unusable — orphan rule blocks @@ -18,26 +22,10 @@ const NS_PER_MS: i64 = bun_core::time::NS_PER_MS as i64; // intrusive heap node; the `match tag { … container_of … }` dispatch lives in // `bun_runtime::dispatch` because it names ~20 high-tier container types. // -// LAYERING: rather than a runtime-registered fn-ptr (init-order -// hazard), the bodies are declared `extern "Rust"` and defined `#[no_mangle]` -// in `bun_runtime`; the linker resolves them. No `AtomicPtr`, no registration. -// // PERF: `__bun_js_timer_epoch` sits on the // heap-compare path. Consider denormalizing `epoch` into `EventLoopTimer` // to drop the cross-crate call if profiling shows it matters. unsafe extern "Rust" { - /// Runtime owns the tag→variant `match`; `vm` is an erased - /// `*mut VirtualMachine`. Defined in `bun_runtime::dispatch`. - /// - /// SAFETY (genuine FFI precondition — NOT a `safe fn` candidate): impl - /// derefs `t`/`now`, recovers the tier-6 container via `container_of` - /// keyed on `(*t).tag`, and may free that container. Caller must pass a - /// live timer just popped from `All.timers` and must not touch `t` after. - fn __bun_fire_timer( - t: *mut EventLoopTimer, - now: *const timespec, - vm: *mut (), - ) -> crate::JsResult<()>; /// Returns the JS-timer epoch (TimerObjectInternals.flags.epoch) for /// TimeoutObject/ImmediateObject/AbortSignalTimeout, else `None`. /// Defined in `bun_runtime::dispatch`. @@ -53,41 +41,55 @@ pub struct EventLoopTimer { /// The absolute time to fire this timer next. pub next: timespec, pub state: State, - pub tag: Tag, - /// Internal heap fields. - pub heap: IntrusiveField, - pub in_heap: InHeap, + /// Fixed at construction: the dispatch `container_of` is keyed on it. + tag: Tag, + /// Internal heap links; written only by [`TimerHeap`]. + heap: IntrusiveField, + /// Which [`TimerHeap`] this slot is linked into; written only by [`TimerHeap`]. + in_heap: InHeap, } -// Duck-typed `.heap` field access for `bun_io::heap::Intrusive`. Implemented -// here (the defining crate) so higher tiers can instantiate -// `Intrusive` without hitting the orphan rule. impl bun_io::heap::HeapNode for EventLoopTimer { #[inline] - fn heap(&mut self) -> &mut IntrusiveField { - &mut self.heap + fn heap(&self) -> &IntrusiveField { + &self.heap } } -#[derive(Copy, Clone, Eq, PartialEq, Default)] +#[derive(Copy, Clone, Eq, PartialEq, Default, Debug)] pub enum InHeap { #[default] None, Regular, Fake, + Wtf, } impl EventLoopTimer { pub fn init_paused(tag: Tag) -> Self { + Self::new(tag, State::PENDING, timespec::EPOCH) + } + + pub fn new(tag: Tag, state: State, next: timespec) -> Self { Self { - next: timespec::EPOCH, - state: State::PENDING, + next, + state, tag, heap: IntrusiveField::default(), in_heap: InHeap::None, } } + #[inline] + pub fn tag(&self) -> Tag { + self.tag + } + + #[inline] + pub fn in_heap(&self) -> InHeap { + self.in_heap + } + pub fn less(_: (), a: &Self, b: &Self) -> bool { let sec_order = a.next.sec.cmp(&b.next.sec); if sec_order != core::cmp::Ordering::Equal { @@ -144,41 +146,235 @@ impl EventLoopTimer { #[inline] pub(crate) fn js_timer_epoch(&self) -> Option { // SAFETY: `self` is a live timer; the extern impl reads `tag` and - // recovers the container via `offset_of`. + // recovers the container via `offset_of` (`TimerOwner` tag contract). unsafe { __bun_js_timer_epoch(self.tag, self) } } +} + +// ─── TimerOwner / TimerRef / TimerHeap ────────────────────────────────────── + +/// A type that embeds one or more [`EventLoopTimer`] slots and lends them to a +/// [`TimerHeap`]. Emitted by [`impl_timer_owner!`]; invoking that macro is the +/// owner's assertion of this contract. +/// +/// # Safety +/// The implementor guarantees, for every slot of `Self` it constructs: +/// - the slot is created with the [`Tag`] whose `bun_runtime::dispatch` arm +/// names `Self` and that field, so the tag→container recovery is an +/// identity (the tag cannot change after construction); +/// - every teardown path of a `Self` unlinks its slots before the value is +/// dropped, freed, or moved. +/// +/// The remaining obligation is the holder's, as for [`bun_ptr::BackRef`]: a +/// `Self` whose slot is linked must be kept alive and in place by whoever owns +/// it (a JS wrapper's ref, a `Box` owned by C++, a field of the per-thread +/// timer state, …) until it is unlinked. +pub unsafe trait TimerOwner {} + +/// Handle to an [`EventLoopTimer`] slot embedded in a live [`TimerOwner`]; the +/// currency of [`TimerHeap`] and `bun_runtime::timer::All`. +/// +/// Like [`bun_ptr::BackRef`], validity is an obligation rather than a borrow: +/// a `TimerRef` is usable while its slot is linked, for the duration of the +/// owner's own call that passed it (arm/disarm), or of the dispatch that +/// popped it (fire, until the owner's handler returns). Do not retain one past +/// those points. It exposes the slot's deadline and state but neither its tag +/// nor its heap links, which only construction and [`TimerHeap`] write. +#[repr(transparent)] +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct TimerRef(NonNull>); + +impl TimerRef { + /// Re-derive `slot` (a field of `*owner`) from `owner`'s address so the + /// handle carries whole-owner provenance for the dispatch `container_of`. + #[inline] + fn project(owner: *const O, owner_size: usize, slot: *const EventLoopTimer) -> Self { + let offset = (slot as usize).wrapping_sub(owner.cast::() as usize); + assert!( + offset.saturating_add(core::mem::size_of::()) <= owner_size, + "TimerRef: slot is not a field of its owner" + ); + // `slot`'s own address (so already aligned), re-derived from `owner`. + #[allow(clippy::cast_ptr_alignment)] + let p = owner + .cast::() + .wrapping_add(offset) + .cast::>() + .cast_mut(); + // SAFETY: `p` addresses a field of `*owner` (checked above), so it is non-null. + TimerRef(unsafe { NonNull::new_unchecked(p) }) + } + + /// `owner`'s `slot` (which must be a field of `*owner`). + #[inline] + pub fn new(owner: &O, slot: fn(&O) -> &JsCell) -> Self { + let cell: *const JsCell = slot(owner); + Self::project(owner, core::mem::size_of_val(owner), cell.cast()) + } + + /// [`new`](Self::new) for owners that hold the slot as a bare field. + #[inline] + pub fn from_mut( + owner: &mut O, + slot: fn(&mut O) -> &mut EventLoopTimer, + ) -> Self { + let owner_size = core::mem::size_of_val(owner); + let cell: *const EventLoopTimer = slot(owner); + Self::project(core::ptr::from_mut(owner), owner_size, cell) + } - /// Fire the timer's callback. - /// - /// The `match self.tag { … container_of … }` body is - /// hot-dispatch over ~20 tier-6 variant types (Subprocess, DevServer, - /// PostgresSQLConnection, …). That match lives in - /// `bun_runtime::dispatch::__bun_fire_timer` (link-time extern). `vm` is - /// the erased `*mut VirtualMachine`. - /// - /// Deliberately takes `this: *mut Self`, NOT - /// `&mut self`. `__bun_fire_timer` dispatches via container_of into a - /// tier-6 timer object whose JS callback can re-enter and re-derive a - /// `&mut EventLoopTimer` to *this same node* (e.g. `clearTimeout()` → - /// `vm.timer.remove()` mutates `(*this).state`/`heap`). A live `&mut self` - /// across that FFI call lets LLVM `noalias` dead-store the re-entrant - /// write. Both callers (`drain_timers`, `get_timeout`) already hold a raw - /// `*mut EventLoopTimer` popped from the heap — pass it directly. - /// - /// The owner returns the exception its handler left pending as `Err` and - /// never reports it itself; the drain loop that called `fire` folds it. - /// /// # Safety - /// `this` is a live timer just popped from `All.timers`; `now` is the - /// snapshot taken by `All::next`; `vm` is the per-thread VM. The handler - /// may free the container — caller must not touch `this` after. - pub unsafe fn fire( - this: *mut Self, - now: ×pec, - vm: *mut (), /* SAFETY: erased *mut VirtualMachine */ - ) -> crate::JsResult<()> { - // SAFETY: per fn contract. - unsafe { __bun_fire_timer(this, now, vm) } + /// `slot` is an [`EventLoopTimer`] slot of a live [`TimerOwner`] (or is + /// otherwise pinned, unlinked before it is freed, and tagged for its + /// container), with provenance over that container. + #[inline] + pub unsafe fn from_raw(slot: *mut EventLoopTimer) -> Self { + debug_assert!(!slot.is_null()); + // SAFETY: non-null per contract; `JsCell` is `repr(transparent)` over `T`. + TimerRef(unsafe { NonNull::new_unchecked(slot.cast()) }) + } + + /// The slot's address, for the `container_of` dispatch and identity checks. + #[inline] + pub fn as_ptr(self) -> *mut EventLoopTimer { + self.0.as_ptr().cast() + } + + #[inline] + fn cell(&self) -> &JsCell { + // SAFETY: `TimerOwner` / holder contract — the slot's owner is alive + // while this handle is in use (see the type docs for when that is). + unsafe { self.0.as_ref() } + } + + #[inline] + pub fn tag(self) -> Tag { + self.cell().get().tag + } + #[inline] + pub fn state(self) -> State { + self.cell().get().state + } + #[inline] + pub fn set_state(self, state: State) { + self.cell().with_mut(|t| t.state = state); + } + #[inline] + pub fn next(self) -> timespec { + self.cell().get().next + } + #[inline] + pub fn set_next(self, next: timespec) { + self.cell().with_mut(|t| t.next = next); + } + #[inline] + pub fn in_heap(self) -> InHeap { + self.cell().get().in_heap + } +} + +#[derive(Default)] +pub struct TimerOrder; + +impl bun_io::heap::HeapContext for TimerOrder { + #[inline] + fn less(&self, a: &EventLoopTimer, b: &EventLoopTimer) -> bool { + EventLoopTimer::less((), a, b) + } +} + +/// Pairing heap of [`EventLoopTimer`] slots, ordered by deadline (then JS +/// epoch). Holds no ownership: each linked slot is kept alive by its +/// [`TimerOwner`]. The heap is the only writer of a slot's links and +/// [`InHeap`] membership, so `insert`/`remove` can refuse a slot that is +/// already linked / not linked here instead of corrupting the structure. +pub struct TimerHeap { + heap: bun_io::heap::Intrusive, + kind: InHeap, +} + +impl TimerHeap { + pub fn new(kind: InHeap) -> Self { + debug_assert!(kind != InHeap::None); + Self { + heap: Default::default(), + kind, + } + } + + #[inline] + fn wrap(t: *mut EventLoopTimer) -> Option { + // SAFETY: every node in the heap came from a `TimerRef` (`insert`). + (!t.is_null()).then(|| unsafe { TimerRef::from_raw(t) }) + } + + #[inline] + pub fn peek(&self) -> Option { + Self::wrap(self.heap.peek()) + } + + /// Link `t`. A slot that is already in a heap is left where it is. + #[inline] + pub fn insert(&self, t: TimerRef) { + let cell = t.cell(); + if cell.get().in_heap != InHeap::None { + debug_assert!( + false, + "TimerHeap::insert: slot already in {:?}", + cell.get().in_heap + ); + return; + } + cell.with_mut(|t| t.in_heap = self.kind); + // SAFETY: unlinked (checked above); the `TimerOwner` / holder contract + // keeps the slot live and in place until it is unlinked again. + unsafe { self.heap.insert(t.as_ptr()) } + } + + /// Unlink `t`. A slot that is not in this heap is left alone. + #[inline] + pub fn remove(&self, t: TimerRef) { + let cell = t.cell(); + if cell.get().in_heap != self.kind { + debug_assert!( + false, + "TimerHeap::remove: slot is in {:?}", + cell.get().in_heap + ); + return; + } + // SAFETY: `t` is live (`TimerOwner` contract) and linked into this heap + // (only `insert` sets `in_heap` to `self.kind`). + unsafe { self.heap.remove(t.as_ptr()) }; + cell.with_mut(|t| t.in_heap = InHeap::None); + } + + #[inline] + pub fn delete_min(&self) -> Option { + let t = Self::wrap(self.heap.delete_min())?; + t.cell().with_mut(|t| t.in_heap = InHeap::None); + Some(t) + } + + /// O(N). + #[inline] + pub fn find_max(&self) -> Option { + Self::wrap(self.heap.find_max()) + } + + /// O(N). + #[inline] + pub fn count(&self) -> usize { + self.heap.count() + } + + /// Every linked slot, in no particular order. + pub fn to_vec(&self) -> Vec { + let mut out = Vec::new(); + // SAFETY: as `wrap` — visited nodes are linked, hence from a `TimerRef`. + self.heap + .for_each(|t| out.push(unsafe { TimerRef::from_raw(t) })); + out } } @@ -228,10 +424,12 @@ impl Tag { /// Stamp out one `unsafe fn $method(*const EventLoopTimer) -> *mut Self` per /// `(method => field)` pair: each recovers the embedding owner from a pointer -/// to the named intrusive [`EventLoopTimer`] slot (typed container_of). +/// to the named intrusive [`EventLoopTimer`] slot (typed container_of), and +/// marks `$Owner` as a [`TimerOwner`] — **invoking this macro asserts that +/// trait's contract** for the named slots. /// /// The accessor layer exists only as a cross-crate visibility shim: the -/// `__bun_fire_timer` tag-dispatch in `bun_runtime` cannot name private timer +/// `fire_timer` tag-dispatch in `bun_runtime` cannot name private timer /// fields on owners defined elsewhere, so each owner exports a named thunk per /// slot. The input is `*const` (so `*mut` / `&mut` / `&` all coerce at the /// call site); the field may be a bare `EventLoopTimer` or any @@ -247,6 +445,8 @@ impl Tag { #[macro_export] macro_rules! impl_timer_owner { ($Owner:ty; $($method:ident => $field:ident),+ $(,)?) => { + // SAFETY: asserted by the invoker — see the macro docs. + unsafe impl $crate::EventLoopTimer::TimerOwner for $Owner {} impl $Owner { $( /// Recover `*mut Self` from a pointer to its intrusive diff --git a/src/install/PackageManager.rs b/src/install/PackageManager.rs index dd33e38ce222..6dc84d2deeff 100644 --- a/src/install/PackageManager.rs +++ b/src/install/PackageManager.rs @@ -2031,13 +2031,7 @@ pub fn init( wr!(options, options); wr!( active_lifecycle_scripts, - crate::lifecycle_script_runner::List { - root: core::ptr::null_mut(), - // `lifecycle_script_runner::List`'s heap comparator never - // dereferences its context arg, so it is modeled as a ZST - // (`StartedAtCtx`) instead of threading a back-pointer. - context: crate::lifecycle_script_runner::StartedAtCtx, - } + crate::lifecycle_script_runner::List::default() ); wr!(network_task_fifo, NetworkQueue::init()); wr!(patch_task_fifo, PatchTaskFifo::init()); @@ -2492,10 +2486,7 @@ fn init_with_runtime_once( ); wr!( active_lifecycle_scripts, - crate::lifecycle_script_runner::List { - root: core::ptr::null_mut(), - context: crate::lifecycle_script_runner::StartedAtCtx, - } + crate::lifecycle_script_runner::List::default() ); wr!(network_task_fifo, NetworkQueue::init()); wr!(log, std::ptr::from_mut(log)); diff --git a/src/install/lifecycle_script_runner.rs b/src/install/lifecycle_script_runner.rs index f415b55cf5e5..4f3544a18982 100644 --- a/src/install/lifecycle_script_runner.rs +++ b/src/install/lifecycle_script_runner.rs @@ -316,21 +316,15 @@ pub type List<'a> = io_heap::Intrusive, StartedAtC impl<'a> io_heap::HeapNode for LifecycleScriptSubprocess<'a> { #[inline] - fn heap(&mut self) -> &mut io_heap::IntrusiveField { - &mut self.heap + fn heap(&self) -> &io_heap::IntrusiveField { + &self.heap } } impl<'a> io_heap::HeapContext> for StartedAtCtx { #[inline] - unsafe fn less( - &self, - a: *mut LifecycleScriptSubprocess<'a>, - b: *mut LifecycleScriptSubprocess<'a>, - ) -> bool { - // SAFETY: `a`/`b` are live heap nodes owned by the intrusive heap; the - // heap only calls `less` on nodes it has been handed via `insert`. - unsafe { (*a).started_at < (*b).started_at } + fn less(&self, a: &LifecycleScriptSubprocess<'a>, b: &LifecycleScriptSubprocess<'a>) -> bool { + a.started_at < b.started_at } } @@ -437,18 +431,15 @@ impl<'a> LifecycleScriptSubprocess<'a> { // SAFETY: caller contract — `this` is non-null and live. unsafe { let manager: *mut PackageManager = (*this).manager.as_ptr(); - let heap = core::ptr::addr_of_mut!((*this).heap); + let heap = core::ptr::addr_of!((*this).heap); // SAFETY: `manager` is non-null and outlives every subprocess (see // `Self::manager`); the install loop is single-threaded here. - let active = &mut (*manager).active_lifecycle_scripts; - if !(*heap).child.is_null() - || !(*heap).next.is_null() - || !(*heap).prev.is_null() - || core::ptr::eq(active.root, this as *const _) - { + let this = this.cast::>(); + let active = &(*manager).active_lifecycle_scripts; + if (*heap).is_linked() || active.is_root(this) { // SAFETY: `this` was inserted via `insert(this)` with allocation- // rooted provenance; the heap holds no other live `&mut` to it here. - active.remove(this.cast::>()); + active.remove(this); } } } diff --git a/src/io/heap.rs b/src/io/heap.rs index 8c98a576f3a2..947e787d2b29 100644 --- a/src/io/heap.rs +++ b/src/io/heap.rs @@ -1,3 +1,4 @@ +use core::cell::Cell; use core::ptr; /// An intrusive heap implementation backed by a pairing heap[1] implementation. @@ -18,30 +19,36 @@ use core::ptr; /// - You can easily make this a min or max heap by inverting the result of /// "less" below. /// +/// Invariant: every node reachable from `root` was handed to [`insert`] and has +/// not been returned by [`delete_min`] / passed to [`remove`] since, so by +/// `insert`'s contract it is live. All node access goes through `&T` and the +/// `Cell` links, so the heap never forms a `&mut T`. +/// /// [1]: https://en.wikipedia.org/wiki/Pairing_heap /// [2]: https://www.boost.org/doc/libs/1_64_0/doc/html/intrusive/intrusive_vs_nontrusive.html +/// +/// [`insert`]: Intrusive::insert +/// [`delete_min`]: Intrusive::delete_min +/// [`remove`]: Intrusive::remove // // The comparator is a trait on `Context` (`HeapContext::less`) rather than // a fn-pointer parameter (fn pointers can't be const generics on stable). // This preserves monomorphization (no indirect call) at the cost of requiring // the caller to impl the trait instead of passing a free fn. pub struct Intrusive> { - pub root: *mut T, + root: Cell<*mut T>, pub context: Context, } /// Trait providing the ordering relation for `Intrusive`. /// Implement this on your `Context` type (or a ZST if no context is needed). pub trait HeapContext { - /// # Safety - /// `a` and `b` must be non-null, aligned, and point to live nodes currently - /// owned by the intrusive heap. Only called from `Intrusive` internals. - unsafe fn less(&self, a: *mut T, b: *mut T) -> bool; + fn less(&self, a: &T, b: &T) -> bool; } /// Trait giving generic access to the embedded `IntrusiveField` on `T`. pub trait HeapNode: Sized { - fn heap(&mut self) -> &mut IntrusiveField; + fn heap(&self) -> &IntrusiveField; } impl> Default for Intrusive @@ -50,119 +57,129 @@ where { fn default() -> Self { Self { - root: ptr::null_mut(), + root: Cell::new(ptr::null_mut()), context: Context::default(), } } } impl> Intrusive { + /// Borrow a linked node. + /// + /// # Safety + /// `n` is non-null and linked into this heap (struct invariant ⇒ live). + #[inline] + unsafe fn node<'a>(n: *mut T) -> &'a T { + // SAFETY: caller contract. + unsafe { &*n } + } + /// Insert a new element v into the heap. An element v can only /// be a member of a single heap at any given time. When compiled /// with runtime-safety, assertions will help verify this property. - pub unsafe fn insert(&mut self, v: *mut T) { - // SAFETY: caller guarantees `v` is a valid, exclusively-owned node not - // currently in any heap; `self.root` is either null or a valid node. - self.root = if !self.root.is_null() { - let root = self.root; - self.meld(v, root) + /// + /// # Safety + /// `v` points at a live `T` that is not linked into any heap, and it stays + /// live at that address until it leaves this heap again (via + /// [`delete_min`](Self::delete_min) or [`remove`](Self::remove)). + pub unsafe fn insert(&self, v: *mut T) { + let root = self.root.get(); + self.root.set(if !root.is_null() { + // SAFETY: `v` per fn contract; `root` per struct invariant. + unsafe { self.meld(v, root) } } else { v - }; + }); } /// Look at the next minimum value but do not remove it. pub fn peek(&self) -> *mut T { - self.root - } - - /// Count the number of elements in the heap. This is an O(N) operation. - pub unsafe fn count(&self) -> usize { - // SAFETY: all reachable nodes from `self.root` are valid for the heap's lifetime. - Self::count_internal(self.root) + self.root.get() } - unsafe fn count_internal(node: *mut T) -> usize { - if node.is_null() { - return 0; - } - let current = node; - let mut result: usize = 1; - - // Count children - // SAFETY: `current` is non-null and valid (checked above / invariant). - result += Self::count_internal((*current).heap().child); - - // Count siblings - result += Self::count_internal((*current).heap().next); - - result + /// `true` if `v` is the root of this heap. + pub fn is_root(&self, v: *const T) -> bool { + ptr::eq(self.root.get(), v) } - /// Look at the next maximum value but do not remove it. This is an O(N) operation. - pub unsafe fn find_max(&self) -> *mut T { - if self.root.is_null() { - return ptr::null_mut(); - } - let root = self.root; - // SAFETY: `root` is non-null and valid. - Self::find_max_internal(&self.context, root, root) + /// Count the number of elements in the heap. This is an O(N) operation. + pub fn count(&self) -> usize { + let mut n = 0; + self.for_each(|_| n += 1); + n } - unsafe fn find_max_internal(ctx: &Context, node: *mut T, current_max: *mut T) -> *mut T { - let mut max_so_far = current_max; - - // Update max if current node is greater - if ctx.less(max_so_far, node) { - max_so_far = node; - } - - // Traverse children - // SAFETY: `node` is a valid heap node (caller invariant). - let child = (*node).heap().child; - if !child.is_null() { - max_so_far = Self::find_max_internal(ctx, child, max_so_far); + /// Visit every linked node (in no particular order). `f` must not link or + /// unlink nodes of this heap. + pub fn for_each(&self, mut f: impl FnMut(*mut T)) { + let root = self.root.get(); + if root.is_null() { + return; } - - // Traverse siblings - let next_sibling = (*node).heap().next; - if !next_sibling.is_null() { - max_so_far = Self::find_max_internal(ctx, next_sibling, max_so_far); + let mut stack: Vec<*mut T> = vec![root]; + while let Some(node) = stack.pop() { + // SAFETY: struct invariant — reachable nodes are live. + let links = unsafe { Self::node(node) }.heap(); + let (child, next) = (links.child.get(), links.next.get()); + if !child.is_null() { + stack.push(child); + } + if !next.is_null() { + stack.push(next); + } + f(node); } + } - max_so_far + /// Look at the next maximum value but do not remove it. This is an O(N) operation. + pub fn find_max(&self) -> *mut T { + let mut max = self.root.get(); + self.for_each(|node| { + // SAFETY: struct invariant — both are linked, live nodes. + let (a, b) = unsafe { (Self::node(max), Self::node(node)) }; + if self.context.less(a, b) { + max = node; + } + }); + max } /// Delete the minimum value from the heap and return it. - pub unsafe fn delete_min(&mut self) -> *mut T { - if self.root.is_null() { + pub fn delete_min(&self) -> *mut T { + let root = self.root.get(); + if root.is_null() { return ptr::null_mut(); } - let root = self.root; - // SAFETY: `root` is non-null and valid. - let child = (*root).heap().child; - self.root = if !child.is_null() { - self.combine_siblings(child) + // SAFETY: struct invariant — `root` is a linked, live node. + let root_links = unsafe { Self::node(root) }.heap(); + let child = root_links.child.get(); + self.root.set(if !child.is_null() { + // SAFETY: `child` is linked (reachable from root). + unsafe { self.combine_siblings(child) } } else { ptr::null_mut() - }; + }); // Clear pointers with runtime safety so we can verify on // insert that values aren't incorrectly being set multiple times. - *(*root).heap() = IntrusiveField::default(); + root_links.clear(); root } /// Remove the value v from the heap. - pub unsafe fn remove(&mut self, v: *mut T) { + /// + /// # Safety + /// `v` points at a live `T` currently linked into this heap. + pub unsafe fn remove(&self, v: *mut T) { // If v doesn't have a previous value, this must be the root // element. If it is NOT the root element, v can't be in this // heap and we trigger an assertion failure. - // SAFETY: caller guarantees `v` is a valid node currently in this heap. - let prev = (*v).heap().prev; + // SAFETY: fn contract. + let v_links = unsafe { Self::node(v) }.heap(); + let prev = v_links.prev.get(); if prev.is_null() { - debug_assert!(self.root == v); + debug_assert!(self.is_root(v)); let _ = self.delete_min(); return; } @@ -171,28 +188,33 @@ impl> Intrusive { // is as if this node never nexisted. The previous value // must point to the proper next value and the pointers // must all be cleaned up. - let v_next = (*v).heap().next; + let v_next = v_links.next.get(); if !v_next.is_null() { - (*v_next).heap().prev = prev; + // SAFETY: linked from `v`, hence in this heap. + unsafe { Self::node(v_next) }.heap().prev.set(prev); } - if (*prev).heap().child == v { - (*prev).heap().child = v_next; + // SAFETY: linked from `v`, hence in this heap. + let prev_links = unsafe { Self::node(prev) }.heap(); + if ptr::eq(prev_links.child.get(), v) { + prev_links.child.set(v_next); } else { - (*prev).heap().next = v_next; + prev_links.next.set(v_next); } - (*v).heap().prev = ptr::null_mut(); - (*v).heap().next = ptr::null_mut(); + v_links.prev.set(ptr::null_mut()); + v_links.next.set(ptr::null_mut()); // If we have children, then we need to merge them back in. - let child = (*v).heap().child; + let child = v_links.child.get(); if child.is_null() { return; } - (*v).heap().child = ptr::null_mut(); - let x = self.combine_siblings(child); - // SAFETY: `self.root` is non-null here — `v` had a `prev`, so it was not the - // root, hence the heap is non-empty. - self.root = self.meld(x, self.root); + v_links.child.set(ptr::null_mut()); + // SAFETY: `child` was linked under `v`; `self.root` is non-null here — + // `v` had a `prev`, so it was not the root, hence the heap is non-empty. + unsafe { + let x = self.combine_siblings(child); + self.root.set(self.meld(x, self.root.get())); + } } /// Meld (union) two heaps together. This isn't a generalized @@ -202,34 +224,41 @@ impl> Intrusive { /// /// For example, when melding a new value "v" with an existing /// root "root", "v" must always be the first param. - unsafe fn meld(&mut self, a: *mut T, b: *mut T) -> *mut T { - // SAFETY: `a` and `b` are distinct valid nodes (caller invariant). - debug_assert!((*a).heap().next.is_null()); - - if self.context.less(a, b) { + /// + /// # Safety + /// `a` and `b` are distinct, live nodes (linked, or being linked by `insert`). + unsafe fn meld(&self, a: *mut T, b: *mut T) -> *mut T { + // SAFETY: fn contract. + let (a_ref, b_ref) = unsafe { (Self::node(a), Self::node(b)) }; + let (al, bl) = (a_ref.heap(), b_ref.heap()); + debug_assert!(al.next.get().is_null()); + + if self.context.less(a_ref, b_ref) { // B points back to A - (*b).heap().prev = a; + bl.prev.set(a); // If B has siblings, then A inherits B's siblings // and B's immediate sibling must point back to A to // maintain the doubly linked list. - let b_next = (*b).heap().next; + let b_next = bl.next.get(); if !b_next.is_null() { - (*a).heap().next = b_next; - (*b_next).heap().prev = a; - (*b).heap().next = ptr::null_mut(); + al.next.set(b_next); + // SAFETY: linked from `b`. + unsafe { Self::node(b_next) }.heap().prev.set(a); + bl.next.set(ptr::null_mut()); } // If A has a child, then B becomes the leftmost sibling // of that child. - let a_child = (*a).heap().child; + let a_child = al.child.get(); if !a_child.is_null() { - (*b).heap().next = a_child; - (*a_child).heap().prev = b; + bl.next.set(a_child); + // SAFETY: linked from `a`. + unsafe { Self::node(a_child) }.heap().prev.set(b); } // B becomes the leftmost child of A - (*a).heap().child = b; + al.child.set(b); return a; } @@ -237,67 +266,92 @@ impl> Intrusive { // Replace A with B in the tree. Any of B's children // become siblings of A. A becomes the leftmost child of B. // A points back to B - (*b).heap().prev = (*a).heap().prev; - (*a).heap().prev = b; - let b_child = (*b).heap().child; + bl.prev.set(al.prev.get()); + al.prev.set(b); + let b_child = bl.child.get(); if !b_child.is_null() { - (*a).heap().next = b_child; - (*b_child).heap().prev = a; + al.next.set(b_child); + // SAFETY: linked from `b`. + unsafe { Self::node(b_child) }.heap().prev.set(a); } - (*b).heap().child = a; + bl.child.set(a); b } /// Combine the siblings of the leftmost value "left" into a single /// new rooted with the minimum value. - unsafe fn combine_siblings(&mut self, left: *mut T) -> *mut T { - // SAFETY: `left` is a valid non-null node (caller invariant). - (*left).heap().prev = ptr::null_mut(); + /// + /// # Safety + /// `left` is a non-null node linked into this heap. + unsafe fn combine_siblings(&self, left: *mut T) -> *mut T { + // SAFETY: (whole body) every pointer followed is a link of a node + // reachable from `left`, hence linked and live (struct invariant). + unsafe { + Self::node(left).heap().prev.set(ptr::null_mut()); + + // Merge pairs right + let mut root: *mut T = 'root: { + let mut a: *mut T = left; + loop { + let mut b = Self::node(a).heap().next.get(); + if b.is_null() { + break 'root a; + } + Self::node(a).heap().next.set(ptr::null_mut()); + b = self.meld(a, b); + let next_a = Self::node(b).heap().next.get(); + if next_a.is_null() { + break 'root b; + } + a = next_a; + } + }; - // Merge pairs right - let mut root: *mut T = 'root: { - let mut a: *mut T = left; + // Merge pairs left loop { - let mut b = (*a).heap().next; + let b = Self::node(root).heap().prev.get(); if b.is_null() { - break 'root a; + return root; } - (*a).heap().next = ptr::null_mut(); - b = self.meld(a, b); - let next_a = (*b).heap().next; - if next_a.is_null() { - break 'root b; - } - a = next_a; - } - }; - - // Merge pairs left - loop { - let b = (*root).heap().prev; - if b.is_null() { - return root; + Self::node(b).heap().next.set(ptr::null_mut()); + root = self.meld(b, root); } - (*b).heap().next = ptr::null_mut(); - root = self.meld(b, root); } } } /// The state that is required for IntrusiveHeap element types. This /// should be set as the "heap" field in the type T. +/// The links are private: only [`Intrusive`] writes them, which is what lets its +/// safe methods trust them. pub struct IntrusiveField { - pub child: *mut T, - pub prev: *mut T, - pub next: *mut T, + child: Cell<*mut T>, + prev: Cell<*mut T>, + next: Cell<*mut T>, +} + +impl IntrusiveField { + /// `true` while any link is set. A lone root has no links, so callers that + /// need "is in heap H" must also ask [`Intrusive::is_root`]. + #[inline] + pub fn is_linked(&self) -> bool { + !self.child.get().is_null() || !self.prev.get().is_null() || !self.next.get().is_null() + } + + #[inline] + fn clear(&self) { + self.child.set(ptr::null_mut()); + self.prev.set(ptr::null_mut()); + self.next.set(ptr::null_mut()); + } } impl Default for IntrusiveField { fn default() -> Self { Self { - child: ptr::null_mut(), - prev: ptr::null_mut(), - next: ptr::null_mut(), + child: Cell::new(ptr::null_mut()), + prev: Cell::new(ptr::null_mut()), + next: Cell::new(ptr::null_mut()), } } } diff --git a/src/jsc/AbortSignal.rs b/src/jsc/AbortSignal.rs index 5e5e64bb4e05..53625273019b 100644 --- a/src/jsc/AbortSignal.rs +++ b/src/jsc/AbortSignal.rs @@ -4,8 +4,7 @@ use core::sync::atomic::Ordering; use crate::{CommonAbortReason, JSGlobalObject, JSValue, VirtualMachineRef as VirtualMachine}; use bun_event_loop::EventLoopTimer::{ - EventLoopTimer, InHeap, IntrusiveField, State as TimerState, Tag as TimerTag, TimerFlags, - Timespec as ElTimespec, + EventLoopTimer, State as TimerState, Tag as TimerTag, TimerFlags, Timespec as ElTimespec, }; bun_opaque::opaque_ffi! { @@ -299,16 +298,14 @@ impl Timeout { .add_ms(i64::try_from(milliseconds).expect("AbortSignal.timeout(ms) overflows i64")); let this: *mut Timeout = bun_core::heap::into_raw(Box::new(Timeout { - event_loop_timer: EventLoopTimer { - next: ElTimespec { + event_loop_timer: EventLoopTimer::new( + TimerTag::AbortSignalTimeout, + TimerState::CANCELLED, + ElTimespec { sec: deadline.sec, nsec: deadline.nsec, }, - tag: TimerTag::AbortSignalTimeout, - state: TimerState::CANCELLED, - heap: IntrusiveField::default(), - in_heap: InHeap::default(), - }, + ), signal: signal_, flags: TimerFlags::default(), generation: VirtualMachine::get().test_isolation_generation, diff --git a/src/jsc/host_fn.rs b/src/jsc/host_fn.rs index b576c8cf0c6c..33156e8dfb15 100644 --- a/src/jsc/host_fn.rs +++ b/src/jsc/host_fn.rs @@ -537,29 +537,64 @@ pub fn host_fn_construct_this( // (Phase 3 of `R-2-design.md` deletes them and drops the `_shared` suffix). // ────────────────────────────────────────────────────────────────────────── -/// Prototype method (`sharedThis`): `fn(&self, &JSGlobalObject, &CallFrame) -> R`. +/// How a `sharedThis` prototype method receives the wrapper's `m_ctx`: as a +/// plain `&T`, or as a [`ThisPtr`](bun_ptr::ThisPtr) when the method needs +/// the allocation's root pointer (to take/release intrusive refs on itself or +/// hand itself to a dispatch path that may). The generated thunk is generic +/// over this, so the method's own signature picks. +pub trait HostReceiver<'a, T: 'a>: Sized { + /// # Safety + /// `this` is the live, non-null `m_ctx` of a JS wrapper that stays rooted + /// for `'a`. + unsafe fn from_m_ctx(this: *mut T) -> Self; +} +impl<'a, T: 'a> HostReceiver<'a, T> for &'a T { + #[inline(always)] + unsafe fn from_m_ctx(this: *mut T) -> Self { + // SAFETY: trait contract. + unsafe { &*this } + } +} +impl<'a, T: 'a> HostReceiver<'a, T> for bun_ptr::ThisPtr { + #[inline(always)] + unsafe fn from_m_ctx(this: *mut T) -> Self { + // SAFETY: trait contract. + unsafe { bun_ptr::ThisPtr::new(this) } + } +} + +/// Prototype method (`sharedThis`) taking `&self` or `this: ThisPtr`. +/// +/// # Safety +/// `this` is the live `m_ctx` of the JS wrapper the call was dispatched on. #[track_caller] #[inline] -pub fn host_fn_this_shared( - this: &T, - global: &JSGlobalObject, - callframe: &CallFrame, - f: impl FnOnce(&T, &JSGlobalObject, &CallFrame) -> R, +pub unsafe fn host_fn_this_ptr<'a, T: 'a, Rcv: HostReceiver<'a, T>, R: IntoHostFnReturn>( + this: *mut T, + global: &'a JSGlobalObject, + callframe: &'a CallFrame, + f: impl FnOnce(Rcv, &'a JSGlobalObject, &'a CallFrame) -> R, ) -> JSValue { + // SAFETY: fn contract. + let this = unsafe { Rcv::from_m_ctx(this) }; host_fn_result(global, || f(this, global, callframe)) } -/// Prototype method (`sharedThis`, passThis): -/// `fn(&self, &JSGlobalObject, &CallFrame, JSValue) -> R`. +/// [`host_fn_this_ptr`] with `passThis`. +/// +/// # Safety +/// As [`host_fn_this_ptr`]. #[track_caller] #[inline] -pub fn host_fn_this_value_shared( - this: &T, - global: &JSGlobalObject, - callframe: &CallFrame, +pub unsafe fn host_fn_this_value_ptr<'a, T: 'a, Rcv: HostReceiver<'a, T>, R: IntoHostFnReturn>( + this: *mut T, + global: &'a JSGlobalObject, + callframe: &'a CallFrame, js_this: JSValue, - f: impl FnOnce(&T, &JSGlobalObject, &CallFrame, JSValue) -> R, + f: impl FnOnce(Rcv, &'a JSGlobalObject, &'a CallFrame, JSValue) -> R, ) -> JSValue { + // SAFETY: fn contract. + let this = unsafe { Rcv::from_m_ctx(this) }; host_fn_result(global, || f(this, global, callframe, js_this)) } diff --git a/src/jsc_macros/lib.rs b/src/jsc_macros/lib.rs index 18a20ef9c62f..4e3eb311b71d 100644 --- a/src/jsc_macros/lib.rs +++ b/src/jsc_macros/lib.rs @@ -136,7 +136,22 @@ fn expand_host_fn(args: &HostFnArgs, func: &ItemFn) -> syn::Result .inputs .first() .is_some_and(|a| matches!(a, FnArg::Receiver(r) if r.mutability.is_none())); - let this_reborrow = if receiver_is_shared { + // A typed `this: ThisPtr` first parameter gets the root pointer + // wrapped instead of reborrowed. + let first_is_this_ptr = func.sig.inputs.first().is_some_and(|a| match a { + FnArg::Typed(pt) => match &*pt.ty { + syn::Type::Path(tp) => tp + .path + .segments + .last() + .is_some_and(|seg| seg.ident == "ThisPtr"), + _ => false, + }, + _ => false, + }); + let this_reborrow = if first_is_this_ptr { + quote! { let __t = unsafe { ::bun_ptr::ThisPtr::new(__this) }; } + } else if receiver_is_shared { quote! { let __t = unsafe { &*__this }; } } else { quote! { let __t = unsafe { &mut *__this }; } diff --git a/src/libuv_sys/libuv.rs b/src/libuv_sys/libuv.rs index 9175e67e2677..6561e148def4 100644 --- a/src/libuv_sys/libuv.rs +++ b/src/libuv_sys/libuv.rs @@ -1506,6 +1506,20 @@ impl Timer { panic!("internal error: uv_timer_stop failed"); } } + /// Milliseconds until the timer is due (0 if overdue or stopped). + #[inline] + pub fn get_due_in(&self) -> u64 { + debug_assert!(!self.loop_.is_null()); + // SAFETY: timer was `init`ed (reads `self.timeout` and `loop_->time`). + unsafe { uv_timer_get_due_in(self) } + } + /// `uv_update_time` on the loop this timer was `init`ed on. + #[inline] + pub fn update_loop_time(&self) { + debug_assert!(!self.loop_.is_null()); + // SAFETY: timer was `init`ed, so `loop_` is the live loop it is registered on. + unsafe { uv_update_time(self.loop_) } + } } // ────────────────────────────────────────────────────────────────────────── diff --git a/src/ptr/js_cell.rs b/src/ptr/js_cell.rs index e9d9c7567c33..3b2b0e517984 100644 --- a/src/ptr/js_cell.rs +++ b/src/ptr/js_cell.rs @@ -41,6 +41,13 @@ impl JsCell { Self(core::cell::UnsafeCell::new(value)) } + /// View exclusively-borrowed storage as a cell (like `Cell::from_mut`). + #[inline(always)] + pub fn from_mut(value: &mut T) -> &mut JsCell { + // SAFETY: `JsCell` is `repr(transparent)` over `UnsafeCell`. + unsafe { &mut *core::ptr::from_mut(core::cell::UnsafeCell::from_mut(value)).cast() } + } + /// Shared-reference read. Caller must not hold a live `get_mut()` borrow /// across this call (single-JS-thread reentrancy makes overlap rare but /// possible — keep borrows short). diff --git a/src/runtime/api/bun/js_bun_spawn_bindings.rs b/src/runtime/api/bun/js_bun_spawn_bindings.rs index 0dfa5e110e62..86a3c26a8bc1 100644 --- a/src/runtime/api/bun/js_bun_spawn_bindings.rs +++ b/src/runtime/api/bun/js_bun_spawn_bindings.rs @@ -1852,7 +1852,7 @@ fn spawn_maybe_sync( // comparable with `now`. if abort_signal_timeout.event_loop_timer.state == crate::timer::EventLoopTimerState::ACTIVE - && abort_signal_timeout.event_loop_timer.in_heap + && abort_signal_timeout.event_loop_timer.in_heap() == crate::timer::InHeap::Regular { let next = &abort_signal_timeout.event_loop_timer.next; diff --git a/src/runtime/api/bun/subprocess.rs b/src/runtime/api/bun/subprocess.rs index b4446c87ce4f..51147687e68f 100644 --- a/src/runtime/api/bun/subprocess.rs +++ b/src/runtime/api/bun/subprocess.rs @@ -626,14 +626,18 @@ impl Subprocess<'_> { return; } self.event_loop_timer_refd.set(refd); - let uws_loop = self.global_this().bun_vm().uws_loop(); let delta: i32 = if refd { 1 } else { -1 }; - Self::timer_all().increment_timer_ref(delta, uws_loop); + Self::timer_all().increment_timer_ref(delta); } #[inline] - fn timer_all() -> &'static mut crate::timer::All { - crate::jsc_hooks::timer_all_mut() + fn timer_all() -> &'static crate::timer::All { + crate::jsc_hooks::timer_all() + } + + #[inline] + fn timer_ref(&self) -> crate::timer::TimerRef { + crate::timer::TimerRef::new(self, |p| &p.event_loop_timer) } pub(crate) fn timeout_callback(&self) { @@ -949,7 +953,7 @@ impl Subprocess<'_> { // kept explicit at the tail for now (no early returns in this body). if self.event_loop_timer.get().state == EventLoopTimerState::ACTIVE { - Self::timer_all().remove(self.event_loop_timer.as_ptr()); + Self::timer_all().remove(self.timer_ref()); } self.set_event_loop_timer_refd(false); @@ -1321,7 +1325,7 @@ impl Subprocess<'_> { self.deref(); } if self.event_loop_timer.get().state == EventLoopTimerState::ACTIVE { - Self::timer_all().remove(self.event_loop_timer.as_ptr()); + Self::timer_all().remove(self.timer_ref()); } self.set_event_loop_timer_refd(false); diff --git a/src/runtime/api/cron.rs b/src/runtime/api/cron.rs index 217db1e30f99..9816a4a17a04 100644 --- a/src/runtime/api/cron.rs +++ b/src/runtime/api/cron.rs @@ -40,7 +40,7 @@ use crate::api::bun::process::SpawnResultExt as _; use crate::api::bun::process::{ self as spawn, Process, ProcessHandle, Rusage, SpawnOptions, Status, }; -use crate::timer::{EventLoopTimer, EventLoopTimerState, EventLoopTimerTag}; +use crate::timer::{EventLoopTimer, EventLoopTimerState, EventLoopTimerTag, TimerRef}; use bun_core::ZStr; use bun_core::strings; use bun_io::pipe_reader::BufferedReaderParent; @@ -62,7 +62,7 @@ fn vm_mut<'a>() -> &'a mut VirtualMachine { VirtualMachine::get_mut() } -use crate::jsc_hooks::timer_all_mut as timer_all; +use crate::jsc_hooks::timer_all; // ============================================================================ // CronJobBase — shared base for CronRegisterJob and CronRemoveJob @@ -1434,11 +1434,16 @@ impl CronJob { } } + #[inline] + fn timer_ref(&self) -> TimerRef { + TimerRef::new(self, |job| &job.event_loop_timer) + } + /// Idempotent — every step checks its own state. fn stop_internal(&self, _vm: &VirtualMachine) { self.stopped.set(true); if self.event_loop_timer.get().state == EventLoopTimerState::ACTIVE { - timer_all().remove(self.event_loop_timer.as_ptr()); + timer_all().remove(self.timer_ref()); } self.poll_ref.with_mut(|p| p.unref(bun_io::js_vm_ctx())); self.maybe_downgrade(); @@ -1541,12 +1546,7 @@ impl CronJob { let Some(next_time) = this.compute_next_timespec() else { return Self::finish_deferred_stop(this, vm); }; - timer_all().update( - this.event_loop_timer - .as_ptr() - .cast::(), - &next_time, - ); + timer_all().update(this.timer_ref(), &next_time); } /// The tick's callback runs here as a top-level call (what it throws @@ -1754,12 +1754,7 @@ impl CronJob { ); job.poll_ref.with_mut(|p| p.ref_(bun_io::js_vm_ctx())); - timer_all().update( - job.event_loop_timer - .as_ptr() - .cast::(), - &next_time, - ); + timer_all().update(job.timer_ref(), &next_time); Ok(js_value) } diff --git a/src/runtime/bake/DevServer.rs b/src/runtime/bake/DevServer.rs index f26d5e23b697..bd12494b0c4b 100644 --- a/src/runtime/bake/DevServer.rs +++ b/src/runtime/bake/DevServer.rs @@ -1070,8 +1070,10 @@ impl Drop for DevServer { } if self.memory_visualizer_timer.state == EventLoopTimerState::ACTIVE { - let timer_ptr: *mut EventLoopTimer = &raw mut self.memory_visualizer_timer; - self.timer_heap().remove(timer_ptr); + let heap = self.timer_heap(); + heap.remove(crate::timer::TimerRef::from_mut(self, |d| { + &mut d.memory_visualizer_timer + })); } self.graph_safety_lock.lock(); // Hand ownership of the heap allocation to the watcher thread (which frees it in @@ -1115,8 +1117,11 @@ impl Drop for DevServer { value.ref_count = 0; } if self.source_maps.weak_ref_sweep_timer.state == EventLoopTimerState::ACTIVE { - let timer_ptr: *mut EventLoopTimer = &raw mut self.source_maps.weak_ref_sweep_timer; - self.timer_heap().remove(timer_ptr); + let heap = self.timer_heap(); + heap.remove(crate::timer::TimerRef::from_mut( + &mut self.source_maps, + |s| &mut s.weak_ref_sweep_timer, + )); } // `Watcher::shutdown` serialises with `dispatch_file_updates` on @@ -5494,8 +5499,8 @@ impl DevServer { pub fn emit_visualizer_message_if_needed(&mut self) {} #[inline] - fn timer_heap(&self) -> &mut crate::timer::All { - crate::jsc_hooks::timer_all_mut() + fn timer_heap(&self) -> &'static crate::timer::All { + crate::jsc_hooks::timer_all() } pub fn emit_memory_visualizer_message_timer( diff --git a/src/runtime/bake/dev_server/hmr_socket.rs b/src/runtime/bake/dev_server/hmr_socket.rs index 671104b4c823..b674e11bd06e 100644 --- a/src/runtime/bake/dev_server/hmr_socket.rs +++ b/src/runtime/bake/dev_server/hmr_socket.rs @@ -127,20 +127,16 @@ impl HmrSocket { // Note (jsc/runtime crate cycle): `vm.timer` is `()` on the // low-tier `VirtualMachine`; the real `timer::All` // lives in `RuntimeState` (see jsc_hooks.rs). - let state = crate::jsc_hooks::runtime_state(); let next = bun_core::Timespec::ms_from_now( bun_core::TimespecMockMode::ForceRealTime, 1000, ); - // SAFETY: `runtime_state()` is non-null after - // `bun_runtime::init()`; JS-thread only, sole - // `&mut` to `timer` in this scope. - unsafe { - (*state).timer.update( - &raw mut dev.memory_visualizer_timer, - &next, - ); - } + crate::jsc_hooks::timer_all().update( + crate::timer::TimerRef::from_mut(dev, |d| { + &mut d.memory_visualizer_timer + }), + &next, + ); } } _ => {} @@ -310,14 +306,10 @@ impl HmrSocket { if dev.emit_incremental_visualizer_events == 0 && dev.memory_visualizer_timer.state == EventLoopTimerState::ACTIVE { - // Note (jsc/runtime crate cycle): `vm.timer` is `()` on the low-tier - // `VirtualMachine`; the real `timer::All` lives in `RuntimeState`. - let state = crate::jsc_hooks::runtime_state(); - // SAFETY: `runtime_state()` is non-null after `bun_runtime::init()`; - // JS-thread only, sole `&mut` to `timer` in this scope. - unsafe { - (*state).timer.remove(&raw mut dev.memory_visualizer_timer); - } + crate::jsc_hooks::timer_all() + .remove(crate::timer::TimerRef::from_mut(dev, |d| { + &mut d.memory_visualizer_timer + })); } } } diff --git a/src/runtime/bake/dev_server/source_map_store.rs b/src/runtime/bake/dev_server/source_map_store.rs index c20acff84659..4dae894b1661 100644 --- a/src/runtime/bake/dev_server/source_map_store.rs +++ b/src/runtime/bake/dev_server/source_map_store.rs @@ -452,8 +452,13 @@ impl SourceMapStore { } #[inline] - fn timer_all<'a>() -> &'a mut crate::timer::All { - crate::jsc_hooks::timer_all_mut() + fn timer_all() -> &'static crate::timer::All { + crate::jsc_hooks::timer_all() + } + + #[inline] + fn sweep_timer_ref(&mut self) -> crate::timer::TimerRef { + crate::timer::TimerRef::from_mut(self, |s| &mut s.weak_ref_sweep_timer) } pub(crate) fn put_or_increment_ref_count( @@ -535,7 +540,7 @@ impl SourceMapStore { if self.weak_ref_sweep_timer.state == EventLoopTimerState::ACTIVE && self.weak_ref_sweep_timer.next.sec == first.expire { - Self::timer_all().remove(core::ptr::addr_of_mut!(self.weak_ref_sweep_timer)); + Self::timer_all().remove(self.sweep_timer_ref()); } } } @@ -550,7 +555,7 @@ impl SourceMapStore { if self.weak_ref_sweep_timer.state != EventLoopTimerState::ACTIVE { map_log!("arming weak ref sweep timer"); - Self::timer_all().update(core::ptr::addr_of_mut!(self.weak_ref_sweep_timer), &expire); + Self::timer_all().update(self.sweep_timer_ref(), &expire); } map_log!("addWeakRef {:x}, ref_count: {}", key.get(), entry_ref_count); } @@ -644,7 +649,7 @@ impl SourceMapStore { store.weak_refs.unget(&[item]).expect("unreachable"); // space exists since the last item was just removed. store.weak_ref_sweep_timer.state = EventLoopTimerState::FIRED; Self::timer_all().update( - core::ptr::addr_of_mut!(store.weak_ref_sweep_timer), + store.sweep_timer_ref(), &Timespec { sec: item.expire + 1, nsec: 0, diff --git a/src/runtime/dispatch.rs b/src/runtime/dispatch.rs index afae984518da..210c6e0d1846 100644 --- a/src/runtime/dispatch.rs +++ b/src/runtime/dispatch.rs @@ -35,7 +35,7 @@ use bun_event_loop::{Task, task_tag}; use bun_io::posix_event_loop::{FilePoll, Flags as PollFlag, poll_tag}; use bun_event_loop::EventLoopTimer::{ - EventLoopTimer, Tag as EventLoopTimerTag, Timespec as ElTimespec, + EventLoopTimer, Tag as EventLoopTimerTag, TimerRef, Timespec as ElTimespec, }; use bun_jsc::event_loop::{EventLoop, Stopped}; @@ -829,9 +829,9 @@ unsafe fn __bun_io_pollable_on_io_error( // `bun_jsc::event_loop` extern impls (link-time) // ════════════════════════════════════════════════════════════════════════════ -/// `__bun_run_immediate_task` body — cast the low-tier erased `*mut ()` to the -/// real `crate::timer::ImmediateObject` and run the task (low tier stores -/// `*mut ()`, high tier owns the cast). +/// `__bun_run_immediate_task` body — recover the queued +/// `crate::timer::ImmediateObject` from the low tier's erased `*mut ()` and +/// run it. /// /// # Safety /// `task` was produced by `enqueue_immediate_task` from a live @@ -841,19 +841,15 @@ unsafe fn __bun_run_immediate_task( task: *mut (), vm: *mut bun_jsc::virtual_machine::VirtualMachine, ) -> bool { - // SAFETY: per fn contract — the only producer (`TimerObjectInternals::init`) - // stores a `*mut crate::timer::ImmediateObject`, so the cast is the identity. - unsafe { - crate::timer::ImmediateObject::run_immediate_task( - task.cast::(), - vm, - ) - } + // SAFETY: per fn contract — the only producer (`ImmediateObject::init`) + // stores a `ThisPtr` the queue holds a ref on. + let (this, vm) = unsafe { (bun_ptr::ThisPtr::new(task.cast()), &*vm) }; + crate::timer::ImmediateObject::run_immediate_task(this, vm) } /// `__bun_cancel_pending_immediate` body — VM-teardown release of the event -/// loop's `+1` ref on a still-queued `ImmediateObject` (low tier stores -/// `*mut ()`, high tier owns the cast). Does not run the callback. +/// loop's `+1` ref on a still-queued `ImmediateObject`. Does not run the +/// callback. /// /// # Safety /// `task` was produced by `enqueue_immediate_task` from a live @@ -864,115 +860,108 @@ unsafe fn __bun_cancel_pending_immediate( task: *mut (), vm: *mut bun_jsc::virtual_machine::VirtualMachine, ) { - // SAFETY: per fn contract — the only producer (`TimerObjectInternals::init`) - // stores a `*mut crate::timer::ImmediateObject`, so the cast is the identity. - unsafe { - crate::timer::ImmediateObject::cancel_pending( - task.cast::(), - vm, - ); - } + // SAFETY: per fn contract — see `__bun_run_immediate_task`. + let (this, vm) = unsafe { (bun_ptr::ThisPtr::new(task.cast()), &*vm) }; + crate::timer::ImmediateObject::cancel_pending(this, vm); } -/// `__bun_run_wtf_timer` body — cast the low-tier erased `*mut ()` to the real -/// `crate::timer::WTFTimer` and fire it. +/// `__bun_run_wtf_timer` body — recover the `crate::timer::WTFTimer` from the +/// low tier's erased `*mut ()` and fire it. /// /// # Safety /// `timer` was published by `WTFTimer::update` into `imminent_gc_timer` and /// remains live until consumed; `vm` is the live per-thread VM. #[unsafe(no_mangle)] -unsafe fn __bun_run_wtf_timer(timer: *mut (), vm: *mut bun_jsc::virtual_machine::VirtualMachine) { +unsafe fn __bun_run_wtf_timer(timer: *mut (), _vm: *mut bun_jsc::virtual_machine::VirtualMachine) { // SAFETY: per fn contract — the only producer (`WTFTimer::update`) stores a - // `*mut crate::timer::WTFTimer`, so the cast is the identity. - let real = timer.cast::(); - // SAFETY: per fn contract — `real` is live until consumed; `vm` is the - // per-thread VM. `run` may re-enter `(*runtime_state()).timer.remove()`; - // no `&mut` held here. - unsafe { crate::timer::WTFTimer::run(real, vm) } + // live `*mut crate::timer::WTFTimer`. The borrow ends before `fire`, which + // may destroy the timer. + let run_loop_timer = unsafe { &*timer.cast::() }.take_for_run(); + crate::timer::wtf_timer::RunLoopTimer::fire(run_loop_timer); } // ════════════════════════════════════════════════════════════════════════════ // EventLoopTimer dispatch // ════════════════════════════════════════════════════════════════════════════ -/// `__bun_fire_timer` body — the tag→`container_of` match for -/// [`EventLoopTimer::fire`]. +/// Recover a JS-timer owner from its popped/linked slot as a dispatch handle. +macro_rules! timer_owner_this { + ($ty:ty, $field:ident, $t:expr) => {{ + // SAFETY: `TimerOwner` contract — the slot's tag names `$ty.$field` + // and its owner is alive (linked, or being fired/cancelled right now). + unsafe { bun_ptr::ThisPtr::<$ty>::new(bun_core::from_field_ptr!($ty, $field, $t.as_ptr())) } + }}; +} + +/// The tag→`container_of` match that fires a due timer. /// -/// Reached from [`crate::timer::All::drain_timers`] (every due heap timer) and -/// [`crate::timer::All::get_timeout`] (WTFTimer side-effect). +/// Reached from [`crate::timer::All::drain_timers`] (every due heap timer), +/// [`crate::timer::All::get_timeout`] (WTFTimer side-effect) and the fake +/// clock (`FakeTimers::fire`). /// /// Each arm is the owner's timer entry with its result surfaced: an owner /// returns the exception it left pending and never reports it; the drain loop /// (`All::drain_timers`) folds every timer's result in one place. Owners whose /// entry cannot enter JS return `()` (`timer_arm!` makes that `Ok(())`). /// -/// # Safety -/// `t` points at a live [`EventLoopTimer`] just popped from `All.timers`; -/// `now` is the snapshot taken by `All::next`; `vm` is the erased -/// `*mut VirtualMachine`. The handler may free the container — do not touch -/// `t` after the per-arm call returns. -#[unsafe(no_mangle)] -pub(crate) unsafe fn __bun_fire_timer( - t: *mut EventLoopTimer, - now: *const ElTimespec, - vm: *mut (), -) -> bun_event_loop::JsResult<()> { - use crate::timer::{ImmediateObject, TimeoutObject, TimerObjectInternals, WTFTimer}; +/// `t` was just popped from a heap; the handler may free its owner, so `t` is +/// dead once this returns. +pub(crate) fn fire_timer(t: TimerRef, now: &ElTimespec, vm: &VirtualMachine) -> JsResult<()> { + use crate::timer::{ImmediateObject, TimeoutObject, TimerObject, WTFTimer}; /// Recover the embedding container from `t` (the popped timer slot). macro_rules! owner { ($ty:ty, $field:ident) => {{ - // SAFETY: §Dispatch — `t.tag` was set together with the container - // at construction; tag uniquely identifies the embedding type and - // `$field` is the `EventLoopTimer` slot `t` points into. - unsafe { bun_core::from_field_ptr!($ty, $field, t) } + // SAFETY: `TimerOwner` contract — `t.tag` was set together with the + // container at construction; tag uniquely identifies the embedding + // type and `$field` is the `EventLoopTimer` slot `t` points into. + unsafe { bun_core::from_field_ptr!($ty, $field, t.as_ptr()) } }}; } - // SAFETY: per fn contract — `t` is live for the dispatch read. - let tag = unsafe { (*t).tag }; - let vm = vm.cast::(); + let tag = t.tag(); + let now: *const ElTimespec = now; + let vm_ref = vm; + let vm: *mut VirtualMachine = core::ptr::from_ref(vm).cast_mut(); /// One match-arm body: recover the container as RAW `*mut $Ty` (never /// `&mut` — the handler may free it or re-enter), bind `now`/`vm`, and run /// `$body` under one `unsafe` covering the per-fn-contract dereferences. /// Defined *after* the `vm` cast so the def-site `vm` ident resolves to - /// the typed `*mut VirtualMachine`, not the erased `*mut ()` param. + /// the typed `*mut VirtualMachine`. // An owner that cannot enter JS: its `()` return is `Ok(())` here. macro_rules! timer_arm { ($Ty:ty, $field:ident, |$c:ident, $now:ident, $vm:ident| $body:expr) => {{ let $c: *mut $Ty = owner!($Ty, $field); let ($now, $vm) = (now, vm); - // SAFETY: per fn contract; container derived from a live `$Ty`. + // SAFETY: `TimerOwner` contract; container derived from a live `$Ty`. let () = unsafe { $body }; Ok(()) }}; } let fired: JsResult<()> = match tag { - // ── JS-exposed timers (TimerObjectInternals::fire) ─────────────── + // ── JS-exposed timers (TimerObject::fire) ──────────────────────── // `Bun__JSTimeout__call` reports the callback's exception itself. EventLoopTimerTag::TimeoutObject => { - let container = owner!(TimeoutObject, event_loop_timer); - // SAFETY: container derived from a live `TimeoutObject`; do NOT - // form `&mut *container` — `internals.fire` may `deref()` and free. - let internals = unsafe { core::ptr::addr_of_mut!((*container).internals) }; - // SAFETY: per fn contract — `now` is the live snapshot; `vm` is the - // per-thread VM. `fire` may free the container; `t` is dead after. - // `fire` takes `*mut Self` (noalias re-entrancy — see its doc). - unsafe { TimerObjectInternals::fire(internals, &*now, vm) }; + TimerObject::fire( + timer_owner_this!(TimeoutObject, event_loop_timer, t), + vm_ref, + ); Ok(()) } EventLoopTimerTag::ImmediateObject => { - let container = owner!(ImmediateObject, event_loop_timer); - // SAFETY: see TimeoutObject arm. - let internals = unsafe { core::ptr::addr_of_mut!((*container).internals) }; - // SAFETY: see TimeoutObject arm. - unsafe { TimerObjectInternals::fire(internals, &*now, vm) }; + TimerObject::fire( + timer_owner_this!(ImmediateObject, event_loop_timer, t), + vm_ref, + ); Ok(()) } EventLoopTimerTag::WTFTimer => { - timer_arm!(WTFTimer, event_loop_timer, |c, now, vm| WTFTimer::fire( - c, &*now, vm - )) + let c: *mut WTFTimer = owner!(WTFTimer, event_loop_timer); + // SAFETY: `TimerOwner` contract — `c` is a live `WTFTimer`. The + // borrow ends before `fire`, which may destroy it. + let run_loop_timer = unsafe { &*c }.take_for_fire(); + crate::timer::wtf_timer::RunLoopTimer::fire(run_loop_timer); + Ok(()) } EventLoopTimerTag::AbortSignalTimeout => { timer_arm!(AbortSignalTimeout, event_loop_timer, |c, _now, vm| { @@ -987,12 +976,12 @@ pub(crate) unsafe fn __bun_fire_timer( ) } EventLoopTimerTag::DateHeaderTimer => { - timer_arm!(DateHeaderTimer, event_loop_timer, |c, _now, vm| (*c) - .run(&mut *vm)) + timer_arm!(DateHeaderTimer, event_loop_timer, |c, _now, _vm| (&*c) + .run(vm_ref, crate::jsc_hooks::timer_all())) } EventLoopTimerTag::EventLoopDelayMonitor => { - timer_arm!(EventLoopDelayMonitor, event_loop_timer, |c, now, vm| { - (*c).on_fire(&mut *vm, &*now) + timer_arm!(EventLoopDelayMonitor, event_loop_timer, |c, now, _vm| { + (&*c).on_fire(&*now, crate::jsc_hooks::timer_all()) }) } EventLoopTimerTag::StatWatcherScheduler => { @@ -1045,28 +1034,29 @@ pub(crate) unsafe fn __bun_fire_timer( EventLoopTimerTag::PostgresSQLConnectionTimeout => { // SAFETY: §Dispatch — tag set together with the container at // construction; `t` is the connection's `timer` field. - let container = unsafe { PostgresSQLConnection::from_timer_ptr(t) }; + let container = unsafe { PostgresSQLConnection::from_timer_ptr(t.as_ptr()) }; // SAFETY: per fn contract. unsafe { (*container).on_connection_timeout() }; Ok(()) } EventLoopTimerTag::PostgresSQLConnectionMaxLifetime => { // SAFETY: §Dispatch — `t` is the connection's `max_lifetime_timer`. - let container = unsafe { PostgresSQLConnection::from_max_lifetime_timer_ptr(t) }; + let container = + unsafe { PostgresSQLConnection::from_max_lifetime_timer_ptr(t.as_ptr()) }; // SAFETY: per fn contract. unsafe { (*container).on_max_lifetime_timeout() }; Ok(()) } EventLoopTimerTag::MySQLConnectionTimeout => { // SAFETY: §Dispatch — `t` is the connection's `timer` field. - let container = unsafe { MySQLConnection::from_timer_ptr(t) }; + let container = unsafe { MySQLConnection::from_timer_ptr(t.as_ptr()) }; // SAFETY: per fn contract. unsafe { (*container).on_connection_timeout() }; Ok(()) } EventLoopTimerTag::MySQLConnectionMaxLifetime => { // SAFETY: §Dispatch — `t` is the connection's `max_lifetime_timer`. - let container = unsafe { MySQLConnection::from_max_lifetime_timer_ptr(t) }; + let container = unsafe { MySQLConnection::from_max_lifetime_timer_ptr(t.as_ptr()) }; // SAFETY: per fn contract. unsafe { (*container).on_max_lifetime_timeout() }; Ok(()) @@ -1089,13 +1079,15 @@ pub(crate) unsafe fn __bun_fire_timer( // `sweep_weak_refs` takes the raw `*EventLoopTimer` and recovers // the store inside. // SAFETY: per fn contract. - SourceMapStore::sweep_weak_refs(t, unsafe { &*now }); + SourceMapStore::sweep_weak_refs(t.as_ptr(), unsafe { &*now }); Ok(()) } EventLoopTimerTag::DevServerMemoryVisualizerTick => { // SAFETY: per fn contract; `t` is the `memory_visualizer_timer` // field of a live DevServer. - DevServer::emit_memory_visualizer_message_timer(unsafe { &mut *t }, unsafe { &*now }); + DevServer::emit_memory_visualizer_message_timer(unsafe { &mut *t.as_ptr() }, unsafe { + &*now + }); Ok(()) } EventLoopTimerTag::BunTest => { @@ -1123,13 +1115,13 @@ pub(crate) unsafe fn __bun_fire_timer( nsec: (*now).nsec, } }; - BunTest::bun_test_timeout_callback(&strong, &now_core, VirtualMachine::get()); + BunTest::bun_test_timeout_callback(&strong, &now_core, vm_ref); Ok(()) } EventLoopTimerTag::CronJob => { let c: *mut CronJob = owner!(CronJob, event_loop_timer); // SAFETY: a scheduled job's JS wrapper keeps it alive; `t` was just popped. - CronJob::on_timer_fire(unsafe { bun_ptr::ThisPtr::new(c) }, VirtualMachine::get()); + CronJob::on_timer_fire(unsafe { bun_ptr::ThisPtr::new(c) }, vm_ref); Ok(()) } EventLoopTimerTag::QuicEndpoint => { @@ -1161,22 +1153,127 @@ pub(crate) fn fold(result: JsResult<()>) { } /// `__bun_js_timer_epoch` body — the tag→`container_of` read for -/// [`EventLoopTimer::js_timer_epoch`]. Returns `internals.flags.epoch` for -/// the three JS-timer container types, else `None`. Sits on the heap-compare -/// hot path +/// [`EventLoopTimer::js_timer_epoch`]. Returns `flags.epoch` for the three +/// JS-timer container types, else `None`. Sits on the heap-compare hot path /// (`EventLoopTimer::less` → `TimerHeap` meld). /// /// # Safety -/// `t` points at a live [`EventLoopTimer`] currently linked into a `TimerHeap`. +/// `t` points at a live [`EventLoopTimer`] slot of a `TimerOwner`. #[unsafe(no_mangle)] pub(crate) unsafe fn __bun_js_timer_epoch( _tag: EventLoopTimerTag, t: *const EventLoopTimer, ) -> Option { - // SAFETY: per fn contract — `t` is live in a `TimerHeap`. `_tag` kept for - // the `extern "Rust"` ABI in `bun_event_loop`; helper re-reads `(*t).tag` - // (same address the caller loaded it from — folds under LTO). - unsafe { crate::timer::js_timer_flags_ptr(t).map(|p| (*p.as_ptr()).epoch()) } + // SAFETY: per fn contract. `_tag` kept for the `extern "Rust"` ABI in + // `bun_event_loop`; `js_timer_epoch` re-reads `(*t).tag` (same address the + // caller loaded it from — folds under LTO). + js_timer_epoch(unsafe { TimerRef::from_raw(t.cast_mut()) }) +} + +/// `flags.epoch` of the JS timer that owns `t` (`TimeoutObject` / +/// `ImmediateObject` / `AbortSignalTimeout`), else `None`. +#[inline] +pub(crate) fn js_timer_epoch(t: TimerRef) -> Option { + use crate::timer::{ImmediateObject, TimeoutObject}; + match t.tag() { + EventLoopTimerTag::TimeoutObject => Some( + timer_owner_this!(TimeoutObject, event_loop_timer, t) + .internals + .flags + .get() + .epoch(), + ), + EventLoopTimerTag::ImmediateObject => Some( + timer_owner_this!(ImmediateObject, event_loop_timer, t) + .internals + .flags + .get() + .epoch(), + ), + EventLoopTimerTag::AbortSignalTimeout => { + // SAFETY: `TimerOwner` contract — `t` is the `event_loop_timer` slot + // of a live boxed `abort_signal::Timeout`. + Some(unsafe { + (*AbortSignalTimeout::from_timer_ptr(t.as_ptr())) + .flags + .epoch() + }) + } + _ => None, + } +} + +/// Stamp `epoch` into the JS timer that owns `t` so equal-deadline JS timers +/// fire in scheduling order. `false` for every other tag. +#[inline] +pub(crate) fn set_js_timer_epoch(t: TimerRef, epoch: u32) -> bool { + use crate::timer::{ImmediateObject, TimeoutObject}; + let flags = match t.tag() { + EventLoopTimerTag::TimeoutObject => { + &timer_owner_this!(TimeoutObject, event_loop_timer, t) + .internals + .flags + } + EventLoopTimerTag::ImmediateObject => { + &timer_owner_this!(ImmediateObject, event_loop_timer, t) + .internals + .flags + } + EventLoopTimerTag::AbortSignalTimeout => { + // SAFETY: `TimerOwner` contract — `t` is the `event_loop_timer` slot + // of a live boxed `abort_signal::Timeout`; JS thread, no other + // borrow of the box is live while `All` (re)links it. + unsafe { + (*AbortSignalTimeout::from_timer_ptr(t.as_ptr())) + .flags + .set_epoch(epoch) + }; + return true; + } + _ => return false, + }; + let mut f = flags.get(); + f.set_epoch(epoch); + flags.set(f); + true +} + +/// Cancel a JS-program-scheduled timer (the +/// [`EventLoopTimerTag::allow_fake_timers`] set plus `ImmediateObject`) on its +/// owner's behalf — VM teardown / `--isolate` swap +/// (`All::cancel_all_timeout_objects`) or the fake clock's `clear` — so the +/// owner releases what the heap entry pinned. `t` may still be linked or +/// already popped. May free the owner; `t` is dead once this returns. +pub(crate) fn cancel_js_timer(t: TimerRef, _vm: &VirtualMachine) { + use crate::timer::{ImmediateObject, TimeoutObject, TimerObject}; + match t.tag() { + EventLoopTimerTag::TimeoutObject => { + TimerObject::release_heap_entry(timer_owner_this!(TimeoutObject, event_loop_timer, t)); + } + EventLoopTimerTag::ImmediateObject => { + TimerObject::cancel(timer_owner_this!(ImmediateObject, event_loop_timer, t)); + } + // `AbortSignal.timeout()` boxes are owned by the C++ `AbortSignal`, so + // each one is handed back to its signal, which unlinks and frees it and + // clears `m_timeout`. Only unlinking the node would leave every + // observed signal's wrapper (under `--isolate`: the retired global its + // listeners close over) pinned by `isReachableFromOpaqueRoots` for the + // rest of the process; see `Timeout::discard`. + EventLoopTimerTag::AbortSignalTimeout => { + // SAFETY: `TimerOwner` contract — `t` is the slot of a live boxed + // `abort_signal::Timeout` still owned by its signal; JS thread; no + // borrow of `All` is held here (`discard` re-enters `All::remove`). + unsafe { AbortSignalTimeout::discard(AbortSignalTimeout::from_timer_ptr(t.as_ptr())) }; + } + EventLoopTimerTag::CronJob => { + CronJob::stop_dropped_from_fake_heap(timer_owner_this!(CronJob, event_loop_timer, t)); + } + tag => debug_assert!( + false, + "{} timer has no release path", + <&'static str>::from(tag), + ), + } } /// `__bun_tick_queue_with_count` body — declared `extern "Rust"` in diff --git a/src/runtime/dns_jsc/dns.rs b/src/runtime/dns_jsc/dns.rs index ca3d6c4289de..11141d13cd23 100644 --- a/src/runtime/dns_jsc/dns.rs +++ b/src/runtime/dns_jsc/dns.rs @@ -3918,18 +3918,14 @@ impl Resolver { sec: now.sec, nsec: now.nsec, }; - let uws_loop = vm.uws_loop(); + let _ = vm; // R-2: `&self` carries no `noalias`, and every field touched below is // UnsafeCell-backed, so the re-entrant `ares_process_fd` callbacks // (`request_completed`, `drain_pending_*`) may freely re-derive // `&Resolver` from their stored ctx without aliasing UB. let deref_this = self.as_ctx_ptr(); scopeguard::defer! { - // jsc/runtime crate cycle: low-tier `VirtualMachine.timer` is `()`; - // resolve via the high-tier `RuntimeState` hook. - let state = crate::jsc_hooks::runtime_state(); - // SAFETY: `state` is the boxed per-thread `RuntimeState`; single-threaded JS heap. - unsafe { (*state).timer.increment_timer_ref(-1, uws_loop) }; + crate::jsc_hooks::timer_all().increment_timer_ref(-1); // SAFETY: `deref_this` is the heap allocation from `init`; releases // `add_timer`'s ref. May be the final release; nothing touches // `*self` after this point. @@ -4021,18 +4017,9 @@ impl Resolver { nsec: next.nsec, } }); - let uws_loop = self.vm().uws_loop(); - let state = crate::jsc_hooks::runtime_state(); - // SAFETY: `state` is the boxed per-thread `RuntimeState`; single-threaded JS heap. - unsafe { - (*state).timer.increment_timer_ref(1, uws_loop); - // whole-struct provenance: `from_field_ptr!` recovers the container on fire - (*state).timer.insert( - core::ptr::addr_of!(self.event_loop_timer) - .cast::() - .cast_mut(), - ); - } + let timers = crate::jsc_hooks::timer_all(); + timers.increment_timer_ref(1); + timers.insert(crate::timer::TimerRef::new(self, |r| &r.event_loop_timer)); true } @@ -4050,16 +4037,13 @@ impl Resolver { // global-resolver permanent pin), so this // `deref` cannot reach 0 while `&self` is live. unsafe { - let uws_loop = (*this).vm().uws_loop(); - let state = crate::jsc_hooks::runtime_state(); - (*state).timer.increment_timer_ref(-1, uws_loop); + crate::jsc_hooks::timer_all().increment_timer_ref(-1); Self::deref(this); } } - let state = crate::jsc_hooks::runtime_state(); - // SAFETY: `state` is the boxed per-thread `RuntimeState`; single-threaded JS heap. - unsafe { (*state).timer.remove(self.event_loop_timer.as_ptr()) }; + crate::jsc_hooks::timer_all() + .remove(crate::timer::TimerRef::new(self, |r| &r.event_loop_timer)); } // ───────────── pending-cache helpers ───────────── diff --git a/src/runtime/dns_jsc/dns_sd.rs b/src/runtime/dns_jsc/dns_sd.rs index 910d69d86b97..625db99e5d47 100644 --- a/src/runtime/dns_jsc/dns_sd.rs +++ b/src/runtime/dns_jsc/dns_sd.rs @@ -328,6 +328,8 @@ pub(crate) struct SharedConnection { early_out_armed_for: Cell, } +bun_event_loop::impl_timer_owner!(SharedConnection; from_early_out_timer_ptr => early_out_timer); + thread_local! { static SHARED: Cell<*mut SharedConnection> = const { Cell::new(ptr::null_mut()) }; } @@ -553,13 +555,10 @@ impl SharedConnection { } let now = bun::timespec::now(bun::TimespecMockMode::ForceRealTime); let next = now.add_ms((deadline - now.ms()).max(1)); - let state = crate::jsc_hooks::runtime_state(); - // SAFETY: this thread's live RuntimeState; the timer slot is valid until `destroy`. - unsafe { - (*state).timer.update( - core::ptr::addr_of!(self.early_out_timer) - .cast::() - .cast_mut(), + // The timer slot is valid until `destroy`. + { + crate::jsc_hooks::timer_all().update( + crate::timer::TimerRef::new(self, |c| &c.early_out_timer), &ElTimespec { sec: next.sec, nsec: next.nsec, @@ -605,12 +604,8 @@ impl SharedConnection { if conn.early_out_timer.get().state == EventLoopTimerState::ACTIVE && VirtualMachine::get_or_null().is_some() { - // SAFETY: this thread's live RuntimeState owns the timer heap. - unsafe { - (*crate::jsc_hooks::runtime_state()) - .timer - .remove(conn.early_out_timer.as_ptr()) - }; + crate::jsc_hooks::timer_all() + .remove(crate::timer::TimerRef::new(&*conn, |c| &c.early_out_timer)); } // SAFETY: `file_poll` is the live hive slot; `deinit` returns it. unsafe { (*conn.file_poll.as_ptr()).deinit() }; diff --git a/src/runtime/hw_exports.rs b/src/runtime/hw_exports.rs index 8773e10d9d11..49da84543d00 100644 --- a/src/runtime/hw_exports.rs +++ b/src/runtime/hw_exports.rs @@ -201,20 +201,26 @@ mod sql_hooks { unsafe { core::ptr::addr_of_mut!((*state).sql_rare) } } unsafe fn timer_heap(_vm: *mut VirtualMachine) -> *mut c_void { - crate::jsc_hooks::timer_all().cast() + core::ptr::from_ref(crate::jsc_hooks::timer_all()) + .cast_mut() + .cast() } unsafe fn timer_insert(heap: *mut c_void, timer: *mut EventLoopTimer) { // SAFETY: `heap` is `&runtime_state().timer` (live for the VM); `timer` - // is a live intrusive heap node owned by the caller. Route through - // `All::insert` (NOT the raw `.timers` field) so the fake-timers - // routing and the `(*timer).state` / `in_heap` bookkeeping happen. - unsafe { (*heap.cast::()).insert(timer) }; + // is the slot of a live `TimerOwner` (the SQL connection). Route + // through `All::insert` (NOT the raw `.timers` field) so the + // fake-timers routing and the `state` / `in_heap` bookkeeping happen. + unsafe { + (*heap.cast::()).insert(crate::timer::TimerRef::from_raw(timer)) + }; } unsafe fn timer_remove(heap: *mut c_void, timer: *mut EventLoopTimer) { // SAFETY: `heap` is `&runtime_state().timer`; `timer` was previously // inserted via `timer_insert`. Route through `All::remove` so // `in_heap` is consulted and reset. - unsafe { (*heap.cast::()).remove(timer) }; + unsafe { + (*heap.cast::()).remove(crate::timer::TimerRef::from_raw(timer)) + }; } unsafe fn ssl_ctx_cache(_vm: *mut VirtualMachine) -> *mut c_void { let state = crate::jsc_hooks::runtime_state(); diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index 029b356845de..f00d4707e631 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -158,41 +158,34 @@ pub(crate) fn runtime_state() -> *mut RuntimeState { RUNTIME_STATE.with(Cell::get) } -/// Recover this thread's `timer::All` heap as a raw pointer. +/// This thread's `timer::All`, or `None` before [`init_runtime_state`] has run +/// (e.g. `bun_jsc` unit tests with no high tier, or `Bun__Timer__getNextID` +/// racing init). /// -/// Note: `bun_jsc::VirtualMachine.timer` is a `()` placeholder; -/// the real `All` lives in [`RuntimeState::timer`] until that slot widens. -/// Null only before [`init_runtime_state`] has run (e.g. `bun_jsc` unit tests -/// with no high tier, or `Bun__Timer__getNextID` racing init). -/// -/// Returns `*mut` (NOT `&mut`) so callers that are themselves fields of `All` -/// (`DateHeaderTimer`, `EventLoopDelayMonitor`, `FakeTimers`) can dereference -/// per-field under `// SAFETY:` without forming an aliased `&mut All` while -/// `&mut self` is live (raw-ptr-per-field re-entry pattern, see `auto_tick`). +/// Note: `bun_jsc::VirtualMachine.timer` is a `()` placeholder; the real `All` +/// lives in [`RuntimeState::timer`] until that slot widens. `All` is +/// `&self`-only (interior mutability), so the shared borrow is sound across +/// the JS re-entry its own methods perform. #[inline] -pub(crate) fn timer_all() -> *mut timer::All { +pub(crate) fn timer_all_opt() -> Option<&'static timer::All> { let state = runtime_state(); if state.is_null() { - return ptr::null_mut(); + return None; } // SAFETY: `state` is the live boxed `RuntimeState` for this thread; - // `timer` is an embedded field at a stable address for the VM lifetime. - unsafe { ptr::addr_of_mut!((*state).timer) } + // `timer` is an embedded field at a stable address for the VM lifetime and + // is never borrowed `&mut`. + Some(unsafe { &*ptr::addr_of!((*state).timer) }) } -/// [`timer_all`] but `&'static mut` — only valid once `RuntimeState` is -/// installed (true for every JS host-call entry point) and only for callers -/// that are NOT themselves fields of `All` (`Subprocess`, `DevServer`, -/// `cron`, sockets). Single JS thread + boxed-for-process-lifetime ⇒ the -/// borrow is sound; callers must not hold it across a JS re-entry that could -/// itself call this (every use is single-expression). +/// [`timer_all_opt`] for callers that run only once `RuntimeState` is +/// installed (every JS host-call entry point and event-loop tick). #[inline] -pub(crate) fn timer_all_mut() -> &'static mut timer::All { +pub(crate) fn timer_all() -> &'static timer::All { let state = runtime_state(); debug_assert!(!state.is_null(), "RuntimeState not installed"); - // SAFETY: `runtime_state()` is non-null after `bun_runtime::init()`; - // single JS thread so no concurrent `&mut`. - unsafe { &mut (*state).timer } + // SAFETY: see `timer_all_opt`; non-null once `init_runtime_state` has run. + unsafe { &*ptr::addr_of!((*state).timer) } } #[inline] @@ -1003,16 +996,8 @@ unsafe fn auto_tick(vm: *mut VirtualMachine) { // ── DateHeaderTimer / imminent-GC ─────────────────────────────────── let state = runtime_state(); if !state.is_null() { - // SAFETY: `state` is the live per-thread `RuntimeState`; `loop_` is the - // live per-thread uws loop; `vm` per fn contract. The re-entrant - // `All::insert`/`update` inside `DateHeaderTimer::enable` touches only - // fields disjoint from `date_header_timer` (raw-ptr-per-field pattern, - // same as `Bun__internal_ensureDateHeaderTimerIsEnabled`). - unsafe { - (*state) - .timer - .update_date_header_timer_if_necessary(&*loop_, vm) - }; + // SAFETY: `loop_` is the live per-thread uws loop; `vm` per fn contract. + timer_all().update_date_header_timer_if_necessary(unsafe { &*loop_ }, unsafe { &*vm }); } // SAFETY: `el` is the live per-thread event loop. unsafe { (*el).run_imminent_gc_timer() }; @@ -1063,31 +1048,19 @@ unsafe fn auto_tick(vm: *mut VirtualMachine) { // timer armed after the poll deadline is computed is not in that deadline. // 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()`. + // `get_timeout` may fire a `WTFTimer`. // `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 = None; - // SAFETY: `state` is the live per-thread `RuntimeState`; the - // `timer` field address is stable for the VM lifetime. - let have_timeout = unsafe { - timer::All::get_timeout( - &mut (*state).timer, - &mut timespec, - has_pending_immediate, - quic_next_tick_us, - vm.cast(), - &mut now, - ) - }; + let have_timeout = timer_all().get_timeout( + &mut timespec, + has_pending_immediate, + quic_next_tick_us, + // SAFETY: per fn contract. + unsafe { &*vm }, + &mut now, + ); let now_ns = now.map_or(bun_uws::NOW_NS_UNKNOWN, |t| t.ns()); // SAFETY: `loop_` is the live per-thread uws loop. unsafe { @@ -1100,19 +1073,9 @@ unsafe fn auto_tick(vm: *mut VirtualMachine) { } } + // SAFETY: per fn contract. #[cfg(unix)] - { - // 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()) }; - } - #[cfg(not(unix))] - let _ = state; + timer_all().drain_timers(unsafe { &*vm }); // SAFETY: per fn contract. unsafe { (*vm).on_after_event_loop() }; @@ -1163,11 +1126,7 @@ unsafe fn auto_tick_active(vm: *mut VirtualMachine) { let state = runtime_state(); if !state.is_null() { // SAFETY: see the matching call in `auto_tick` above. - unsafe { - (*state) - .timer - .update_date_header_timer_if_necessary(&*loop_, vm) - }; + timer_all().update_date_header_timer_if_necessary(unsafe { &*loop_ }, unsafe { &*vm }); } if state.is_null() { @@ -1203,18 +1162,14 @@ 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 = None; - // SAFETY: `state` is the live per-thread `RuntimeState`; see - // Note on `auto_tick` re: aliased-&mut across `fire()`. - let have_timeout = unsafe { - timer::All::get_timeout( - &mut (*state).timer, - &mut timespec, - has_pending_immediate, - quic_next_tick_us, - vm.cast(), - &mut now, - ) - }; + let have_timeout = timer_all().get_timeout( + &mut timespec, + has_pending_immediate, + quic_next_tick_us, + // SAFETY: per fn contract. + unsafe { &*vm }, + &mut now, + ); let now_ns = now.map_or(bun_uws::NOW_NS_UNKNOWN, |t| t.ns()); // SAFETY: `loop_` is the live per-thread uws loop. unsafe { @@ -1227,12 +1182,9 @@ unsafe fn auto_tick_active(vm: *mut VirtualMachine) { } } + // SAFETY: per fn contract. #[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()) }; - } + timer_all().drain_timers(unsafe { &*vm }); #[cfg(not(unix))] let _ = state; @@ -1295,9 +1247,9 @@ unsafe fn timer_insert( // SAFETY: per fn contract. let state = unsafe { runtime_state_of(vm) }; debug_assert!(!state.is_null(), "timer_insert before init_runtime_state"); - // SAFETY: this leaf hook runs no JS, so a short-lived `&mut RuntimeState` - // does not alias anything. `Timer::All::insert` re-derefs `t` per-field. - unsafe { &mut (*state).timer }.insert(t); + // SAFETY: `state` is live (leaf hook, JS thread); `t` per fn contract is + // the slot of a live `TimerOwner`. + unsafe { (*state).timer.insert(timer::TimerRef::from_raw(t)) }; } /// `vm.timer.remove(timer)` — counterpart to [`timer_insert`]. @@ -1311,8 +1263,8 @@ unsafe fn timer_remove( // SAFETY: per fn contract. let state = unsafe { runtime_state_of(vm) }; debug_assert!(!state.is_null(), "timer_remove before init_runtime_state"); - // SAFETY: see `timer_insert` — leaf hook, short-lived `&mut RuntimeState`. - unsafe { &mut (*state).timer }.remove(t); + // SAFETY: see `timer_insert`. + unsafe { (*state).timer.remove(timer::TimerRef::from_raw(t)) }; } /// `Node.fs.NodeFS{ .vm = … }` lazy creation. @@ -1643,11 +1595,8 @@ unsafe fn cancel_all_timers(vm: *mut VirtualMachine) { unsafe { crate::node::node_fs_stat_watcher::StatWatcherScheduler::shutdown_for_exit(vm); } - // SAFETY: `state` is the live boxed per-thread `RuntimeState`; `vm` per fn - // contract. `addr_of_mut!` does not materialize a `&mut RuntimeState`. - unsafe { - crate::timer::All::cancel_all_timeout_objects(ptr::addr_of_mut!((*state).timer), vm); - } + // SAFETY: `vm` per fn contract. + timer_all().cancel_all_timeout_objects(unsafe { &*vm }); } /// `RuntimeHooks::close_timer_loop_handles_after_vm_destroyed`: teardown-only companion of @@ -1657,12 +1606,7 @@ unsafe fn cancel_all_timers(vm: *mut VirtualMachine) { /// `runtime_state()` is installed; JS thread; the JSC VM is already destroyed. unsafe fn close_timer_loop_handles_after_vm_destroyed(_vm: *mut VirtualMachine) { #[cfg(windows)] - { - let state = runtime_state(); - debug_assert!(!state.is_null()); - // SAFETY: live boxed per-thread RuntimeState (fn contract). - unsafe { (*state).timer.close_loop_handles_for_vm_teardown() }; - } + timer_all().close_loop_handles_for_vm_teardown(); } /// `RuntimeHooks::stop_active_handles_for_vm_teardown` — see [`stop_active_handles_for_vm_teardown`]. @@ -1676,12 +1620,9 @@ unsafe fn stop_active_handles_for_vm_teardown_hook(vm: *mut VirtualMachine) -> S /// `RuntimeHooks::disarm_all_timers_for_vm_teardown`. unsafe fn disarm_all_timers_for_vm_teardown(_vm: *mut VirtualMachine) { - let all = timer_all(); - if all.is_null() { - return; + if let Some(all) = timer_all_opt() { + all.disarm_all_for_vm_teardown(); } - // SAFETY: live per-thread `All`; JS thread; teardown has forbidden script. - unsafe { crate::timer::All::disarm_all_for_vm_teardown(all) }; } /// `RuntimeHooks::stop_dns_for_vm_teardown` — destroy the per-VM global DNS @@ -1746,19 +1687,10 @@ fn stop_active_handles(vm: &mut VirtualMachine, reason: StopReason) -> SweepResu // touch the outgoing signals. { let all = timer_all(); - // SAFETY: `state` is non-null so `timer_all()` is non-null; single - // JS thread, no re-entry while we hold the field borrow. - if !all.is_null() && unsafe { (*all).fake_timers.is_active() } { - let global = vm.global(); - // SAFETY: as above; only touches `fake_timers.active` and the - // `CURRENT_TIME` static. - unsafe { (*all).fake_timers.reset_for_isolation(global) }; - } - if !all.is_null() { - // SAFETY: as above; `disable` borrows only `event_loop_delay` and - // reaches the heap through `timer_all()` (disjoint-field access). - unsafe { (*all).event_loop_delay.disable() }; + if all.fake_timers.is_active() { + all.fake_timers.reset_for_isolation(vm.global()); } + all.event_loop_delay.disable(all); } // Entries that stay registered across a test-isolation swap. let mut kept: Vec = Vec::new(); diff --git a/src/runtime/node/node_fs_stat_watcher.rs b/src/runtime/node/node_fs_stat_watcher.rs index ccc56e4790e4..9e5e11ce8b32 100644 --- a/src/runtime/node/node_fs_stat_watcher.rs +++ b/src/runtime/node/node_fs_stat_watcher.rs @@ -191,19 +191,20 @@ impl StatWatcherScheduler { // the per-thread `runtime_state()` (single JS thread; see jsc_hooks.rs). // SAFETY: main-thread-only per fn contract; `runtime_state()` is non-null // after `bun_runtime::init()`. Raw-ptr-per-field re-entry pattern. - let timer_all = unsafe { &mut (*crate::jsc_hooks::runtime_state()).timer }; + let timer_all = crate::jsc_hooks::timer_all(); // SAFETY: `this` is live: a scheduler ref is held for the whole call by // every caller (`set_interval`'s caller, `timer_callback`'s `&mut self`, // `shutdown_for_exit`'s `RareData` ref) or, for `StatWatcherTimerUpdate` // (whose `scheduler` is a non-owning `ParentRef`), by the watcher's // `RefPtr` held across the hop. - let elt = unsafe { core::ptr::addr_of_mut!((*this).event_loop_timer) }; + let elt = unsafe { + crate::timer::TimerRef::from_raw(core::ptr::addr_of_mut!((*this).event_loop_timer)) + }; // if the interval is 0 means that we stop the timer if interval == 0 { // if the timer is active we need to remove it - // SAFETY: `elt` is the live embedded EventLoopTimer. - if unsafe { (*elt).state } == EventLoopTimerState::ACTIVE { + if elt.state() == EventLoopTimerState::ACTIVE { timer_all.remove(elt); } return; @@ -233,7 +234,6 @@ impl StatWatcherScheduler { || self.vm().script_execution_status() != jsc::ScriptExecutionStatus::Running; self.event_loop_timer.state = EventLoopTimerState::FIRED; - self.event_loop_timer.heap = Default::default(); if has_been_cleared || self.is_shutdown.load(Ordering::Relaxed) { return; diff --git a/src/runtime/node/quic/endpoint.rs b/src/runtime/node/quic/endpoint.rs index 1ab19fc195d1..a8a97d23eba8 100644 --- a/src/runtime/node/quic/endpoint.rs +++ b/src/runtime/node/quic/endpoint.rs @@ -14,7 +14,7 @@ use bun_jsc::{ use bun_lsquic_sys as lsquic; use bun_uws as uws; -use crate::jsc_hooks::timer_all_mut as timer_all; +use crate::jsc_hooks::timer_all; use crate::timer::{EventLoopTimer, EventLoopTimerState, EventLoopTimerTag}; use super::callbacks; @@ -1582,9 +1582,7 @@ impl QuicEndpoint { self.mark_driver_pending(); let next = bun_core::Timespec::ms_from_now(bun_core::TimespecMockMode::ForceRealTime, 1); timer_all().update( - core::ptr::addr_of!(self.event_loop_timer) - .cast::() - .cast_mut(), + crate::timer::TimerRef::new(self, |e| &e.event_loop_timer), &next, ); } @@ -1625,9 +1623,7 @@ impl QuicEndpoint { ms as i64, ); timer_all().update( - core::ptr::addr_of!(self.event_loop_timer) - .cast::() - .cast_mut(), + crate::timer::TimerRef::new(self, |e| &e.event_loop_timer), &next, ); } @@ -2077,7 +2073,7 @@ impl QuicEndpoint { // Unlink first: the driver walk must never reach a freed endpoint. self.unlink_loop_driver(); if self.event_loop_timer.get().state == EventLoopTimerState::ACTIVE { - timer_all().remove(self.event_loop_timer.as_ptr()); + timer_all().remove(crate::timer::TimerRef::new(self, |e| &e.event_loop_timer)); } for engine in [ self.server_engine.replace(null_mut()), @@ -2126,9 +2122,7 @@ impl QuicEndpoint { self.pending_endpoint_close.set(true); let next = bun_core::Timespec::ms_from_now(bun_core::TimespecMockMode::ForceRealTime, 1); timer_all().update( - core::ptr::addr_of!(self.event_loop_timer) - .cast::() - .cast_mut(), + crate::timer::TimerRef::new(self, |e| &e.event_loop_timer), &next, ); } @@ -2652,7 +2646,7 @@ impl QuicEndpoint { // Remove the timer before the backing storage drops, or a later heap // operation dereferences the freed node. if self.event_loop_timer.get().state == EventLoopTimerState::ACTIVE { - timer_all().remove(self.event_loop_timer.as_ptr()); + timer_all().remove(crate::timer::TimerRef::new(&*self, |e| &e.event_loop_timer)); } self.release_native(); } diff --git a/src/runtime/socket/UpgradedDuplex.rs b/src/runtime/socket/UpgradedDuplex.rs index 89bfb8456e08..ea27d28d282c 100644 --- a/src/runtime/socket/UpgradedDuplex.rs +++ b/src/runtime/socket/UpgradedDuplex.rs @@ -108,7 +108,7 @@ pub struct Handlers { pub(crate) on_keylog: fn(*mut (), &[u8]), } -use crate::jsc_hooks::timer_all_mut as timer_all; +use crate::jsc_hooks::timer_all; /// Lazily create-and-cache a JS host-function callback in `shadow`, mirrored /// into the owning `JSTLSSocket` wrapper's visited `values:` slot (the GC @@ -359,10 +359,8 @@ impl UpgradedDuplex { vm.script_execution_status() != bun_jsc::ScriptExecutionStatus::Running }); - self.event_loop_timer.with_mut(|t| { - t.state = EventLoopTimerState::FIRED; - t.heap = Default::default(); - }); + self.event_loop_timer + .with_mut(|t| t.state = EventLoopTimerState::FIRED); if has_been_cleared { return; @@ -589,7 +587,7 @@ impl UpgradedDuplex { fn set_timeout_in_milliseconds(&self, ms: c_uint) { if self.event_loop_timer.get().state == EventLoopTimerState::ACTIVE { - timer_all().remove(self.event_loop_timer.as_ptr()); + timer_all().remove(crate::timer::TimerRef::new(self, |d| &d.event_loop_timer)); } self.current_timeout.set(ms); @@ -609,11 +607,7 @@ impl UpgradedDuplex { nsec: next.nsec, }; }); - timer_all().insert( - core::ptr::addr_of!(self.event_loop_timer) - .cast::() - .cast_mut(), - ); + timer_all().insert(crate::timer::TimerRef::new(self, |d| &d.event_loop_timer)); } #[uws_callback(export = "UpgradedDuplex__set_timeout")] diff --git a/src/runtime/socket/WindowsNamedPipe.rs b/src/runtime/socket/WindowsNamedPipe.rs index 427bd39f026e..b0fe1db9265f 100644 --- a/src/runtime/socket/WindowsNamedPipe.rs +++ b/src/runtime/socket/WindowsNamedPipe.rs @@ -53,7 +53,7 @@ pub type CertError = crate::socket::upgraded_duplex::CertError; type WrapperType = SSLWrapper<*mut WindowsNamedPipe>; -use crate::jsc_hooks::timer_all_mut as timer_all; +use crate::jsc_hooks::timer_all; pub struct WindowsNamedPipe { pub(crate) wrapper: JsCell>, @@ -538,10 +538,8 @@ impl WindowsNamedPipe { let has_been_cleared = self.event_loop_timer.get().state == EventLoopTimerState::CANCELLED || self.vm.script_execution_status() != bun_jsc::ScriptExecutionStatus::Running; - self.event_loop_timer.with_mut(|t| { - t.state = EventLoopTimerState::FIRED; - t.heap = Default::default(); - }); + self.event_loop_timer + .with_mut(|t| t.state = EventLoopTimerState::FIRED); if has_been_cleared { return; @@ -1032,7 +1030,7 @@ impl WindowsNamedPipe { pub(crate) fn set_timeout_in_milliseconds(&self, ms: c_uint) { if self.event_loop_timer.get().state == EventLoopTimerState::ACTIVE { - timer_all().remove(self.event_loop_timer.as_ptr()); + timer_all().remove(crate::timer::TimerRef::new(self, |p| &p.event_loop_timer)); } self.current_timeout.set(ms); @@ -1051,11 +1049,7 @@ impl WindowsNamedPipe { nsec: next.nsec, }; }); - timer_all().insert( - core::ptr::addr_of!(self.event_loop_timer) - .cast::() - .cast_mut(), - ); + timer_all().insert(crate::timer::TimerRef::new(self, |p| &p.event_loop_timer)); } #[bun_uws::uws_callback(export = "WindowsNamedPipe__set_timeout")] diff --git a/src/runtime/test_runner/bun_test.rs b/src/runtime/test_runner/bun_test.rs index 98f2ae78561b..51b073d365d0 100644 --- a/src/runtime/test_runner/bun_test.rs +++ b/src/runtime/test_runner/bun_test.rs @@ -27,10 +27,8 @@ pub(crate) use group_begin; /// Recover this thread's `timer::All` heap (jsc/runtime crate cycle: `vm.timer` is `()` in /// the low-tier `VirtualMachine`; the real value lives in `RuntimeState`). #[inline] -pub(super) fn vm_timer<'a>() -> &'a mut crate::timer::All { - // SAFETY: `runtime_state()` is non-null after `bun_runtime::init()`; - // single JS thread, raw-ptr-per-field re-entry pattern (jsc_hooks.rs). - unsafe { &mut (*crate::jsc_hooks::runtime_state()).timer } +pub(super) fn vm_timer() -> &'static crate::timer::All { + crate::jsc_hooks::timer_all() } /// `bun.timespec.orderIgnoreEpoch` — epoch == "no timeout", treated as +∞. @@ -982,14 +980,14 @@ impl BunTest { bun_core::scoped_log!(bun_test_group, "-> setting timer to {:?}", min_timeout); if self.timer.next != ElTimespec::EPOCH { bun_core::scoped_log!(bun_test_group, "-> removing existing timer"); - vm_timer().remove(&raw mut self.timer); + vm_timer().remove(crate::timer::TimerRef::from_mut(self, |t| &mut t.timer)); } // `EventLoopTimer.next` uses the event-loop crate's local // `Timespec` (distinct from `bun_core::Timespec`); convert by field. self.timer.next = ElTimespec { sec: min_timeout.sec, nsec: min_timeout.nsec }; if self.timer.next != ElTimespec::EPOCH { bun_core::scoped_log!(bun_test_group, "-> inserting timer"); - vm_timer().insert(&raw mut self.timer); + vm_timer().insert(crate::timer::TimerRef::from_mut(self, |t| &mut t.timer)); if debug::group::get_log_enabled() { let duration = min_timeout.since_now_force_real_time(); bun_core::scoped_log!(bun_test_group, "-> timer duration: {}", duration); @@ -1359,7 +1357,7 @@ impl Drop for BunTest { if self.timer.state == EventLoopTimerState::ACTIVE { // must remove an active timer to prevent UAF (if the timer were to trigger after BunTest deinit) - vm_timer().remove(&raw mut self.timer); + vm_timer().remove(crate::timer::TimerRef::from_mut(self, |t| &mut t.timer)); } for entry in self.extra_execution_entries.drain(..) { diff --git a/src/runtime/test_runner/jest.rs b/src/runtime/test_runner/jest.rs index d0ddb7033f31..a141b0ed65df 100644 --- a/src/runtime/test_runner/jest.rs +++ b/src/runtime/test_runner/jest.rs @@ -206,7 +206,7 @@ impl<'a> TestRunner<'a> { return; } let _ = vm; - bun_test::vm_timer().remove(&raw mut active_file.timer); + bun_test::vm_timer().remove(crate::timer::TimerRef::from_mut(active_file, |t| &mut t.timer)); } diff --git a/src/runtime/test_runner/timers/FakeTimers.rs b/src/runtime/test_runner/timers/FakeTimers.rs index 9604d487b296..e684ebfd464d 100644 --- a/src/runtime/test_runner/timers/FakeTimers.rs +++ b/src/runtime/test_runner/timers/FakeTimers.rs @@ -4,13 +4,11 @@ use bun_threading::RwLock; use bun_core::Environment; use bun_core::Timespec; +use core::cell::Cell; + use bun_jsc::{CallFrame, JSFunction, JSGlobalObject, JSHostFn, JSValue, JsResult}; -use crate::api::cron::CronJob; use crate::jsc::virtual_machine::VirtualMachine; -use crate::timer::{ - AbortSignalTimeout, ElTimespec, EventLoopTimer, EventLoopTimerState, EventLoopTimerTag, - InHeap, TimerObjectInternals, TimeoutObject, TimerHeap, -}; +use crate::timer::{EventLoopTimerState, InHeap, TimerHeap, TimerRef}; // JSMock C++ bindings (fake timers are only used by bun:test, so these stay local). unsafe extern "C" { @@ -18,9 +16,8 @@ unsafe extern "C" { safe fn JSMock__getCurrentUnixTimeMs() -> f64; } -#[derive(Default)] pub struct FakeTimers { - active: bool, + active: Cell, /// The sorted fake timers. TimerHeap is not optimal here because we need these operations: /// - peek/takeFirst (provided by TimerHeap) /// - peekLast (cannot be implemented efficiently with TimerHeap) @@ -28,6 +25,15 @@ pub struct FakeTimers { pub(crate) timers: TimerHeap, } +impl Default for FakeTimers { + fn default() -> Self { + Self { + active: Cell::new(false), + timers: TimerHeap::new(InHeap::Fake), + } + } +} + // `date_now_offset` is stored as `AtomicU64` (f64 bits) so the static is `Sync` // without `static mut`. pub(crate) struct CurrentTime { @@ -122,67 +128,38 @@ extern "C" fn Bun__FakeTimers__setSystemTime(global: &JSGlobalObject, ms: f64) { use crate::jsc_hooks::timer_all; -#[inline] -fn from_el_timespec(t: &ElTimespec) -> Timespec { - Timespec { sec: t.sec, nsec: t.nsec } -} - -/// Owners of the nodes [`FakeTimers::clear`] popped, still to be told their -/// timer is gone. Released only once the `FakeTimers` borrow has ended: these -/// paths re-enter `timer::All` (`TimerObjectInternals::cancel` → `All::remove`, -/// `Timeout` deinit → `timer_remove`). +/// Owners of the slots [`FakeTimers::clear`] popped, still to be told their +/// timer is gone: marking `state = CANCELLED` alone would strand a +/// `TimeoutObject` (the heap's ref and its `this_value` Strong pin each +/// other), leave an `AbortSignal.timeout()` box as its signal's `m_timeout` +/// (pinning an observed signal's wrapper), and leave a `Bun.cron()` job keeping +/// the event loop alive. Releasing re-enters `timer::All`. #[derive(Default)] #[must_use] -struct ClearedTimers { - /// Marking `state = CANCELLED` alone strands the `Box`: its - /// refcount sticks at 2 (wrapper +1 from `init_with`, heap +1 from - /// `reschedule`) and `internals.this_value` still GC-roots the wrapper, so - /// neither side ever frees. - pinned: Vec>, - /// Likewise, an unlinked `AbortSignal.timeout()` timer is still its - /// signal's `m_timeout`, and `JSAbortSignalOwner::isReachableFromOpaqueRoots` - /// pins an observed signal's wrapper for as long as that is set. Only the - /// signal's `cancelTimer()` clears it (and frees the box). - signal_timeouts: Vec<*mut AbortSignalTimeout>, - /// A `Bun.cron()` job keeps the event loop alive until it is stopped. - cron_jobs: Vec<*mut CronJob>, -} +struct ClearedTimers(Vec); impl ClearedTimers { - fn release(self, vm: *mut VirtualMachine) { - for p in self.pinned { - TimerObjectInternals::release_heap_pin(p, vm); - } - for t in self.signal_timeouts { - // SAFETY: `clear` popped `t` from the fake heap, so its box is - // still owned by a live signal; JS thread; the `FakeTimers` borrow - // ended before this call. `t` is freed by the call. - unsafe { AbortSignalTimeout::discard(t) }; - } - for job in self.cron_jobs { - // SAFETY: `clear` popped `job`'s node from the fake heap, so the - // job was scheduled and its JS wrapper (strong while scheduled) - // keeps it alive; no JS has run since; the `FakeTimers` borrow - // ended before this call. - CronJob::stop_dropped_from_fake_heap(unsafe { bun_ptr::ThisPtr::new(job) }); + fn release(self, vm: &VirtualMachine) { + for t in self.0 { + crate::dispatch::cancel_js_timer(t, vm); } } } impl FakeTimers { pub(crate) fn is_active(&self) -> bool { - self.active + self.active.get() } - fn activate(&mut self, js_now: f64, global: &JSGlobalObject) { - self.active = true; + fn activate(&self, js_now: f64, global: &JSGlobalObject) { + self.active.set(true); CURRENT_TIME.set(global, &Timespec::EPOCH, Some(js_now)); } - fn deactivate(&mut self, global: &JSGlobalObject) -> ClearedTimers { + fn deactivate(&self, global: &JSGlobalObject) -> ClearedTimers { let cleared = self.clear(); CURRENT_TIME.clear(global); - self.active = false; + self.active.set(false); cleared } @@ -191,54 +168,31 @@ impl FakeTimers { /// `cancel_all_timeout_objects` (which runs after the outgoing global's /// JS has stopped) can walk the still-populated fake heap and release /// `TimeoutObject` pins and discard `AbortSignalTimeout` timers. - pub(crate) fn reset_for_isolation(&mut self, global: &JSGlobalObject) { + pub(crate) fn reset_for_isolation(&self, global: &JSGlobalObject) { CURRENT_TIME.clear(global); - self.active = false; + self.active.set(false); } /// Pop every fake timer. Popping only unlinks the nodes; the owners that /// need to hear about it are returned for the caller to release. - fn clear(&mut self) -> ClearedTimers { + fn clear(&self) -> ClearedTimers { let mut cleared = ClearedTimers::default(); while let Some(timer) = self.timers.delete_min() { - // SAFETY: `delete_min` returned a live node; the owner it belongs - // to stays live until the caller's release pass. - unsafe { - (*timer).in_heap = InHeap::None; - (*timer).state = EventLoopTimerState::CANCELLED; - match (*timer).tag { - EventLoopTimerTag::TimeoutObject => { - let parent = TimeoutObject::from_timer_ptr(timer); - cleared.pinned.push(core::ptr::NonNull::new_unchecked( - core::ptr::addr_of_mut!((*parent).internals), - )); - } - EventLoopTimerTag::AbortSignalTimeout => { - cleared - .signal_timeouts - .push(AbortSignalTimeout::from_timer_ptr(timer)); - } - EventLoopTimerTag::CronJob => { - cleared.cron_jobs.push(CronJob::from_timer_ptr(timer)); - } - tag => debug_assert!( - false, - "{} timer in the fake heap has no release path", - <&'static str>::from(tag), - ), - } - } + timer.set_state(EventLoopTimerState::CANCELLED); + debug_assert!( + timer.tag().allow_fake_timers(), + "{} timer in the fake heap has no release path", + <&'static str>::from(timer.tag()), + ); + cleared.0.push(timer); } cleared } fn execute_next(global: &JSGlobalObject) -> JsResult { - // SAFETY: `timer_all()` is the live per-thread `All`; the borrow ends - // at this statement, before `fire` re-enters `All::insert`. - let next = match unsafe { (*timer_all()).fake_timers.timers.delete_min() } { - Some(n) => n, - None => return Ok(false), + let Some(next) = timer_all().fake_timers.timers.delete_min() else { + return Ok(false); }; Self::fire(global, next)?; @@ -249,21 +203,17 @@ impl FakeTimers { /// clock is a timer drain of its own: like `All::drain_timers`, a /// timer whose callback threw is reported and the drain goes on; only the /// VM's termination stops it, thrown to the `jest` host function driving it. - fn fire(global: &JSGlobalObject, next: *mut EventLoopTimer) -> JsResult<()> { - let _vm = global.bun_vm(); + fn fire(global: &JSGlobalObject, next: TimerRef) -> JsResult<()> { + let vm = global.bun_vm(); - // SAFETY: `next` was just popped from our heap; live until callback completes. - let now_el = unsafe { (*next).next }; - let now = from_el_timespec(&now_el); + let now = next.next(); if Environment::CI_ASSERT { let prev = CURRENT_TIME.get_timespec_now(); debug_assert!(prev.is_some()); debug_assert!(now.eql(&prev.unwrap()) || now.greater(&prev.unwrap())); } CURRENT_TIME.set(global, &now, None); - // SAFETY: `next` is live; `fire` takes `*mut Self` (noalias re-entrancy) - // and an erased `*mut ()` for the VM. - let fired = unsafe { EventLoopTimer::fire(next, &now_el, bun_jsc::virtual_machine::VirtualMachine::get_mut_ptr().cast()) }; + let fired = crate::dispatch::fire_timer(next, &now, vm); match fired { Ok(()) => Ok(()), Err(err) => bun_jsc::task::report_error_or_terminate(global, err) @@ -272,38 +222,28 @@ impl FakeTimers { } fn execute_until(global: &JSGlobalObject, until: Timespec) -> JsResult<()> { - let all = timer_all(); - 'outer: loop { - let next = 'blk: { - // SAFETY: `all` is the live per-thread `All`; each borrow - // lasts one statement and none spans `fire`. - let Some(peek) = (unsafe { (*all).fake_timers.timers.peek() }) else { - break 'outer; - }; - // SAFETY: `peek` is the heap root; live while linked. - if from_el_timespec(unsafe { &(*peek).next }).greater(&until) { - break 'outer; - } - // bun.assert always evaluates its arg; debug_assert! does NOT in release. - // Hoist the side-effecting delete_min() out so the timer is removed in all builds. - // SAFETY: as above. - let min = unsafe { (*all).fake_timers.timers.delete_min() }.expect("unreachable"); - debug_assert!(core::ptr::eq(min, peek)); - break 'blk min; + let timers = &timer_all().fake_timers.timers; + loop { + let Some(peek) = timers.peek() else { + break; }; + if peek.next().greater(&until) { + break; + } + // bun.assert always evaluates its arg; debug_assert! does NOT in release. + // Hoist the side-effecting delete_min() out so the timer is removed in all builds. + let next = timers.delete_min().expect("unreachable"); + debug_assert!(next == peek); Self::fire(global, next)?; } Ok(()) } fn execute_only_pending_timers(global: &JSGlobalObject) -> JsResult<()> { - // SAFETY: `timer_all()` is the live per-thread `All`. - let until = match unsafe { (*timer_all()).fake_timers.timers.find_max() } { - // SAFETY: `t` is reachable in the heap and live while linked. - Some(t) => from_el_timespec(unsafe { &(*t).next }), - None => return Ok(()), + let Some(last) = timer_all().fake_timers.timers.find_max() else { + return Ok(()); }; - Self::execute_until(global, until) + Self::execute_until(global, last.next()) } fn execute_all_timers(global: &JSGlobalObject) -> JsResult<()> { @@ -317,8 +257,7 @@ impl FakeTimers { // === fn error_unless_fake_timers(global: &JSGlobalObject) -> JsResult<()> { - // SAFETY: per-thread `timer::All`, live for the VM lifetime. - if unsafe { (*timer_all()).fake_timers.is_active() } { + if timer_all().fake_timers.is_active() { return Ok(()); } Err(global.throw(format_args!( @@ -384,8 +323,7 @@ fn use_fake_timers(global: &JSGlobalObject, frame: &CallFrame) -> JsResult JsResult JsResult { - // SAFETY: per-thread `timer::All`; the borrow ends before `release`. - let cleared = unsafe { (*timer_all()).fake_timers.deactivate(global) }; - cleared.release(global.bun_vm_ptr()); + let cleared = timer_all().fake_timers.deactivate(global); + cleared.release(global.bun_vm()); // Remove the setTimeout.clock marker when switching back to real timers. set_fake_timer_marker(global, false)?; @@ -473,8 +410,7 @@ fn run_all_timers(global: &JSGlobalObject, frame: &CallFrame) -> JsResult JsResult { error_unless_fake_timers(global)?; - // SAFETY: per-thread `timer::All`, live for the VM lifetime. - let count = unsafe { (*timer_all()).fake_timers.timers.count() }; + let count = timer_all().fake_timers.timers.count(); Ok(JSValue::js_number(count as f64)) } @@ -483,17 +419,15 @@ fn get_timer_count(global: &JSGlobalObject, _frame: &CallFrame) -> JsResult JsResult { error_unless_fake_timers(global)?; - // SAFETY: per-thread `timer::All`; the borrow ends before `release`. - let cleared = unsafe { (*timer_all()).fake_timers.clear() }; - cleared.release(global.bun_vm_ptr()); + let cleared = timer_all().fake_timers.clear(); + cleared.release(global.bun_vm()); Ok(frame.this()) } #[bun_jsc::host_fn] fn is_fake_timers(_global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { - // SAFETY: per-thread `timer::All`, live for the VM lifetime. - let is_active = unsafe { (*timer_all()).fake_timers.is_active() }; + let is_active = timer_all().fake_timers.is_active(); Ok(JSValue::from(is_active)) } diff --git a/src/runtime/timer/DateHeaderTimer.rs b/src/runtime/timer/DateHeaderTimer.rs index ef98d16b1965..9c93931fac1f 100644 --- a/src/runtime/timer/DateHeaderTimer.rs +++ b/src/runtime/timer/DateHeaderTimer.rs @@ -13,20 +13,15 @@ //! //! Note that we only check for potential updates ot this timer once per event loop tick. +use crate::jsc_hooks::timer_all_opt; use bun_jsc::virtual_machine::VirtualMachine; -use bun_uws::Loop; -use crate::jsc_hooks::timer_all; - -#[unsafe(no_mangle)] -extern "C" fn Bun__internal_ensureDateHeaderTimerIsEnabled(loop_: *mut Loop) { - if let Some(vm_ptr) = VirtualMachine::get_or_null() { - // SAFETY: loop_ is a valid uws Loop pointer passed from C++ and lives - // for the call duration. - let loop_ref = unsafe { &*loop_ }; - // SAFETY: single JS thread; `timer_all()` returns the live per-thread - // `All` (non-null after init). `update_date_header_timer_if_necessary` - // takes the VM by raw pointer to avoid aliased-`&mut` (jsc/runtime crate cycle). - unsafe { (*timer_all()).update_date_header_timer_if_necessary(loop_ref, vm_ptr) }; +// HOST_EXPORT(Bun__internal_ensureDateHeaderTimerIsEnabled, c) +pub fn ensure_date_header_timer_is_enabled(loop_: &bun_uws::Loop) { + if VirtualMachine::get_or_null().is_none() { + return; + } + if let Some(all) = timer_all_opt() { + all.update_date_header_timer_if_necessary(loop_, VirtualMachine::get()); } } diff --git a/src/runtime/timer/EventLoopDelayMonitor.rs b/src/runtime/timer/EventLoopDelayMonitor.rs index 10765c65869b..83a542a037ac 100644 --- a/src/runtime/timer/EventLoopDelayMonitor.rs +++ b/src/runtime/timer/EventLoopDelayMonitor.rs @@ -1,30 +1,20 @@ use bun_jsc::JSValue; -use bun_jsc::virtual_machine::VirtualMachine; -// Export functions for C++ -#[unsafe(no_mangle)] -extern "C" fn Timer_enableEventLoopDelayMonitoring( - vm: *mut VirtualMachine, +use crate::jsc_hooks::timer_all; + +// HOST_EXPORT(Timer_enableEventLoopDelayMonitoring, c) +pub fn enable_event_loop_delay_monitoring( + vm: &bun_jsc::virtual_machine::VirtualMachine, histogram: JSValue, resolution_ms: i32, ) { - // SAFETY: vm is a valid non-null pointer passed from C++. - let vm = unsafe { &mut *vm }; - // `vm.timer` is `()` (jsc/runtime crate cycle) — recover `All` via runtime_state(). - let state = crate::jsc_hooks::runtime_state(); - // SAFETY: `runtime_state()` is non-null after `bun_runtime::init()`; single - // JS thread, raw-ptr-per-field re-entry pattern (jsc_hooks.rs). - unsafe { - (*state) - .timer - .event_loop_delay - .enable(vm, histogram, resolution_ms) - }; + let all = timer_all(); + all.event_loop_delay + .enable(vm, all, histogram, resolution_ms); } -#[unsafe(no_mangle)] -extern "C" fn Timer_disableEventLoopDelayMonitoring() { - let state = crate::jsc_hooks::runtime_state(); - // SAFETY: see `Timer_enableEventLoopDelayMonitoring`. - unsafe { (*state).timer.event_loop_delay.disable() }; +// HOST_EXPORT(Timer_disableEventLoopDelayMonitoring, c) +pub fn disable_event_loop_delay_monitoring() { + let all = timer_all(); + all.event_loop_delay.disable(all); } diff --git a/src/runtime/timer/ImmediateObject.rs b/src/runtime/timer/ImmediateObject.rs index c0a744cc1636..bb20d60c8fba 100644 --- a/src/runtime/timer/ImmediateObject.rs +++ b/src/runtime/timer/ImmediateObject.rs @@ -1,15 +1,37 @@ +use core::cell::Cell; + use bun_jsc::virtual_machine::VirtualMachine; use bun_jsc::{JSGlobalObject, JSValue}; +use bun_ptr::{JsCell, RefPtr, ThisPtr}; -use super::{Kind, TimerObjectInternals}; +use super::{EventLoopTimer, IdMap, Kind, Maps, TimerObject, TimerObjectInternals}; // `jsc.Codegen.JSImmediate` — the C++ JSCell wrapper stays generated; this -// struct is the `m_ctx` payload. Struct + `RefCounted`/`Default` impls + the +// struct is the `m_ctx` payload. Struct + `RefCounted`/`Drop` impls + the // forwarder host-fns (`to_primitive`/`do_ref`/`do_unref`/`has_ref`/ -// `get_destroyed`/`dispose`/`constructor`/`finalize`/`ref_`/`deref`/`deinit`/ -// `init_with`) — see `impl_timer_object!` in `super` (timer/mod.rs). +// `get_destroyed`/`dispose`/`constructor`/`finalize`/`init_with`) — see +// `impl_timer_object!` in `super` (timer/mod.rs). super::impl_timer_object!(ImmediateObject, ImmediateObject, "Immediate"); +impl TimerObject for ImmediateObject { + #[inline] + fn internals(&self) -> &TimerObjectInternals { + &self.internals + } + #[inline] + fn event_loop_timer(&self) -> &JsCell { + &self.event_loop_timer + } + #[inline] + fn heap_ref(&self) -> &Cell>> { + &self.heap_ref + } + #[inline] + fn id_map(maps: &mut Maps, _kind: Kind) -> &mut IdMap { + &mut maps.set_immediate + } +} + impl ImmediateObject { pub(crate) fn init( global: &JSGlobalObject, @@ -20,35 +42,19 @@ impl ImmediateObject { Self::init_with(global, id, Kind::SetImmediate, 0, callback, arguments) } - /// Thin forwarder to - /// `internals.run_immediate_task`. Reached from `bun_jsc::event_loop` - /// via `__bun_run_immediate_task` (definer in [`crate::dispatch`]). - /// - /// Returns `true` if an exception was thrown. - /// - /// # Safety - /// `this` was produced by `enqueue_immediate_task` from a live - /// heap-allocated `ImmediateObject`; `vm` is the live per-thread VM. + /// Reached from `bun_jsc::event_loop` via `__bun_run_immediate_task` + /// (definer in [`crate::dispatch`]). Returns `true` if an exception was + /// thrown. `this` carries the immediate queue's ref; it may be gone once + /// this returns. #[inline] - pub(crate) unsafe fn run_immediate_task(this: *mut Self, vm: *mut VirtualMachine) -> bool { - // SAFETY: per fn contract — `this` is live; `internals` is an embedded - // field. Do NOT form `&mut *this` (the body may `deref()` and free). - // `run_immediate_task` takes `*mut Self` (noalias re-entrancy). - unsafe { - TimerObjectInternals::run_immediate_task(core::ptr::addr_of_mut!((*this).internals), vm) - } + pub(crate) fn run_immediate_task(this: ThisPtr, vm: &VirtualMachine) -> bool { + TimerObject::run_immediate_task(this, vm) } - /// # Safety - /// `this` must be a live heap-allocated `ImmediateObject`. + /// Release the immediate queue's ref without running the callback (VM + /// teardown). `this` may be gone once this returns. #[inline] - pub(crate) unsafe fn cancel_pending(this: *mut Self, vm: *mut VirtualMachine) { - // SAFETY: do not form `&mut *this` — the body derefs and may free `*this`. - unsafe { - TimerObjectInternals::cancel_pending_immediate( - core::ptr::addr_of_mut!((*this).internals), - vm, - ); - } + pub(crate) fn cancel_pending(this: ThisPtr, _vm: &VirtualMachine) { + TimerObject::cancel_pending_immediate(this); } } diff --git a/src/runtime/timer/TimeoutObject.rs b/src/runtime/timer/TimeoutObject.rs index 5fd26f857fea..cff272951e7b 100644 --- a/src/runtime/timer/TimeoutObject.rs +++ b/src/runtime/timer/TimeoutObject.rs @@ -1,14 +1,39 @@ +use core::cell::Cell; + use bun_jsc::generated::JSTimeout as js; use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; +use bun_ptr::{JsCell, RefPtr, ThisPtr}; -use super::Kind; +use super::{EventLoopTimer, IdMap, Kind, Maps, TimerObject, TimerObjectInternals}; -// Struct + `RefCounted`/`Default` impls + the forwarder host-fns +// Struct + `RefCounted`/`Drop` impls + the forwarder host-fns // (`to_primitive`/`do_ref`/`do_unref`/`has_ref`/`get_destroyed`/`dispose`/ -// `constructor`/`finalize`/`ref_`/`deref`/`deinit`/`init_with`) — see -// `impl_timer_object!` in `super` (timer/mod.rs). +// `constructor`/`finalize`/`init_with`) — see `impl_timer_object!` in `super` +// (timer/mod.rs). super::impl_timer_object!(TimeoutObject, TimeoutObject, "Timeout"); +impl TimerObject for TimeoutObject { + #[inline] + fn internals(&self) -> &TimerObjectInternals { + &self.internals + } + #[inline] + fn event_loop_timer(&self) -> &JsCell { + &self.event_loop_timer + } + #[inline] + fn heap_ref(&self) -> &Cell>> { + &self.heap_ref + } + #[inline] + fn id_map(maps: &mut Maps, kind: Kind) -> &mut IdMap { + match kind { + Kind::SetInterval => &mut maps.set_interval, + _ => &mut maps.set_timeout, + } + } +} + impl TimeoutObject { pub(crate) fn init( global: &JSGlobalObject, @@ -23,16 +48,20 @@ impl TimeoutObject { #[bun_jsc::host_fn(method)] pub(crate) fn do_refresh( - this: &Self, + this: ThisPtr, global: &JSGlobalObject, frame: &CallFrame, ) -> JsResult { - this.internals.do_refresh(global, frame.this()) + TimerObject::do_refresh(this, global, frame.this()) } #[bun_jsc::host_fn(method)] - pub fn close(this: &Self, global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - this.internals.cancel(global.bun_vm_ptr()); + pub fn close( + this: ThisPtr, + _global: &JSGlobalObject, + frame: &CallFrame, + ) -> JsResult { + TimerObject::cancel(this); Ok(frame.this()) } @@ -41,16 +70,12 @@ impl TimeoutObject { // Signature does not match the standard `host_fn(getter/setter)` shape; the // `#[JsClass]` derive emits the C-ABI shims directly. - pub(crate) fn get_on_timeout( - _this: &Self, - this_value: JSValue, - _global: &JSGlobalObject, - ) -> JSValue { + pub(crate) fn get_on_timeout(&self, this_value: JSValue, _global: &JSGlobalObject) -> JSValue { js::callback_get_cached(this_value).unwrap() } pub(crate) fn set_on_timeout( - _this: &Self, + &self, this_value: JSValue, global: &JSGlobalObject, value: JSValue, @@ -59,7 +84,7 @@ impl TimeoutObject { } pub(crate) fn get_idle_timeout( - _this: &Self, + &self, this_value: JSValue, _global: &JSGlobalObject, ) -> JSValue { @@ -67,7 +92,7 @@ impl TimeoutObject { } pub(crate) fn set_idle_timeout( - _this: &Self, + &self, this_value: JSValue, global: &JSGlobalObject, value: JSValue, @@ -75,39 +100,26 @@ impl TimeoutObject { js::idle_timeout_set_cached(this_value, global, value); } - pub(crate) fn get_repeat( - _this: &Self, - this_value: JSValue, - _global: &JSGlobalObject, - ) -> JSValue { + pub(crate) fn get_repeat(&self, this_value: JSValue, _global: &JSGlobalObject) -> JSValue { js::repeat_get_cached(this_value).unwrap() } - pub(crate) fn set_repeat( - _this: &Self, - this_value: JSValue, - global: &JSGlobalObject, - value: JSValue, - ) { + pub(crate) fn set_repeat(&self, this_value: JSValue, global: &JSGlobalObject, value: JSValue) { js::repeat_set_cached(this_value, global, value); } - pub(crate) fn get_idle_start( - _this: &Self, - this_value: JSValue, - _global: &JSGlobalObject, - ) -> JSValue { + pub(crate) fn get_idle_start(&self, this_value: JSValue, _global: &JSGlobalObject) -> JSValue { js::idle_start_get_cached(this_value).unwrap() } pub(crate) fn set_idle_start( - this: &Self, + &self, this_value: JSValue, global: &JSGlobalObject, value: JSValue, ) { if let Some(ms) = value.get_number() { - this.internals.set_idle_start(ms); + TimerObject::set_idle_start(self, ms); } js::idle_start_set_cached(this_value, global, value); } diff --git a/src/runtime/timer/Timer.rs b/src/runtime/timer/Timer.rs index b840af4df82b..21f499a30cb2 100644 --- a/src/runtime/timer/Timer.rs +++ b/src/runtime/timer/Timer.rs @@ -7,62 +7,46 @@ //! `DateHeaderTimer`, …) live in `mod.rs`; this module only adds the JS-facing //! `impl super::All { … }` surface plus the C-ABI export thunks. -#![allow(clippy::missing_safety_doc)] - use bun_core::String as BunString; use bun_core::{Timespec, TimespecMockMode}; use bun_jsc::virtual_machine::VirtualMachine; -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsClass as _, JsResult, StringJsc as _}; +use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult, StringJsc as _}; +use bun_ptr::ThisPtr; use bun_uws::Loop as UwsLoop; use super::{ - All, CountdownOverflowBehavior, DateHeaderTimer, EventLoopTimer, EventLoopTimerState, - EventLoopTimerTag, ImmediateObject, Kind, TimeoutObject, TimeoutWarning, TimerObjectInternals, + All, CountdownOverflowBehavior, DateHeaderTimer, EventLoopTimerState, ImmediateObject, Kind, + TimeoutObject, TimeoutWarning, TimerObject, }; -use crate::jsc_hooks::{timer_all, timer_all_mut}; +use crate::jsc_hooks::{timer_all, timer_all_opt}; // ════════════════════════════════════════════════════════════════════════════ // JS-facing surface on `super::All` // ════════════════════════════════════════════════════════════════════════════ -impl All { - #[unsafe(no_mangle)] - pub(crate) extern "C" fn Bun__Timer__getNextID() -> i32 { - let all = timer_all(); - if all.is_null() { - return 0; - } - // SAFETY: `all` is the live per-thread `All`; single-threaded JS heap. - unsafe { - (*all).last_id = (*all).last_id.wrapping_add(1); - (*all).last_id - } - } +// HOST_EXPORT(Bun__Timer__getNextID, c) +pub fn get_next_id() -> i32 { + let Some(all) = timer_all_opt() else { + return 0; + }; + all.next_id().wrapping_add(1) +} - /// # Safety - /// `vm` must point to the live per-thread `VirtualMachine`. - // Forwards `vm` to `DateHeaderTimer::enable` without dereferencing it here; - // the raw pointer is intentional (avoids aliased-`&mut` across the - // jsc/runtime crate cycle — see DateHeaderTimer.rs). Opaque-token - // forwarding makes not_unsafe_ptr_arg_deref a false positive. - #[allow(clippy::not_unsafe_ptr_arg_deref)] +impl All { pub(crate) fn update_date_header_timer_if_necessary( - &mut self, + &self, loop_: &UwsLoop, - vm: *mut VirtualMachine, + vm: &VirtualMachine, ) { if loop_.should_enable_date_header_timer() { - // `is_date_timer_active()` is private to mod.rs; inline. - if self.date_header_timer.event_loop_timer.state != EventLoopTimerState::ACTIVE { - // SAFETY: caller contract guarantees `vm` is valid. - unsafe { - self.date_header_timer.enable( - vm, - // Be careful to avoid adding extra calls to bun.timespec.now() - // when it's not needed. - &Timespec::now(TimespecMockMode::ForceRealTime), - ); - } + if self.date_header_timer.event_loop_timer.get().state != EventLoopTimerState::ACTIVE { + self.date_header_timer.enable( + vm, + self, + // Be careful to avoid adding extra calls to bun.timespec.now() + // when it's not needed. + &Timespec::now(TimespecMockMode::ForceRealTime), + ); } } else { // don't un-schedule it here. @@ -127,7 +111,7 @@ impl All { /// Convert an arbitrary JavaScript value to a number of milliseconds used to schedule a timer. fn js_value_to_countdown( - &mut self, + &self, global_this: &JSGlobalObject, countdown: JSValue, overflow_behavior: CountdownOverflowBehavior, @@ -156,8 +140,8 @@ impl All { countdown_double, TimeoutWarning::TimeoutOverflowWarning, )?; - } else if countdown_double < 0.0 && !self.warned_negative_number { - self.warned_negative_number = true; + } else if countdown_double < 0.0 && !self.warned_negative_number.get() { + self.warned_negative_number.set(true); Self::warn_invalid_countdown( global_this, countdown_double, @@ -166,9 +150,9 @@ impl All { } else if !countdown.is_undefined() && countdown.is_number() && countdown_double.is_nan() - && !self.warned_not_number + && !self.warned_not_number.get() { - self.warned_not_number = true; + self.warned_not_number.set(true); Self::warn_invalid_countdown( global_this, countdown_double, @@ -196,9 +180,8 @@ impl All { ) -> JsResult { bun_jsc::mark_binding!(); debug_assert!(!promise.is_empty() && !countdown.is_empty()); - let all = timer_all_mut(); - let id = all.last_id; - all.last_id = all.last_id.wrapping_add(1); + let all = timer_all(); + let id = all.next_id(); let countdown_int = all.js_value_to_countdown(global, countdown, CountdownOverflowBehavior::Clamp, true)?; @@ -220,9 +203,8 @@ impl All { ) -> JsResult { bun_jsc::mark_binding!(); debug_assert!(!callback.is_empty() && !arguments.is_empty()); - let all = timer_all_mut(); - let id = all.last_id; - all.last_id = all.last_id.wrapping_add(1); + let all = timer_all(); + let id = all.next_id(); let wrapped_callback = callback.with_async_context_if_needed(global); Ok(ImmediateObject::init( @@ -241,9 +223,8 @@ impl All { ) -> JsResult { bun_jsc::mark_binding!(); debug_assert!(!callback.is_empty() && !arguments.is_empty() && !countdown.is_empty()); - let all = timer_all_mut(); - let id = all.last_id; - all.last_id = all.last_id.wrapping_add(1); + let all = timer_all(); + let id = all.next_id(); let wrapped_callback = callback.with_async_context_if_needed(global); let countdown_int = @@ -266,9 +247,8 @@ impl All { ) -> JsResult { bun_jsc::mark_binding!(); debug_assert!(!callback.is_empty() && !arguments.is_empty() && !countdown.is_empty()); - let all = timer_all_mut(); - let id = all.last_id; - all.last_id = all.last_id.wrapping_add(1); + let all = timer_all(); + let id = all.next_id(); let wrapped_callback = callback.with_async_context_if_needed(global); let countdown_int = @@ -291,17 +271,16 @@ impl All { (f64::from(id) == number).then_some(id) } - fn remove_timer_by_id(&mut self, id: i32) -> Option<*mut TimeoutObject> { - let value: *mut EventLoopTimer = if let Some(idx) = self.maps.set_timeout.get_index(&id) { - self.maps.set_timeout.swap_remove_at(idx).1 - } else { - let idx = self.maps.set_interval.get_index(&id)?; - self.maps.set_interval.swap_remove_at(idx).1 - }; - // SAFETY: entry value points to EventLoopTimer embedded in a TimeoutObject - debug_assert!(unsafe { (*value).tag } == EventLoopTimerTag::TimeoutObject); - // SAFETY: entry value points to TimeoutObject.event_loop_timer - Some(unsafe { TimeoutObject::from_timer_ptr(value) }) + fn remove_timer_by_id(&self, id: i32) -> Option> { + self.maps.with_mut(|maps| { + let entry = if let Some(idx) = maps.set_timeout.get_index(&id) { + maps.set_timeout.swap_remove_at(idx).1 + } else { + let idx = maps.set_interval.get_index(&id)?; + maps.set_interval.swap_remove_at(idx).1 + }; + Some(entry.this_ptr()) + }) } pub(crate) fn clear_timer( @@ -311,10 +290,9 @@ impl All { ) -> JsResult<()> { bun_jsc::mark_binding!(); - let vm = global_this.bun_vm_ptr(); - let all = timer_all_mut(); + let all = timer_all(); - let timer: Option<*mut TimerObjectInternals> = 'brk: { + let timer: ThisPtr = 'brk: { if timer_id_value.is_number() { // Node.js looks the id up by value (`knownTimersById[id]`): a double holding an // integer names the same timer as the int32. Anything else clears nothing. @@ -325,8 +303,7 @@ impl All { let Some(t) = all.remove_timer_by_id(id) else { return Ok(()); }; - // SAFETY: t is a valid TimeoutObject pointer - break 'brk Some(unsafe { core::ptr::addr_of_mut!((*t).internals) }); + break 'brk t; } else if timer_id_value.is_string_literal() { // Primitive string only (JSType::String) — boxed `new String(..)` // must fall through to `from_js` below and be a no-op, matching @@ -383,34 +360,27 @@ impl All { let Some(t) = all.remove_timer_by_id(parsed) else { return Ok(()); }; - // SAFETY: t is a valid TimeoutObject pointer - break 'brk Some(unsafe { core::ptr::addr_of_mut!((*t).internals) }); + break 'brk t; } - if let Some(timeout) = TimeoutObject::from_js(timer_id_value) { + if let Some(timeout) = timer_id_value.as_class_this_ptr::() { // clearImmediate should be a noop if anything other than an Immediate is passed to it. if kind != Kind::SetImmediate { - // SAFETY: `timeout` is a valid TimeoutObject pointer - break 'brk Some(unsafe { core::ptr::addr_of_mut!((*timeout).internals) }); - } else { - return Ok(()); + break 'brk timeout; } - } else if let Some(immediate) = ImmediateObject::from_js(timer_id_value) { + return Ok(()); + } else if let Some(immediate) = timer_id_value.as_class_this_ptr::() { // setImmediate can only be cleared by clearImmediate, not by clearTimeout or clearInterval. if kind == Kind::SetImmediate { - // SAFETY: `immediate` is a valid ImmediateObject pointer - break 'brk Some(unsafe { core::ptr::addr_of_mut!((*immediate).internals) }); - } else { - return Ok(()); + TimerObject::cancel(immediate); } + return Ok(()); } else { - break 'brk None; + return Ok(()); } }; - let Some(timer) = timer else { return Ok(()) }; - // SAFETY: timer points to a live TimerObjectInternals - unsafe { (*timer).cancel(vm) }; + TimerObject::cancel(timer); Ok(()) } @@ -449,39 +419,22 @@ impl DateHeaderTimer { /// 1. If the timer was recently updated (< 1 second ago), just reschedule it /// 2. If the timer is stale (> 1 second since last update), update the date /// immediately and reschedule - /// - /// # Safety - /// `vm` must point to the live per-thread `VirtualMachine`; its `uws_loop()` - /// must outlive this call. - unsafe fn enable(&mut self, vm: *mut VirtualMachine, now: &Timespec) { - debug_assert!(self.event_loop_timer.state != EventLoopTimerState::ACTIVE); - - // `EventLoopTimer.next` is the lower-tier `ElTimespec` stub - // (same `{sec,nsec}` layout) until bun_event_loop switches to bun_core::Timespec. - let last_update = Timespec { - sec: self.event_loop_timer.next.sec, - nsec: self.event_loop_timer.next.nsec, - }; + fn enable(&self, vm: &VirtualMachine, all: &All, now: &Timespec) { + debug_assert!(self.event_loop_timer.get().state != EventLoopTimerState::ACTIVE); + + let last_update = self.event_loop_timer.get().next; let elapsed = now.duration(&last_update).ms(); // If the last update was more than 1 second ago, the date is stale if elapsed >= 1000 { // Update the date immediately since it's stale // updateDate() is an expensive function. - // SAFETY: `vm` is the live per-thread VM; `uws_loop()` returns its - // owned uws loop, which outlives this call. - unsafe { (*(*vm).uws_loop()).update_date() }; - - let elt: *mut EventLoopTimer = &raw mut self.event_loop_timer; - // SAFETY: single JS thread; nothing `All::update` touches overlaps - // `date_header_timer`, which `self` aliases (raw-ptr-per-field - // re-entry pattern, see jsc_hooks.rs). - unsafe { (*Self::timer_all()).update(elt, &now.add_ms(1000)) }; + vm.uws_loop_mut().update_date(); + + all.update(self.timer_ref(), &now.add_ms(1000)); } else { // The date was updated recently, just reschedule for the next second - let elt: *mut EventLoopTimer = &raw mut self.event_loop_timer; - // SAFETY: see above — disjoint-field access on `All`. - unsafe { (*Self::timer_all()).insert(elt) }; + all.insert(self.timer_ref()); } } } diff --git a/src/runtime/timer/WTFTimer.rs b/src/runtime/timer/WTFTimer.rs index 9a0841e581cf..36ecf4854158 100644 --- a/src/runtime/timer/WTFTimer.rs +++ b/src/runtime/timer/WTFTimer.rs @@ -3,112 +3,103 @@ //! //! jsc/runtime crate cycle: the low-tier `bun_jsc::VirtualMachine.timer` is a //! `()` placeholder, so this module resolves the timer heap through -//! [`crate::jsc_hooks::runtime_state`] instead — the same pattern -//! `TimerObjectInternals` uses. +//! [`crate::jsc_hooks::timer_all`] instead. -use core::ffi::c_void; use core::ptr::{self, NonNull}; -use core::sync::atomic::{AtomicPtr, Ordering}; +use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; use bun_core::{Timespec, TimespecMockMode}; +use bun_ptr::{BackRef, JsCell}; use crate::jsc::virtual_machine::VirtualMachine; use crate::webcore::script_execution_context::Identifier as ScriptExecutionContextIdentifier; -use super::{ - ElTimespec, EventLoopTimer, EventLoopTimerState, EventLoopTimerTag, InHeap, IntrusiveField, -}; +use super::{All, EventLoopTimer, EventLoopTimerState, EventLoopTimerTag, TimerRef}; const NS_PER_S: i64 = bun_core::time::NS_PER_S as i64; bun_opaque::opaque_ffi! { /// This is `WTF::RunLoop::TimerBase` from WebKit — opaque FFI handle. - pub(crate) struct RunLoopTimer; + pub struct RunLoopTimer; } impl RunLoopTimer { - /// Takes `NonNull` (not `&self`) so callers holding the raw FFI handle - /// don't need an `unsafe { as_ref() }` just to forward it — `NonNull` - /// is ABI-identical to `*mut T` and the extern is `safe fn`. + /// Run the C++ timer. Its `fired()` may destroy the `TimerBase` — and with + /// it the `WTFTimer` this handle was read from — so callers hold no + /// `&WTFTimer` across this call. #[inline] - fn fire(this: NonNull) { + pub(crate) fn fire(this: NonNull) { WTFTimer__fire(this) } } -/// A timer created by WTF code and invoked by Bun's event loop. -pub(crate) struct WTFTimer { - // Backref to the owning VirtualMachine (captured from the thread-local VM - // in `WTFTimer__create`); never owned here. The C++ `RunLoop::TimerBase` - // that owns this wrapper lives on the VM's run loop, so the VM outlives - // the timer. - vm: NonNull, +/// A timer created by WTF code and invoked by Bun's event loop. Owned (boxed) +/// by the C++ `RunLoop::TimerBase` that `WTFTimer__create`d it; `update` / +/// `cancel` may arrive from any thread. +pub struct WTFTimer { + /// `Timer::All` of the VM whose JS thread created this timer. The C++ + /// `RunLoop::TimerBase` that owns this wrapper lives on that VM's run loop + /// and is destroyed with the JSC VM, before `RuntimeState` (which holds + /// `All`) is freed. Off that thread only `wtf_arm`/`wtf_disarm`/the + /// `wtf_timers` lock may be used through it. + timers: BackRef, // FFI handle into WebKit's RunLoop::TimerBase; owned by C++. run_loop_timer: NonNull, - pub(crate) event_loop_timer: EventLoopTimer, - // Backref into `vm.eventLoop().imminent_gc_timer`. Low tier stores - // `AtomicPtr<()>` (PORTING.md §Dispatch); `self` is cast to `*mut ()` at - // each compare_exchange (the hook in `dispatch.rs` casts back to - // `*mut WTFTimer`). - imminent: bun_ptr::BackRef>, - repeat: bool, + /// Linked into `All.wtf_timers`. Unlike every other `JsCell`, this one is + /// shared across threads: it is only read or written under that heap's lock. + pub(crate) event_loop_timer: JsCell, + /// `vm.eventLoop().imminent_gc_timer`. Low tier stores `AtomicPtr<()>` + /// (§Dispatch); `self` is published as `*mut ()` and `__bun_run_wtf_timer` + /// (in `dispatch.rs`) recovers the `&WTFTimer`. + imminent: BackRef>, + repeat: AtomicBool, script_execution_context_id: ScriptExecutionContextIdentifier, } bun_event_loop::impl_timer_owner!(WTFTimer; from_timer_ptr => event_loop_timer); impl WTFTimer { - /// Fire the underlying `RunLoop::TimerBase`, - /// removing `self` from the timer heap first if it's currently scheduled. - /// Reached from `bun_jsc::event_loop` via `__bun_run_wtf_timer` - /// (definer in [`crate::dispatch`]). - /// - /// # Safety - /// `this` was published by [`WTFTimer::update`] into - /// `imminent_gc_timer` and remains live; `vm` is the live VM that owns - /// this timer. - pub(crate) unsafe fn run(this: *mut Self, vm: *mut VirtualMachine) { - // SAFETY: per fn contract — `this` is live; `ThisPtr` vends only fresh - // short-lived `&Self` per Deref so no `&WTFTimer` spans the - // `All::wtf_disarm` raw write to `event_loop_timer`. - let t = unsafe { bun_ptr::ThisPtr::new(this) }; - // SAFETY: `vm` is the live VM that owns this timer's heap. - unsafe { - let state = crate::jsc_hooks::runtime_state_of(vm); - (*state) - .timer - .wtf_disarm(ptr::addr_of_mut!((*this).event_loop_timer)); - } - t.run_without_removing(); + #[inline] + fn timer_ref(&self) -> TimerRef { + TimerRef::new(self, |t| &t.event_loop_timer) } + /// The identity `imminent_gc_timer` knows this timer by. #[inline] - fn run_without_removing(&self) { - RunLoopTimer::fire(self.run_loop_timer); + fn as_opaque(&self) -> *mut () { + ptr::from_ref(self).cast_mut().cast() + } + + /// `imminent_gc_timer` named this timer (see `update`): unlink it and hand + /// back the C++ timer for the caller (`__bun_run_wtf_timer` in + /// [`crate::dispatch`]) to [`RunLoopTimer::fire`] once no `&self` is live. + pub(crate) fn take_for_run(&self) -> NonNull { + self.timers.wtf_disarm(self.timer_ref()); + self.run_loop_timer } #[bun_uws::uws_callback(export = "WTFTimer__isActive", no_catch)] pub(crate) fn is_active(&self) -> bool { - if self.event_loop_timer.state == EventLoopTimerState::ACTIVE { + let state = { + let _lock = self.timers.wtf_timers.lock(); + self.event_loop_timer.get().state + }; + if state == EventLoopTimerState::ACTIVE { return true; } - // `imminent` is a `BackRef` into the VM's event loop, which outlives this timer. - let loaded = self.imminent.load(Ordering::SeqCst); - // Null can never equal `this`, so a single pointer compare suffices. - loaded.cast_const().cast::() == ptr::from_ref(self) + // Null can never equal `self`, so a single pointer compare suffices. + self.imminent.load(Ordering::SeqCst) == self.as_opaque() } #[bun_uws::uws_callback(export = "WTFTimer__secondsUntilTimer", no_catch)] pub(crate) fn seconds_until_timer(&self) -> f64 { - if self.event_loop_timer.state == EventLoopTimerState::ACTIVE { - let next = &self.event_loop_timer.next; - // bun_event_loop carries a local `Timespec` stub; re-pack - // into bun_core::Timespec to call `duration`. - let until = Timespec { - sec: next.sec, - nsec: next.nsec, - } - .duration(&Timespec::now(TimespecMockMode::ForceRealTime)); + let (state, next) = { + let _lock = self.timers.wtf_timers.lock(); + let timer = self.event_loop_timer.get(); + (timer.state, timer.next) + }; + if state == EventLoopTimerState::ACTIVE { + let until = next.duration(&Timespec::now(TimespecMockMode::ForceRealTime)); let sec = until.sec as f64; let nsec = until.nsec as f64; return sec + nsec / NS_PER_S as f64; @@ -116,31 +107,21 @@ impl WTFTimer { f64::INFINITY } - /// # Safety - /// `this` must point at a live heap-allocated `WTFTimer`. - pub(crate) unsafe fn update(this: *mut Self, seconds: f64, repeat: bool) { - let self_opaque = this.cast::<()>(); - // SAFETY: per fn contract — `this` is live; `ThisPtr` vends only fresh - // short-lived `&Self` per Deref. Copy the `BackRef` out so the - // subsequent `&AtomicPtr` borrow is detached from `*this`. - let t = unsafe { bun_ptr::ThisPtr::new(this) }; - let imminent_br = t.imminent; - let imminent = imminent_br.get(); - + pub(crate) fn update(&self, seconds: f64, repeat: bool) { // There's only one of these per VM, and each VM has its own imminent_gc_timer. // Only set imminent if it's not already set to avoid overwriting another timer. if seconds.partial_cmp(&0.0) != Some(core::cmp::Ordering::Greater) { - let _ = imminent.compare_exchange( + let _ = self.imminent.compare_exchange( ptr::null_mut(), - self_opaque, + self.as_opaque(), Ordering::SeqCst, Ordering::SeqCst, ); return; } // Clear imminent if this timer was the one that set it. - let _ = imminent.compare_exchange( - self_opaque, + let _ = self.imminent.compare_exchange( + self.as_opaque(), ptr::null_mut(), Ordering::SeqCst, Ordering::SeqCst, @@ -161,147 +142,91 @@ impl WTFTimer { interval.nsec -= NS_PER_S; } - // SAFETY: `t.vm` owns this timer's heap; `wtf_arm` is safe from any thread. - unsafe { - let state = crate::jsc_hooks::runtime_state_of(t.vm.as_ptr()); - (*state) - .timer - .wtf_arm(ptr::addr_of_mut!((*this).event_loop_timer), &interval); - (*this).repeat = repeat; - } + self.timers.wtf_arm(self.timer_ref(), interval); + self.repeat.store(repeat, Ordering::Relaxed); } - /// # Safety - /// `this` must point at a live heap-allocated `WTFTimer`. - pub(crate) unsafe fn cancel(this: *mut Self) { - // SAFETY: per fn contract — `this` outlives this scope. `ThisPtr` vends - // only fresh short-lived `&Self` per Deref. - let t = unsafe { bun_ptr::ThisPtr::new(this) }; - - if t.script_execution_context_id.valid() { + pub(crate) fn cancel(&self) { + if self.script_execution_context_id.valid() { // Only clear imminent if this timer was the one that set it. - let self_opaque = this.cast::<()>(); - // `imminent` is a `BackRef` into the VM's event loop, which - // outlives this timer. - let imminent_br = t.imminent; - let _ = imminent_br.compare_exchange( - self_opaque, + let _ = self.imminent.compare_exchange( + self.as_opaque(), ptr::null_mut(), Ordering::SeqCst, Ordering::SeqCst, ); - // SAFETY: `t.vm` owns this timer's heap; `wtf_disarm` is safe from - // any thread and is a no-op for a node that is no longer linked. - unsafe { - let state = crate::jsc_hooks::runtime_state_of(t.vm.as_ptr()); - (*state) - .timer - .wtf_disarm(ptr::addr_of_mut!((*this).event_loop_timer)); - } + // No-op for a slot that is no longer linked. + self.timers.wtf_disarm(self.timer_ref()); } } - /// `EventLoopTimer.fire` dispatch arm body for `Tag::WTFTimer`. - /// - /// # Safety - /// `this` is the container of an `EventLoopTimer` just popped from - /// `All.wtf_timers`; `_vm` is the live per-thread VM. - pub(crate) unsafe fn fire(this: *mut Self, _now: &ElTimespec, _vm: *mut VirtualMachine) { - // SAFETY: per fn contract — `this` is live; `ThisPtr` vends only fresh - // short-lived `&Self` per Deref. - let t = unsafe { bun_ptr::ThisPtr::new(this) }; + /// Timer-heap dispatch arm for `Tag::WTFTimer`: `self`'s slot was just + /// popped from `All.wtf_timers`. Hands back the C++ timer for the caller to + /// [`RunLoopTimer::fire`] once no `&self` is live. + pub(crate) fn take_for_fire(&self) -> NonNull { // Only clear imminent if this timer was the one that set it. - let self_opaque = this.cast::<()>(); - // `imminent` is a `BackRef` into the VM's event loop, which outlives - // this timer. - let imminent_br = t.imminent; - let _ = imminent_br.compare_exchange( - self_opaque, + let _ = self.imminent.compare_exchange( + self.as_opaque(), ptr::null_mut(), Ordering::SeqCst, Ordering::SeqCst, ); - t.run_without_removing(); + self.run_loop_timer } +} - /// # Safety - /// `this` must be the unique owner of a `WTFTimer` produced by `WTFTimer__create`. - pub(crate) unsafe fn deinit(this: *mut Self) { - // SAFETY: per fn contract. - unsafe { Self::cancel(this) }; - // SAFETY: `WTFTimer__create` handed its `Box` over via `heap::into_raw`, - // so `heap::take` is the paired reclaim. - drop(unsafe { bun_core::heap::take(this) }); +impl Drop for WTFTimer { + fn drop(&mut self) { + self.cancel(); } } -/// A `WTF::RunLoop` timer on this thread, backed by this thread's event loop. Null when the thread has no Bun +/// A `WTF::RunLoop` timer on this thread, backed by this thread's event loop. `None` when the thread has no Bun /// `VirtualMachine` (a `JSC::VM` on a bundler thread generating bytecode, say): the timer then never fires, which /// `RunLoop::TimerBase` accepts. -/// -/// # Safety -/// `run_loop_timer` must be a non-null, live `WTF::RunLoop::TimerBase` owned -/// by the caller for the lifetime of the returned `WTFTimer`. -#[unsafe(no_mangle)] -unsafe extern "C" fn WTFTimer__create(run_loop_timer: *mut RunLoopTimer) -> *mut c_void { - let Some(vm) = VirtualMachine::get_or_null() else { - return ptr::null_mut(); - }; +// HOST_EXPORT(WTFTimer__create, c) +pub fn create( + run_loop_timer: core::ptr::NonNull, +) -> Option> { + if !VirtualMachine::is_loaded() { + return None; + } - // SAFETY: `vm` is the thread-local VirtualMachine; `run_loop_timer` is - // non-null per caller contract; `event_loop().imminent_gc_timer` lives as - // long as the VM. - let this = unsafe { - let vm_ref = &*vm; - let el = &*vm_ref.event_loop(); - Box::new(WTFTimer { - vm: NonNull::new_unchecked(vm), - imminent: bun_ptr::BackRef::new(&el.imminent_gc_timer), - event_loop_timer: EventLoopTimer { - next: ElTimespec { - sec: i64::MAX, - nsec: 0, - }, - tag: EventLoopTimerTag::WTFTimer, - state: EventLoopTimerState::CANCELLED, - heap: IntrusiveField::default(), - in_heap: InHeap::None, + let vm = VirtualMachine::get(); + Some(Box::new(WTFTimer { + timers: BackRef::new(crate::jsc_hooks::timer_all()), + imminent: BackRef::new(&vm.event_loop_mut().imminent_gc_timer), + event_loop_timer: JsCell::new(EventLoopTimer::new( + EventLoopTimerTag::WTFTimer, + EventLoopTimerState::CANCELLED, + Timespec { + sec: i64::MAX, + nsec: 0, }, - run_loop_timer: NonNull::new_unchecked(run_loop_timer), - repeat: false, - script_execution_context_id: ScriptExecutionContextIdentifier( - vm_ref.initial_script_execution_context_identifier as u32, - ), - }) - }; - - bun_core::heap::into_raw(this).cast::() + )), + run_loop_timer, + repeat: AtomicBool::new(false), + script_execution_context_id: ScriptExecutionContextIdentifier( + vm.initial_script_execution_context_identifier as u32, + ), + })) } -/// # Safety -/// `this` must point at a live `WTFTimer` produced by [`WTFTimer__create`]. -#[unsafe(no_mangle)] -unsafe extern "C" fn WTFTimer__update(this: *mut WTFTimer, seconds: f64, repeat: bool) { - // SAFETY: per fn contract. - unsafe { WTFTimer::update(this, seconds, repeat) }; +// HOST_EXPORT(WTFTimer__update, c) +pub fn update(this: &crate::timer::WTFTimer, seconds: f64, repeat: bool) { + this.update(seconds, repeat); } -/// # Safety -/// `this` must be the unique owner of a `WTFTimer` produced by -/// [`WTFTimer__create`]; it is freed by this call. -#[unsafe(no_mangle)] -unsafe extern "C" fn WTFTimer__deinit(this: *mut WTFTimer) { - // SAFETY: per fn contract. - unsafe { WTFTimer::deinit(this) }; +/// Frees `this`. +// HOST_EXPORT(WTFTimer__deinit, c) +pub fn deinit(this: Box) { + drop(this); } -/// # Safety -/// `this` must point at a live `WTFTimer` produced by [`WTFTimer__create`]. -#[unsafe(no_mangle)] -unsafe extern "C" fn WTFTimer__cancel(this: *mut WTFTimer) { - // SAFETY: per fn contract. - unsafe { WTFTimer::cancel(this) }; +// HOST_EXPORT(WTFTimer__cancel, c) +pub fn cancel(this: &crate::timer::WTFTimer) { + this.cancel(); } unsafe extern "C" { diff --git a/src/runtime/timer/mod.rs b/src/runtime/timer/mod.rs index 97b1e578bffb..4b2c8f6ce78b 100644 --- a/src/runtime/timer/mod.rs +++ b/src/runtime/timer/mod.rs @@ -1,26 +1,26 @@ //! Timer subsystem: setTimeout/setInterval/setImmediate scheduling and the //! event-loop timer heap. +use core::cell::Cell; + use bun_collections::ArrayHashMap; use bun_core::{Timespec, TimespecMockMode}; #[cfg(windows)] use bun_libuv_sys::UvHandle as _; +use bun_ptr::{BackRef, JsCell}; #[cfg(windows)] use bun_sys::windows::libuv as uv; use bun_threading::Guarded; -// Low-tier timer node + tag (per §Dispatch hot-path list, the `match tag` -// dispatch lives in this crate; `bun_event_loop` only stores `(tag, ptr)`). +/// `EventLoopTimer.next`'s type; the low tier re-exports `bun_core::Timespec`. +pub(crate) use bun_event_loop::EventLoopTimer::Timespec as ElTimespec; pub use bun_event_loop::EventLoopTimer::{ - EventLoopTimer, InHeap, IntrusiveField, State as EventLoopTimerState, Tag as EventLoopTimerTag, + EventLoopTimer, InHeap, State as EventLoopTimerState, Tag as EventLoopTimerTag, TimerHeap, + TimerOwner, TimerRef, }; -// bun_event_loop carries a local `Timespec` stub instead of -// `bun_core::Timespec`. Same `{sec: i64, nsec: i64}` shape; alias it here so -// `fire()`/`next` accesses type-check without a transmute. -// TODO: remove this alias once the lower tier switches to `bun_core::Timespec`. -pub(crate) use bun_event_loop::EventLoopTimer::Timespec as ElTimespec; use crate::jsc::JSValue; +use crate::jsc::virtual_machine::VirtualMachine; // ─── JS-facing surface (`impl All { set_timeout / clear_* / … }`) ──────────── // Named `timer` so codegen (`generated_js2native.rs`) resolves @@ -32,25 +32,21 @@ pub mod timer; // ─── impl_timer_object! ────────────────────────────────────────────────────── // Shared scaffold for `TimeoutObject` / `ImmediateObject`: both are a -// `#[JsClass]` payload of `{ref_count, event_loop_timer, internals}` whose -// JS-facing host-fns are pure forwarders to `TimerObjectInternals`. The macro +// `#[JsClass]` payload of `{ref_count, event_loop_timer, internals, heap_ref}` +// whose JS-facing host-fns are pure forwarders to [`TimerObject`]. The macro // emits the parts shared by both types so each `*.rs` file holds only its -// type-specific surface (`init`, `do_refresh`, cached-prop accessors, -// `run_immediate_task`). +// type-specific surface (`init`, `do_refresh`, cached-prop accessors, the +// [`TimerObject`] impl). // // Emits, at the call-site module path (so `#[JsClass]`/`#[host_fn]` produce the // same extern symbol names as before — `Timeout__create`, `TimeoutPrototype__*`, // `ImmediateClass__construct`, …): -// - `#[bun_jsc::JsClass(name = $js_name)] pub struct $T { … }` +// - `#[bun_jsc::JsClass(name = $js_name)] #[derive(RefCounted)] pub struct $T { … }` // - `bun_event_loop::impl_timer_owner!($T; from_timer_ptr => event_loop_timer)` -// - `impl RefCounted for $T` (intrusive `ref_count` field, `deinit` destructor) -// - `impl Default for $T` (`EventLoopTimer::init_paused(EventLoopTimerTag::$tag)`) -// - `impl $T`: `ref_`/`deref`/`deinit`/`init_with`/`constructor`/`finalize` -// and the forwarder host-fns `to_primitive`/`do_ref`/`do_unref`/`has_ref`/ -// `get_destroyed`/`dispose`. -// -// Type-specific items (`init`, `do_refresh`, `close`, cached-prop get/set, -// `run_immediate_task`) go in a *second* `impl $T` block in the caller's file. +// - `impl Drop for $T` (unlinks from `All` before the box is freed) +// - `impl $T`: `init_with`/`constructor`/`finalize` and the forwarder +// host-fns `to_primitive`/`do_ref`/`do_unref`/`has_ref`/`get_destroyed`/ +// `dispose`. // // Paths in the body are written `super::…` / `::crate_name::…` because the // macro is invoked *from the child module* (`super::impl_timer_object!(…)`), @@ -61,62 +57,28 @@ macro_rules! impl_timer_object { #[derive(::bun_ptr::RefCounted)] pub struct $T { pub ref_count: ::bun_ptr::RefCount, - pub event_loop_timer: super::EventLoopTimer, + pub event_loop_timer: ::bun_ptr::JsCell, pub internals: super::TimerObjectInternals, + /// The ref held while this timer is scheduled — by the timer heap + /// for a `Timeout`, by the event loop's immediate queue for an + /// `Immediate`. Released when it fires for the last time or is + /// cancelled. + pub heap_ref: ::core::cell::Cell>>, } ::bun_event_loop::impl_timer_owner!($T; from_timer_ptr => event_loop_timer); - impl ::core::ops::Drop for $T { + impl Drop for $T { fn drop(&mut self) { - // SAFETY: last ref gone; JS thread with RuntimeState installed. - unsafe { self.internals.deinit() } - } - } - - impl ::core::default::Default for $T { - fn default() -> Self { - Self { - ref_count: ::bun_ptr::RefCount::init(), - // `init_paused`: next=EPOCH, state=PENDING, heap zeroed. - event_loop_timer: super::EventLoopTimer::init_paused( - super::EventLoopTimerTag::$tag, - ), - // Default-constructed here, then overwritten in `init()`. - internals: super::TimerObjectInternals::default(), - } + ::unschedule_for_drop(self); } } impl $T { - // Re-export the refcount mixin's ops as inherent fns so - // `TimerObjectInternals`'s `container_of` dispatch resolves. - - /// Increment the intrusive refcount. - /// - /// # Safety - /// `this` must point to a live, `heap::alloc`-allocated `Self`. - #[inline] - pub unsafe fn ref_(this: *mut Self) { - // SAFETY: caller contract. - unsafe { ::bun_ptr::RefCount::::ref_(this) } - } - - /// Decrement the intrusive refcount; on zero drops the `Box`. - /// After this returns `this` may dangle. - /// - /// # Safety - /// `this` must point to a live, `heap::alloc`-allocated `Self`. - #[inline] - pub unsafe fn deref(this: *mut Self) { - // SAFETY: caller contract. - unsafe { ::bun_ptr::RefCount::::deref(this) } - } - /// Shared body of `TimeoutObject::init` / `ImmediateObject::init`: - /// heap-allocate → `to_js_ptr` → `internals.init` → - /// inspector `did_schedule_async_call`. The per-type `init` fn - /// picks `kind`/`interval` and forwards here. + /// allocate → wrap in the JS cell → schedule → inspector + /// `did_schedule_async_call`. The per-type `init` fn picks + /// `kind`/`interval` and forwards here. pub fn init_with( global: &::bun_jsc::JSGlobalObject, id: i32, @@ -125,30 +87,25 @@ macro_rules! impl_timer_object { callback: ::bun_jsc::JSValue, arguments: ::bun_jsc::JSValue, ) -> ::bun_jsc::JSValue { - // Heap-allocate; `*mut Self` is the - // `m_ctx` payload of the codegen'd JSCell wrapper. Ownership - // transfers to the wrapper via `to_js_ptr`; freed by - // `deref → deinit → heap::take`. - let payload: *mut Self = - ::bun_core::heap::into_raw(::std::boxed::Box::new(Self::default())); - // SAFETY: `to_js_ptr` is the `#[JsClass]`-generated `*__create` - // shim; `payload` is a fresh heap allocation whose ownership - // transfers to the GC wrapper. - let js_value = unsafe { Self::to_js_ptr(payload, global) }; - // Round-trip ABI check. + let vm = global.bun_vm(); + let timer = ::bun_ptr::RefPtr::new(Self { + ref_count: ::bun_ptr::RefCount::init(), + event_loop_timer: ::bun_ptr::JsCell::new(super::EventLoopTimer::init_paused( + super::EventLoopTimerTag::$tag, + )), + internals: super::TimerObjectInternals::new(id, kind, interval, vm), + heap_ref: ::core::cell::Cell::new(None), + }); + // `timer`'s ref moves to the JS wrapper (released via `finalize`). + let js_value = Self::to_js_nonnull(timer.as_non_null(), global); debug_assert!( - ::from_js(js_value) == Some(payload), + ::from_js(js_value) == Some(timer.as_ptr()), concat!($js_name, "__create ABI mismatch"), ); + let this = timer.into_this_ptr(); let _keep = ::bun_jsc::EnsureStillAlive(js_value); - // SAFETY: `payload` was just allocated above and is exclusively - // owned here; `internals.init()` writes every field. - unsafe { - (*payload).internals.init( - js_value, global, id, kind, interval, callback, arguments, - ); - } - if global.bun_vm().as_mut().is_inspector_enabled() { + ::schedule(this, js_value, global, callback, arguments); + if vm.is_inspector_enabled() { ::bun_jsc::Debugger::did_schedule_async_call( global, ::bun_jsc::Debugger::AsyncCallType::DOMTimer, @@ -171,59 +128,62 @@ macro_rules! impl_timer_object { #[::bun_jsc::host_fn(method)] pub fn to_primitive( - this: &Self, + this: ::bun_ptr::ThisPtr, _global: &::bun_jsc::JSGlobalObject, _frame: &::bun_jsc::CallFrame, ) -> ::bun_jsc::JsResult<::bun_jsc::JSValue> { - this.internals.to_primitive() + ::to_primitive(this) } #[::bun_jsc::host_fn(method)] pub fn do_ref( - this: &Self, - global: &::bun_jsc::JSGlobalObject, + &self, + _global: &::bun_jsc::JSGlobalObject, frame: &::bun_jsc::CallFrame, ) -> ::bun_jsc::JsResult<::bun_jsc::JSValue> { - this.internals.do_ref(global, frame.this()) + ::do_ref(self, frame.this()) } #[::bun_jsc::host_fn(method)] pub fn do_unref( - this: &Self, - global: &::bun_jsc::JSGlobalObject, + &self, + _global: &::bun_jsc::JSGlobalObject, frame: &::bun_jsc::CallFrame, ) -> ::bun_jsc::JsResult<::bun_jsc::JSValue> { - this.internals.do_unref(global, frame.this()) + ::do_unref(self, frame.this()) } #[::bun_jsc::host_fn(method)] pub fn has_ref( - this: &Self, + &self, _global: &::bun_jsc::JSGlobalObject, _frame: &::bun_jsc::CallFrame, ) -> ::bun_jsc::JsResult<::bun_jsc::JSValue> { - this.internals.has_ref() + ::has_ref(self) } + /// `.classes.ts` `refCounted: true` — runs on the mutator thread + /// during lazy sweep, before the wrapper's ref is dropped. Do not + /// touch any `JSValue`/`Strong` content. pub fn finalize(&self) { - self.internals.finalize() + self.internals.this_value.with_mut(|r| r.finalize()) } #[::bun_jsc::host_fn(getter)] pub fn get_destroyed( - this: &Self, + &self, _global: &::bun_jsc::JSGlobalObject, ) -> ::bun_jsc::JsResult<::bun_jsc::JSValue> { - Ok(::bun_jsc::JSValue::from(this.internals.get_destroyed())) + Ok(::bun_jsc::JSValue::from(::get_destroyed(self))) } #[::bun_jsc::host_fn(method)] pub fn dispose( - this: &Self, - global: &::bun_jsc::JSGlobalObject, + this: ::bun_ptr::ThisPtr, + _global: &::bun_jsc::JSGlobalObject, _frame: &::bun_jsc::CallFrame, ) -> ::bun_jsc::JsResult<::bun_jsc::JSValue> { - this.internals.cancel(global.bun_vm_ptr()); + ::cancel(this); Ok(::bun_jsc::JSValue::UNDEFINED) } } @@ -238,162 +198,69 @@ pub mod timeout_object; pub mod immediate_object; #[path = "DateHeaderTimer.rs"] -mod date_header_timer_draft; +pub mod date_header_timer; #[path = "EventLoopDelayMonitor.rs"] -mod event_loop_delay_monitor_draft; - -// ─── TimerHeap ─────────────────────────────────────────────────────────────── -// Real intrusive pairing-heap (meld/remove/combine_siblings) implemented in -// `bun_io::heap::Intrusive`. `EventLoopTimer` now embeds the real -// `bun_io::heap::IntrusiveField` and impls `HeapNode` in its defining crate -// (`bun_event_loop`), so the orphan-rule block is gone. `TimerHeap` is a thin -// newtype that adapts `*mut T` ↔ `Option<*mut T>` for the existing call-sites -// (`All::insert/remove/next/get_timeout`). - -/// Stateless context for the heap comparator. -#[derive(Default)] -pub(crate) struct TimerHeapCtx; - -impl bun_io::heap::HeapContext for TimerHeapCtx { - #[inline] - unsafe fn less(&self, a: *mut EventLoopTimer, b: *mut EventLoopTimer) -> bool { - // SAFETY: `Intrusive` only ever calls `less` with non-null nodes that - // are live members of the heap (caller invariant on insert/meld). - EventLoopTimer::less((), unsafe { &*a }, unsafe { &*b }) - } -} - -#[derive(Default)] -pub struct TimerHeap(bun_io::heap::Intrusive); - -impl TimerHeap { - #[inline] - pub(crate) fn peek(&self) -> Option<*mut EventLoopTimer> { - let r = self.0.peek(); - if r.is_null() { None } else { Some(r) } - } - - /// # Safety - /// `v` is a valid, exclusively-owned node not currently in any heap - /// (its `IntrusiveField` links are null). - #[inline] - unsafe fn insert(&mut self, v: *mut EventLoopTimer) { - // SAFETY: forwarded — see fn contract. - unsafe { self.0.insert(v) }; - } - - /// # Safety - /// `v` is a node currently in *this* heap. - #[inline] - unsafe fn remove(&mut self, v: *mut EventLoopTimer) { - // SAFETY: forwarded — see fn contract. - unsafe { self.0.remove(v) }; - } - - #[inline] - pub(crate) fn delete_min(&mut self) -> Option<*mut EventLoopTimer> { - // SAFETY: all reachable nodes were inserted via `insert()` and remain - // live until popped (intrusive invariant maintained by `All`). - let r = unsafe { self.0.delete_min() }; - if r.is_null() { None } else { Some(r) } - } +pub mod event_loop_delay_monitor; - #[inline] - pub(crate) fn find_max(&self) -> Option<*mut EventLoopTimer> { - // SAFETY: all reachable nodes were inserted via `insert()` and remain - // live for the heap's lifetime (intrusive invariant maintained by `All`). - let r = unsafe { self.0.find_max() }; - if r.is_null() { None } else { Some(r) } - } - - #[inline] - pub(crate) fn count(&self) -> usize { - // SAFETY: all reachable nodes were inserted via `insert()` and remain - // live for the heap's lifetime (intrusive invariant maintained by `All`). - unsafe { self.0.count() } - } -} +/// `clearTimeout(id)` lookup table: the object's `Drop` removes its entry, so +/// every `BackRef` in here points at a live timer. +pub(crate) type IdMap = ArrayHashMap>; /// i32 is exposed to JavaScript and can be used with clearTimeout, clearInterval, etc. -pub(crate) type TimeoutMap = ArrayHashMap; - #[derive(Default)] pub struct Maps { - pub(crate) set_timeout: TimeoutMap, - pub(crate) set_interval: TimeoutMap, - pub(crate) set_immediate: TimeoutMap, -} - -impl Maps { - #[inline] - fn get(&mut self, kind: Kind) -> &mut TimeoutMap { - match kind { - Kind::SetTimeout => &mut self.set_timeout, - Kind::SetInterval => &mut self.set_interval, - Kind::SetImmediate => &mut self.set_immediate, - } - } + pub(crate) set_timeout: IdMap, + pub(crate) set_interval: IdMap, + pub(crate) set_immediate: IdMap, } // ─── FakeTimers ────────────────────────────────────────────────────────────── // Real definition lives in `runtime/test_runner/timers/FakeTimers.rs` and -// depends on `TimerHeap` (defined above). Now that `pub mod test_runner` is -// declared in lib.rs, re-export so `All.fake_timers` and the test_runner +// depends on `TimerHeap`. Re-export so `All.fake_timers` and the test_runner // host fns see the same nominal type. pub(crate) use crate::test_runner::timers::fake_timers::FakeTimers; -// ─── DateHeaderTimer / EventLoopDelayMonitor (struct-only) ─────────────────── -// Method bodies (`enable`/`run`) call `vm.timer.*` and `vm.uws_loop()` which -// need `VirtualMachine.timer: All` (currently `()` in bun_jsc). Struct shape -// is real so `All` embeds them by value with the correct layout. +// ─── DateHeaderTimer / EventLoopDelayMonitor ───────────────────────────────── pub struct DateHeaderTimer { - pub(crate) event_loop_timer: EventLoopTimer, + pub(crate) event_loop_timer: JsCell, } +bun_event_loop::impl_timer_owner!(DateHeaderTimer; from_timer_ptr => event_loop_timer); impl Default for DateHeaderTimer { fn default() -> Self { Self { - event_loop_timer: EventLoopTimer::init_paused(EventLoopTimerTag::DateHeaderTimer), + event_loop_timer: JsCell::new(EventLoopTimer::init_paused( + EventLoopTimerTag::DateHeaderTimer, + )), } } } impl DateHeaderTimer { #[inline] - fn timer_all() -> *mut All { - crate::jsc_hooks::timer_all() + fn timer_ref(&self) -> TimerRef { + TimerRef::new(self, |t| &t.event_loop_timer) } /// Refresh the cached `Date:` header and /// reschedule for 1s later iff there are active connections. - pub(crate) fn run(&mut self, vm: &mut bun_jsc::virtual_machine::VirtualMachine) { - self.event_loop_timer.state = EventLoopTimerState::FIRED; - // `uws_loop_mut` is the audited safe accessor (loop owned by the VM, - // separate allocation from `RuntimeState.timer` so no aliasing with - // `&mut self`). + pub(crate) fn run(&self, vm: &VirtualMachine, all: &All) { + self.event_loop_timer + .with_mut(|t| t.state = EventLoopTimerState::FIRED); let loop_ = vm.uws_loop_mut(); let now = Timespec::now(TimespecMockMode::ForceRealTime); // Record when we last ran it. - self.event_loop_timer.next = ElTimespec { - sec: now.sec, - nsec: now.nsec, - }; + self.event_loop_timer.with_mut(|t| t.next = now); // updateDate() is an expensive function. loop_.update_date(); if loop_.internal_loop_data.sweep_timer_count > 0 { // Reschedule it automatically for 1 second later. - let next = now.add_ms(1000); - self.event_loop_timer.next = ElTimespec { - sec: next.sec, - nsec: next.nsec, - }; - let elt: *mut EventLoopTimer = &raw mut self.event_loop_timer; - // SAFETY: single JS thread; nothing `All::insert` touches - // overlaps `date_header_timer`, which `self` aliases. - unsafe { (*Self::timer_all()).insert(elt) }; + self.event_loop_timer + .with_mut(|t| t.next = now.add_ms(1000)); + all.insert(self.timer_ref()); } } } @@ -401,90 +268,79 @@ impl DateHeaderTimer { pub struct EventLoopDelayMonitor { /// Weak, so a leaked monitor does not pin the retired `--isolate` realm. /// `stop_active_handles` drops it before `~VM` (`All` outlives the heap). - histogram: bun_jsc::Weak<()>, - pub(crate) event_loop_timer: EventLoopTimer, - pub(crate) resolution_ms: i32, - pub(crate) last_fire_ns: u64, - pub(crate) enabled: bool, + histogram: JsCell>, + pub(crate) event_loop_timer: JsCell, + pub(crate) resolution_ms: Cell, + pub(crate) last_fire_ns: Cell, + pub(crate) enabled: Cell, } +bun_event_loop::impl_timer_owner!(EventLoopDelayMonitor; from_timer_ptr => event_loop_timer); impl Default for EventLoopDelayMonitor { fn default() -> Self { Self { - histogram: bun_jsc::Weak::default(), - event_loop_timer: EventLoopTimer::init_paused(EventLoopTimerTag::EventLoopDelayMonitor), - resolution_ms: 10, - last_fire_ns: 0, - enabled: false, + histogram: JsCell::new(bun_jsc::Weak::default()), + event_loop_timer: JsCell::new(EventLoopTimer::init_paused( + EventLoopTimerTag::EventLoopDelayMonitor, + )), + resolution_ms: Cell::new(10), + last_fire_ns: Cell::new(0), + enabled: Cell::new(false), } } } impl EventLoopDelayMonitor { #[inline] - fn timer_all() -> *mut All { - crate::jsc_hooks::timer_all() + fn timer_ref(&self) -> TimerRef { + TimerRef::new(self, |t| &t.event_loop_timer) } - fn enable( - &mut self, - vm: &mut bun_jsc::virtual_machine::VirtualMachine, - histogram: JSValue, - resolution_ms: i32, - ) { - self.disable(); - self.histogram = bun_jsc::Weak::create_passive(histogram, vm.global()); - self.resolution_ms = resolution_ms; - self.enabled = true; + fn enable(&self, vm: &VirtualMachine, all: &All, histogram: JSValue, resolution_ms: i32) { + self.disable(all); + self.histogram + .set(bun_jsc::Weak::create_passive(histogram, vm.global())); + self.resolution_ms.set(resolution_ms); + self.enabled.set(true); // Schedule timer let now = Timespec::now(TimespecMockMode::ForceRealTime); - let next = now.add_ms(i64::from(resolution_ms)); - self.event_loop_timer.next = ElTimespec { - sec: next.sec, - nsec: next.nsec, - }; - let elt: *mut EventLoopTimer = &raw mut self.event_loop_timer; - // SAFETY: single JS thread; nothing `All::insert` touches overlaps - // `event_loop_delay`, which `self` aliases. - unsafe { (*Self::timer_all()).insert(elt) }; + self.event_loop_timer + .with_mut(|t| t.next = now.add_ms(i64::from(resolution_ms))); + all.insert(self.timer_ref()); } - pub(crate) fn disable(&mut self) { - if !self.enabled { + pub(crate) fn disable(&self, all: &All) { + if !self.enabled.get() { return; } - self.enabled = false; - self.histogram = bun_jsc::Weak::default(); - self.last_fire_ns = 0; + self.enabled.set(false); + self.histogram.set(bun_jsc::Weak::default()); + self.last_fire_ns.set(0); // FIRED (not linked) when called from `on_fire`. - if self.event_loop_timer.state == EventLoopTimerState::ACTIVE { - let elt: *mut EventLoopTimer = &raw mut self.event_loop_timer; - // SAFETY: see `enable` — disjoint-field access on `All`. - unsafe { (*Self::timer_all()).remove(elt) }; + if self.event_loop_timer.get().state == EventLoopTimerState::ACTIVE { + all.remove(self.timer_ref()); } } /// Record `now - last_fire_ns` /// into the JS histogram and reschedule. - pub(crate) fn on_fire( - &mut self, - _vm: &mut bun_jsc::virtual_machine::VirtualMachine, - now: &bun_event_loop::EventLoopTimer::Timespec, - ) { - self.event_loop_timer.state = EventLoopTimerState::FIRED; - if !self.enabled { + pub(crate) fn on_fire(&self, now: &Timespec, all: &All) { + self.event_loop_timer + .with_mut(|t| t.state = EventLoopTimerState::FIRED); + if !self.enabled.get() { return; } - let Some(histogram) = self.histogram.get() else { - self.disable(); + let Some(histogram) = self.histogram.get().get() else { + self.disable(all); return; }; let now_ns = now.ns(); - if self.last_fire_ns > 0 { - let expected_ns = u64::try_from(self.resolution_ms) + let last_fire_ns = self.last_fire_ns.get(); + if last_fire_ns > 0 { + let expected_ns = u64::try_from(self.resolution_ms.get()) .expect("int cast") .saturating_mul(1_000_000); - let actual_ns = now_ns - self.last_fire_ns; + let actual_ns = now_ns - last_fire_ns; if actual_ns > expected_ns { let delay_ns = @@ -499,81 +355,28 @@ impl EventLoopDelayMonitor { } } - self.last_fire_ns = now_ns; + self.last_fire_ns.set(now_ns); // Reschedule - let next = Timespec { - sec: now.sec, - nsec: now.nsec, - } - .add_ms(i64::from(self.resolution_ms)); - self.event_loop_timer.next = ElTimespec { - sec: next.sec, - nsec: next.nsec, - }; - let elt: *mut EventLoopTimer = &raw mut self.event_loop_timer; - // SAFETY: see `enable` — disjoint-field access on `All`. - unsafe { (*Self::timer_all()).insert(elt) }; + let next = now.add_ms(i64::from(self.resolution_ms.get())); + self.event_loop_timer.with_mut(|t| t.next = next); + all.insert(self.timer_ref()); } } // ─── TimerObjectInternals / TimeoutObject / ImmediateObject ───────────────── pub mod timer_object_internals; -pub use timer_object_internals::{Flags as TimerFlags, TimerObjectInternals}; +pub use timer_object_internals::{Flags as TimerFlags, TimerObject, TimerObjectInternals}; /// `jsc.WebCore.AbortSignal.Timeout` — real struct lives in `bun_jsc` (which -/// this crate depends on). Re-exported here so `All::update`'s -/// field-parent-pointer epoch-bump and `dispatch::fire_timer` resolve the same +/// this crate depends on). Re-exported here so `dispatch` resolves the same /// `event_loop_timer`/`flags` offsets the low tier wrote. pub use crate::jsc::abort_signal::Timeout as AbortSignalTimeout; pub use self::immediate_object::ImmediateObject; pub use self::timeout_object::TimeoutObject; -/// Recover the -/// [`TimerFlags`] slot for the three JS-timer container tags -/// (`TimeoutObject` / `ImmediateObject` / `AbortSignalTimeout`), else `None`. -/// -/// Returns a raw `NonNull` so the caller decides read vs. write: -/// [`EventLoopTimer::less`] reads `.epoch()` on the heap-compare hot path; -/// [`All::update`] writes `.set_epoch()` on the JS thread. The two -/// `internals.flags` arms store `Cell`; `Cell` is -/// `#[repr(transparent)]` so the `addr_of!` → `.cast()` is layout-sound. -/// -/// # Safety -/// `t` points at a live [`EventLoopTimer`] whose `tag` was set at -/// construction and never re-tagged (the JS-timer-tag invariant). When the -/// tag matches, `t` is the `event_loop_timer` field of the named container -/// with whole-container provenance. -#[inline] -pub(crate) unsafe fn js_timer_flags_ptr( - t: *const EventLoopTimer, -) -> Option> { - use core::ptr::{NonNull, addr_of}; - // SAFETY: caller contract — `t` is live; tag invariant per fn docs. - unsafe { - let p: *const TimerFlags = match (*t).tag { - EventLoopTimerTag::TimeoutObject => { - let parent = TimeoutObject::from_timer_ptr(t); - addr_of!((*parent).internals.flags).cast() - } - EventLoopTimerTag::ImmediateObject => { - let parent = ImmediateObject::from_timer_ptr(t); - addr_of!((*parent).internals.flags).cast() - } - // `AbortSignal.Timeout` stores - // `flags` directly (not under `.internals`, not `Cell`-wrapped). - EventLoopTimerTag::AbortSignalTimeout => { - let parent = AbortSignalTimeout::from_timer_ptr(t); - addr_of!((*parent).flags) - } - _ => return None, - }; - Some(NonNull::new_unchecked(p.cast_mut())) - } -} - /// A timer created by WTF code and invoked by Bun's event loop. #[path = "WTFTimer.rs"] pub mod wtf_timer; @@ -581,26 +384,33 @@ pub(crate) use wtf_timer::WTFTimer; // ─── All ───────────────────────────────────────────────────────────────────── +/// The per-VM timer state. Lives in `RuntimeState` (boxed, JS-thread-owned, +/// stable address for the VM's lifetime) and is reached through +/// [`crate::jsc_hooks::timer_all`]. Every method takes `&self`: timer +/// callbacks re-enter this struct (a `setInterval` callback calling +/// `clearTimeout`, `refresh()`, `setTimeout`, …), so state is held in +/// `Cell`/`JsCell` and no `&mut All` ever exists. Only `wtf_timers` is touched +/// off the JS thread (under its lock). pub(crate) struct All { - pub(crate) last_id: i32, - pub(crate) thread_id: std::thread::ThreadId, + last_id: Cell, + thread_id: std::thread::ThreadId, pub(crate) timers: TimerHeap, - pub(crate) active_timer_count: i32, + active_timer_count: Cell, #[cfg(windows)] - pub(crate) uv_timer: bun_sys::windows::libuv::Timer, + uv_timer: JsCell, /// Whether we have emitted a warning for passing a negative timeout duration - pub(crate) warned_negative_number: bool, + warned_negative_number: Cell, /// Whether we have emitted a warning for passing NaN for the timeout duration - pub(crate) warned_not_number: bool, + warned_not_number: Cell, /// Incremented when timers are scheduled or rescheduled. See - /// TimerObjectInternals.epoch. Masked to 25 bits on increment. - pub(crate) epoch: u32, - pub(crate) immediate_ref_count: i32, + /// TimerFlags.epoch. Masked to 25 bits on increment. + epoch: Cell, + immediate_ref_count: Cell, #[cfg(windows)] - pub(crate) uv_idle: bun_sys::windows::libuv::uv_idle_t, + uv_idle: JsCell, pub(crate) event_loop_delay: EventLoopDelayMonitor, pub(crate) fake_timers: FakeTimers, - pub(crate) maps: Maps, + pub(crate) maps: JsCell, pub(crate) date_header_timer: DateHeaderTimer, pub(crate) wtf_timers: Guarded, } @@ -608,26 +418,40 @@ pub(crate) struct All { impl All { pub(crate) fn init() -> Self { Self { - last_id: 1, + last_id: Cell::new(1), thread_id: std::thread::current().id(), - timers: TimerHeap::default(), - active_timer_count: 0, + timers: TimerHeap::new(InHeap::Regular), + active_timer_count: Cell::new(0), #[cfg(windows)] - uv_timer: bun_core::ffi::zeroed(), - warned_negative_number: false, - warned_not_number: false, - epoch: 0, - immediate_ref_count: 0, + uv_timer: JsCell::new(bun_core::ffi::zeroed()), + warned_negative_number: Cell::new(false), + warned_not_number: Cell::new(false), + epoch: Cell::new(0), + immediate_ref_count: Cell::new(0), #[cfg(windows)] - uv_idle: bun_core::ffi::zeroed(), + uv_idle: JsCell::new(bun_core::ffi::zeroed()), event_loop_delay: EventLoopDelayMonitor::default(), fake_timers: FakeTimers::default(), - maps: Maps::default(), + maps: JsCell::new(Maps::default()), date_header_timer: DateHeaderTimer::default(), - wtf_timers: Guarded::init(TimerHeap::default()), + wtf_timers: Guarded::init(TimerHeap::new(InHeap::Wtf)), } } + /// Hand out the next JS-visible timer id. + #[inline] + pub(crate) fn next_id(&self) -> i32 { + let id = self.last_id.get(); + self.last_id.set(id.wrapping_add(1)); + id + } + + /// The epoch a freshly created JS timer starts with (see [`TimerFlags`]). + #[inline] + pub(crate) fn epoch(&self) -> u32 { + self.epoch.get() + } + #[inline] fn assert_js_thread(&self) { debug_assert!( @@ -636,37 +460,25 @@ impl All { ); } - #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub(crate) fn insert(&mut self, timer: *mut EventLoopTimer) { + pub(crate) fn insert(&self, timer: TimerRef) { self.assert_js_thread(); - // SAFETY: caller guarantees `timer` is a valid live EventLoopTimer. - let tag = unsafe { (*timer).tag }; + let tag = timer.tag(); debug_assert!(tag != EventLoopTimerTag::WTFTimer, "use wtf_arm"); // Bump the global epoch into the per-timer flags so equal-deadline JS // timers (setTimeout/setInterval/AbortSignal.timeout) fire in insertion // order. Before heap insert: `EventLoopTimer::less` reads epoch as tiebreak. - // SAFETY: `timer` is live (caller contract). - if let Some(flags) = unsafe { js_timer_flags_ptr(timer) } { - self.epoch = self.epoch.wrapping_add(1) & ((1u32 << 25) - 1); - // SAFETY: `flags` points into the live container recovered above. - unsafe { (*flags.as_ptr()).set_epoch(self.epoch) }; + let next_epoch = self.epoch.get().wrapping_add(1) & ((1u32 << 25) - 1); + if crate::dispatch::set_js_timer_epoch(timer, next_epoch) { + self.epoch.set(next_epoch); } if self.fake_timers.is_active() && tag.allow_fake_timers() { - // SAFETY: see fn contract - unsafe { - self.fake_timers.timers.insert(timer); - (*timer).state = EventLoopTimerState::ACTIVE; - (*timer).in_heap = InHeap::Fake; - } + self.fake_timers.timers.insert(timer); + timer.set_state(EventLoopTimerState::ACTIVE); } else { - // SAFETY: see fn contract - unsafe { - self.timers.insert(timer); - (*timer).state = EventLoopTimerState::ACTIVE; - (*timer).in_heap = InHeap::Regular; - } + self.timers.insert(timer); + timer.set_state(EventLoopTimerState::ACTIVE); #[cfg(windows)] self.ensure_uv_timer(); } @@ -678,17 +490,21 @@ impl All { /// handle queue when the teardown closes the loop — before this struct's /// storage is freed. #[cfg(windows)] - pub(crate) fn close_loop_handles_for_vm_teardown(&mut self) { - unsafe extern "C" fn timer_closed(_: *mut uv::Timer) {} - unsafe extern "C" fn idle_closed(_: *mut uv::uv_idle_t) {} - if !self.uv_timer.data.is_null() { - self.uv_timer.stop(); - self.uv_timer.close(timer_closed); - } - if !self.uv_idle.data.is_null() { - self.uv_idle.stop(); - self.uv_idle.close(idle_closed); - } + pub(crate) fn close_loop_handles_for_vm_teardown(&self) { + extern "C" fn timer_closed(_: *mut uv::Timer) {} + extern "C" fn idle_closed(_: *mut uv::uv_idle_t) {} + self.uv_timer.with_mut(|timer| { + if !timer.data.is_null() { + timer.stop(); + timer.close(timer_closed); + } + }); + self.uv_idle.with_mut(|idle| { + if !idle.data.is_null() { + idle.stop(); + idle.close(idle_closed); + } + }); } /// Lazily `uv_timer_init` the @@ -696,7 +512,7 @@ impl All { /// across both heaps. On Windows there is no epoll/kqueue fallback; this /// `uv_timer_t` is the ONLY thing that wakes `uv_run` for JS timers. #[cfg(windows)] - fn ensure_uv_timer(&mut self) { + fn ensure_uv_timer(&self) { // `vm` here means the OWNING VM (the one this timer is embedded in), // not the calling thread's. Guard the TLS fallback so a cross-thread // caller fails loudly instead of silently arming a fresh `uv_loop_t` @@ -705,41 +521,26 @@ impl All { self.thread_id == std::thread::current().id(), "ensure_uv_timer: called off the owning JS thread; TLS loop/VM would diverge from vm.event_loop_handle", ); - if self.uv_timer.data.is_null() { - self.uv_timer.init(uv::Loop::get()); - self.uv_timer.data = - bun_jsc::virtual_machine::VirtualMachine::get_mut_ptr().cast::(); - self.uv_timer.unref(); - } - debug_assert!( - !self.uv_timer.is_closing(), - "timer scheduled after teardown closed the heap's uv timer" - ); - - let reg_next = self.timers.peek().map(|timer| { - // SAFETY: `peek` returns a live heap node. - let next = unsafe { &(*timer).next }; - Timespec { - sec: next.sec, - nsec: next.nsec, - } - }); - let wtf_next = self.wtf_timers.lock().peek().map(|timer| { - // SAFETY: `peek` returns a live heap node. - let next = unsafe { &(*timer).next }; - Timespec { - sec: next.sec, - nsec: next.nsec, - } + self.uv_timer.with_mut(|timer| { + if timer.data.is_null() { + timer.init(uv::Loop::get()); + // `data` is only a non-null "initialized" sentinel. + timer.data = VirtualMachine::get_mut_ptr().cast::(); + timer.unref(); + } + debug_assert!( + !timer.is_closing(), + "timer scheduled after teardown closed the heap's uv timer" + ); }); + + let reg_next = self.timers.peek().map(TimerRef::next); + let wtf_next = self.wtf_timers.lock().peek().map(TimerRef::next); let Some(next_ts) = Self::soonest(reg_next, wtf_next) else { return; }; - // SAFETY: `uv_timer.data` is non-null past the lazy-init block, so - // `uv_timer_init` has run and the handle's `loop` field points at - // the owning VM's live `uv_loop_t` (== `vm.uvLoop()` per spec). - unsafe { uv::uv_update_time(self.uv_timer.get_loop()) }; + self.uv_timer.get().update_loop_time(); let now = Timespec::now(TimespecMockMode::ForceRealTime); let wait = if next_ts.greater(&now) { next_ts.duration(&now) @@ -751,116 +552,72 @@ impl All { // https://github.com/nodejs/node/blob/f552c86fecd6c2ba9e832ea129b731dd63abdbe2/src/env.cc#L1512 let wait_ms = core::cmp::max(1, wait.ms_unsigned()); - // SAFETY: `uv_timer_init` ran above; the handle is live. - let due_in = unsafe { uv::uv_timer_get_due_in(&self.uv_timer) }; - // Restarting an overdue handle shifts the wakeup out by 1ms. Done - // on every insert, the already-due callback never runs. - if !(self.uv_timer.is_active() && due_in <= wait_ms) { - self.uv_timer.start(wait_ms, 0, Some(Self::on_uv_timer)); - } + let active_timer_count = self.active_timer_count.get(); + self.uv_timer.with_mut(|timer| { + // Restarting an overdue handle shifts the wakeup out by 1ms. Done + // on every insert, the already-due callback never runs. + if !(timer.is_active() && timer.get_due_in() <= wait_ms) { + timer.start(wait_ms, 0, Some(Self::on_uv_timer)); + } - if self.active_timer_count > 0 { - self.uv_timer.ref_(); - } else { - self.uv_timer.unref(); - } + if active_timer_count > 0 { + timer.ref_(); + } else { + timer.unref(); + } + }); } /// libuv timer callback; drain due - /// timers then re-arm for the next deadline. Only ever invoked by libuv - /// (coerces to the `uv_timer_cb` fn-pointer type at the `Timer::start` - /// call site); body wraps its derefs explicitly. + /// timers then re-arm for the next deadline. Only ever invoked by libuv on + /// the loop's (= this `All`'s) thread, so the handle pointer is not needed: + /// the thread's `All` is the one that armed it. #[cfg(windows)] - extern "C" fn on_uv_timer(uv_timer_t: *mut uv::Timer) { - // SAFETY: `uv_timer_t` is the address of `All.uv_timer` (libuv passes - // back exactly the handle pointer we registered in `ensure_uv_timer`); - // recover the containing `All` via container_of. - let all: *mut All = unsafe { bun_core::from_field_ptr!(All, uv_timer, uv_timer_t) }; - // SAFETY: `data` was set to the VM ptr in `ensure_uv_timer` (non-null). - let vm: *mut () = unsafe { (*uv_timer_t).data.cast() }; - // SAFETY: callback fires on the JS thread (libuv invokes on the loop's - // thread); `all` is live for the VM lifetime. `drain_timers` may - // re-enter `(*runtime_state()).timer` — it forms only short-lived - // `&mut All` around heap pop/peek, so the raw-ptr deref here is sound. - unsafe { (*all).drain_timers(vm) }; - // SAFETY: see above; re-arm for the next-soonest deadline (if any). - unsafe { (*all).ensure_uv_timer() }; + extern "C" fn on_uv_timer(_: *mut uv::Timer) { + let all = crate::jsc_hooks::timer_all(); + all.drain_timers(VirtualMachine::get()); + all.ensure_uv_timer(); } - #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub(crate) fn remove(&mut self, timer: *mut EventLoopTimer) { + pub(crate) fn remove(&self, timer: TimerRef) { self.assert_js_thread(); - // SAFETY: caller guarantees `timer` is a valid live EventLoopTimer. - // Note (§Forbidden aliased-&mut): `TimerHeap::remove` forms a - // fresh `&mut EventLoopTimer` via `(*v).heap()` for the same - // allocation, so we must NOT hold a `&mut *timer` across that call. - // Read `in_heap` and write the post-remove bookkeeping via raw deref. - match unsafe { (*timer).in_heap } { - InHeap::None => { - // can't remove a timer that was not inserted - debug_assert!(false); + match timer.in_heap() { + InHeap::Regular => self.timers.remove(timer), + InHeap::Fake => self.fake_timers.timers.remove(timer), + // can't remove a timer that was not inserted + InHeap::None | InHeap::Wtf => { + debug_assert!(false, "remove: timer is in {:?}", timer.in_heap()) } - // SAFETY: timer is in `self.timers` per `in_heap` - InHeap::Regular => unsafe { self.timers.remove(timer) }, - // SAFETY: timer is in `self.fake_timers.timers` per `in_heap` - InHeap::Fake => unsafe { self.fake_timers.timers.remove(timer) }, - } - // SAFETY: `timer` is still a valid live EventLoopTimer. - unsafe { - (*timer).in_heap = InHeap::None; - (*timer).state = EventLoopTimerState::CANCELLED; } + timer.set_state(EventLoopTimerState::CANCELLED); } /// Remove the EventLoopTimer if necessary, then re-insert at `time`. - /// - /// # Safety - /// `timer` must point to a live `EventLoopTimer` with whole-container - /// provenance for its tag (see [`js_timer_flags_ptr`]). - #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub(crate) fn update(&mut self, timer: *mut EventLoopTimer, time: &Timespec) { + pub(crate) fn update(&self, timer: TimerRef, time: &Timespec) { self.assert_js_thread(); - // SAFETY: caller guarantees `timer` is a valid live EventLoopTimer. - // Read `state` via raw deref so we don't hold a `&mut *timer` across - // `remove` (which also `&mut`-derefs the same pointer); overlapping - // `&mut` is UB under Stacked Borrows. - if unsafe { (*timer).state } == EventLoopTimerState::ACTIVE { + if timer.in_heap() != InHeap::None { self.remove(timer); } - // SAFETY: `timer` is still a valid live EventLoopTimer; safe to derive - // an exclusive reference now that no other borrow is outstanding. - // `time` cannot alias `timer.next`: `time` is a `&bun_core::Timespec` - // while `next` is `ElTimespec` — distinct types, so safe code cannot - // construct the alias. Re-add a - // `debug_assert!(!core::ptr::eq(time as *const _ as *const u8, &raw const (*timer).next as *const u8))` - // when the Timespec types unify (see the ElTimespec alias note at the - // top of this file). - let timer_ref = unsafe { &mut *timer }; - timer_ref.next.sec = time.sec; - timer_ref.next.nsec = time.nsec; + timer.set_next(*time); // `insert` bumps the global epoch and writes it into the per-timer // flags so equal-deadline JS timers fire in refresh order. self.insert(timer); } - #[allow(clippy::not_unsafe_ptr_arg_deref)] - fn wtf_arm(&mut self, timer: *mut EventLoopTimer, time: &Timespec) { - // SAFETY: caller guarantees `timer` is a valid live EventLoopTimer. - debug_assert!(unsafe { (*timer).tag } == EventLoopTimerTag::WTFTimer); + /// (Re)arm a `WTFTimer`. Any thread. + fn wtf_arm(&self, timer: TimerRef, time: Timespec) { + debug_assert!(timer.tag() == EventLoopTimerTag::WTFTimer); { - let mut wtf = self.wtf_timers.lock(); - // SAFETY: `timer` is live; its state and heap links only change under this guard. - unsafe { - if (*timer).state == EventLoopTimerState::ACTIVE { - wtf.remove(timer); - } - (*timer).next.sec = time.sec; - (*timer).next.nsec = time.nsec; - wtf.insert(timer); - (*timer).state = EventLoopTimerState::ACTIVE; + let wtf = self.wtf_timers.lock(); + // The slot's state and heap links only change under this guard. + if timer.state() == EventLoopTimerState::ACTIVE { + wtf.remove(timer); } + timer.set_next(time); + wtf.insert(timer); + timer.set_state(EventLoopTimerState::ACTIVE); } #[cfg(windows)] if self.thread_id == std::thread::current().id() { @@ -868,59 +625,41 @@ impl All { } } - #[allow(clippy::not_unsafe_ptr_arg_deref)] - fn wtf_disarm(&mut self, timer: *mut EventLoopTimer) { - // SAFETY: caller guarantees `timer` is a valid live EventLoopTimer. - debug_assert!(unsafe { (*timer).tag } == EventLoopTimerTag::WTFTimer); - let mut wtf = self.wtf_timers.lock(); - // SAFETY: `timer` is live; its state and heap links only change under this guard. - unsafe { - if (*timer).state == EventLoopTimerState::ACTIVE { - wtf.remove(timer); - (*timer).state = EventLoopTimerState::CANCELLED; - } + /// Disarm a `WTFTimer`; no-op if it is not linked. Any thread. + fn wtf_disarm(&self, timer: TimerRef) { + debug_assert!(timer.tag() == EventLoopTimerTag::WTFTimer); + let wtf = self.wtf_timers.lock(); + // The slot's state and heap links only change under this guard. + if timer.state() == EventLoopTimerState::ACTIVE { + wtf.remove(timer); + timer.set_state(EventLoopTimerState::CANCELLED); } } - unsafe fn drain_due_wtf_timers( - this: *mut Self, + fn drain_due_wtf_timers( + &self, maybe_now: &mut Option, - vm: *mut (), + vm: &VirtualMachine, ) -> Option { loop { - let min = { - // SAFETY: `this` is live; the guard drops before `fire`. - let mut wtf = unsafe { &(*this).wtf_timers }.lock(); - let min = wtf.peek()?; - // SAFETY: `peek` returned a live heap node. - let min_next = unsafe { - Timespec { - sec: (*min).next.sec, - nsec: (*min).next.nsec, - } - }; + let (min, now) = { + // The guard drops before `fire`. + let wtf = self.wtf_timers.lock(); + let min_next = wtf.peek()?.next(); let now = *maybe_now .get_or_insert_with(|| Timespec::now(TimespecMockMode::ForceRealTime)); if min_next.greater(&now) { return Some(min_next); } let min = wtf.delete_min().expect("peek succeeded"); - // SAFETY: `min` is the node `peek` returned above. - unsafe { (*min).state = EventLoopTimerState::FIRED }; - min + min.set_state(EventLoopTimerState::FIRED); + (min, now) }; - let now = maybe_now.expect("set before the pop"); - let el_now = ElTimespec { - sec: now.sec, - nsec: now.nsec, - }; - // SAFETY: `min` is live; no guard or borrow of `All` is held here. - let fired = unsafe { EventLoopTimer::fire(min, &el_now, vm) }; + let fired = crate::dispatch::fire_timer(min, &now, vm); // WTF timers run JSC-internal work, not user JS; a stop found here // is the loop's to act on at its next gate, and the heap's next // deadline is still reported to the poll. - // SAFETY: `vm` is the erased per-thread VM per fn contract. - let _ = unsafe { fold_timer(vm, fired) }; + let _ = fold_timer(vm, fired); } } @@ -936,23 +675,15 @@ impl All { /// Returns `true` if `spec` was written. `now_out` receives the monotonic reading this /// took, if any, for the caller to share with the tick (see `NOW_NS_UNKNOWN`). /// - /// Note (b2): `vm` is erased per §Dispatch (the caller is in - /// `bun_jsc::event_loop` which can't name `bun_runtime`). The two reads - /// it needs — `event_loop.immediate_tasks.len()` and the QUIC tick — are - /// passed in pre-computed until the cycle is broken. - /// - /// # Safety - /// `vm` is the erased `*mut VirtualMachine` for the calling JS thread and - /// must remain live across any `EventLoopTimer::fire` re-entry. - // Forwards `vm` to `__bun_fire_timer` without dereferencing it; - // not_unsafe_ptr_arg_deref is a false positive on opaque-token forwarding. - #[allow(clippy::not_unsafe_ptr_arg_deref)] + /// Note (b2): the caller is in `bun_jsc::event_loop`, which can't name + /// `bun_runtime`. The two reads it needs — `event_loop.immediate_tasks.len()` + /// and the QUIC tick — are passed in pre-computed until the cycle is broken. pub(crate) fn get_timeout( - &mut self, + &self, spec: &mut Timespec, has_pending_immediate: bool, quic_next_tick_us: Option, - vm: *mut (), /* erased *mut VirtualMachine, forwarded to fire() */ + vm: &VirtualMachine, now_out: &mut Option, ) -> bool { #[cfg(unix)] @@ -963,21 +694,10 @@ impl All { #[cfg(not(unix))] let _ = has_pending_immediate; - let this: *mut Self = self; let maybe_now: &mut Option = now_out; - // SAFETY: `this` is the live per-thread `All`; `vm` per fn contract. - let wtf_next = unsafe { Self::drain_due_wtf_timers(this, maybe_now, vm) }; - - // SAFETY: `this` is live, and only this thread touches the regular heap. - let reg_next = (unsafe { &*this }).timers.peek().map(|min| { - // SAFETY: `peek` returns a live heap node. - let next = unsafe { &(*min).next }; - Timespec { - sec: next.sec, - nsec: next.nsec, - } - }); + let wtf_next = self.drain_due_wtf_timers(maybe_now, vm); + let reg_next = self.timers.peek().map(TimerRef::next); let Some(next) = Self::soonest(wtf_next, reg_next) else { if let Some(us) = quic_next_tick_us { @@ -1020,197 +740,116 @@ impl All { /// Pop the next due timer. `now` is filled lazily on first call so we /// don't pay for `clock_gettime` when the heap is empty. - fn next(&mut self, has_set_now: &mut bool, now: &mut Timespec) -> Option<*mut EventLoopTimer> { + fn next(&self, has_set_now: &mut bool, now: &mut Timespec) -> Option { let timer = self.timers.peek()?; if !*has_set_now { // Real clock: this heap is the opt-out-of-fake-timers set. *now = Timespec::now(TimespecMockMode::ForceRealTime); *has_set_now = true; } - // SAFETY: peek returns a live heap node - let next = unsafe { &(*timer).next }; - if (Timespec { - sec: next.sec, - nsec: next.nsec, - }) - .greater(now) - { + if timer.next().greater(now) { return None; } let deleted = self.timers.delete_min().expect("peek succeeded"); - debug_assert!(core::ptr::eq(deleted, timer)); + debug_assert!(deleted == timer); Some(timer) } - /// # Safety - /// `vm` is the erased `*mut VirtualMachine` for the calling JS thread and - /// must remain live across any `EventLoopTimer::fire` re-entry. - // Forwards `vm` to `__bun_fire_timer` without dereferencing it; - // not_unsafe_ptr_arg_deref is a false positive on opaque-token forwarding. - #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub(crate) fn drain_timers(&mut self, vm: *mut () /* erased *mut VirtualMachine */) { - // Note (§Forbidden aliased-&mut): fired handlers re-enter `vm.timer` - // (e.g. setInterval reschedule → `vm.timer.update(...)`, `cancel()` → - // `vm.timer.remove(...)`). In Rust those re-entrant calls resolve to - // `(*runtime_state()).timer.{update,remove}()`, minting a fresh - // `&mut All` to this same allocation while the outer `&mut self` is - // live → UB under Stacked Borrows. Convert `self` to a raw pointer - // up-front and form a *short-lived* `&mut` only around `next()`, - // dropping it before `fire()` so no `&mut All` is held across the - // re-entrant call (mirroring the raw-ptr pattern in - // `TimerObjectInternals::run_immediate_task`). - // - // TODO: the call-site auto-ref at jsc_hooks.rs (`(*state).timer - // .drain_timers(...)`) still creates a `&mut All` for the call frame - // itself; switch it to `All::drain_timers(core::ptr::addr_of_mut!( - // (*state).timer), vm)` and change this signature to `this: *mut Self`. - let this: *mut Self = self; - + /// Fire every due timer. Handlers re-enter `self` (setInterval reschedule + /// → `update`, `clearTimeout` → `remove`, …), which is why nothing here + /// holds a borrow of the heap across `fire_timer`. + pub(crate) fn drain_timers(&self, vm: &VirtualMachine) { let mut wtf_now: Option = None; - // SAFETY: `this` is the live per-thread `All`; `vm` per fn contract. - let _ = unsafe { Self::drain_due_wtf_timers(this, &mut wtf_now, vm) }; + let _ = self.drain_due_wtf_timers(&mut wtf_now, vm); let mut now = Timespec { sec: 0, nsec: 0 }; let mut has_set_now = false; - loop { - // SAFETY: `this` derived from `&mut self`; short-lived exclusive - // borrow scoped to this `next()` call only — dropped before fire(). - let Some(t) = (unsafe { &mut *this }).next(&mut has_set_now, &mut now) else { - break; - }; - // Note: re-pack into bun_event_loop's local Timespec stub - // until the lower tier unifies on bun_core::Timespec. - let el_now = ElTimespec { - sec: now.sec, - nsec: now.nsec, - }; - // SAFETY: `t` was just popped from the intrusive heap and is live. - // `fire` dispatches through the FIRE_TIMER hook (§Dispatch hot - // path) and may re-enter `(*runtime_state()).timer` — no `&mut` - // to `All` is live here. - let fired = unsafe { EventLoopTimer::fire(t, &el_now, vm) }; - // SAFETY: `vm` per fn contract. - if unsafe { fold_timer(vm, fired) }.is_err() { + while let Some(t) = self.next(&mut has_set_now, &mut now) { + let fired = crate::dispatch::fire_timer(t, &now, vm); + if fold_timer(vm, fired).is_err() { break; } } } - /// # Safety - /// `uws_loop` must point to the calling VM's live uws loop. - // `uws_loop` is an FFI handle held as `*mut` by every caller; contract is - // documented in `# Safety` above. Cannot be `&mut` without breaking the - // out-of-file call sites that hold raw pointers. - #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub(crate) fn increment_immediate_ref(&mut self, delta: i32, uws_loop: *mut bun_uws_sys::Loop) { - let old = self.immediate_ref_count; + pub(crate) fn increment_immediate_ref(&self, delta: i32) { + let old = self.immediate_ref_count.get(); let new = old + delta; - self.immediate_ref_count = new; + self.immediate_ref_count.set(new); if old <= 0 && new > 0 { #[cfg(not(windows))] - // SAFETY: caller passes the VM's live uws loop - unsafe { &mut *uws_loop }.ref_(); + VirtualMachine::get().uws_loop_mut().ref_(); #[cfg(windows)] { // Lazy-init the idle handle and start // it with a no-op callback so `uv_run` does not block in poll // while immediates are pending (matches Node.js). - if self.uv_idle.data.is_null() { - self.uv_idle.init(uv::Loop::get()); - // Note: `data` is only used as a - // non-null "initialized" sentinel — never dereferenced. - self.uv_idle.data = bun_jsc::virtual_machine::VirtualMachine::get_mut_ptr() - .cast::(); - } - self.uv_idle.start(Some(Self::on_uv_idle_noop)); + self.uv_idle.with_mut(|idle| { + if idle.data.is_null() { + idle.init(uv::Loop::get()); + // `data` is only a non-null "initialized" sentinel. + idle.data = VirtualMachine::get_mut_ptr().cast::(); + } + idle.start(Some(Self::on_uv_idle_noop)); + }); } } else if old > 0 && new <= 0 { #[cfg(not(windows))] - // SAFETY: caller passes the VM's live uws loop - unsafe { &mut *uws_loop }.unref(); + VirtualMachine::get().uws_loop_mut().unref(); #[cfg(windows)] - if !self.uv_idle.data.is_null() { - self.uv_idle.stop(); - } + self.uv_idle.with_mut(|idle| { + if !idle.data.is_null() { + idle.stop(); + } + }); } - #[cfg(windows)] - let _ = uws_loop; } /// Empty `uv_idle` callback. Its presence alone /// keeps `uv_run` from blocking in the poll phase; the body is a no-op. - /// No preconditions (the handle pointer is unused), so the fn is safe; the - /// safe fn item coerces into the `uv_idle_cb` fn-pointer slot. #[cfg(windows)] extern "C" fn on_uv_idle_noop(_: *mut uv::uv_idle_t) { // prevent libuv from polling forever } - /// # Safety - /// `uws_loop` must point to the calling VM's live uws loop. - // `uws_loop` is an FFI handle held as `*mut` by every caller; contract is - // documented in `# Safety` above. Cannot be `&mut` without breaking the - // out-of-file call sites that hold raw pointers. - #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub(crate) fn increment_timer_ref(&mut self, delta: i32, uws_loop: *mut bun_uws_sys::Loop) { - let old = self.active_timer_count; + pub(crate) fn increment_timer_ref(&self, delta: i32) { + let old = self.active_timer_count.get(); let new = old + delta; debug_assert!(new >= 0); - self.active_timer_count = new; + self.active_timer_count.set(new); if old <= 0 && new > 0 { #[cfg(not(windows))] - // SAFETY: caller passes the VM's live uws loop - unsafe { &mut *uws_loop }.ref_(); + VirtualMachine::get().uws_loop_mut().ref_(); // `uv_timer.ref()` is intentionally unconditional (no `data != // null` guard). Invariant: every path that reaches a positive // `active_timer_count` first inserts a timer, and `insert` // → `ensure_uv_timer` lazily `uv_timer_init`s the handle. Guarding // here would silently drop the ref and let the loop exit early. #[cfg(windows)] - self.uv_timer.ref_(); + self.uv_timer.with_mut(|t| t.ref_()); } else if old > 0 && new <= 0 { #[cfg(not(windows))] - // SAFETY: caller passes the VM's live uws loop - unsafe { &mut *uws_loop }.unref(); + VirtualMachine::get().uws_loop_mut().unref(); #[cfg(windows)] - self.uv_timer.unref(); + self.uv_timer.with_mut(|t| t.unref()); } - #[cfg(windows)] - let _ = uws_loop; + } + + /// Every slot linked into `timers` or `fake_timers.timers`. + fn linked_timers(&self) -> Vec { + let mut nodes = self.timers.to_vec(); + nodes.append(&mut self.fake_timers.timers.to_vec()); + nodes } /// VM teardown, after `cancel_all_timeout_objects`: unlink every timer still /// in either heap, whatever its kind. Owners keep their nodes (now /// `CANCELLED`, which their own `state == ACTIVE` checks respect); nothing - /// can fire afterwards even if the loop turns again. - /// - /// # Safety - /// `this` is the live per-thread `All`; JS thread; never on a VM that keeps running. - pub(crate) unsafe fn disarm_all_for_vm_teardown(this: *mut Self) { - let mut nodes: Vec<*mut EventLoopTimer> = Vec::new(); - let mut stack: Vec<*mut EventLoopTimer> = Vec::new(); - // SAFETY: fn contract. - let roots = unsafe { [(*this).timers.0.root, (*this).fake_timers.timers.0.root] }; - for root in roots { - if !root.is_null() { - stack.push(root); - } - } - while let Some(node) = stack.pop() { - // SAFETY: intrusive-heap invariant — reachable nodes are live while linked. - let (child, next) = unsafe { ((*node).heap.child, (*node).heap.next) }; - if !child.is_null() { - stack.push(child); - } - if !next.is_null() { - stack.push(next); - } - nodes.push(node); - } - for node in nodes { - // SAFETY: collected from the live heap above; `remove` relinks the - // others but every node stays a valid allocation owned elsewhere. - unsafe { (*this).remove(node) }; + /// can fire afterwards even if the loop turns again. JS thread; never on a + /// VM that keeps running. + pub(crate) fn disarm_all_for_vm_teardown(&self) { + for node in self.linked_timers() { + self.remove(node); } } @@ -1221,84 +860,27 @@ impl All { /// every `AbortSignal.timeout()` timer through its signal so the signal /// stops reporting an active timer. /// - /// # Safety - /// JS thread only, with the TLS `RuntimeState` still installed and `vm` - /// the live per-thread VM. Must run BEFORE JSC teardown - /// (`Zig__GlobalObject__destructOnExit` / `WebWorker__teardownJSCVM`) and - /// BEFORE `runtime_state` is nulled — the GC sweep frees the - /// `TimeoutObject` boxes whose `event_loop_timer` fields the heap nodes - /// alias, and the `AbortSignal`s that own the `AbortSignalTimeout` boxes. - pub(crate) unsafe fn cancel_all_timeout_objects( - this: *mut Self, - vm: *mut crate::jsc::virtual_machine::VirtualMachine, - ) { - let mut to_cancel: Vec<*const TimerObjectInternals> = Vec::new(); - let mut signal_timeouts: Vec<*mut AbortSignalTimeout> = Vec::new(); - let mut stack: Vec<*mut EventLoopTimer> = Vec::new(); - - // SAFETY: `this` is the live per-thread `All` (JS thread only). - let roots = unsafe { [(*this).timers.0.root, (*this).fake_timers.timers.0.root] }; - for root in roots { - if !root.is_null() { - stack.push(root); - } - } - while let Some(node) = stack.pop() { - // SAFETY: intrusive-heap invariant — every node reachable from a - // root is a live `EventLoopTimer` while linked. Read-only walk. - let (tag, child, next) = - unsafe { ((*node).tag, (*node).heap.child, (*node).heap.next) }; - if !child.is_null() { - stack.push(child); - } - if !next.is_null() { - stack.push(next); - } - match tag { - EventLoopTimerTag::TimeoutObject => { - // SAFETY: tag invariant — `node` IS the `event_loop_timer` - // field of a live `TimeoutObject`. - let parent = unsafe { TimeoutObject::from_timer_ptr(node) }; - // SAFETY: `parent` points at the live `TimeoutObject` recovered - // above; `addr_of!` projects the in-bounds `internals` field. - to_cancel.push(unsafe { core::ptr::addr_of!((*parent).internals) }); - } - EventLoopTimerTag::ImmediateObject => { - // SAFETY: tag invariant — see above. - let parent = unsafe { ImmediateObject::from_timer_ptr(node) }; - // SAFETY: `parent` points at the live `ImmediateObject` recovered - // above; `addr_of!` projects the in-bounds `internals` field. - to_cancel.push(unsafe { core::ptr::addr_of!((*parent).internals) }); - } - EventLoopTimerTag::AbortSignalTimeout => { - // SAFETY: tag invariant — `node` IS the `event_loop_timer` - // field of a live boxed `abort_signal::Timeout`. - signal_timeouts.push(unsafe { AbortSignalTimeout::from_timer_ptr(node) }); + /// JS thread only. Must run BEFORE JSC teardown + /// (`Zig__GlobalObject__destructOnExit` / `WebWorker__teardownJSCVM`) — the + /// GC sweep frees the `TimeoutObject` boxes whose `event_loop_timer` slots + /// the heap links, and the `AbortSignal`s that own the `AbortSignalTimeout` + /// boxes. + pub(crate) fn cancel_all_timeout_objects(&self, vm: &VirtualMachine) { + let mut timeouts: Vec = Vec::new(); + let mut signal_timeouts: Vec = Vec::new(); + for t in self.linked_timers() { + match t.tag() { + EventLoopTimerTag::TimeoutObject | EventLoopTimerTag::ImmediateObject => { + timeouts.push(t) } + EventLoopTimerTag::AbortSignalTimeout => signal_timeouts.push(t), _ => {} } } - - for internals in to_cancel { - // SAFETY: each pointer was collected from the live heap; the - // parent box is still alive (the +1 ref `cancel()` releases is - // exactly the one keeping it pinned). `cancel()` may free the - // parent on the final deref — never touched again. - unsafe { (*internals).cancel(vm) }; - } - - // `AbortSignal.timeout()` boxes are owned by the C++ `AbortSignal`, so - // each one is handed back to its signal, which unlinks and frees it and - // clears `m_timeout`. Only unlinking the node here would leave every - // observed signal's wrapper (under `--isolate`: the retired global its - // listeners close over) pinned by `isReachableFromOpaqueRoots` for the - // rest of the process; see `Timeout::discard`. - for t in signal_timeouts { - // SAFETY: each `t` was collected from the live heap above, so its - // box (and therefore its owning signal) is still alive; JS thread; - // no borrow of `*this` is held across the call (`discard` re-enters - // `remove` through `timer_remove`). `t` is freed by the call. - unsafe { AbortSignalTimeout::discard(t) }; + // Each call may free the owner (the `+1` it releases is exactly the one + // keeping it pinned) and re-enters `remove`; no heap borrow is held. + for t in timeouts.into_iter().chain(signal_timeouts) { + crate::dispatch::cancel_js_timer(t, vm); } } } @@ -1352,24 +934,18 @@ const NS_PER_US: i64 = bun_core::time::NS_PER_US as i64; /// The timer drain's fold: report what a fired timer's handler left pending /// as uncaught, or — if it is the VM's termination — tell the drain to stop. -/// -/// # Safety -/// `vm` is the erased per-thread `*mut VirtualMachine`. #[inline] -unsafe fn fold_timer( - vm: *mut (), +fn fold_timer( + vm: &VirtualMachine, fired: bun_event_loop::JsResult<()>, ) -> Result<(), bun_jsc::Stopped> { #[cold] #[inline(never)] - unsafe fn report(vm: *mut (), err: bun_jsc::JsError) -> Result<(), bun_jsc::Stopped> { - // SAFETY: fn contract. - let global = unsafe { (*vm.cast::()).global() }; - bun_jsc::task::report_error_or_terminate(global, err) + fn report(vm: &VirtualMachine, err: bun_jsc::JsError) -> Result<(), bun_jsc::Stopped> { + bun_jsc::task::report_error_or_terminate(vm.global(), err) } match fired { Ok(()) => Ok(()), - // SAFETY: fn contract. - Err(err) => unsafe { report(vm, err) }, + Err(err) => report(vm, err), } } diff --git a/src/runtime/timer/timer_object_internals.rs b/src/runtime/timer/timer_object_internals.rs index 4760250bf099..29f29009c282 100644 --- a/src/runtime/timer/timer_object_internals.rs +++ b/src/runtime/timer/timer_object_internals.rs @@ -1,28 +1,28 @@ -//! `TimerObjectInternals` — fields shared by `TimeoutObject` / `ImmediateObject`. +//! `TimerObjectInternals` — fields shared by `TimeoutObject` / `ImmediateObject`, +//! and [`TimerObject`] — the behaviour shared by both, generic over the +//! owning type so the timer slot, the heap's ref and the id map are reached +//! through `self` instead of a `container_of`. //! -//! Struct + `Flags` packed-u32 state machine. `run_immediate_task()` + -//! helpers (`event_loop_timer`/`ref_`/`deref_`/ -//! `set_enable_keeping_event_loop_alive`/`run`) drive the -//! `__bun_run_immediate_task` dispatch path. `fire()` + `reschedule()`/ -//! `should_reschedule_timer()`/`convert_to_interval()` drive the -//! `FIRE_TIMER` dispatch path (Timeout/Immediate arms). `init()` backs the -//! `TimeoutObject::init` / `ImmediateObject::init` constructors. +//! `run_immediate_task()` drives the `__bun_run_immediate_task` dispatch +//! path; `fire()` + `reschedule()`/`should_reschedule_timer()`/ +//! `convert_to_interval()` drive the timer-heap dispatch path +//! (Timeout/Immediate arms); `schedule()` backs the `TimeoutObject::init` / +//! `ImmediateObject::init` constructors. + +use core::cell::Cell; use bun_core::{Timespec, TimespecMockMode}; +use bun_ptr::{BackRef, JsCell, RefPtr, ThisPtr}; -use crate::jsc::JsCell; +use crate::jsc::virtual_machine::VirtualMachine; use crate::jsc::{ Debugger, JSGlobalObject, JSValue, JsRef, JsResult, ScriptExecutionStatus, generated::{JSImmediate, JSTimeout}, }; -use core::cell::Cell; -// Note: `bun_jsc::VirtualMachine` is a *module* alias; the struct lives at -// `virtual_machine::VirtualMachine`. -use crate::jsc::virtual_machine::VirtualMachine; +use crate::jsc_hooks::timer_all; use super::{ - ElTimespec, EventLoopTimer, EventLoopTimerState, ID, ImmediateObject, Kind, KindBig, - TimeoutObject, + EventLoopTimer, EventLoopTimerState, ID, IdMap, Kind, KindBig, Maps, TimerOwner, TimerRef, }; /// Data that TimerObject and ImmediateObject have in common. @@ -38,6 +38,19 @@ pub struct TimerObjectInternals { } impl TimerObjectInternals { + pub(crate) fn new(id: i32, kind: Kind, interval: u32, vm: &VirtualMachine) -> Self { + let mut flags = Flags::default(); + flags.set_kind(kind); + flags.set_epoch(timer_all().epoch()); + Self { + id, + interval: Cell::new(interval), + this_value: JsCell::new(JsRef::empty()), + flags: Cell::new(flags), + generation: vm.test_isolation_generation, + } + } + /// Read-modify-write `self.flags` through the `Cell` (R-2: `flags` is /// `Cell` so the write is interior-mutable, callable from /// `&self` host-fns that re-enter JS). @@ -47,17 +60,14 @@ impl TimerObjectInternals { f(&mut fl); self.flags.set(fl); } -} -impl Default for TimerObjectInternals { - fn default() -> Self { - Self { - id: -1, - interval: Cell::new(0), - this_value: JsCell::new(JsRef::empty()), - flags: Cell::new(Flags::default()), - generation: 0, + #[inline] + pub(crate) fn async_id(&self) -> u64 { + ID { + id: self.id, + kind: self.flags.get().kind().into(), } + .async_id() } } @@ -67,10 +77,6 @@ impl Default for TimerObjectInternals { // `TimerObjectInternals`/`All::update` callers see the same nominal type. pub use bun_event_loop::EventLoopTimer::TimerFlags as Flags; -// ────────────────────────────────────────────────────────────────────────── -// `runImmediateTask` path for `__bun_run_immediate_task` (dispatch.rs). -// ────────────────────────────────────────────────────────────────────────── - // C++ symbol emitted from ImmediateList.cpp / setTimeout.cpp; already linked. unsafe extern "C" { safe fn Bun__JSTimeout__call( @@ -81,306 +87,172 @@ unsafe extern "C" { ) -> bool; } -/// Typed result of `@fieldParentPtr("internals", self)` discriminated by -/// `flags.kind()`. Raw `*mut` (NOT `&mut`) so callers may hold it across -/// re-entrant JS calls without minting an aliased `&mut` (PORTING.md -/// §Forbidden — the callback can reach the same field via `cancel()`/ -/// `refresh()`). Provenance is `&self`-derived (read-only); the `*mut` is a -/// type-only cast — writes must go through `Cell`/`UnsafeCell` fields. -enum TimerParent { - Immediate(*mut ImmediateObject), - Timeout(*mut TimeoutObject), -} +/// The behaviour shared by [`TimeoutObject`](super::TimeoutObject) and +/// [`ImmediateObject`](super::ImmediateObject). +/// +/// Re-entrancy: every method that runs the JS callback takes +/// `this: ThisPtr` / `&self` (never `&mut`) — the callback can reach +/// this same object again through its JS wrapper (`clearTimeout()`, +/// `refresh()`, the `_destroyed` getter), so all state is in `Cell`/`JsCell`. +/// Methods that may drop the heap's ref (and with it possibly the last ref) +/// take `ThisPtr`; after they release it `this` may be gone. +pub trait TimerObject: + bun_ptr::RefCounted + TimerOwner + Sized + 'static +{ + fn internals(&self) -> &TimerObjectInternals; + fn event_loop_timer(&self) -> &JsCell; + /// The slot for the ref held while this timer is scheduled (see the + /// struct field docs). + fn heap_ref(&self) -> &Cell>>; + /// The `clearTimeout(id)` table a timer of `kind` registers in. + fn id_map(maps: &mut Maps, kind: Kind) -> &mut IdMap; -impl TimerObjectInternals { - /// `@fieldParentPtr("internals", self)` — the single `container_of` site. - /// Every other helper (`event_loop_timer`, `ref_`, `deref`, `init`, - /// `event_loop_timer_state`) routes through this so the `from_field_ptr!` - /// invariant — `flags.kind()` ⇔ container type, established in `init()` — - /// lives in exactly one place. #[inline] - fn parent_ptr(&self) -> TimerParent { - bun_core::assert_not_freeze!(TimerObjectInternals, TimerParent); - let this = std::ptr::from_ref::(self).cast_mut(); - match self.flags.get().kind() { - // SAFETY: `kind == SetImmediate` ⇒ `self` is the `internals` field - // of a live `ImmediateObject` (set in `init()`). - Kind::SetImmediate => TimerParent::Immediate(unsafe { - bun_core::from_field_ptr!(ImmediateObject, internals, this) - }), - // SAFETY: `kind ∈ {SetTimeout, SetInterval}` ⇒ `self` is the - // `internals` field of a live `TimeoutObject`. - Kind::SetTimeout | Kind::SetInterval => TimerParent::Timeout(unsafe { - bun_core::from_field_ptr!(TimeoutObject, internals, this) - }), - } - } - - /// `@fieldParentPtr("internals", self).event_loop_timer`. Returns a raw - /// pointer (NOT `&mut`) so callers can hold it across re-entrant JS calls - /// without minting aliased `&mut` (PORTING.md §Forbidden — the callback - /// may reach this same field via `cancel()`/`refresh()`). - fn event_loop_timer(&self) -> *mut EventLoopTimer { - match self.parent_ptr() { - // SAFETY: `p` points into a live container per `parent_ptr()`. - TimerParent::Immediate(p) => unsafe { core::ptr::addr_of_mut!((*p).event_loop_timer) }, - // SAFETY: as above. - TimerParent::Timeout(p) => unsafe { core::ptr::addr_of_mut!((*p).event_loop_timer) }, - } + fn timer_ref(&self) -> TimerRef { + TimerRef::new(self, Self::event_loop_timer) } - /// Increment the parent container's intrusive refcount. - fn ref_(&self) { - match self.parent_ptr() { - // SAFETY: `p` is a live container per `parent_ptr()`. - TimerParent::Immediate(p) => unsafe { ImmediateObject::ref_(p) }, - // SAFETY: as above. - TimerParent::Timeout(p) => unsafe { TimeoutObject::ref_(p) }, - } + #[inline] + fn event_loop_timer_state(&self) -> EventLoopTimerState { + self.event_loop_timer().get().state } - /// Release a `TimeoutObject`/`ImmediateObject` that was unlinked from a - /// timer heap by something other than [`Self::cancel`] (e.g. - /// `FakeTimers::clear`'s `delete_min` drain). Downgrades the `Strong` JS - /// pin and releases the `+1` taken by `reschedule()`, so GC can collect - /// the wrapper and the box frees on the final deref. - /// - /// `cancel()` skips its own `remove`/`deref` because `state` is already - /// `CANCELLED`, which is why the explicit `deref` follows. - /// - /// `vm` is the live per-thread VM; no borrow of `All` may be live across - /// this call (`cancel()` reaches `All::remove`, which forms its own - /// `&mut All`). - pub(crate) fn release_heap_pin(this: core::ptr::NonNull, vm: *mut VirtualMachine) { - // SAFETY: caller guarantees the parent box is live (refcount ≥ 1). - let internals = unsafe { this.as_ref() }; - internals.cancel(vm); - internals.deref(); + #[inline] + fn set_event_loop_timer_state(&self, state: EventLoopTimerState) { + self.event_loop_timer().with_mut(|t| t.state = state); } - /// Decrement the parent container's intrusive refcount; frees on 0. - /// After this returns, `self` may be dangling — do not touch. - fn deref(&self) { - match self.parent_ptr() { - // SAFETY: `p` is a live container per `parent_ptr()`. - TimerParent::Immediate(p) => unsafe { ImmediateObject::deref(p) }, - // SAFETY: as above. - TimerParent::Timeout(p) => unsafe { TimeoutObject::deref(p) }, - } + /// Take the scheduled-timer ref on behalf of the heap / immediate queue, + /// unless it is already held. + #[inline] + fn hold_heap_ref(this: ThisPtr) { + let slot = this.heap_ref(); + let held = slot.take(); + slot.set(Some(held.unwrap_or_else(|| RefPtr::from_this(this)))); } + /// Release the scheduled-timer ref, if held. May free `this`. #[inline] - pub(crate) fn async_id(&self) -> u64 { - ID { - id: self.id, - kind: self.flags.get().kind().into(), + fn release_heap_ref(this: ThisPtr) { + if let Some(held) = this.heap_ref().take() { + held.deref(); } - .async_id() } - /// Note (jsc/runtime crate cycle): the low-tier - /// `bun_jsc::VirtualMachine.timer` is `()`, - /// so resolve `Timer::All` via the per-thread `RuntimeState` instead. - fn set_enable_keeping_event_loop_alive(&self, vm: *mut VirtualMachine, enable: bool) { - if self.flags.get().is_keeping_event_loop_alive() == enable { + fn set_enable_keeping_event_loop_alive(&self, enable: bool) { + let internals = self.internals(); + if internals.flags.get().is_keeping_event_loop_alive() == enable { return; } - self.update_flags(|f| f.set_is_keeping_event_loop_alive(enable)); + internals.update_flags(|f| f.set_is_keeping_event_loop_alive(enable)); - let state = crate::jsc_hooks::runtime_state(); - debug_assert!(!state.is_null(), "RuntimeState not installed"); - // SAFETY: `vm` is the live per-thread VM (hook contract); field read only. - let uws_loop = unsafe { (*vm).uws_loop() }; let delta = if enable { 1 } else { -1 }; - match self.flags.get().kind() { - // SAFETY: `state` points at the boxed per-thread `RuntimeState`; - // single-threaded JS heap so no concurrent `&mut` to `.timer`. - Kind::SetTimeout | Kind::SetInterval => unsafe { - (*state).timer.increment_timer_ref(delta, uws_loop) - }, + match internals.flags.get().kind() { + Kind::SetTimeout | Kind::SetInterval => timer_all().increment_timer_ref(delta), // setImmediate has slightly different event loop logic - // SAFETY: as above. - Kind::SetImmediate => unsafe { - (*state).timer.increment_immediate_ref(delta, uws_loop) - }, + Kind::SetImmediate => timer_all().increment_immediate_ref(delta), } } - /// Invoke the JS callback via the - /// C++ `Bun__JSTimeout__call` thunk (which handles exceptions internally). - /// Returns `true` if an exception was thrown. - /// - /// Note (noalias re-entrancy): takes `*mut Self`, NOT `&mut self`. - /// The JS callback can re-enter `cancel()`/`do_refresh()` on this same - /// object via a fresh `&mut Self` derived from the JS wrapper's `m_ptr`. - /// With `&mut self` here, LLVM's `noalias` lets it keep `self.flags` in a - /// register across the FFI call, so `set_in_callback(false)`'s RMW - /// clobbers the `has_cleared_timer` bit that `cancel()` set — the interval - /// re-fires forever. A raw pointer carries no aliasing guarantee, so use - /// one here. - /// - /// # Safety - /// `this` points at a live `TimerObjectInternals` embedded in its parent - /// container, pinned for the duration of the call by the caller's `ref_()`. - /// Both callers (`fire`, `run_immediate_task`) also take `*mut Self`, so - /// no `noalias` `&mut Self` is live anywhere in the call chain across - /// `Bun__JSTimeout__call` — inlining is safe. - unsafe fn run( - this: *mut Self, - global_this: *mut JSGlobalObject, + /// Invoke the JS callback via the C++ `Bun__JSTimeout__call` thunk (which + /// handles exceptions internally). Returns `true` if an exception was + /// thrown. The caller pins `self` with a ref across the call. + fn run( + &self, + global: &JSGlobalObject, timer: JSValue, callback: JSValue, arguments: JSValue, async_id: u64, - vm: *mut VirtualMachine, + vm: &VirtualMachine, ) -> bool { - // SAFETY: `this` live per fn contract; pinned by caller's `ref_()`. - // `&Self` (NOT `&mut`) — fields are `Cell`/`JsCell` so re-entrant JS - // touching this object via another `&Self` is sound (no `noalias`). - let s = unsafe { &*this }; - // `JSGlobalObject` is an `opaque_ffi!` ZST — `opaque_ref` is the safe - // deref (panics on null; `vm.global` is never null). - let global = JSGlobalObject::opaque_ref(global_this); - // SAFETY: `vm` is the live per-thread VM (hook contract). - if unsafe { (*vm).is_inspector_enabled() } { + let internals = self.internals(); + if vm.is_inspector_enabled() { Debugger::will_dispatch_async_call(global, Debugger::AsyncCallType::DOMTimer, async_id); } // Bun__JSTimeout__call handles exceptions. // `Cell` RMW so the `in_callback` write reaches memory before JS - // runs (re-entrant `_destroyed` getter reads it via a different pointer). - s.update_flags(|f| f.set_in_callback(true)); + // runs (re-entrant `_destroyed` getter reads it through the wrapper). + internals.update_flags(|f| f.set_in_callback(true)); let result = Bun__JSTimeout__call(global, timer, callback, arguments); // No early returns between the `in_callback` set and this clear. - // `Cell` RMW: must reload `flags` from memory — re-entrant - // `cancel()` may have set `has_cleared_timer` / cleared - // `is_keeping_event_loop_alive`. - s.update_flags(|f| f.set_in_callback(false)); + // Fresh `Cell` read: re-entrant `cancel()` may have set + // `has_cleared_timer` / cleared `is_keeping_event_loop_alive`. + internals.update_flags(|f| f.set_in_callback(false)); - // SAFETY: as above. - if unsafe { (*vm).is_inspector_enabled() } { + if vm.is_inspector_enabled() { Debugger::did_dispatch_async_call(global, Debugger::AsyncCallType::DOMTimer, async_id); } result } - /// Out-param constructor; `self` is - /// the embedded `internals` field of a freshly `heap::alloc`'d - /// `ImmediateObject`/`TimeoutObject`. Cannot be - /// reshaped to `-> Self` because the body needs the parent pointer to - /// enqueue/reschedule before returning. - /// - /// Note (jsc/runtime crate cycle): `vm.timer.epoch` resolved via `runtime_state()` - /// (low-tier `VirtualMachine.timer` is `()`). - pub(crate) fn init( - &mut self, + /// Constructor tail: wire the JS wrapper's cached slots and hand the timer + /// to the heap (`Timeout`) or the immediate queue (`Immediate`). + fn schedule( + this: ThisPtr, timer: JSValue, global: &JSGlobalObject, - id: i32, - kind: Kind, - interval: u32, callback: JSValue, arguments: JSValue, ) { - let vm = VirtualMachine::get_mut_ptr(); - let state = crate::jsc_hooks::runtime_state(); - debug_assert!(!state.is_null(), "RuntimeState not installed"); - - *self = Self { - id, - flags: { - let mut f = Flags::default(); - f.set_kind(kind); - // SAFETY: `state` is the boxed per-thread `RuntimeState`. - f.set_epoch(unsafe { (*state).timer.epoch }); - Cell::new(f) - }, - interval: Cell::new(interval), - // SAFETY: `vm` is the live per-thread VM; field read only. - generation: unsafe { (*vm).test_isolation_generation }, - this_value: JsCell::new(JsRef::empty()), - }; - - if kind == Kind::SetImmediate { + let internals = this.internals(); + if internals.flags.get().kind() == Kind::SetImmediate { JSImmediate::arguments_set_cached(timer, global, arguments); JSImmediate::callback_set_cached(timer, global, callback); - // `flags.kind` was just set to `SetImmediate` above. - let TimerParent::Immediate(parent) = self.parent_ptr() else { - unreachable!() - }; - // SAFETY: `vm` is the live per-thread VM. Low tier stores `*mut ()` - // (PORTING.md §Dispatch); `__bun_run_immediate_task` casts it back - // to `*mut ImmediateObject`. - unsafe { (*vm).enqueue_immediate_task(parent.cast()) }; - self.set_enable_keeping_event_loop_alive(vm, true); + // Low tier stores `*mut ()` (§Dispatch); `__bun_run_immediate_task` + // recovers the `ThisPtr`. + global + .bun_vm() + .event_loop_mut() + .enqueue_immediate_task(this.as_ptr().cast()); + this.set_enable_keeping_event_loop_alive(true); // ref'd by event loop - self.ref_(); + Self::hold_heap_ref(this); } else { JSTimeout::arguments_set_cached(timer, global, arguments); JSTimeout::callback_set_cached(timer, global, callback); JSTimeout::idle_timeout_set_cached( timer, global, - JSValue::js_number(f64::from(interval)), + JSValue::js_number(f64::from(internals.interval.get())), ); JSTimeout::repeat_set_cached( timer, global, - if kind == Kind::SetInterval { - JSValue::js_number(f64::from(interval)) + if internals.flags.get().kind() == Kind::SetInterval { + JSValue::js_number(f64::from(internals.interval.get())) } else { JSValue::NULL }, ); - // this increments the refcount and sets _idleStart - self.reschedule(timer, vm, global.as_ptr()); + // this takes the heap's ref and sets _idleStart + Self::reschedule(this, timer, global); } - self.this_value.with_mut(|r| r.set_strong(timer, global)); + internals + .this_value + .with_mut(|r| r.set_strong(timer, global)); } - /// Returns `true` if an - /// exception was thrown. - /// - /// Note (noalias re-entrancy): takes `*mut Self`, NOT `&mut self`. - /// `Self::run` re-enters JS which can `cancel()`/`do_refresh()` this same - /// object via the JS wrapper's `m_ptr`. With `&mut self` LLVM may cache - /// `self.flags`/`event_loop_timer().state` across the call and clobber the - /// re-entrant write (see `run()` doc). Use a raw - /// pointer; helper calls `(*this).foo()` materialise short-lived `&mut` - /// scoped to each statement only — none span the JS call. - /// - /// Also takes `*mut VirtualMachine` (NOT `&mut`) — the body calls - /// `vm.event_loop().enter()` then re-enters JS which may itself touch the - /// VM/EventLoop; aliased `&mut` would be UB. - /// - /// # Safety - /// `this` points at a live `TimerObjectInternals` embedded in its - /// `ImmediateObject` parent (FIRE_TIMER hook contract); `vm` is the live - /// per-thread VM. - pub(crate) unsafe fn run_immediate_task(this: *mut Self, vm: *mut VirtualMachine) -> bool { - // SAFETY: per fn contract — `this` live. `&Self` (NOT `&mut`) — fields - // are `Cell`/`JsCell` so re-entrant JS touching this object via another - // `&Self` is sound (no `noalias`). Last use of `s` is the final - // `s.deref()` below; `*this` may be freed only after that point. - let s = unsafe { &*this }; + /// `__bun_run_immediate_task` body. Returns `true` if an exception was + /// thrown. + fn run_immediate_task(this: ThisPtr, vm: &VirtualMachine) -> bool { + let s = this.internals(); let cleared = s.flags.get().has_cleared_timer() // The VM's stop was requested: nothing more enters script (as `fire`). - // SAFETY: `vm` is the live per-thread VM (hook contract). - || unsafe { (*vm).script_execution_status() } != ScriptExecutionStatus::Running - // SAFETY: as above. - || s.generation != unsafe { (*vm).test_isolation_generation } + || vm.script_execution_status() != ScriptExecutionStatus::Running + || s.generation != vm.test_isolation_generation // unref'd setImmediate callbacks should only run if there are things // keeping the event loop alive other than setImmediates || (!s.flags.get().is_keeping_event_loop_alive() - // SAFETY: `vm` live per hook contract. - && !unsafe { (*vm).is_event_loop_alive_excluding_immediates() }); + && !vm.is_event_loop_alive_excluding_immediates()); if cleared { - s.set_enable_keeping_event_loop_alive(vm, false); + this.set_enable_keeping_event_loop_alive(false); s.this_value.with_mut(|r| r.downgrade()); - s.deref(); + Self::release_heap_ref(this); return false; } @@ -389,48 +261,40 @@ impl TimerObjectInternals { panic!("TimerObjectInternals.runImmediateTask: this_object is null"); #[cfg(not(debug_assertions))] { - s.set_enable_keeping_event_loop_alive(vm, false); - s.deref(); + this.set_enable_keeping_event_loop_alive(false); + Self::release_heap_ref(this); return false; } }; - // SAFETY: `vm` is live; `global` is the per-VM JSGlobalObject pointer. - let global_this = unsafe { (*vm).global }; + let global = vm.global(); s.this_value.with_mut(|r| r.downgrade()); - s.set_event_loop_timer_state(EventLoopTimerState::FIRED); - s.set_enable_keeping_event_loop_alive(vm, false); + this.set_event_loop_timer_state(EventLoopTimerState::FIRED); + this.set_enable_keeping_event_loop_alive(false); timer.ensure_still_alive(); - // SAFETY: `vm` is live; `event_loop()` returns `*mut` to the embedded - // EventLoop. Re-entrancy is permitted by the raw-ptr contract above. - unsafe { (*(*vm).event_loop()).enter() }; + vm.event_loop_mut().enter(); let callback = JSImmediate::callback_get_cached(timer).expect("ImmediateObject callback slot"); let arguments = JSImmediate::arguments_get_cached(timer).expect("ImmediateObject arguments slot"); let exception_thrown = { - s.ref_(); + let _pin = this.ref_guard(); let async_id = s.async_id(); - // SAFETY: `this` is the live `internals` per fn contract; `ref_()` - // above pins the parent across re-entrancy. - let result = - unsafe { Self::run(this, global_this, timer, callback, arguments, async_id, vm) }; - // `Self::run` has no early return so the deref ordering below is - // preserved. After the second `deref()` `*this` may be - // freed; do not touch it past this block. - // Fresh read: re-entrant `cancel()`/`refresh()` may have changed - // `state` (`ref_()` above pins the parent). - if s.event_loop_timer_state() == EventLoopTimerState::FIRED { - s.deref(); + let result = this.run(global, timer, callback, arguments, async_id, vm); + // Fresh read: re-entrant `cancel()` may have changed `state`. + if this.event_loop_timer_state() == EventLoopTimerState::FIRED { + Self::release_heap_ref(this); } - s.deref(); result + // `_pin` drops here; after that `this` may be gone. }; // --- after this point, the timer is no longer guaranteed to be alive --- - // SAFETY: `vm` is live; see `enter()` note above. - if unsafe { (*(*vm).event_loop()).exit_maybe_drain_microtasks(!exception_thrown) }.is_err() + if vm + .event_loop_mut() + .exit_maybe_drain_microtasks(!exception_thrown) + .is_err() { return true; } @@ -438,69 +302,36 @@ impl TimerObjectInternals { exception_thrown } - /// # Safety - /// `this` must be the live `internals` of a queued `ImmediateObject`. - pub(crate) unsafe fn cancel_pending_immediate(this: *mut Self, vm: *mut VirtualMachine) { - // SAFETY: per fn contract. - let s = unsafe { &*this }; - s.set_enable_keeping_event_loop_alive(vm, false); - s.this_value.with_mut(|r| r.downgrade()); - s.deref(); + /// VM-teardown release of the immediate queue's ref on a still-queued + /// `ImmediateObject`, without running it. + fn cancel_pending_immediate(this: ThisPtr) { + this.set_enable_keeping_event_loop_alive(false); + this.internals().this_value.with_mut(|r| r.downgrade()); + Self::release_heap_ref(this); } - /// `EventLoopTimer.fire` dispatch - /// arm body for `Tag::TimeoutObject`/`Tag::ImmediateObject`. Pops the JS - /// timer, invokes its callback via `run()`, then either reschedules - /// (setInterval / `t._repeat`) or releases the heap ref. - /// - /// Note: takes `*mut VirtualMachine` (NOT `&mut`) — the body calls - /// `vm.event_loop().enter()` then re-enters JS which may itself touch the - /// VM/EventLoop (and `(*runtime_state()).timer` via `cancel()`/`refresh()`); - /// aliased `&mut` would be UB. Dereference per-use under `// SAFETY:`. - /// - /// Note (noalias re-entrancy): takes `*mut Self`, NOT `&mut self`. - /// `Self::run` re-enters JS which can `cancel()`/`do_refresh()` this same - /// object via the JS wrapper's `m_ptr`. With `&mut self` LLVM may cache - /// `self.flags`/`event_loop_timer().state` across the call and dead-store - /// the post-call reloads in `should_reschedule_timer`/`is_timer_done` — - /// the interval re-fires forever. Use a raw pointer; - /// helper calls `(*this).foo()` materialise short-lived `&mut` scoped to - /// each statement only — none span the JS call. - /// - /// Note (jsc/runtime crate cycle): `vm.timer` resolved via - /// `crate::jsc_hooks::runtime_state()` — low-tier `VirtualMachine.timer` - /// is `()` (see `set_enable_keeping_event_loop_alive`). - /// - /// # Safety - /// `this` points at a live `TimerObjectInternals` embedded in its - /// `TimeoutObject`/`ImmediateObject` parent (FIRE_TIMER hook contract); - /// `vm` is the live per-thread VM. - pub(crate) unsafe fn fire(this: *mut Self, _now: &ElTimespec, vm: *mut VirtualMachine) { - // SAFETY: per fn contract — `this` live. `&Self` (NOT `&mut`) — fields - // are `Cell`/`JsCell` so re-entrant JS touching this object via another - // `&Self` is sound (no `noalias`; LLVM cannot cache `Cell` reads across - // `Self::run`). Last use of `s` is the final `s.deref()` at the end of - // the pinned block; `*this` may be freed only after that point. - let s = unsafe { &*this }; + /// Timer-heap dispatch arm for `Tag::TimeoutObject`/`Tag::ImmediateObject`: + /// the JS timer's slot was just popped; invoke its callback via `run()`, + /// then either reschedule (setInterval / `t._repeat`) or release the heap's + /// ref. + fn fire(this: ThisPtr, vm: &VirtualMachine) { + let s = this.internals(); let id = s.id; let kind: KindBig = s.flags.get().kind().into(); let async_id = ID { id, kind }; - let has_been_cleared = s.event_loop_timer_state() == EventLoopTimerState::CANCELLED + let has_been_cleared = this.event_loop_timer_state() == EventLoopTimerState::CANCELLED || s.flags.get().has_cleared_timer() - // SAFETY: `vm` is the live per-thread VM (hook contract). - || unsafe { (*vm).script_execution_status() } != ScriptExecutionStatus::Running - // SAFETY: `vm` live per hook contract. - || s.generation != unsafe { (*vm).test_isolation_generation }; + || vm.script_execution_status() != ScriptExecutionStatus::Running + || s.generation != vm.test_isolation_generation; - s.set_event_loop_timer_state(EventLoopTimerState::FIRED); + this.set_event_loop_timer_state(EventLoopTimerState::FIRED); - // SAFETY: `vm` is live; `global` is the per-VM JSGlobalObject pointer. - let global_this = unsafe { (*vm).global }; + let global = vm.global(); let Some(this_object) = s.this_value.get().try_get() else { - s.set_enable_keeping_event_loop_alive(vm, false); + this.set_enable_keeping_event_loop_alive(false); s.update_flags(|f| f.set_has_cleared_timer(true)); s.this_value.with_mut(|r| r.downgrade()); - s.deref(); + Self::release_heap_ref(this); return; }; @@ -528,19 +359,17 @@ impl TimerObjectInternals { }; if has_been_cleared || !callback.to_boolean() { - // SAFETY: `vm`/`global_this` live per hook contract. - if unsafe { (*vm).is_inspector_enabled() } { + if vm.is_inspector_enabled() { Debugger::did_cancel_async_call( - // `opaque_ffi!` ZST — safe deref; `vm.global` never null. - JSGlobalObject::opaque_ref(global_this), + global, Debugger::AsyncCallType::DOMTimer, async_id.async_id(), ); } - s.set_enable_keeping_event_loop_alive(vm, false); + this.set_enable_keeping_event_loop_alive(false); s.update_flags(|f| f.set_has_cleared_timer(true)); s.this_value.with_mut(|r| r.downgrade()); - s.deref(); + Self::release_heap_ref(this); return; } @@ -557,32 +386,19 @@ impl TimerObjectInternals { } this_object.ensure_still_alive(); - let state = crate::jsc_hooks::runtime_state(); - debug_assert!(!state.is_null(), "RuntimeState not installed"); - - // SAFETY: `vm` is live; `event_loop()` returns `*mut` to the embedded - // EventLoop. Re-entrancy is permitted by the raw-ptr contract above. - unsafe { (*(*vm).event_loop()).enter() }; + vm.event_loop_mut().enter(); { // Ensure it stays alive for this scope. - s.ref_(); - // The matching `deref()` is at the end of this - // block. Every path through the labelled-block + `is_timer_done` - // tail reaches it (no `return` between here and the deref). - - // SAFETY: `this` is the live `internals` per fn contract; `ref_()` - // above pins the parent across re-entrancy. - let _ = unsafe { - Self::run( - this, - global_this, - this_object, - callback, - arguments, - async_id.async_id(), - vm, - ) - }; + let _pin = this.ref_guard(); + + let _ = this.run( + global, + this_object, + callback, + arguments, + async_id.async_id(), + vm, + ); match kind { KindBig::SetTimeout | KindBig::SetInterval => { @@ -594,47 +410,29 @@ impl TimerObjectInternals { KindBig::SetImmediate => {} } - // Every `s.flags.get()` below is a fresh `Cell` read — re-entrant - // `cancel()`/`refresh()` writes during `Self::run` above are - // observed (no `noalias` on `Cell` contents). + // Every `s.flags.get()` / `state` read below is fresh — re-entrant + // `cancel()`/`refresh()` writes during `run` above are observed. let is_timer_done = 'is_timer_done: { // Node doesn't drain microtasks after each timer callback. if kind == KindBig::SetInterval { - if !s.should_reschedule_timer(repeat, idle_timeout) { + if !this.should_reschedule_timer(repeat, idle_timeout) { break 'is_timer_done true; } - // `ref_()` above pins the parent across the deref. - match s.event_loop_timer_state() { + match this.event_loop_timer_state() { EventLoopTimerState::FIRED => { // If we didn't clear the setInterval, reschedule it starting from - // SAFETY: `state` is the boxed per-thread `RuntimeState`; - // single-threaded JS heap so no concurrent `&mut` to - // `.timer`. `event_loop_timer()` derives a fresh raw - // ptr (no `&mut` aliasing across `update`). - unsafe { - (*state) - .timer - .update(s.event_loop_timer(), &time_before_call) - }; + timer_all().update(this.timer_ref(), &time_before_call); if s.flags.get().has_js_ref() { - s.set_enable_keeping_event_loop_alive(vm, true); + this.set_enable_keeping_event_loop_alive(true); } - // The ref count doesn't change. It wasn't decremented. + // The heap keeps its ref. } EventLoopTimerState::ACTIVE => { - // The developer called timer.refresh() synchronously in the callback. - // SAFETY: as above. - unsafe { - (*state) - .timer - .update(s.event_loop_timer(), &time_before_call) - }; - - // Balance out the ref count. - // the transition from "FIRED" -> "ACTIVE" caused it to increment. - s.deref(); + // The developer called timer.refresh() synchronously in the callback; + // `reschedule()` saw the heap's ref still held and re-linked under it. + timer_all().update(this.timer_ref(), &time_before_call); } _ => { break 'is_timer_done true; @@ -644,30 +442,26 @@ impl TimerObjectInternals { if kind == KindBig::SetTimeout && !repeat.is_null() { if let Some(num) = idle_timeout.get_number() { if num != -1.0 { - // reschedule() inside convertToInterval will see state == .FIRED - // and add a ref; fall through to the switch below so the .ACTIVE - // arm can balance it. - s.convert_to_interval(global_this, this_object, repeat, vm); + // reschedule() inside convertToInterval re-links under the + // heap's still-held ref; the .ACTIVE arm below keeps it. + Self::convert_to_interval(this, global, this_object, repeat); } } } - // `ref_()` above pins the parent across the deref. - match s.event_loop_timer_state() { + match this.event_loop_timer_state() { EventLoopTimerState::FIRED => { break 'is_timer_done true; } EventLoopTimerState::ACTIVE => { // The developer called timer.refresh() synchronously in the callback, - // or the timer was converted to an interval via t._repeat. Balance out - // the ref count: the transition from "FIRED" -> "ACTIVE" via - // reschedule() caused it to increment. - s.deref(); + // or the timer was converted to an interval via t._repeat. It is + // linked again; the heap keeps its ref. } _ => { // The developer called clearTimeout() synchronously in the callback. - // cancel() saw state == .FIRED and skipped its deref, so release the - // heap ref here. + // cancel() saw state == .FIRED and left the heap's ref, so release + // it here. break 'is_timer_done true; } } @@ -677,37 +471,28 @@ impl TimerObjectInternals { }; if is_timer_done { - s.set_enable_keeping_event_loop_alive(vm, false); + this.set_enable_keeping_event_loop_alive(false); // The timer will not be re-entered into the event loop at this point. - s.deref(); + Self::release_heap_ref(this); } - - // End of pinned scope. After - // this `*this` may be freed; do not touch past this block. - s.deref(); + // `_pin` drops here; after that `this` may be gone. } // --- after this point, the timer is no longer guaranteed to be alive --- - // SAFETY: `vm` is live; see `enter()` note above. - unsafe { (*(*vm).event_loop()).exit() }; + vm.event_loop_mut().exit(); } /// A `setTimeout` whose /// `t._repeat` was assigned promotes itself to a `setInterval` after its /// first fire (Node `lib/internal/timers.js:613`). - /// - /// Note: takes `vm` explicitly instead of `global.bun_vm()` so the - /// raw-ptr contract from `fire()` is preserved (no fresh `&mut VM`). - /// `&self` (not `&mut`) — all writes go through `Cell`/`JsCell`; the sole - /// caller (`fire()`) holds only a `&Self`. fn convert_to_interval( - &self, - global: *mut JSGlobalObject, + this: ThisPtr, + global: &JSGlobalObject, timer: JSValue, repeat: JSValue, - vm: *mut VirtualMachine, ) { - debug_assert!(self.flags.get().kind() == Kind::SetTimeout); + let internals = this.internals(); + debug_assert!(internals.flags.get().kind() == Kind::SetTimeout); let new_interval: u32 = if let Some(num) = repeat.get_number() { if num < 1.0 || num > f64::from(u32::MAX >> 1) { @@ -720,18 +505,17 @@ impl TimerObjectInternals { }; // https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L613 - // `opaque_ffi!` ZST — safe deref; `vm.global` never null. - let global_ref = JSGlobalObject::opaque_ref(global); - JSTimeout::idle_timeout_set_cached(timer, global_ref, repeat); - self.this_value - .with_mut(|r| r.set_strong(timer, global_ref)); - self.update_flags(|f| f.set_kind(Kind::SetInterval)); - self.interval.set(new_interval); - self.reschedule(timer, vm, global); + JSTimeout::idle_timeout_set_cached(timer, global, repeat); + internals + .this_value + .with_mut(|r| r.set_strong(timer, global)); + internals.update_flags(|f| f.set_kind(Kind::SetInterval)); + internals.interval.set(new_interval); + Self::reschedule(this, timer, global); } fn should_reschedule_timer(&self, repeat: JSValue, idle_timeout: JSValue) -> bool { - if self.flags.get().kind() == Kind::SetInterval && repeat.is_null() { + if self.internals().flags.get().kind() == Kind::SetInterval && repeat.is_null() { return false; } if let Some(num) = idle_timeout.get_number() { @@ -742,18 +526,12 @@ impl TimerObjectInternals { true } - /// Re-insert the parent's - /// `EventLoopTimer` into the heap at `now + interval`. Called from - /// `init()`, `do_refresh()`, and `convert_to_interval()` above. - /// - /// Note (jsc/runtime crate cycle): `vm.timer` resolved via `runtime_state()`. - pub(crate) fn reschedule( - &self, - timer: JSValue, - vm: *mut VirtualMachine, - global_this: *mut JSGlobalObject, - ) { - if self.flags.get().kind() == Kind::SetImmediate { + /// (Re-)insert the timer's slot into the heap at `now + interval`, taking + /// the heap's ref if it is not already held. Called from `schedule()`, + /// `do_refresh()`, and `convert_to_interval()`. + fn reschedule(this: ThisPtr, timer: JSValue, global: &JSGlobalObject) { + let internals = this.internals(); + if internals.flags.get().kind() == Kind::SetImmediate { return; } @@ -762,149 +540,94 @@ impl TimerObjectInternals { let repeat = JSTimeout::repeat_get_cached(timer).expect("TimeoutObject repeat slot"); // https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L612 - if !self.should_reschedule_timer(repeat, idle_timeout) { + if !this.should_reschedule_timer(repeat, idle_timeout) { return; } - let state = crate::jsc_hooks::runtime_state(); - debug_assert!(!state.is_null(), "RuntimeState not installed"); - let now = Timespec::now(TimespecMockMode::AllowMockedTime); - let scheduled_time = now.add_ms(i64::from(self.interval.get())); - let was_active = self.event_loop_timer_state() == EventLoopTimerState::ACTIVE; + let scheduled_time = now.add_ms(i64::from(internals.interval.get())); + let was_active = this.event_loop_timer_state() == EventLoopTimerState::ACTIVE; if was_active { - // SAFETY: `state` is the boxed per-thread `RuntimeState`; fresh - // `&mut` to `.timer` for this call only. - unsafe { (*state).timer.remove(self.event_loop_timer()) }; + timer_all().remove(this.timer_ref()); } else { - self.ref_(); + Self::hold_heap_ref(this); } - // SAFETY: as above — `event_loop_timer()` derives a fresh raw ptr (no - // `&mut` aliasing across `update`). - unsafe { - (*state) - .timer - .update(self.event_loop_timer(), &scheduled_time) - }; - self.update_flags(|f| f.set_has_cleared_timer(false)); + timer_all().update(this.timer_ref(), &scheduled_time); + internals.update_flags(|f| f.set_has_cleared_timer(false)); // Set _idleStart to the current monotonic timestamp in milliseconds // This mimics Node.js's behavior where _idleStart is the libuv timestamp when the timer was scheduled JSTimeout::idle_start_set_cached( timer, - // `opaque_ffi!` ZST — safe deref; `vm.global` never null. - JSGlobalObject::opaque_ref(global_this), + global, JSValue::js_number(now.ms_unsigned() as f64), ); - if self.flags.get().has_js_ref() { - self.set_enable_keeping_event_loop_alive(vm, true); + if internals.flags.get().has_js_ref() { + this.set_enable_keeping_event_loop_alive(true); } } - /// Final teardown, invoked from the parent container's `Drop` (count hit - /// zero). Unlinks the parent from every `Timer::All` data structure it may - /// still be reachable from so the free cannot leave a dangling - /// `*mut EventLoopTimer` in the heap or a leaked keep-alive count. - /// `this_value` is released by `JsRef: Drop` right after. - /// - /// # Safety - /// `self` is the `internals` field of a live heap-allocated - /// `TimeoutObject`/`ImmediateObject` whose refcount has just reached zero. - /// The per-thread `RuntimeState` and `VirtualMachine` are installed (always - /// true on the JS thread by the time a timer can be dropped). - pub(crate) unsafe fn deinit(&mut self) { - let vm = VirtualMachine::get_mut_ptr(); - let kind = self.flags.get().kind(); - - let state = crate::jsc_hooks::runtime_state(); - debug_assert!(!state.is_null(), "RuntimeState not installed"); - - // (b) `vm.timer.remove(eventLoopTimer())` if state == .ACTIVE — without - // this the freed parent stays linked into `All.timers` and the next - // `delete_min`/`drain_timers` dereferences freed memory. - if self.event_loop_timer_state() == EventLoopTimerState::ACTIVE { - // SAFETY: `state` is the boxed per-thread `RuntimeState`; - // single-threaded JS heap so no concurrent `&mut` to `.timer`. - unsafe { (*state).timer.remove(self.event_loop_timer()) }; - } + /// `Drop` body (the refcount reached zero): unlink `self` from every + /// `timer::All` structure it may still be reachable from so the imminent + /// free cannot leave a dangling slot in the heap, a dangling id-map entry, + /// or a leaked keep-alive count. `this_value` is released by `JsRef: Drop`. + fn unschedule_for_drop(&mut self) { + let kind = self.internals().flags.get().kind(); + let id = self.internals().id; - // (c) `vm.timer.maps.get(kind).swapRemove(id)` if - // `has_accessed_primitive` — drops the i32→*mut EventLoopTimer - // entry minted by `to_primitive`. Swap-remove: the id map is only - // ever keyed into, never iterated in order, and `deinit` runs for - // every id-accessed timer a GC sweep collects, so the ordered - // remove's O(n) shift + index rebuild here was O(n²) across a - // sweep. - if self.flags.get().has_accessed_primitive() { - // SAFETY: as above — fresh `&mut` to `.timer.maps` for this call. - let map = unsafe { (*state).timer.maps.get(kind) }; - if map.swap_remove(&self.id) { - // If this map got - // large, shrink it back down. Keys are i32, values are one - // pointer (~12 bytes per entry), so 21,000 timers accessed by - // ID ≈ 252 KiB; reclaim once the slack exceeds 256 KiB. - const ENTRY_SIZE: usize = - core::mem::size_of::() + core::mem::size_of::<*mut EventLoopTimer>(); - let allocated_bytes = map.capacity() * ENTRY_SIZE; - let used_bytes = map.count() * ENTRY_SIZE; - if allocated_bytes - used_bytes > 256 * 1024 { - map.shrink_and_free(map.count() + 8); + if self.event_loop_timer_state() == EventLoopTimerState::ACTIVE { + timer_all().remove(self.timer_ref()); + } + + // Drop the `id → timer` entry minted by `to_primitive`. Swap-remove: the + // id map is only ever keyed into, never iterated in order, and this runs + // for every id-accessed timer a GC sweep collects, so the ordered + // remove's O(n) shift + index rebuild here was O(n²) across a sweep. + if self.internals().flags.get().has_accessed_primitive() { + timer_all().maps.with_mut(|maps| { + let map = Self::id_map(maps, kind); + if map.swap_remove(&id) { + // If this map got + // large, shrink it back down. Keys are i32, values are one + // pointer (~12 bytes per entry), so 21,000 timers accessed by + // ID ≈ 252 KiB; reclaim once the slack exceeds 256 KiB. + const ENTRY_SIZE: usize = + core::mem::size_of::() + core::mem::size_of::<*mut EventLoopTimer>(); + let allocated_bytes = map.capacity() * ENTRY_SIZE; + let used_bytes = map.count() * ENTRY_SIZE; + if allocated_bytes - used_bytes > 256 * 1024 { + map.shrink_and_free(map.count() + 8); + } + } else if kind == Kind::SetInterval { + // A `setTimeout` promoted to a `setInterval` by + // `convert_to_interval()` keeps the entry minted by + // `to_primitive` in `maps.set_timeout`. Remove it from there + // too, or `remove_timer_by_id` would hand out a dangling + // entry after the parent is freed. + maps.set_timeout.swap_remove(&id); } - } else if kind == Kind::SetInterval { - // A `setTimeout` promoted to a `setInterval` by - // `convert_to_interval()` keeps the entry minted by - // `to_primitive` in `maps.set_timeout`. Remove it from there - // too, or `remove_timer_by_id` would hand out a dangling - // `*mut EventLoopTimer` after the parent is freed. - // SAFETY: as above. - unsafe { (*state).timer.maps.set_timeout.swap_remove(&self.id) }; - } + }); } - // (d) `setEnableKeepingEventLoopAlive(vm, false)` — without this a - // dropped-while-ref'd timer leaks `active_timer_count` / - // `immediate_ref_count` and the process hangs at exit. - self.set_enable_keeping_event_loop_alive(vm, false); - } -} - -// ────────────────────────────────────────────────────────────────────────── -// JS-host-method facade — `do_ref`/`do_unref`/`do_refresh`/`has_ref`/ -// `to_primitive`/`get_destroyed`/`finalize`/`cancel`, called from -// `TimeoutObject.rs` / `ImmediateObject.rs` host-fn shims. -// ────────────────────────────────────────────────────────────────────────── -impl TimerObjectInternals { - /// Read-only `container_of` to the owning `EventLoopTimer.state`. - /// - /// Single back-ref deref site for the read path: every former - /// `unsafe { (*self.event_loop_timer()).state }` routes through here. - fn event_loop_timer_state(&self) -> EventLoopTimerState { - // SAFETY: ptr into the live parent per `parent_ptr()`; read-only deref. - unsafe { (*self.event_loop_timer()).state } + // Without this a dropped-while-ref'd timer leaks `active_timer_count` / + // `immediate_ref_count` and the process hangs at exit. + self.set_enable_keeping_event_loop_alive(false); } - /// Write the owning `EventLoopTimer.state`. Paired write-side accessor for - /// [`event_loop_timer_state`]; centralises the back-ref deref so call sites - /// stay safe. - fn set_event_loop_timer_state(&self, state: EventLoopTimerState) { - // SAFETY: ptr into the live parent per `parent_ptr()`. `state` is a - // plain `Copy` enum; writes happen on the single JS thread, and - // `event_loop_timer()` returns a raw `*mut` precisely so re-entrant - // `cancel()`/`refresh()` cannot alias a `&mut` (see its doc comment). - unsafe { (*self.event_loop_timer()).state = state }; - } + // ────────────────────────────────────────────────────────────────────── + // JS-host-method facade — `do_ref`/`do_unref`/`do_refresh`/`has_ref`/ + // `to_primitive`/`get_destroyed`/`cancel`, called from the + // `TimeoutObject.rs` / `ImmediateObject.rs` host-fn shims. + // ────────────────────────────────────────────────────────────────────── - pub(crate) fn do_ref( - &self, - _global: &JSGlobalObject, - this_value: JSValue, - ) -> JsResult { + fn do_ref(&self, this_value: JSValue) -> JsResult { this_value.ensure_still_alive(); - let did_have_js_ref = self.flags.get().has_js_ref(); - self.update_flags(|f| f.set_has_js_ref(true)); + let internals = self.internals(); + let did_have_js_ref = internals.flags.get().has_js_ref(); + internals.update_flags(|f| f.set_has_js_ref(true)); // https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L256 // and @@ -914,24 +637,21 @@ impl TimerObjectInternals { // has `has_cleared_timer == false` but is still destroyed. Calling `.unref(); .ref()` // on such a timer would otherwise leak an event-loop ref and hang the process. if !did_have_js_ref && !self.get_destroyed() { - self.set_enable_keeping_event_loop_alive(VirtualMachine::get_mut_ptr(), true); + self.set_enable_keeping_event_loop_alive(true); } Ok(this_value) } - pub(crate) fn do_unref( - &self, - _global: &JSGlobalObject, - this_value: JSValue, - ) -> JsResult { + fn do_unref(&self, this_value: JSValue) -> JsResult { this_value.ensure_still_alive(); - let did_have_js_ref = self.flags.get().has_js_ref(); - self.update_flags(|f| f.set_has_js_ref(false)); + let internals = self.internals(); + let did_have_js_ref = internals.flags.get().has_js_ref(); + internals.update_flags(|f| f.set_has_js_ref(false)); if did_have_js_ref { - self.set_enable_keeping_event_loop_alive(VirtualMachine::get_mut_ptr(), false); + self.set_enable_keeping_event_loop_alive(false); } Ok(this_value) @@ -939,100 +659,81 @@ impl TimerObjectInternals { /// Node's deadline is `_idleStart + _idleTimeout`; writing `_idleStart` /// must move the heap entry so `t2._idleStart = t1._idleStart` works. - pub(crate) fn set_idle_start(&self, idle_start_ms: f64) { - if self.flags.get().kind() == Kind::SetImmediate - || self.flags.get().has_cleared_timer() + fn set_idle_start(&self, idle_start_ms: f64) { + let internals = self.internals(); + if internals.flags.get().kind() == Kind::SetImmediate + || internals.flags.get().has_cleared_timer() || self.event_loop_timer_state() != EventLoopTimerState::ACTIVE || !idle_start_ms.is_finite() { return; } - let state = crate::jsc_hooks::runtime_state(); - debug_assert!(!state.is_null(), "RuntimeState not installed"); - let ms = (idle_start_ms as i64) - .saturating_add(i64::from(self.interval.get())) + .saturating_add(i64::from(internals.interval.get())) .max(0); let scheduled_time = Timespec::EPOCH.add_ms(ms); - // SAFETY: `state` is the boxed per-thread `RuntimeState`; fresh - // `&mut` to `.timer` for this call only. The timer is ACTIVE so - // `update()` removes then re-inserts with no refcount change. - unsafe { - (*state) - .timer - .update(self.event_loop_timer(), &scheduled_time) - }; + // The timer is ACTIVE so `update()` removes then re-inserts; the heap + // keeps its ref. + timer_all().update(self.timer_ref(), &scheduled_time); } - pub(crate) fn do_refresh( - &self, + fn do_refresh( + this: ThisPtr, global_object: &JSGlobalObject, this_value: JSValue, ) -> JsResult { + let internals = this.internals(); // Immediates do not have a refresh function, and our binding generator should not let this // function be reached even if you override the `this` value calling a Timeout object's // `refresh` method - debug_assert!(self.flags.get().kind() != Kind::SetImmediate); + debug_assert!(internals.flags.get().kind() != Kind::SetImmediate); // setImmediate does not support refreshing and we do not support refreshing after cleanup - if self.id == -1 - || self.flags.get().kind() == Kind::SetImmediate - || self.flags.get().has_cleared_timer() + if internals.id == -1 + || internals.flags.get().kind() == Kind::SetImmediate + || internals.flags.get().has_cleared_timer() { return Ok(this_value); } - self.this_value + internals + .this_value .with_mut(|r| r.set_strong(this_value, global_object)); - self.reschedule( - this_value, - VirtualMachine::get_mut_ptr(), - global_object.as_ptr(), - ); + Self::reschedule(this, this_value, global_object); Ok(this_value) } - pub(crate) fn has_ref(&self) -> JsResult { + fn has_ref(&self) -> JsResult { Ok(JSValue::from( - self.flags.get().is_keeping_event_loop_alive(), + self.internals().flags.get().is_keeping_event_loop_alive(), )) } - /// First access mints an - /// `id → *mut EventLoopTimer` entry in `All.maps` so `clearTimeout(+t)` / - /// `clearImmediate(+t)` (numeric-id form) can resolve it. - /// - /// Note (jsc/runtime crate cycle): `vm.timer.maps` resolved via `runtime_state()`. - pub(crate) fn to_primitive(&self) -> JsResult { - if !self.flags.get().has_accessed_primitive() { - self.update_flags(|f| f.set_has_accessed_primitive(true)); - let state = crate::jsc_hooks::runtime_state(); - debug_assert!(!state.is_null(), "RuntimeState not installed"); - // Note: reshaped for borrowck — capture `event_loop_timer` ptr - // before borrowing `(*state).timer.maps`. - let elt = self.event_loop_timer(); - // SAFETY: `state` is the boxed per-thread `RuntimeState`; - // single-threaded JS heap so no concurrent `&mut` to `.timer.maps`. - unsafe { - (*state) - .timer - .maps - .get(self.flags.get().kind()) - .put(self.id, elt) - }?; - } - Ok(JSValue::js_number(f64::from(self.id))) + /// First access mints an `id → timer` entry in `All.maps` so + /// `clearTimeout(+t)` / `clearImmediate(+t)` (numeric-id form) can resolve + /// it. `Drop` removes the entry. + fn to_primitive(this: ThisPtr) -> JsResult { + let internals = this.internals(); + if !internals.flags.get().has_accessed_primitive() { + internals.update_flags(|f| f.set_has_accessed_primitive(true)); + let kind = internals.flags.get().kind(); + timer_all() + .maps + .with_mut(|maps| Self::id_map(maps, kind).put(internals.id, BackRef::from(this)))?; + } + Ok(JSValue::js_number(f64::from(internals.id))) } /// Getter for `_destroyed` /// on JS Timeout and Immediate objects. - pub(crate) fn get_destroyed(&self) -> bool { - if self.flags.get().has_cleared_timer() { + fn get_destroyed(&self) -> bool { + let internals = self.internals(); + if internals.flags.get().has_cleared_timer() { return true; } - if self.flags.get().in_callback() { + if internals.flags.get().in_callback() { return false; } match self.event_loop_timer_state() { @@ -1041,43 +742,40 @@ impl TimerObjectInternals { } } - /// `.classes.ts` finalizer hook. - /// Runs on the mutator thread during lazy sweep; do not touch any - /// `JSValue`/`Strong` content here. - pub fn finalize(&self) { - self.this_value.with_mut(|r| r.finalize()); - } - /// `clearTimeout`/`clearInterval` - /// / `clearImmediate` / `Timeout#[Symbol.dispose]` body. - /// - /// Note: takes `*mut VirtualMachine` (NOT `&mut`) — callers hand over - /// `global.bun_vm()` (raw ptr) and the body forwards to - /// `set_enable_keeping_event_loop_alive` which already uses the raw-ptr - /// contract. `vm.timer` resolved via `runtime_state()` (jsc/runtime crate cycle). - pub(crate) fn cancel(&self, vm: *mut VirtualMachine) { - self.set_enable_keeping_event_loop_alive(vm, false); - self.update_flags(|f| f.set_has_cleared_timer(true)); - - if self.flags.get().kind() == Kind::SetImmediate { + /// / `clearImmediate` / `Timeout#[Symbol.dispose]` body. May free `this`. + fn cancel(this: ThisPtr) { + let internals = this.internals(); + this.set_enable_keeping_event_loop_alive(false); + internals.update_flags(|f| f.set_has_cleared_timer(true)); + + if internals.flags.get().kind() == Kind::SetImmediate { // Release the strong reference so the GC can collect the JS object. // The immediate task is still in the event loop queue and will be skipped // by runImmediateTask when it sees has_cleared_timer == true. - self.this_value.with_mut(|r| r.downgrade()); + internals.this_value.with_mut(|r| r.downgrade()); return; } - let was_active = self.event_loop_timer_state() == EventLoopTimerState::ACTIVE; - self.set_event_loop_timer_state(EventLoopTimerState::CANCELLED); - self.this_value.with_mut(|r| r.downgrade()); + let was_active = this.event_loop_timer_state() == EventLoopTimerState::ACTIVE; + this.set_event_loop_timer_state(EventLoopTimerState::CANCELLED); + internals.this_value.with_mut(|r| r.downgrade()); if was_active { - let state = crate::jsc_hooks::runtime_state(); - debug_assert!(!state.is_null(), "RuntimeState not installed"); - // SAFETY: `state` is the boxed per-thread `RuntimeState`; - // single-threaded JS heap so no concurrent `&mut` to `.timer`. - unsafe { (*state).timer.remove(self.event_loop_timer()) }; - self.deref(); + timer_all().remove(this.timer_ref()); + Self::release_heap_ref(this); + } + } + + /// [`cancel`](Self::cancel) on behalf of something other than the timer + /// itself (VM teardown, the fake clock's `clear`), which may already have + /// popped the slot: also releases the heap's ref when `cancel()` finds the + /// slot no longer `ACTIVE`. May free `this`. + fn release_heap_entry(this: ThisPtr) { + let held = this.heap_ref().take(); + Self::cancel(this); + if let Some(held) = held { + held.deref(); } } } From 5300f40daea3d96ea047c9c0ab11b1390d6ec810 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 10:21:57 +0000 Subject: [PATCH 2/8] timer: fire WTF timers without reading the slot, make All::remove a no-op for unlinked slots - drain_due_wtf_timers fires the WTFTimer directly (dispatch::fire_wtf_timer) instead of going through the tag dispatch, so nothing reads a WTF slot outside the wtf_timers lock; the tag asserts in wtf_arm/wtf_disarm move under the lock. - All::remove is the single 'disarm if armed' entry point: an unlinked slot is left alone (and marked CANCELLED) rather than debug-asserting, and the DevServerMemoryVisualizerTick arm marks its slot FIRED like every other owner, so DevServer's state == ACTIVE guards stay consistent with the heap. - get_timeout no longer needs the VM. - drop the unused JsCell::from_mut. --- src/ptr/js_cell.rs | 7 ------ src/runtime/dispatch.rs | 21 ++++++++++++----- src/runtime/jsc_hooks.rs | 4 ---- src/runtime/timer/mod.rs | 49 +++++++++++++++++++--------------------- 4 files changed, 38 insertions(+), 43 deletions(-) diff --git a/src/ptr/js_cell.rs b/src/ptr/js_cell.rs index 3b2b0e517984..e9d9c7567c33 100644 --- a/src/ptr/js_cell.rs +++ b/src/ptr/js_cell.rs @@ -41,13 +41,6 @@ impl JsCell { Self(core::cell::UnsafeCell::new(value)) } - /// View exclusively-borrowed storage as a cell (like `Cell::from_mut`). - #[inline(always)] - pub fn from_mut(value: &mut T) -> &mut JsCell { - // SAFETY: `JsCell` is `repr(transparent)` over `UnsafeCell`. - unsafe { &mut *core::ptr::from_mut(core::cell::UnsafeCell::from_mut(value)).cast() } - } - /// Shared-reference read. Caller must not hold a live `get_mut()` borrow /// across this call (single-JS-thread reentrancy makes overlap rare but /// possible — keep borrows short). diff --git a/src/runtime/dispatch.rs b/src/runtime/dispatch.rs index 210c6e0d1846..023d38470682 100644 --- a/src/runtime/dispatch.rs +++ b/src/runtime/dispatch.rs @@ -893,6 +893,18 @@ macro_rules! timer_owner_this { }}; } +/// Fire the `WTFTimer` whose slot `t` was just popped from `All.wtf_timers`. +/// Does not read the slot (a GC thread may be re-arming it under the lock). +pub(crate) fn fire_wtf_timer(t: TimerRef) { + use crate::timer::WTFTimer; + // SAFETY: only `WTFTimer`s are linked into `All.wtf_timers` (`wtf_arm`), + // and the C++ `TimerBase` that owns it is alive while it is scheduled. The + // borrow ends before `fire`, which may destroy it. + let timer = unsafe { &*bun_core::from_field_ptr!(WTFTimer, event_loop_timer, t.as_ptr()) }; + let run_loop_timer = timer.take_for_fire(); + crate::timer::wtf_timer::RunLoopTimer::fire(run_loop_timer); +} + /// The tag→`container_of` match that fires a due timer. /// /// Reached from [`crate::timer::All::drain_timers`] (every due heap timer), @@ -907,7 +919,7 @@ macro_rules! timer_owner_this { /// `t` was just popped from a heap; the handler may free its owner, so `t` is /// dead once this returns. pub(crate) fn fire_timer(t: TimerRef, now: &ElTimespec, vm: &VirtualMachine) -> JsResult<()> { - use crate::timer::{ImmediateObject, TimeoutObject, TimerObject, WTFTimer}; + use crate::timer::{ImmediateObject, TimeoutObject, TimerObject}; /// Recover the embedding container from `t` (the popped timer slot). macro_rules! owner { @@ -956,11 +968,7 @@ pub(crate) fn fire_timer(t: TimerRef, now: &ElTimespec, vm: &VirtualMachine) -> Ok(()) } EventLoopTimerTag::WTFTimer => { - let c: *mut WTFTimer = owner!(WTFTimer, event_loop_timer); - // SAFETY: `TimerOwner` contract — `c` is a live `WTFTimer`. The - // borrow ends before `fire`, which may destroy it. - let run_loop_timer = unsafe { &*c }.take_for_fire(); - crate::timer::wtf_timer::RunLoopTimer::fire(run_loop_timer); + fire_wtf_timer(t); Ok(()) } EventLoopTimerTag::AbortSignalTimeout => { @@ -1083,6 +1091,7 @@ pub(crate) fn fire_timer(t: TimerRef, now: &ElTimespec, vm: &VirtualMachine) -> Ok(()) } EventLoopTimerTag::DevServerMemoryVisualizerTick => { + t.set_state(bun_event_loop::EventLoopTimer::State::FIRED); // SAFETY: per fn contract; `t` is the `memory_visualizer_timer` // field of a live DevServer. DevServer::emit_memory_visualizer_message_timer(unsafe { &mut *t.as_ptr() }, unsafe { diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index f00d4707e631..d92d2406581c 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -1057,8 +1057,6 @@ unsafe fn auto_tick(vm: *mut VirtualMachine) { &mut timespec, has_pending_immediate, quic_next_tick_us, - // SAFETY: per fn contract. - unsafe { &*vm }, &mut now, ); let now_ns = now.map_or(bun_uws::NOW_NS_UNKNOWN, |t| t.ns()); @@ -1166,8 +1164,6 @@ unsafe fn auto_tick_active(vm: *mut VirtualMachine) { &mut timespec, has_pending_immediate, quic_next_tick_us, - // SAFETY: per fn contract. - unsafe { &*vm }, &mut now, ); let now_ns = now.map_or(bun_uws::NOW_NS_UNKNOWN, |t| t.ns()); diff --git a/src/runtime/timer/mod.rs b/src/runtime/timer/mod.rs index 4b2c8f6ce78b..70f478decd0d 100644 --- a/src/runtime/timer/mod.rs +++ b/src/runtime/timer/mod.rs @@ -579,15 +579,17 @@ impl All { all.ensure_uv_timer(); } + /// Disarm `timer`: unlink it if it is linked (a slot that already left the + /// heap — popped to fire, or never inserted — is fine) and mark it + /// `CANCELLED`. This is the one "remove if armed" entry point; callers need + /// not check first. pub(crate) fn remove(&self, timer: TimerRef) { self.assert_js_thread(); match timer.in_heap() { InHeap::Regular => self.timers.remove(timer), InHeap::Fake => self.fake_timers.timers.remove(timer), - // can't remove a timer that was not inserted - InHeap::None | InHeap::Wtf => { - debug_assert!(false, "remove: timer is in {:?}", timer.in_heap()) - } + InHeap::None => {} + InHeap::Wtf => debug_assert!(false, "use wtf_disarm"), } timer.set_state(EventLoopTimerState::CANCELLED); } @@ -606,12 +608,12 @@ impl All { self.insert(timer); } - /// (Re)arm a `WTFTimer`. Any thread. + /// (Re)arm a `WTFTimer`. Any thread; the slot is only touched under the + /// `wtf_timers` lock. fn wtf_arm(&self, timer: TimerRef, time: Timespec) { - debug_assert!(timer.tag() == EventLoopTimerTag::WTFTimer); { let wtf = self.wtf_timers.lock(); - // The slot's state and heap links only change under this guard. + debug_assert!(timer.tag() == EventLoopTimerTag::WTFTimer); if timer.state() == EventLoopTimerState::ACTIVE { wtf.remove(timer); } @@ -625,25 +627,23 @@ impl All { } } - /// Disarm a `WTFTimer`; no-op if it is not linked. Any thread. + /// Disarm a `WTFTimer`; no-op if it is not linked. Any thread; the slot + /// is only touched under the `wtf_timers` lock. fn wtf_disarm(&self, timer: TimerRef) { - debug_assert!(timer.tag() == EventLoopTimerTag::WTFTimer); let wtf = self.wtf_timers.lock(); - // The slot's state and heap links only change under this guard. + debug_assert!(timer.tag() == EventLoopTimerTag::WTFTimer); if timer.state() == EventLoopTimerState::ACTIVE { wtf.remove(timer); timer.set_state(EventLoopTimerState::CANCELLED); } } - fn drain_due_wtf_timers( - &self, - maybe_now: &mut Option, - vm: &VirtualMachine, - ) -> Option { + /// Fire every due `WTFTimer` and return the next WTF deadline, if any. The + /// popped slot is only read under the lock; once it drops nothing here + /// touches the slot again (a GC thread may re-arm it concurrently). + fn drain_due_wtf_timers(&self, maybe_now: &mut Option) -> Option { loop { - let (min, now) = { - // The guard drops before `fire`. + let min = { let wtf = self.wtf_timers.lock(); let min_next = wtf.peek()?.next(); let now = *maybe_now @@ -653,13 +653,11 @@ impl All { } let min = wtf.delete_min().expect("peek succeeded"); min.set_state(EventLoopTimerState::FIRED); - (min, now) + min }; - let fired = crate::dispatch::fire_timer(min, &now, vm); - // WTF timers run JSC-internal work, not user JS; a stop found here - // is the loop's to act on at its next gate, and the heap's next - // deadline is still reported to the poll. - let _ = fold_timer(vm, fired); + // Only `WTFTimer`s are ever in this heap. They run JSC-internal + // work, not user JS, so there is nothing to fold. + crate::dispatch::fire_wtf_timer(min); } } @@ -683,7 +681,6 @@ impl All { spec: &mut Timespec, has_pending_immediate: bool, quic_next_tick_us: Option, - vm: &VirtualMachine, now_out: &mut Option, ) -> bool { #[cfg(unix)] @@ -696,7 +693,7 @@ impl All { let maybe_now: &mut Option = now_out; - let wtf_next = self.drain_due_wtf_timers(maybe_now, vm); + let wtf_next = self.drain_due_wtf_timers(maybe_now); let reg_next = self.timers.peek().map(TimerRef::next); let Some(next) = Self::soonest(wtf_next, reg_next) else { @@ -760,7 +757,7 @@ impl All { /// holds a borrow of the heap across `fire_timer`. pub(crate) fn drain_timers(&self, vm: &VirtualMachine) { let mut wtf_now: Option = None; - let _ = self.drain_due_wtf_timers(&mut wtf_now, vm); + let _ = self.drain_due_wtf_timers(&mut wtf_now); let mut now = Timespec { sec: 0, nsec: 0 }; let mut has_set_now = false; From a77039ecc23dca8814247c3f93b7bdcf518d41ab Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 10:51:07 +0000 Subject: [PATCH 3/8] dispatch: the WTFTimer arm of fire_timer is unreachable now; drop WTFTimer's write-only repeat flag --- src/runtime/dispatch.rs | 11 ++++------- src/runtime/timer/WTFTimer.rs | 7 ++----- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/runtime/dispatch.rs b/src/runtime/dispatch.rs index 023d38470682..1a1b7639d9e5 100644 --- a/src/runtime/dispatch.rs +++ b/src/runtime/dispatch.rs @@ -907,9 +907,9 @@ pub(crate) fn fire_wtf_timer(t: TimerRef) { /// The tag→`container_of` match that fires a due timer. /// -/// Reached from [`crate::timer::All::drain_timers`] (every due heap timer), -/// [`crate::timer::All::get_timeout`] (WTFTimer side-effect) and the fake -/// clock (`FakeTimers::fire`). +/// Reached from [`crate::timer::All::drain_timers`] (every due heap timer) +/// and the fake clock (`FakeTimers::fire`); `WTFTimer`s live in their own +/// heap and go through [`fire_wtf_timer`] instead. /// /// Each arm is the owner's timer entry with its result surfaced: an owner /// returns the exception it left pending and never reports it; the drain loop @@ -967,10 +967,7 @@ pub(crate) fn fire_timer(t: TimerRef, now: &ElTimespec, vm: &VirtualMachine) -> ); Ok(()) } - EventLoopTimerTag::WTFTimer => { - fire_wtf_timer(t); - Ok(()) - } + EventLoopTimerTag::WTFTimer => unreachable!("WTFTimers are only in All::wtf_timers"), EventLoopTimerTag::AbortSignalTimeout => { timer_arm!(AbortSignalTimeout, event_loop_timer, |c, _now, vm| { AbortSignalTimeout::run(c, vm) diff --git a/src/runtime/timer/WTFTimer.rs b/src/runtime/timer/WTFTimer.rs index 36ecf4854158..25db21c6a489 100644 --- a/src/runtime/timer/WTFTimer.rs +++ b/src/runtime/timer/WTFTimer.rs @@ -6,7 +6,7 @@ //! [`crate::jsc_hooks::timer_all`] instead. use core::ptr::{self, NonNull}; -use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; +use core::sync::atomic::{AtomicPtr, Ordering}; use bun_core::{Timespec, TimespecMockMode}; use bun_ptr::{BackRef, JsCell}; @@ -52,7 +52,6 @@ pub struct WTFTimer { /// (§Dispatch); `self` is published as `*mut ()` and `__bun_run_wtf_timer` /// (in `dispatch.rs`) recovers the `&WTFTimer`. imminent: BackRef>, - repeat: AtomicBool, script_execution_context_id: ScriptExecutionContextIdentifier, } @@ -107,7 +106,7 @@ impl WTFTimer { f64::INFINITY } - pub(crate) fn update(&self, seconds: f64, repeat: bool) { + pub(crate) fn update(&self, seconds: f64, _repeat: bool) { // There's only one of these per VM, and each VM has its own imminent_gc_timer. // Only set imminent if it's not already set to avoid overwriting another timer. if seconds.partial_cmp(&0.0) != Some(core::cmp::Ordering::Greater) { @@ -143,7 +142,6 @@ impl WTFTimer { } self.timers.wtf_arm(self.timer_ref(), interval); - self.repeat.store(repeat, Ordering::Relaxed); } pub(crate) fn cancel(&self) { @@ -206,7 +204,6 @@ pub fn create( }, )), run_loop_timer, - repeat: AtomicBool::new(false), script_execution_context_id: ScriptExecutionContextIdentifier( vm.initial_script_execution_context_identifier as u32, ), From 64f661ed29ef8398b4d8271017c1a2cd2b1723ae Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 12:13:22 +0000 Subject: [PATCH 4/8] rebase on #40192: timer id maps hold BackRef<_, Root> --- src/runtime/timer/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/timer/mod.rs b/src/runtime/timer/mod.rs index 70f478decd0d..9171ac973a0d 100644 --- a/src/runtime/timer/mod.rs +++ b/src/runtime/timer/mod.rs @@ -205,7 +205,7 @@ pub mod event_loop_delay_monitor; /// `clearTimeout(id)` lookup table: the object's `Drop` removes its entry, so /// every `BackRef` in here points at a live timer. -pub(crate) type IdMap = ArrayHashMap>; +pub(crate) type IdMap = ArrayHashMap>; /// i32 is exposed to JavaScript and can be used with clearTimeout, clearInterval, etc. #[derive(Default)] From 47d49128da8997e1ff11b1a7a8aa97eeeb4f4413 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 12:43:13 +0000 Subject: [PATCH 5/8] jsc_hooks: drop the leftover cfg(not(unix)) state suppression in auto_tick_active --- src/runtime/jsc_hooks.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index d92d2406581c..0611eb0d7266 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -1181,8 +1181,6 @@ unsafe fn auto_tick_active(vm: *mut VirtualMachine) { // SAFETY: per fn contract. #[cfg(unix)] timer_all().drain_timers(unsafe { &*vm }); - #[cfg(not(unix))] - let _ = state; // SAFETY: per fn contract. unsafe { (*vm).on_after_event_loop() }; From 27162da7c6d9be165f212a0ca19a2c766684a8b7 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 27 Aug 2026 08:50:08 +0000 Subject: [PATCH 6/8] timer_object_internals: RefPtr releases on drop (rebase onto #40478) --- src/runtime/timer/timer_object_internals.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/runtime/timer/timer_object_internals.rs b/src/runtime/timer/timer_object_internals.rs index 29f29009c282..9b1df9c94f32 100644 --- a/src/runtime/timer/timer_object_internals.rs +++ b/src/runtime/timer/timer_object_internals.rs @@ -97,7 +97,7 @@ unsafe extern "C" { /// Methods that may drop the heap's ref (and with it possibly the last ref) /// take `ThisPtr`; after they release it `this` may be gone. pub trait TimerObject: - bun_ptr::RefCounted + TimerOwner + Sized + 'static + bun_ptr::RefCounted + TimerOwner + Sized + 'static { fn internals(&self) -> &TimerObjectInternals; fn event_loop_timer(&self) -> &JsCell; @@ -134,9 +134,7 @@ pub trait TimerObject: /// Release the scheduled-timer ref, if held. May free `this`. #[inline] fn release_heap_ref(this: ThisPtr) { - if let Some(held) = this.heap_ref().take() { - held.deref(); - } + drop(this.heap_ref().take()); } fn set_enable_keeping_event_loop_alive(&self, enable: bool) { @@ -279,7 +277,7 @@ pub trait TimerObject: JSImmediate::arguments_get_cached(timer).expect("ImmediateObject arguments slot"); let exception_thrown = { - let _pin = this.ref_guard(); + let _pin = RefPtr::from_this(this); let async_id = s.async_id(); let result = this.run(global, timer, callback, arguments, async_id, vm); // Fresh read: re-entrant `cancel()` may have changed `state`. @@ -389,7 +387,7 @@ pub trait TimerObject: vm.event_loop_mut().enter(); { // Ensure it stays alive for this scope. - let _pin = this.ref_guard(); + let _pin = RefPtr::from_this(this); let _ = this.run( global, @@ -774,8 +772,6 @@ pub trait TimerObject: fn release_heap_entry(this: ThisPtr) { let held = this.heap_ref().take(); Self::cancel(this); - if let Some(held) = held { - held.deref(); - } + drop(held); } } From 633023a7e772d0e078f60c9bf369122721599413 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 27 Aug 2026 09:53:11 +0000 Subject: [PATCH 7/8] timer: clearImmediate ignores primitive ids; QuicEndpoint::timer_ref helper; WTFTimer teardown-order doc --- src/runtime/node/quic/endpoint.rs | 24 +++++++++------------ src/runtime/timer/Timer.rs | 5 +++++ src/runtime/timer/WTFTimer.rs | 14 ++++++------ src/runtime/timer/timer_object_internals.rs | 4 +--- test/js/web/timers/setImmediate.test.js | 16 ++++++++++++++ 5 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src/runtime/node/quic/endpoint.rs b/src/runtime/node/quic/endpoint.rs index a8a97d23eba8..2f60de543389 100644 --- a/src/runtime/node/quic/endpoint.rs +++ b/src/runtime/node/quic/endpoint.rs @@ -287,6 +287,11 @@ unsafe extern "C" { } impl QuicEndpoint { + #[inline] + fn timer_ref(&self) -> crate::timer::TimerRef { + crate::timer::TimerRef::new(self, |e| &e.event_loop_timer) + } + /// Links this endpoint into the loop's driver list. Idempotent. fn link_loop_driver(&self) { if self.nq_registered.replace(true) { @@ -1581,10 +1586,7 @@ impl QuicEndpoint { // time-driven state (RTO, ACK delay, idle) and the deferred close. self.mark_driver_pending(); let next = bun_core::Timespec::ms_from_now(bun_core::TimespecMockMode::ForceRealTime, 1); - timer_all().update( - crate::timer::TimerRef::new(self, |e| &e.event_loop_timer), - &next, - ); + timer_all().update(self.timer_ref(), &next); } fn rearm_timer(&self) { @@ -1622,10 +1624,7 @@ impl QuicEndpoint { bun_core::TimespecMockMode::ForceRealTime, ms as i64, ); - timer_all().update( - crate::timer::TimerRef::new(self, |e| &e.event_loop_timer), - &next, - ); + timer_all().update(self.timer_ref(), &next); } } @@ -2073,7 +2072,7 @@ impl QuicEndpoint { // Unlink first: the driver walk must never reach a freed endpoint. self.unlink_loop_driver(); if self.event_loop_timer.get().state == EventLoopTimerState::ACTIVE { - timer_all().remove(crate::timer::TimerRef::new(self, |e| &e.event_loop_timer)); + timer_all().remove(self.timer_ref()); } for engine in [ self.server_engine.replace(null_mut()), @@ -2121,10 +2120,7 @@ impl QuicEndpoint { self.poll_ref.with_mut(|p| p.ref_(bun_io::js_vm_ctx())); self.pending_endpoint_close.set(true); let next = bun_core::Timespec::ms_from_now(bun_core::TimespecMockMode::ForceRealTime, 1); - timer_all().update( - crate::timer::TimerRef::new(self, |e| &e.event_loop_timer), - &next, - ); + timer_all().update(self.timer_ref(), &next); } fn apply_server_session_options( @@ -2646,7 +2642,7 @@ impl QuicEndpoint { // Remove the timer before the backing storage drops, or a later heap // operation dereferences the freed node. if self.event_loop_timer.get().state == EventLoopTimerState::ACTIVE { - timer_all().remove(crate::timer::TimerRef::new(&*self, |e| &e.event_loop_timer)); + timer_all().remove(self.timer_ref()); } self.release_native(); } diff --git a/src/runtime/timer/Timer.rs b/src/runtime/timer/Timer.rs index 21f499a30cb2..ede50974b2a4 100644 --- a/src/runtime/timer/Timer.rs +++ b/src/runtime/timer/Timer.rs @@ -293,6 +293,11 @@ impl All { let all = timer_all(); let timer: ThisPtr = 'brk: { + // Immediates have no numeric id (Node.js: `clearImmediate` only + // accepts the Immediate object), so a primitive never names one. + if kind == Kind::SetImmediate && !timer_id_value.is_object() { + return Ok(()); + } if timer_id_value.is_number() { // Node.js looks the id up by value (`knownTimersById[id]`): a double holding an // integer names the same timer as the int32. Anything else clears nothing. diff --git a/src/runtime/timer/WTFTimer.rs b/src/runtime/timer/WTFTimer.rs index 25db21c6a489..1426151e3081 100644 --- a/src/runtime/timer/WTFTimer.rs +++ b/src/runtime/timer/WTFTimer.rs @@ -37,11 +37,11 @@ impl RunLoopTimer { /// by the C++ `RunLoop::TimerBase` that `WTFTimer__create`d it; `update` / /// `cancel` may arrive from any thread. pub struct WTFTimer { - /// `Timer::All` of the VM whose JS thread created this timer. The C++ - /// `RunLoop::TimerBase` that owns this wrapper lives on that VM's run loop - /// and is destroyed with the JSC VM, before `RuntimeState` (which holds - /// `All`) is freed. Off that thread only `wtf_arm`/`wtf_disarm`/the - /// `wtf_timers` lock may be used through it. + /// `Timer::All` of the VM whose JS thread created this timer. Live while + /// `script_execution_context_id` is valid; a C++ `RunLoop::TimerBase` can + /// outlive `RuntimeState` (which holds `All`) on Worker teardown, so + /// `cancel`/`Drop` only follow this after that check. Off the JS thread + /// only `wtf_arm`/`wtf_disarm`/the `wtf_timers` lock may be used through it. timers: BackRef, // FFI handle into WebKit's RunLoop::TimerBase; owned by C++. run_loop_timer: NonNull, @@ -154,7 +154,9 @@ impl WTFTimer { Ordering::SeqCst, ); - // No-op for a slot that is no longer linked. + // No-op for a slot that is no longer linked. Not reached once the + // context is gone: `timers` may already be freed by then (see the + // field), and nothing walks `wtf_timers` after that point. self.timers.wtf_disarm(self.timer_ref()); } } diff --git a/src/runtime/timer/timer_object_internals.rs b/src/runtime/timer/timer_object_internals.rs index 9b1df9c94f32..f910c716b014 100644 --- a/src/runtime/timer/timer_object_internals.rs +++ b/src/runtime/timer/timer_object_internals.rs @@ -96,9 +96,7 @@ unsafe extern "C" { /// `refresh()`, the `_destroyed` getter), so all state is in `Cell`/`JsCell`. /// Methods that may drop the heap's ref (and with it possibly the last ref) /// take `ThisPtr`; after they release it `this` may be gone. -pub trait TimerObject: - bun_ptr::RefCounted + TimerOwner + Sized + 'static -{ +pub trait TimerObject: bun_ptr::RefCounted + TimerOwner + Sized + 'static { fn internals(&self) -> &TimerObjectInternals; fn event_loop_timer(&self) -> &JsCell; /// The slot for the ref held while this timer is scheduled (see the diff --git a/test/js/web/timers/setImmediate.test.js b/test/js/web/timers/setImmediate.test.js index 27fea6fdbc18..4ebf9a6c05ca 100644 --- a/test/js/web/timers/setImmediate.test.js +++ b/test/js/web/timers/setImmediate.test.js @@ -55,6 +55,22 @@ it("clearImmediate", async () => { await promise; }); +it("clearImmediate with a numeric or string id does not clear a timeout or interval (Node.js parity)", async () => { + const timeoutFired = Promise.withResolvers(); + const intervalFired = Promise.withResolvers(); + const t = setTimeout(() => timeoutFired.resolve(true), 1); + const i = setInterval(() => { + clearInterval(i); + intervalFired.resolve(true); + }, 1); + clearImmediate(+t); + clearImmediate(String(+t)); + clearImmediate(+i); + clearImmediate(String(+i)); + expect(await timeoutFired.promise).toBe(true); + expect(await intervalFired.promise).toBe(true); +}); + it("setImmediate should not keep the process alive forever", async () => { let process = null; const success = async () => { From 03a55c23df7c3e46852719c153f36162642dbf28 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 29 Aug 2026 09:20:51 +0000 Subject: [PATCH 8/8] timers: an interval that stops itself Node-style lets go of its wrapper; Heap::insert asserts the node is unlinked; host_fn treats a ThisPtr first parameter as the receiver; libuv Timer::get_due_in/update_loop_time tolerate an un-init'ed timer --- src/io/heap.rs | 2 ++ src/jsc_macros/lib.rs | 37 +++++++++++---------- src/libuv_sys/libuv.rs | 16 +++++---- src/runtime/timer/timer_object_internals.rs | 4 +++ test/js/web/timers/setInterval.test.js | 34 ++++++++++++++++++- 5 files changed, 68 insertions(+), 25 deletions(-) diff --git a/src/io/heap.rs b/src/io/heap.rs index 947e787d2b29..a283fc1e2b55 100644 --- a/src/io/heap.rs +++ b/src/io/heap.rs @@ -83,6 +83,8 @@ impl> Intrusive { /// live at that address until it leaves this heap again (via /// [`delete_min`](Self::delete_min) or [`remove`](Self::remove)). pub unsafe fn insert(&self, v: *mut T) { + // SAFETY: `v` is live per fn contract. + debug_assert!(!unsafe { Self::node(v) }.heap().is_linked() && !self.is_root(v)); let root = self.root.get(); self.root.set(if !root.is_null() { // SAFETY: `v` per fn contract; `root` per struct invariant. diff --git a/src/jsc_macros/lib.rs b/src/jsc_macros/lib.rs index 4e3eb311b71d..f0a623bb0f79 100644 --- a/src/jsc_macros/lib.rs +++ b/src/jsc_macros/lib.rs @@ -118,11 +118,25 @@ fn expand_host_fn(args: &HostFnArgs, func: &ItemFn) -> syn::Result // shim is emitted *inside* the surrounding `impl` block so it can name // `Self`; the C-ABI signature passes `*mut Self` as the first argument // (the codegen'd C++ passes `m_ctx`). - let has_receiver = func - .sig - .inputs - .first() - .is_some_and(|a| matches!(a, FnArg::Receiver(_))); + // A typed `this: ThisPtr` first parameter gets the root pointer + // wrapped instead of reborrowed, and counts as a receiver. + let first_is_this_ptr = func.sig.inputs.first().is_some_and(|a| match a { + FnArg::Typed(pt) => match &*pt.ty { + syn::Type::Path(tp) => tp + .path + .segments + .last() + .is_some_and(|seg| seg.ident == "ThisPtr"), + _ => false, + }, + _ => false, + }); + let has_receiver = first_is_this_ptr + || func + .sig + .inputs + .first() + .is_some_and(|a| matches!(a, FnArg::Receiver(_))); // R-2 (PORT_NOTES_PLAN): for `&self` receivers, materialise `&*__this` // (NOT `&mut *__this`). A method that calls back into JS can be re-entered @@ -136,19 +150,6 @@ fn expand_host_fn(args: &HostFnArgs, func: &ItemFn) -> syn::Result .inputs .first() .is_some_and(|a| matches!(a, FnArg::Receiver(r) if r.mutability.is_none())); - // A typed `this: ThisPtr` first parameter gets the root pointer - // wrapped instead of reborrowed. - let first_is_this_ptr = func.sig.inputs.first().is_some_and(|a| match a { - FnArg::Typed(pt) => match &*pt.ty { - syn::Type::Path(tp) => tp - .path - .segments - .last() - .is_some_and(|seg| seg.ident == "ThisPtr"), - _ => false, - }, - _ => false, - }); let this_reborrow = if first_is_this_ptr { quote! { let __t = unsafe { ::bun_ptr::ThisPtr::new(__this) }; } } else if receiver_is_shared { diff --git a/src/libuv_sys/libuv.rs b/src/libuv_sys/libuv.rs index 6561e148def4..15995cda42df 100644 --- a/src/libuv_sys/libuv.rs +++ b/src/libuv_sys/libuv.rs @@ -1506,18 +1506,22 @@ impl Timer { panic!("internal error: uv_timer_stop failed"); } } - /// Milliseconds until the timer is due (0 if overdue or stopped). + /// Milliseconds until the timer is due (0 if overdue, stopped, or never `init`ed). #[inline] pub fn get_due_in(&self) -> u64 { - debug_assert!(!self.loop_.is_null()); - // SAFETY: timer was `init`ed (reads `self.timeout` and `loop_->time`). + if self.loop_.is_null() { + return 0; + } + // SAFETY: `loop_` is set, so the timer was `init`ed (reads `self.timeout` and `loop_->time`). unsafe { uv_timer_get_due_in(self) } } - /// `uv_update_time` on the loop this timer was `init`ed on. + /// `uv_update_time` on the loop this timer was `init`ed on; no-op before `init`. #[inline] pub fn update_loop_time(&self) { - debug_assert!(!self.loop_.is_null()); - // SAFETY: timer was `init`ed, so `loop_` is the live loop it is registered on. + if self.loop_.is_null() { + return; + } + // SAFETY: `loop_` is set, so it is the live loop this timer was `init`ed on. unsafe { uv_update_time(self.loop_) } } } diff --git a/src/runtime/timer/timer_object_internals.rs b/src/runtime/timer/timer_object_internals.rs index f910c716b014..8e50d5180b06 100644 --- a/src/runtime/timer/timer_object_internals.rs +++ b/src/runtime/timer/timer_object_internals.rs @@ -412,6 +412,10 @@ pub trait TimerObject: bun_ptr::RefCounted + TimerOwner + Sized + 'static { // Node doesn't drain microtasks after each timer callback. if kind == KindBig::SetInterval { if !this.should_reschedule_timer(repeat, idle_timeout) { + // Stopped Node-style (`_repeat = null` / `_idleTimeout = -1`) + // rather than through `cancel()`, so nothing has let go of + // the wrapper yet. + s.this_value.with_mut(|r| r.downgrade()); break 'is_timer_done true; } match this.event_loop_timer_state() { diff --git a/test/js/web/timers/setInterval.test.js b/test/js/web/timers/setInterval.test.js index 9e21456e78ad..98c5111d8650 100644 --- a/test/js/web/timers/setInterval.test.js +++ b/test/js/web/timers/setInterval.test.js @@ -1,5 +1,5 @@ import { expect, it } from "bun:test"; -import { bunRun, isWindows } from "harness"; +import { bunEnv, bunExe, bunRun, isWindows } from "harness"; import { join } from "path"; it("setInterval", async () => { @@ -133,3 +133,35 @@ it.concurrent( }, 30_000, ); + +it.concurrent("an interval that stops itself via _repeat = null / _idleTimeout = -1 is collectable", async () => { + const code = /* js */ ` + const { heapStats } = require("bun:jsc"); + const N = 200; + let fired = 0; + await new Promise(resolve => { + for (let i = 0; i < N; i++) { + setInterval(function () { + if (i % 2) this._repeat = null; + else this._idleTimeout = -1; + if (++fired === N) resolve(); + }, 1); + } + }); + await new Promise(r => setTimeout(r, 10)); + Bun.gc(true); + await new Promise(r => setTimeout(r, 10)); + Bun.gc(true); + const left = heapStats().objectTypeCounts.Timeout ?? 0; + console.log(fired, left < N / 2 ? "collected" : "leaked " + left); + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", code], + env: bunEnv, + stdout: "pipe", + stderr: "inherit", + }); + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + expect(stdout.trim()).toBe("200 collected"); + expect(exitCode).toBe(0); +});