From deefb42cf840f30a8e65e518e5d3678f7bf5269c Mon Sep 17 00:00:00 2001 From: Kyle Cannon Date: Tue, 2 Jun 2026 13:41:45 -0700 Subject: [PATCH] perf(pg-pool): cut per-query allocations in the acquire/release path Two allocation reductions, no behavior change: - client.on('error', onError) instead of once: the query callback always removes the listener and onError self-guards via clientReleased, so once's per-query onceWrapper allocation was unnecessary. - Bind _pulseQueue once in the constructor (_pulseQueueBound) and reuse it for the connect-path nextTick and the three _remove callbacks, instead of allocating a fresh bound function per acquire/release. Benchmark (mock-client pool-micro, alternating-sampled vs base, conc=50): ~+2-3% qps (all 3 rounds favored this change). The O(1) pending-queue dequeue (separate PR) adds more at high concurrency. Full pg-pool suite passes (85 passing; the one failure is the unrelated pg-native bindings test, which requires libpq). Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/pg-pool/index.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/pg-pool/index.js b/packages/pg-pool/index.js index ab514fa88..11797c2ea 100644 --- a/packages/pg-pool/index.js +++ b/packages/pg-pool/index.js @@ -106,6 +106,9 @@ class Pool extends EventEmitter { this._endCallback = undefined this.ending = false this.ended = false + // bound once so the hot acquire/release paths reuse it instead of allocating + // a fresh closure/bound function per query (nextTick scheduling, _remove cbs) + this._pulseQueueBound = this._pulseQueue.bind(this) } _promiseTry(f) { @@ -200,7 +203,7 @@ class Pool extends EventEmitter { if (this._isFull() || this._idle.length) { // if we have idle clients schedule a pulse immediately if (this._idle.length) { - process.nextTick(() => this._pulseQueue()) + process.nextTick(this._pulseQueueBound) } if (!this.options.connectionTimeoutMillis) { @@ -394,14 +397,14 @@ class Pool extends EventEmitter { this.log('remove expended client') } - return this._remove(client, this._pulseQueue.bind(this)) + return this._remove(client, this._pulseQueueBound) } const isExpired = this._expired.has(client) if (isExpired) { this.log('remove expired client') this._expired.delete(client) - return this._remove(client, this._pulseQueue.bind(this)) + return this._remove(client, this._pulseQueueBound) } // idle timeout @@ -410,7 +413,7 @@ class Pool extends EventEmitter { tid = setTimeout(() => { if (this._isAboveMin()) { this.log('remove idle client') - this._remove(client, this._pulseQueue.bind(this)) + this._remove(client, this._pulseQueueBound) } }, this.options.idleTimeoutMillis) @@ -461,7 +464,10 @@ class Pool extends EventEmitter { cb(err) } - client.once('error', onError) + // `on` rather than `once`: the query callback below always removes this + // listener, and `onError` self-guards via `clientReleased`, so we don't + // need `once`'s wrapper allocation per query. + client.on('error', onError) this.log('dispatching query') try { client.query(text, values, (err, res) => {