diff --git a/src/env.cc b/src/env.cc index 04807ffae13437..137ef87d781b41 100644 --- a/src/env.cc +++ b/src/env.cc @@ -1505,7 +1505,11 @@ void Environment::RequestInterruptFromV8() { void Environment::ScheduleTimer(int64_t duration_ms) { if (started_cleanup_) return; - uv_timer_start(timer_handle(), RunTimers, duration_ms, 0); + // Add 1ms to compensate for libuv's uv_now() truncating sub-millisecond + // time. Without this, timers can fire up to 1ms before the requested + // delay when measured with high-resolution clocks (process.hrtime(), + // Date.now()). See: https://github.com/nodejs/node/issues/26578 + uv_timer_start(timer_handle(), RunTimers, duration_ms + 1, 0); } void Environment::ToggleTimerRef(bool ref) { diff --git a/test/parallel/test-timers-no-early-fire.js b/test/parallel/test-timers-no-early-fire.js new file mode 100644 index 00000000000000..e7cdd36938fee0 --- /dev/null +++ b/test/parallel/test-timers-no-early-fire.js @@ -0,0 +1,45 @@ +'use strict'; + +// This test verifies that setTimeout never fires its callback before the +// requested delay has elapsed, as measured by process.hrtime.bigint(). +// +// The bug: libuv's uv_now() truncates sub-millisecond time to integer +// milliseconds. When a timer is scheduled at real time 100.7ms, uv_now() +// returns 100. The timer fires when uv_now() >= 100 + delay, but real +// elapsed time is only delay - 0.7ms. The interaction with setImmediate() +// makes this more likely to occur because it affects when uv_update_time() +// is called. +// +// See: https://github.com/nodejs/node/issues/26578 + +const common = require('../common'); +const assert = require('assert'); + +const DELAY_MS = 100; +const ITERATIONS = 50; + +let completed = 0; + +function test() { + const start = process.hrtime.bigint(); + + setTimeout(common.mustCall(() => { + const elapsed = process.hrtime.bigint() - start; + const elapsedMs = Number(elapsed) / 1e6; + + assert( + elapsedMs >= DELAY_MS, + `setTimeout(${DELAY_MS}) fired after only ${elapsedMs.toFixed(3)}ms` + ); + + completed++; + if (completed < ITERATIONS) { + // Use setImmediate to schedule the next iteration, which is critical + // to reproducing the original bug (the interaction between + // setImmediate and setTimeout affects uv_update_time() timing). + setImmediate(test); + } + }), DELAY_MS); +} + +test();