New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use writeHead instead of setHeader/getHeader. #813
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious: could you explain what is "ArgumentsAdaptorTrampoline"?
@@ -19,7 +19,6 @@ const opts = { | |||
|
|||
fastify | |||
.get('/', opts, function (req, reply) { | |||
reply.header('Content-Type', 'application/json').code(200) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this line is removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it was not needed, those are the defaults.
The trampoline is added by V8 when there is an arity mishmatch between how you call a function and how is defined. This normally has no influence on performance, unless when it does. |
Fixing the performance of calling functions with a different arity is on the V8 roadmap if anyone's interested in following along: https://bugs.chromium.org/p/v8/issues/detail?id=7460 |
var Request = context.Request | ||
var Reply = context.Reply | ||
var request = new Request(params, req, urlUtil.parse(req.url, true).query, headers, req.log) | ||
var reply = new Reply(res, context, request) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the old code prevent V8 from optimizing the constructor call?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not really, but I had to fix a bad bug in my change and I forgot these in. I can revert this bit if you want.
lib/reply.js
Outdated
@@ -47,20 +48,20 @@ Reply.prototype.send = function (payload) { | |||
return | |||
} | |||
|
|||
const contentType = this.res.getHeader('Content-Type') | |||
const contentType = this._headers['content-type'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Content-Type
could have been set on the res
object by a hook or middleware. I am pretty worried about the potential complications of having response headers stored in 2 different places.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be easily solved with a reply.getHeader
that fallbacks to the basic response headers.
I've tested this in my local copy and it works perfectly.
If @mcollina is ok I can send the change directly in this branch.
return | ||
} | ||
|
||
if (typeof payload !== 'string' && !Buffer.isBuffer(payload)) { | ||
throw new TypeError(`Attempted to send payload of invalid type '${typeof payload}'. Expected a string or Buffer.`) | ||
} | ||
|
||
if (!reply.res.getHeader('Content-Length')) { | ||
reply.res.setHeader('Content-Length', '' + Buffer.byteLength(payload)) | ||
if (!reply._headers['content-length']) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same thing as my other comment about the header possibly being set on res
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the final handler of the onSend
hooks, so if a user wants to alter the headers it will use the reply
interface. I hope that no one will set the content-length
header inside a onRequest
or preHandler
hook.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also hope that no one will do that, but we can't stop them from doing it so it's a case that needs to be handled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't agree, I don't think we should drop a considerable amount of ops/sec just because a user may do something that is wrong.
I've removed I've added a fast-path to avoid performance issues for Node 8+. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
On my machine this change give us 1500/2000 req/sec more! 🚀 |
const contentType = this.res.getHeader('Content-Type') | ||
const contentTypeNotSet = contentType === undefined | ||
var contentType = getHeader(this, 'content-type') | ||
var contentTypeNotSet = contentType === undefined |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the change to var
here an optimization?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be, but I didn't measure without.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe change it back for consistency?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jsumners as far as I know var
is slightly faster than const
because of the scoping.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which means the answer to the question is "yes".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
key = key.toLowerCase() | ||
var res = reply.res | ||
var value = reply._headers[key] | ||
if (value === undefined && res.hasHeader(key)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using res.hasHeader
doesn't seem like it would help much. It's almost the exact same code as res.getHeader
so it would be simpler just to have one function that's the same as getHeaderFallback
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not have investigated why, but res.getHeader
slow us down, while res.hasHeader
does not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh that is very interesting.
lib/reply.js
Outdated
if (payload === undefined || payload === null) { | ||
reply.sent = true | ||
reply.res.end() | ||
reply._headers['content-length'] = '0' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Content-Length
header should not be set here. There's rules about it in RFC 7230.
@delvedor's idea about having a public |
lib/reply.js
Outdated
} | ||
|
||
reply.sent = true | ||
reply.res.end(payload) | ||
|
||
res.writeHead(res.statusCode, reply._headers) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can avoid the ArgumentsAdaptorTrampoline in res.writeHead
calls too by passing in a third argument.
https://github.com/nodejs/node/blob/master/lib/_http_server.js#L200
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately if I pass null
or undefined
all tests fails.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh that's too bad. I guess maybe the writeHead
code is different for older versions of Node (it looks like it should work with the current code on master).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you tried res.writeHead(res.statusCode, null, reply._headers)
?
Or res.writeHead(res.statusCode, http.STATUS_CODE[res.statusCode], reply._headers)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@allevo can't make it to work, but I would like to land this before the 1st of March, and this can be worked out afterwards.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's another problem. Using the three parameters version on http2 a warning is emitted:
(node:2103) UnsupportedWarning: Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)
Having |
@nwoltman updated to not set |
An option might be to remove |
@mcollina I agree having |
@@ -105,7 +108,7 @@ Reply.prototype.serializer = function (fn) { | |||
} | |||
|
|||
Reply.prototype.type = function (type) { | |||
this.res.setHeader('Content-Type', type) | |||
this._headers['content-type'] = type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.header()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why going for a slow-path when there is a fast-path?
lib/reply.js
Outdated
} | ||
|
||
reply.sent = true | ||
reply.res.end(payload) | ||
|
||
res.writeHead(res.statusCode, reply._headers) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you tried res.writeHead(res.statusCode, null, reply._headers)
?
Or res.writeHead(res.statusCode, http.STATUS_CODE[res.statusCode], reply._headers)
?
lib/reply.js
Outdated
@@ -196,11 +207,15 @@ function sendStream (payload, res, reply) { | |||
} | |||
}) | |||
|
|||
for (var key in reply._headers) { | |||
res.setHeader(key, reply._headers[key]) | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not res.writeHead
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’ll add a comment. We want to return 500s if the streams error before trying to send any data. That is the only way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I'm 👍 for the comment
lib/reply.js
Outdated
// ponyfill for hasHeader. It has been intoroduced into Node 7.7, | ||
// so it's ok to use it in 8+ | ||
{ | ||
const v = process.version.match(/v(\d)/)[1] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
process.version.match(/v(\d+)/)[1]
(+ added)
test/get.test.js
Outdated
@@ -362,7 +362,7 @@ fastify.listen(0, err => { | |||
}, (err, response, body) => { | |||
t.error(err) | |||
t.strictEqual(response.statusCode, 200) | |||
t.strictEqual(response.headers['content-length'], '0') | |||
t.strictEqual(response.headers['content-length'], undefined) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO this is wrong.
The content length should always be set
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The spec allows for it to not be set -- https://tools.ietf.org/html/rfc2616#section-14.13
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah and in the rfc:
Applications SHOULD use this field to indicate the transfer-length of the message-body
where "SHOULD" means:
This word, or the adjective "RECOMMENDED", mean that there may exist valid reasons in particular circumstances to ignore a particular item, but the full implications must be understood and carefully weighed before choosing a different course
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but it isn't a "MUST".
test/internals/reply.test.js
Outdated
@@ -193,7 +194,7 @@ test('within an instance', t => { | |||
const url = 'http://localhost:' + fastify.server.address().port + '/redirect-onsend' | |||
http.get(url, (response) => { | |||
t.strictEqual(response.headers['x-onsend'], 'yes') | |||
t.strictEqual(response.headers['content-length'], '0') | |||
t.strictEqual(response.headers['content-length'], undefined) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
// we cannot send a content-length for 304 and 204, and all status code | ||
// < 200. | ||
if (statusCode >= 200 && statusCode !== 204 && statusCode !== 304) { | ||
reply._headers['content-length'] = '0' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A server MAY send a Content-Length header field in a response to a HEAD request (Section 4.3.2 of [RFC7231]); a server MUST NOT send Content-Length in such a response unless its field-value equals the decimal number of octets that would have been sent in the payload body of a response if the same request had used the GET method.
If a user sends a 200 response to a HEAD request, this will most likely be the wrong Content-Length.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Except this is within the condition that the payload is either undefined
or null
. Thus, there isn't a content length.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what should it be in that case? Should we check if there is a content-length set before setting it to zero?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m completely lost. This code passes all our tests in master without changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mcollina I believe you are correct with the code as it is. Look at the full context:
Lines 149 to 163 in 578517d
if (payload === undefined || payload === null) { | |
reply.sent = true | |
// according to https://tools.ietf.org/html/rfc7230#section-3.3.2 | |
// we cannot send a content-length for 304 and 204, and all status code | |
// < 200. | |
if (statusCode >= 200 && statusCode !== 204 && statusCode !== 304) { | |
reply._headers['content-length'] = '0' | |
} | |
res.writeHead(statusCode, reply._headers) | |
// avoid ArgumentsAdaptorTrampoline from V8 | |
res.end(null, null, null) | |
return | |
} |
The payload size should always be 0 where you are explicitly setting the content length because a payload does not exist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jsumners If it's a HEAD request, the user may decide to only set the headers and not send the payload itself (even though it would be stripped by Node). Users can do this to avoid spending time generating a payload.
@mcollina In that case the Content-Length should not be set if it was not already set by a user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's okay. I'm not going to block this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's still appropriate to send Content-length: 0
in that case because it is true. Headers do not count.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
This gives us a nice +10% on throughput.
Before:
After:
Checklist
npm run test
andnpm run benchmark