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,