Skip to content

Commit b933d20

Browse files
mcollinanodejs-github-bot
authored andcommitted
http: avoid dictionary-mode objects in responses
`setHeader()` stored headers in a `{ __proto__: null }` literal and `OutgoingMessage` let EventEmitter create the same literal for its listener table. V8 creates null-prototype literals in dictionary mode, so every response paid for a hash table allocation on the first `setHeader()`/`on()` call and for dictionary lookups on every header access, including the `for...in` in `_storeHeader()`. Use a fast-mode object with an empty null-prototype chain for the headers, and preset the listener table with the common events, as streams already do. Hello-world server (`res.setHeader()` + `res.end()`), one core: 40.9k -> 50.0k req/s (+22%); CPU 24.4 -> 20.0 us/req; 4374 -> 3905 B of young-gen allocation per request. Responses are byte-identical. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #66420 Reviewed-By: Tim Perry <pimterry@gmail.com> Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de>
1 parent 1ee5d29 commit b933d20

1 file changed

Lines changed: 22 additions & 1 deletion

File tree

‎lib/_http_outgoing.js‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
8787
});
8888

8989
const kCorked = Symbol('corked');
90+
91+
// Container for the headers set through `setHeader()`, keyed by the
92+
// lower-cased header name. It has no enumerable or inherited properties, like
93+
// a `{ __proto__: null }` literal, but instances are fast-mode objects: V8
94+
// creates null-prototype literals in dictionary mode, which costs a hash table
95+
// allocation per response and slower property access for every header.
96+
class OutgoingHeaders {}
97+
ObjectSetPrototypeOf(OutgoingHeaders.prototype, null);
98+
delete OutgoingHeaders.prototype.constructor;
9099
const kSocket = Symbol('kSocket');
91100
const kChunkedBuffer = Symbol('kChunkedBuffer');
92101
const kChunkedLength = Symbol('kChunkedLength');
@@ -124,6 +133,18 @@ function OutgoingMessage(options) {
124133
return deprecateInstantiation(OutgoingMessage, 'DEP0195', options);
125134
}
126135

136+
// Preset the listener table with the events that are commonly listened
137+
// for, mirroring streams. The presence of the properties keeps `_events`
138+
// in fast mode: the first `on()` call would otherwise have to allocate a
139+
// dictionary for the `{ __proto__: null }` object created by EventEmitter.
140+
this._events ??= {
141+
close: undefined,
142+
error: undefined,
143+
finish: undefined,
144+
prefinish: undefined,
145+
drain: undefined,
146+
};
147+
127148
Stream.call(this);
128149

129150
// Queue that holds all currently pending data, until the response will be
@@ -805,7 +826,7 @@ OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
805826

806827
let headers = this[kOutHeaders];
807828
if (headers === null)
808-
this[kOutHeaders] = headers = { __proto__: null };
829+
this[kOutHeaders] = headers = new OutgoingHeaders();
809830

810831
headers[name.toLowerCase()] = [name, value];
811832
return this;

0 commit comments

Comments
 (0)