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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 deletions src/event_loop/EventLoopTimer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,18 +205,17 @@ pub enum Tag {
}

impl Tag {
/// Whether `jest.useFakeTimers()` captures this timer. Only timers a
/// program schedules itself are faked; runtime-internal timeouts stay on
/// the real clock, as in Jest. A fakeable owner arms with
/// `AllowMockedTime` and has a release arm in `FakeTimers::clear`; every
/// other owner arms with `ForceRealTime`, the clock the real heap is
/// drained against.
Comment thread
robobun marked this conversation as resolved.
pub fn allow_fake_timers(self) -> bool {
match self {
Tag::WTFTimer // internal
| Tag::BunTest // for test timeouts
| Tag::EventLoopDelayMonitor // probably important
| Tag::StatWatcherScheduler
| Tag::GcRepeating // internal GC pacing
| Tag::QuicEndpoint
| Tag::DnsSdConnection // internal lookup pacing
=> false,
_ => true,
}
matches!(
self,
Tag::TimeoutObject | Tag::AbortSignalTimeout | Tag::CronJob
)
}
}

Expand Down
6 changes: 4 additions & 2 deletions src/event_loop/SpawnSyncEventLoop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,12 +380,14 @@ impl SpawnSyncEventLoop {

/// Tick the isolated event loop with an optional timeout
/// This is similar to the main event loop's tick but completely isolated
///
/// `timeout` is an absolute deadline on the real (never mocked) clock.
Comment thread
robobun marked this conversation as resolved.
pub fn tick_with_timeout(&mut self, timeout: Option<&Timespec>) -> TickState {
let duration_storage: Option<Timespec>;
let duration: Option<&Timespec> = match timeout {
Some(ts) => {
duration_storage =
Some(ts.duration(&Timespec::now(TimespecMockMode::AllowMockedTime)));
Some(ts.duration(&Timespec::now(TimespecMockMode::ForceRealTime)));
duration_storage.as_ref()
}
None => None,
Expand Down Expand Up @@ -454,7 +456,7 @@ impl SpawnSyncEventLoop {
#[cfg(not(windows))]
{
self.did_timeout.set(
Timespec::now(TimespecMockMode::AllowMockedTime).order(ts)
Timespec::now(TimespecMockMode::ForceRealTime).order(ts)
!= core::cmp::Ordering::Less,
);
}
Expand Down
13 changes: 9 additions & 4 deletions src/runtime/api/bun/js_bun_spawn_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1657,8 +1657,7 @@ fn spawn_maybe_sync<const IS_SYNC: bool>(
// This must go before other things happen so that the exit handler is
// registered before onProcessExit can potentially be called.
if let Some(timeout_val) = timeout {
let ts =
Timespec::ms_from_now(TimespecMockMode::AllowMockedTime, i64::from(timeout_val));
let ts = Timespec::ms_from_now(TimespecMockMode::ForceRealTime, i64::from(timeout_val));
// Note: `EventLoopTimer.next` is a local-stub Timespec until
// `bun_event_loop` switches to `bun_core::Timespec`; copy fieldwise.
subprocess.event_loop_timer.with_mut(|t| {
Expand Down Expand Up @@ -1875,7 +1874,7 @@ fn spawn_maybe_sync<const IS_SYNC: bool>(
// This ensures JavaScript timers don't fire and stdin/stdout from the main process aren't affected
{
let mut absolute_timespec = Timespec::EPOCH;
let mut now = Timespec::now(TimespecMockMode::AllowMockedTime);
let mut now = Timespec::now(TimespecMockMode::ForceRealTime);
let mut user_timespec: Timespec = if let Some(timeout_ms) = timeout {
now.add_ms(i64::from(timeout_ms))
} else {
Expand All @@ -1889,8 +1888,14 @@ fn spawn_maybe_sync<const IS_SYNC: bool>(
if let Some(abort_signal_timeout) = signal.get_timeout() {
// Note: `AbortSignal::Timeout.event_loop_timer` uses the
// bun_event_loop-local `Timespec` stub; convert fieldwise.
//
// A fake-heap deadline is on the mocked clock, which cannot
// advance while this call blocks; only a real-heap one is
// comparable with `now`.
Comment thread
robobun marked this conversation as resolved.
if abort_signal_timeout.event_loop_timer.state
== crate::timer::EventLoopTimerState::ACTIVE
&& abort_signal_timeout.event_loop_timer.in_heap
== crate::timer::InHeap::Regular
{
let next = &abort_signal_timeout.event_loop_timer.next;
let next_ts = Timespec {
Expand Down Expand Up @@ -1959,7 +1964,7 @@ fn spawn_maybe_sync<const IS_SYNC: bool>(
}) {
TickState::Completed => {}
TickState::Timeout => {
now = Timespec::now(TimespecMockMode::AllowMockedTime);
now = Timespec::now(TimespecMockMode::ForceRealTime);
let did_user_timeout = has_user_timespec
&& (absolute_timespec.eql(&user_timespec)
|| user_timespec.order(&now) == core::cmp::Ordering::Less);
Expand Down
11 changes: 11 additions & 0 deletions src/runtime/api/cron.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1640,6 +1640,17 @@ impl CronJob {
Self::remove_from_list(this, vm);
}

/// The fake heap dropped this job's timer (`useRealTimers()` /
/// `clearAllTimers()`): stop the job as `stop()` would, so it does not
/// keep the event loop alive for a timer that can no longer fire.
///
/// # Safety
/// `this` was recovered from a node just popped off the fake heap and no
/// JS has run since; a scheduled job's wrapper keeps it alive.
Comment thread
robobun marked this conversation as resolved.
pub(crate) unsafe fn stop_dropped_from_fake_heap(this: *mut Self) {
Self::self_stop(this, VirtualMachine::get());
}

fn self_stop(this: *mut Self, vm: &VirtualMachine) {
let this_ref = Self::from_ctx_ptr(this);
// While the callback is on the stack or its promise is pending, defer
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/bake/dev_server/hmr_socket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ impl HmrSocket {
// 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::AllowMockedTime,
bun_core::TimespecMockMode::ForceRealTime,
1000,
);
// SAFETY: `runtime_state()` is non-null after
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/bake/dev_server/source_map_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ impl SourceMapStore {
}

let expire = Timespec::ms_from_now(
TimespecMockMode::AllowMockedTime,
TimespecMockMode::ForceRealTime,
WEAK_REF_EXPIRY_SECONDS * 1000,
);
self.weak_refs
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/dns_jsc/dns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4132,7 +4132,7 @@ impl Resolver {
self.ref_();
let now_ts = now
.copied()
.unwrap_or_else(|| bun::timespec::now(bun::TimespecMockMode::AllowMockedTime));
.unwrap_or_else(|| bun::timespec::now(bun::TimespecMockMode::ForceRealTime));
let next = now_ts.add_ms(1000);
// `EventLoopTimer.next` uses the event-loop crate's local
// `Timespec` (distinct from `bun_core::Timespec`); convert by field.
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/socket/UpgradedDuplex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ impl UpgradedDuplex {
// Note: `EventLoopTimer.next` is the lower-tier `ElTimespec` stub;
// bridge from `bun_core::Timespec` until the lower tier switches.
let next =
bun_core::Timespec::ms_from_now(bun_core::TimespecMockMode::AllowMockedTime, ms as i64);
bun_core::Timespec::ms_from_now(bun_core::TimespecMockMode::ForceRealTime, ms as i64);
self.event_loop_timer.with_mut(|t| {
t.next = ElTimespec {
sec: next.sec,
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/socket/WindowsNamedPipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1023,7 +1023,7 @@ impl WindowsNamedPipe {
// reschedule the timer
// `EventLoopTimer.next` is the lower-tier `ElTimespec` stub;
// bridge from `bun_core::Timespec` until the lower tier switches.
let next = timespec::ms_from_now(bun_core::TimespecMockMode::AllowMockedTime, ms as i64);
let next = timespec::ms_from_now(bun_core::TimespecMockMode::ForceRealTime, ms as i64);
self.event_loop_timer.with_mut(|t| {
t.next = ElTimespec {
sec: next.sec,
Expand Down
21 changes: 19 additions & 2 deletions src/runtime/test_runner/timers/FakeTimers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use bun_threading::RwLock;
use bun_core::Environment;
use bun_core::Timespec;
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,
Expand Down Expand Up @@ -116,7 +117,7 @@ fn from_el_timespec(t: &ElTimespec) -> Timespec {
}

/// Owners of the nodes [`FakeTimers::clear`] popped, still to be told their
/// timer is gone. Released only once the `FakeTimers` borrow has ended: both
/// 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`).
#[derive(Default)]
Expand All @@ -132,6 +133,8 @@ struct ClearedTimers {
/// 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>,
}

impl ClearedTimers {
Expand All @@ -145,6 +148,13 @@ impl ClearedTimers {
// 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.
unsafe { CronJob::stop_dropped_from_fake_heap(job) };
}
}
}

Expand Down Expand Up @@ -197,7 +207,14 @@ impl FakeTimers {
.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),
),
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/timer/Timer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ impl All {
vm,
// Be careful to avoid adding extra calls to bun.timespec.now()
// when it's not needed.
&Timespec::now(TimespecMockMode::AllowMockedTime),
&Timespec::now(TimespecMockMode::ForceRealTime),
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/timer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ impl DateHeaderTimer {
// separate allocation from `RuntimeState.timer` so no aliasing with
// `&mut self`).
let loop_ = vm.uws_loop_mut();
let now = Timespec::now(TimespecMockMode::AllowMockedTime);
let now = Timespec::now(TimespecMockMode::ForceRealTime);

// Record when we last ran it.
self.event_loop_timer.next = ElTimespec {
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/valkey_jsc/js_valkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ impl RefCountedTimer {
return;
}
let now = bun_core::Timespec::ms_from_now(
bun_core::TimespecMockMode::AllowMockedTime,
bun_core::TimespecMockMode::ForceRealTime,
i64::from(ms),
);
self.event_loop_timer.with_mut(|t| {
Expand Down
4 changes: 2 additions & 2 deletions src/sql_jsc/mysql/JSMySQLConnection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ impl JSMySQLConnection {
}

self.timer.with_mut(|t| {
t.next = timespec::ms_from_now(TimespecMockMode::AllowMockedTime, interval.into());
t.next = timespec::ms_from_now(TimespecMockMode::ForceRealTime, interval.into());
});
// whole-struct provenance: the fire path recovers the container from this pointer.
let t = core::ptr::addr_of!(self.timer)
Expand Down Expand Up @@ -339,7 +339,7 @@ impl JSMySQLConnection {

self.max_lifetime_timer.with_mut(|t| {
t.next = timespec::ms_from_now(
TimespecMockMode::AllowMockedTime,
TimespecMockMode::ForceRealTime,
self.max_lifetime_interval_ms.into(),
);
});
Expand Down
4 changes: 2 additions & 2 deletions src/sql_jsc/postgres/PostgresSQLConnection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ impl PostgresSQLConnection {
return;
}
t.next = bun_core::Timespec::ms_from_now(
bun_core::TimespecMockMode::AllowMockedTime,
bun_core::TimespecMockMode::ForceRealTime,
i64::from(interval),
);
});
Expand Down Expand Up @@ -500,7 +500,7 @@ impl PostgresSQLConnection {
}
self.max_lifetime_timer.with_mut(|t| {
t.next = bun_core::Timespec::ms_from_now(
bun_core::TimespecMockMode::AllowMockedTime,
bun_core::TimespecMockMode::ForceRealTime,
i64::from(self.max_lifetime_interval_ms),
);
});
Expand Down
Loading