Conversation
`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>
The existing `setHeader` and `setHeaderWH` cases set `Content-Length` explicitly, and `normal` passes every header to `writeHead()`. The very common handler shape `res.setHeader(...)` + `res.end(body)`, where `end()` derives the framing, exercises a different path and was not covered. Signed-off-by: Matteo Collina <hello@matteocollina.com>
|
Review requested:
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #66420 +/- ##
==========================================
- Coverage 90.39% 90.38% -0.01%
==========================================
Files 792 792
Lines 275580 275704 +124
Branches 52840 52857 +17
==========================================
+ Hits 249104 249193 +89
- Misses 16897 16937 +40
+ Partials 9579 9574 -5
🚀 New features to boost your workflow:
|
Commit Queue failedThis pull request has multiple commits, but no landing policy was selected. Add
commit-queue-squash
The pull request was removed from the Commit Queue and labeled
commit-queue-failed
Full Commit Queue output |
`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>
The existing `setHeader` and `setHeaderWH` cases set `Content-Length` explicitly, and `normal` passes every header to `writeHead()`. The very common handler shape `res.setHeader(...)` + `res.end(body)`, where `end()` derives the framing, exercises a different path and was not covered. 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>
|
Landed in 1ee5d29...40933d5 |
The existing `setHeader` and `setHeaderWH` cases set `Content-Length` explicitly, and `normal` passes every header to `writeHead()`. The very common handler shape `res.setHeader(...)` + `res.end(body)`, where `end()` derives the framing, exercises a different path and was not covered. 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>
The existing `setHeader` and `setHeaderWH` cases set `Content-Length` explicitly, and `normal` passes every header to `writeHead()`. The very common handler shape `res.setHeader(...)` + `res.end(body)`, where `end()` derives the framing, exercises a different path and was not covered. 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>
`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>
setHeader()stored headers in a{ __proto__: null }literal andOutgoingMessageletEventEmittercreate 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 firstsetHeader()/on()call, for runtime-miss stores (the profile showsStoreIC::Store,Factory::NewStoreHandlerandTieringManager::NotifyICChangedin steady state) and for dictionary lookups on every header access, including thefor...inin_storeHeader().This PR uses a fast-mode object with an empty null-prototype chain for the headers (same keys, same enumeration, no inherited properties), and presets the listener table with the common events, as
Readable/Writablealready do.Hello-world server (
res.setHeader('Content-Type', ...)+res.end('Hello World')), server pinned to one core,wrk -t2 -c50on two other physical cores, 4 interleaved rounds:The
_eventspreset accounts for ~2% of that; the header container for the rest. With 8 request headers and a handler that reads two of them: 36.2k → 43.2k req/s. Responses are byte-identical.How much the dictionary-mode container costs depends on the response pattern (same harness, 2 rounds each):
setHeader('Content-Type')+end(body)setHeader('Content-Type')+setHeader('Content-Length')+end(body)writeHead(200, { ... })+end(body)(nosetHeader(),benchmark/fixtures"normal")The existing
benchmark/httpfixtures either setContent-LengththroughsetHeader()or pass every header towriteHead(), so they only show the smaller gains. The second commit adds asetHeaderImplicitcase tohttp/set-header.js(res.setHeader('Content-Type', ...)+res.end(body), framing left toend()).benchmark/compare.js --runs 10 --filter set-header http:simple.js,headers.jsandincoming_headers.jsare unchanged within noise.—-
Ai generated, humanly reviewed.