Skip to content

timers: reuse Timeout objects in setStreamTimeout#64254

Open
mcollina wants to merge 1 commit into
nodejs:mainfrom
mcollina:timers-reuse-stream-timeout
Open

timers: reuse Timeout objects in setStreamTimeout#64254
mcollina wants to merge 1 commit into
nodejs:mainfrom
mcollina:timers-reuse-stream-timeout

Conversation

@mcollina

@mcollina mcollina commented Jul 2, 2026

Copy link
Copy Markdown
Member

Rearm the stream's existing Timeout object instead of allocating a new Timeout and a bound callback on every setTimeout() call. This is the internal reuse path anticipated by the TODO in unenroll() (lib/timers.js).

The HTTP server disarms the socket timeout at request start and rearms the keep-alive timeout on response finish, so every request over a keep-alive connection currently pays a Timeout allocation, a .bind() closure allocation, and the associated timer-list/async-resource churn. With this change the socket's cleared Timeout object is kept referenced in this[kTimeout] and reused on the next rearm. The async resource is re-initialized on reuse (via the existing insertGuarded path), so async_hooks observes the same init/destroy sequence and AsyncLocalStorage context capture is unchanged.

Performance

A microbenchmark of the setTimeout(msecs)/setTimeout(0) cycle on a net.Socket improves by 2.2x (205.9 ns → 92.8 ns per cycle).

End-to-end, benchmark/http/simple.js (type=buffer len=1024 chunks=1 chunkedEnc=0 c=50, wrk) improves by +3.70% (t=2.15, 40 samples per binary, server and wrk pinned to disjoint CPU sets).

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 <hello@matteocollina.com>
@nodejs-github-bot nodejs-github-bot added needs-ci PRs that need a full CI run. timers Issues and PRs related to the timers subsystem / setImmediate, setInterval, setTimeout. labels Jul 2, 2026
@mcollina mcollina requested a review from anonrig July 2, 2026 12:32
Comment thread lib/internal/timers.js
Comment on lines +431 to +434
const args = timer._timerArgs;
if (args === undefined || args[0] !== arg) {
timer._timerArgs = [arg];
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const args = timer._timerArgs;
if (args === undefined || args[0] !== arg) {
timer._timerArgs = [arg];
}
const args = timer._timerArgs;
if (args === undefined) {
timer._timerArgs = [arg];
} else {
args[0] = arg;
}

Nit: Instead of assigning a new array, update _timerArgs directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-ci PRs that need a full CI run. timers Issues and PRs related to the timers subsystem / setImmediate, setInterval, setTimeout.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants