From 9151208c7b421585c3bf0aed5f9d8773540afa65 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 2 Jul 2026 14:26:34 +0200 Subject: [PATCH] timers: reuse Timeout objects in setStreamTimeout Rearm the stream's existing Timeout object instead of allocating a new Timeout and a bound callback on every setTimeout() call. The HTTP server disarms and rearms the keep-alive timeout twice per request, so this saves two allocations and the associated async resource churn on every request over a keep-alive connection. The async resource is re-initialized on reuse, so async_hooks observes the same init/destroy sequence as before. A microbenchmark of the setTimeout(msecs)/setTimeout(0) cycle improves by 2.2x (205.9ns to 92.8ns), and benchmark/http/simple.js (type=buffer len=1024 chunks=1 chunkedEnc=0 c=50) improves by 3.70% (t=2.15, 40 samples per binary with server and load generator pinned to disjoint CPU sets). Assisted-by: Claude Fable 5 (Claude Code) Signed-off-by: Matteo Collina --- lib/internal/stream_base_commons.js | 11 +++++++++-- lib/internal/timers.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/lib/internal/stream_base_commons.js b/lib/internal/stream_base_commons.js index 76a87ab74b74e6..6d144f8a0fa696 100644 --- a/lib/internal/stream_base_commons.js +++ b/lib/internal/stream_base_commons.js @@ -22,7 +22,7 @@ const { const { owner_symbol } = require('internal/async_hooks').symbols; const { kTimeout, - setUnrefTimeout, + reuseOrCreateUnrefTimeout, getTimerDuration, } = require('internal/timers'); const { isUint8Array } = require('internal/util/types'); @@ -233,6 +233,10 @@ function onStreamRead(arrayBuffer) { } } +function onStreamTimeout(stream) { + stream._onTimeout(); +} + function setStreamTimeout(msecs, callback) { if (this.destroyed) return this; @@ -244,6 +248,8 @@ function setStreamTimeout(msecs, callback) { // Attempt to clear an existing timer in both cases - // even if it will be rescheduled we don't want to leak an existing timer. + // The cleared Timeout stays referenced in this[kTimeout] so that it can be + // reused the next time a timeout is set, e.g. on keep-alive connections. clearTimeout(this[kTimeout]); if (msecs === 0) { @@ -252,7 +258,8 @@ function setStreamTimeout(msecs, callback) { this.removeListener('timeout', callback); } } else { - this[kTimeout] = setUnrefTimeout(this._onTimeout.bind(this), msecs); + this[kTimeout] = + reuseOrCreateUnrefTimeout(this[kTimeout], onStreamTimeout, msecs, this); if (this[kSession]) this[kSession][kUpdateTimer](); if (this[kBoundSession]) this[kBoundSession][kUpdateTimer](); diff --git a/lib/internal/timers.js b/lib/internal/timers.js index 9c7366d6ca772f..a7083e41fb31fc 100644 --- a/lib/internal/timers.js +++ b/lib/internal/timers.js @@ -417,6 +417,34 @@ function setUnrefTimeout(callback, after) { return timer; } +// Just like setUnrefTimeout() but reuses `timer` if it is a Timeout that is +// no longer scheduled (fired or cleared), avoiding the allocation of a new +// Timeout on rearm. This is the internal reuse path anticipated by the +// TODO in unenroll() (lib/timers.js), used by hot paths such as the +// keep-alive timeout handling in the HTTP server. `arg` is passed to +// `callback` when the timer fires. +function reuseOrCreateUnrefTimeout(timer, callback, after, arg) { + if (timer !== undefined && timer !== null && + timer._destroyed && timer._repeat === null && !timer[kHasPrimitive]) { + timer._idleTimeout = after; + timer._onTimeout = callback; + const args = timer._timerArgs; + if (args === undefined || args[0] !== arg) { + timer._timerArgs = [arg]; + } + // Re-inserts the timer and re-initializes its async resource, so + // async_hooks observes the same init/destroy sequence as if a new + // Timeout had been allocated. + unrefActive(timer); + return timer; + } + + timer = new Timeout(callback, after, [arg], false, false); + insert(timer, timer._idleTimeout); + + return timer; +} + // Type checking used by timers.enroll() and Socket#setTimeout() function getTimerDuration(msecs, name) { validateNumber(msecs, name); @@ -704,6 +732,7 @@ module.exports = { kHasPrimitive, initAsyncResource, setUnrefTimeout, + reuseOrCreateUnrefTimeout, getTimerDuration, immediateQueue, getTimerCallbacks,