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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions src/bun_core/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5114,49 +5114,50 @@ pub mod timespec_mode {

/// Mocked-time storage. The data lives at T0 so `Timespec::now` reads it
/// directly; the test-runner (`useFakeTimers`) writes via `set`/`clear`
/// from `bun_runtime::test_runner::timers::FakeTimers::CurrentTime`.
/// Sentinel `i64::MIN` / `NaN` ⇒ not mocked.
/// from `bun_runtime::test_runner::timers::FakeTimers`. Thread-local so each
/// VM (main thread, each `Worker`) has its own fake clock, matching the
/// per-thread fake `TimerHeap` it drives.
pub mod mock_time {
use core::sync::atomic::{AtomicI64, AtomicU64, Ordering};
use core::cell::Cell;

static MOCKED_TIME_NS: AtomicI64 = AtomicI64::new(i64::MIN);
// Mocked wall-clock `Date.now()` in ms, stored as f64 bits; NaN = unset.
static MOCKED_WALL_MS: AtomicU64 = AtomicU64::new(f64::NAN.to_bits());
std::thread_local! {
static MOCKED_TIME_NS: Cell<Option<i64>> = const { Cell::new(None) };
/// Mocked wall-clock `Date.now()` in ms.
static MOCKED_WALL_MS: Cell<Option<f64>> = const { Cell::new(None) };
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// Set the mocked monotonic time (nanoseconds). Called by fake-timers.
#[inline]
pub fn set(ns: i64) {
MOCKED_TIME_NS.store(ns, Ordering::Relaxed);
MOCKED_TIME_NS.set(Some(ns));
}
/// Clear the mocked time so `Timespec::now(AllowMockedTime)` reads the
/// real clock again.
#[inline]
pub fn clear() {
MOCKED_TIME_NS.store(i64::MIN, Ordering::Relaxed);
MOCKED_TIME_NS.set(None);
}
/// Current mocked time, or `None` if not mocked.
#[inline]
pub fn get() -> Option<i64> {
let v = MOCKED_TIME_NS.load(Ordering::Relaxed);
if v == i64::MIN { None } else { Some(v) }
MOCKED_TIME_NS.get()
}
/// Set the mocked wall-clock time (`Date.now()` in ms). Called by
/// fake-timers alongside `set` so calendar-based consumers and the
/// monotonic timer heap move through the same mock.
#[inline]
pub fn set_wall_ms(ms: f64) {
MOCKED_WALL_MS.store(ms.to_bits(), Ordering::Relaxed);
MOCKED_WALL_MS.set(Some(ms));
Comment thread
dylan-conway marked this conversation as resolved.
}
/// Clear the mocked wall-clock time.
#[inline]
pub fn clear_wall() {
MOCKED_WALL_MS.store(f64::NAN.to_bits(), Ordering::Relaxed);
MOCKED_WALL_MS.set(None);
}
/// Current mocked wall-clock time in ms, or `None` if not mocked.
#[inline]
pub(crate) fn wall_ms() -> Option<f64> {
let v = f64::from_bits(MOCKED_WALL_MS.load(Ordering::Relaxed));
if v.is_nan() { None } else { Some(v) }
MOCKED_WALL_MS.get()
}
}

Expand Down
5 changes: 4 additions & 1 deletion src/runtime/dns_jsc/dns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2317,8 +2317,11 @@ pub mod internal {
// To preserve memory, we use a 32 bit timestamp
// However, we're almost out of time to use 32 bit timestamps for anything
// So we set the epoch to January 1st, 2024 instead.
//
// Real time: the cache is process-global, shared by every VM's JS
// thread and the HTTP thread, so no single VM's fake clock applies.
fn get_cache_timestamp() -> u32 {
(bun::Timespec::now(bun::TimespecMockMode::AllowMockedTime).ms_unsigned() / 1000) as u32
(bun::Timespec::now(bun::TimespecMockMode::ForceRealTime).ms_unsigned() / 1000) as u32
}

fn is_nearly_full(&self) -> bool {
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/jsc_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ unsafe fn init_runtime_state(
RUNTIME_STATE.with(|c| c.set(state));

// `Timespec::now_allow_mocked_time` reads `bun_core::mock_time` directly;
// `FakeTimers::CurrentTime::{set,clear}` write that storage so timers
// `FakeTimers::{set_now,clear_now}` write that storage so timers
// scheduled under `jest.useFakeTimers()` use the mocked epoch.

// ── vm.transpiler ────────────────────────────────────────────────────
Expand Down Expand Up @@ -1773,8 +1773,8 @@ fn stop_active_handles(vm: &mut VirtualMachine, reason: StopReason) -> SweepResu
// 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.
// SAFETY: as above; only touches `fake_timers`' clock state and
// the VM, never the heaps.
unsafe { (*all).fake_timers.reset_for_isolation(global) };
}
}
Expand Down
130 changes: 50 additions & 80 deletions src/runtime/test_runner/timers/FakeTimers.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
use std::sync::atomic::{AtomicU64, Ordering};

use bun_threading::RwLock;

use bun_core::Environment;
use bun_core::Timespec;
use bun_jsc::{CallFrame, JSFunction, JSGlobalObject, JSHostFn, JSValue, JsResult};
Expand All @@ -20,72 +16,46 @@ unsafe extern "C" {

#[derive(Default)]
pub struct FakeTimers {
active: bool,
/// 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)
/// - count (cannot be implemented efficiently with TimerHeap)
pub(crate) timers: TimerHeap,
/// The fake monotonic clock; starts at 0 on `useFakeTimers()`, `None`
/// while real timers are in use.
now: Option<Timespec>,
/// `Date.now()` minus `now.ms()`.
date_now_offset: f64,
}

// `date_now_offset` is stored as `AtomicU64` (f64 bits) so the static is `Sync`
// without `static mut`.
pub(crate) struct CurrentTime {
/// starts at 0. offset in milliseconds.
offset_raw: RwLock<Timespec>,
date_now_offset: AtomicU64,
}

const MIN_TIMESPEC: Timespec = Timespec { sec: i64::MIN, nsec: i64::MIN };

static CURRENT_TIME: CurrentTime = CurrentTime {
offset_raw: RwLock::new(MIN_TIMESPEC),
date_now_offset: AtomicU64::new(0f64.to_bits()),
};

impl CurrentTime {
pub(crate) fn get_timespec_now(&self) -> Option<Timespec> {
let value = *self.offset_raw.read();
if value.eql(&MIN_TIMESPEC) {
return None;
}
Some(value)
impl FakeTimers {
pub(crate) fn is_active(&self) -> bool {
self.now.is_some()
}

pub(crate) fn set(&self, global: &JSGlobalObject, offset: &Timespec, js: Option<f64>) {
let vm = global.bun_vm().as_mut();
{
*self.offset_raw.write() = *offset;
}
fn set_now(&mut self, global: &JSGlobalObject, now: &Timespec, js: Option<f64>) {
self.now = Some(*now);
// Mirror into T0 storage so `Timespec::now(AllowMockedTime)` sees
// the fake clock.
bun_core::mock_time::set(offset.ns() as i64);
let timespec_ms: f64 = offset.ms() as f64;
let mut date_now_offset = f64::from_bits(self.date_now_offset.load(Ordering::Relaxed));
bun_core::mock_time::set(now.ns() as i64);
let timespec_ms: f64 = now.ms() as f64;
if let Some(js) = js {
date_now_offset = js.floor() - timespec_ms;
self.date_now_offset.store(date_now_offset.to_bits(), Ordering::Relaxed);
self.date_now_offset = js.floor() - timespec_ms;
}
let date_now = date_now_offset + timespec_ms;
// SAFETY: FFI call into C++ JSMock; global is a valid &JSGlobalObject
let date_now = self.date_now_offset + timespec_ms;
JSMock__setOverridenDateNow(global, date_now);
bun_core::mock_time::set_wall_ms(date_now);

vm.overridden_performance_now = Some(offset.ns());
global.bun_vm().as_mut().overridden_performance_now = Some(now.ns());
}

pub(crate) fn clear(&self, global: &JSGlobalObject) {
let vm = global.bun_vm().as_mut();
{
*self.offset_raw.write() = MIN_TIMESPEC;
}
fn clear_now(&mut self, global: &JSGlobalObject) {
self.now = None;
bun_core::mock_time::clear();
bun_core::mock_time::clear_wall();
// NaN is JSGlobalObject::overridenDateNow's "no override" sentinel; a
// real -1 would pin Date.now() at 1969-12-31T23:59:59.999Z.
// SAFETY: FFI call into C++ JSMock; global is a valid &JSGlobalObject
JSMock__setOverridenDateNow(global, f64::NAN);
vm.overridden_performance_now = None;
global.bun_vm().as_mut().overridden_performance_now = None;
}
}

Expand All @@ -99,13 +69,13 @@ extern "C" fn Bun__FakeTimers__setSystemTime(ms: f64) {
if ms.is_nan() {
return;
}
let Some(current) = CURRENT_TIME.get_timespec_now() else {
// SAFETY: called from `jest.setSystemTime` on the JS thread, whose
// per-thread `timer::All` is live; nothing here re-enters `All`.
let fake_timers = unsafe { &mut (*timer_all()).fake_timers };
let Some(current) = fake_timers.now else {
return;
};
let date_now_offset = ms - current.ms() as f64;
CURRENT_TIME
.date_now_offset
.store(date_now_offset.to_bits(), Ordering::Relaxed);
fake_timers.date_now_offset = ms - current.ms() as f64;
bun_core::mock_time::set_wall_ms(ms);
}

Expand Down Expand Up @@ -159,19 +129,13 @@ impl ClearedTimers {
}

impl FakeTimers {
pub(crate) fn is_active(&self) -> bool {
self.active
}

fn activate(&mut self, js_now: f64, global: &JSGlobalObject) {
self.active = true;
CURRENT_TIME.set(global, &Timespec::EPOCH, Some(js_now));
self.set_now(global, &Timespec::EPOCH, Some(js_now));
Comment thread
dylan-conway marked this conversation as resolved.
Comment thread
dylan-conway marked this conversation as resolved.
}

fn deactivate(&mut self, global: &JSGlobalObject) -> ClearedTimers {
let cleared = self.clear();
CURRENT_TIME.clear(global);
self.active = false;
self.clear_now(global);
cleared
}

Expand All @@ -181,8 +145,7 @@ impl FakeTimers {
/// 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) {
CURRENT_TIME.clear(global);
self.active = false;
self.clear_now(global);
}

/// Pop every fake timer. Popping only unlinks the nodes; the owners that
Expand Down Expand Up @@ -239,17 +202,18 @@ impl FakeTimers {
/// 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();

// 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);
// SAFETY: `timer_all()` is the live per-thread `All`; the borrow ends
// before `EventLoopTimer::fire` re-enters it.
let this = unsafe { &mut (*timer_all()).fake_timers };
if Environment::CI_ASSERT {
let prev = CURRENT_TIME.get_timespec_now();
let prev = this.now;
debug_assert!(prev.is_some());
debug_assert!(now.eql(&prev.unwrap()) || now.greater(&prev.unwrap()));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
CURRENT_TIME.set(global, &now, None);
this.set_now(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()) };
Expand Down Expand Up @@ -305,14 +269,19 @@ impl FakeTimers {
// JS Functions
// ===

fn error_unless_fake_timers(global: &JSGlobalObject) -> JsResult<()> {
/// The current fake clock, or a thrown "not active" error.
fn fake_now(global: &JSGlobalObject) -> JsResult<Timespec> {
// SAFETY: per-thread `timer::All`, live for the VM lifetime.
if unsafe { (*timer_all()).fake_timers.is_active() } {
return Ok(());
match unsafe { (*timer_all()).fake_timers.now } {
Some(now) => Ok(now),
None => Err(global.throw(format_args!(
"Fake timers are not active. Call useFakeTimers() first."
))),
}
Err(global.throw(format_args!(
"Fake timers are not active. Call useFakeTimers() first."
)))
}

fn error_unless_fake_timers(global: &JSGlobalObject) -> JsResult<()> {
fake_now(global).map(|_| ())
}

/// Set or remove the "clock" property on setTimeout to indicate that fake timers are active.
Expand Down Expand Up @@ -399,19 +368,14 @@ fn advance_timers_to_next_timer(global: &JSGlobalObject, frame: &CallFrame) -> J

#[bun_jsc::host_fn]
fn advance_timers_by_time(global: &JSGlobalObject, frame: &CallFrame) -> JsResult<JSValue> {
error_unless_fake_timers(global)?;
let current = fake_now(global)?;

let arg = frame.arguments_as_array::<1>()[0];
if !arg.is_number() {
return Err(global.throw_invalid_arguments(format_args!(
"advanceTimersToNextTimer() expects a number of milliseconds"
)));
}
let Some(current) = CURRENT_TIME.get_timespec_now() else {
return Err(global.throw_invalid_arguments(format_args!(
"Fake timers not initialized. Initialize with useFakeTimers() first."
)));
};
let arg_number = arg.as_number();
let max_advance = u32::MAX;
if arg_number < 0.0 || arg_number > max_advance as f64 {
Expand All @@ -427,7 +391,13 @@ fn advance_timers_by_time(global: &JSGlobalObject, frame: &CallFrame) -> JsResul
let target = current.add_ms_float(effective_advance);

let advanced = FakeTimers::execute_until(global, target);
CURRENT_TIME.set(global, &target, None);
// SAFETY: per-thread `timer::All`; `set_now` does not re-enter `All`.
let fake_timers = unsafe { &mut (*timer_all()).fake_timers };
// A fired callback may have called `useRealTimers()`; don't re-arm the
// clock behind its back.
if fake_timers.is_active() {
fake_timers.set_now(global, &target, None);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
advanced?;

Ok(frame.this())
Expand Down
Loading