Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
28 changes: 28 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.

25 changes: 25 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,25 @@ 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 in drain_timers at 100% CPU; bound it so the
// assertions below fail with a clean diff instead of a runner timeout.
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);
},
30_000,
);
Loading