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
5 changes: 3 additions & 2 deletions src/runtime/node/node_fs_stat_watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,10 +279,11 @@ impl StatWatcherScheduler {
return;
}

// reschedule the timer
// reschedule the timer — this tag opts out of fake timers, so the
// deadline lives in the real heap and must be in real-clock units.
timer_all.update(
elt,
&Timespec::ms_from_now(TimespecMockMode::AllowMockedTime, i64::from(interval)),
&Timespec::ms_from_now(TimespecMockMode::ForceRealTime, i64::from(interval)),
);
}

Expand Down
8 changes: 6 additions & 2 deletions src/runtime/timer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -914,8 +914,11 @@ impl All {
// deref and fire via raw deref (mirroring `drain_timers`).
let (min_next_sec, min_next_nsec, min_tag) =
unsafe { ((*min).next.sec, (*min).next.nsec, (*min).tag) };
// Real clock: `self.timers` is the opt-out-of-fake-timers set, all
// armed in real-time units. Comparing against the mocked clock made
// internal pacing (GC, WTFTimer, test timeouts) spin on re-arm.
let now =
*maybe_now.get_or_insert_with(|| Timespec::now(TimespecMockMode::AllowMockedTime));
*maybe_now.get_or_insert_with(|| Timespec::now(TimespecMockMode::ForceRealTime));

// bun_event_loop carries its own Timespec stub; compare field-wise.
let min_next = Timespec {
Expand Down Expand Up @@ -983,7 +986,8 @@ impl All {
let out = (|| {
let timer = self.timers.peek()?;
if !*has_set_now {
*now = Timespec::now(TimespecMockMode::AllowMockedTime);
// 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
Expand Down
23 changes: 23 additions & 0 deletions test/js/bun/test/test-timers-gc-spin-fixture.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions test/js/bun/test/test-timers.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { bunEnv, bunExe } from "harness";
import path from "node:path";

test("we can go back in time", () => {
const DateBeforeMocked = Date;
const orig = new Date();
Expand Down Expand Up @@ -73,3 +76,21 @@ test("setSystemTime accepts pre-epoch and epoch times and resets with no argumen
jest.useRealTimers();
}
});

test("real timer heap is ticked against the real clock under useFakeTimers", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "test", path.join(import.meta.dir, "test-timers-gc-spin-fixture.ts")],
env: { ...bunEnv, BUN_GC_TIMER_DISABLE: undefined, BUN_GC_TIMER_INTERVAL: undefined },
stdout: "pipe",
stderr: "pipe",
// Pre-fix the child spins at 100% CPU; bound it so it doesn't outlive the
// runner by long when the parent test times out on the unfixed build.
timeout: 20_000,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
if (exitCode !== 0) console.error(stderr);
expect(stdout).toContain("DRAIN_OK");
// null => exited on its own; non-null => killed by the spawn timeout (spun).
expect(proc.signalCode).toBeNull();
expect(exitCode).toBe(0);
});
Loading