Skip to content
Open
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
6 changes: 5 additions & 1 deletion src/env.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
45 changes: 45 additions & 0 deletions test/parallel/test-timers-no-early-fire.js
Original file line number Diff line number Diff line change
@@ -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();
Loading