-
Notifications
You must be signed in to change notification settings - Fork 29.6k
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
tls: improve TLSSocket & Server performance #15575
Conversation
Limit the number of closures created and functions called throughout the lifecycle of TLSSocket, Server & connect.
There's an issue with the error codes in Lines 119 to 121 in 150b8f7
going out of ASCIIbetical order. I'll open a PR for that. |
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.
Just a few small things and the CI must be happy but otherwise this is LGTM.
lib/_tls_wrap.js
Outdated
cb(null); | ||
owner.server && | ||
!owner.server.emit('resumeSession', hello.sessionId, onSession)) { | ||
loadSessionDone(null, owner); |
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.
All entries of loadSessionDone have a a single line that they would call in the function. This can always be inlined directly. There is no need for calling that function.
lib/_tls_wrap.js
Outdated
if (once) | ||
return cb(new errors.Error('ERR_MULTIPLE_CALLBACK')); | ||
return requestOCSP(new errors.Error('ERR_MULTIPLE_CALLBACK'), owner); |
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 is no need to call the function if you know that self.destroy(err)
is going to be called. This can be inlined and the function can be inlined for all error cases and that case can be removed from the function.
lib/_tls_wrap.js
Outdated
}); | ||
} | ||
|
||
|
||
function requestOCSP(self, hello, ctx, cb) { | ||
function requestOCSP(err, self, hello) { |
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 calling functions calls hello
info. This is somewhat inconsistent.
I see why you changed owner
to self
but I am also not very happy about that naming either. Right now I am not certain what would be best.
lib/_tls_wrap.js
Outdated
if (!self._handle) | ||
return cb(new errors.Error('ERR_SOCKET_CLOSED')); | ||
if (self._handle === null) | ||
return requestOCSPDone(new errors.Error('ERR_SOCKET_CLOSED'), self); |
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 as above again. Inlining would be good.
lib/_tls_wrap.js
Outdated
function oncertcb(info) { | ||
var self = this; | ||
var servername = info.servername; | ||
if (self._handle === null) |
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.
If I am not mistaken, it is not possible to reach this. self._handle
should never be null.
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.
Since the user has control over that callback, it could indeed be called after _handle has been nulled. Ignore me, thought this referred to the one above it.
lib/_tls_wrap.js
Outdated
return cb(null); | ||
return requestOCSPDone(null, self); | ||
|
||
let ctx = self._handle.sni_context; | ||
|
||
if (!ctx) | ||
ctx = self.server._sharedCreds; | ||
|
||
// TLS socket is using a `net.Server` instead of a tls.TLSServer. | ||
// Some TLS properties like `server._sharedCreds` will not be present | ||
if (!ctx) |
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 could be simplified by moving this in the upper if (!ctx)
check. This would safe a check in case ctx exists from the beginning on.
owner.destroy(err); | ||
} else if (owner._tlsOptions.isServer && | ||
owner._rejectUnauthorized && | ||
/peer did not return a certificate/.test(err.message)) { |
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.
Do we know if the error message is static / never changed before? In this case it would be faster to compare the whole message as string.
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 sure and we don't have a test that covers this. I don't think we should change in this PR. I tried to avoid anything that could actually break things.
lib/_tls_wrap.js
Outdated
}, | ||
set: function set(value) { | ||
res.reading = value; | ||
set: (value) => { |
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.
When changing this, you can directly switch to get() {
and set(value) {
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 needs to be an arrow function. (I'm trying to avoid referencing res so the unsetting can happen in a separate function and there's no leak.)
lib/_tls_wrap.js
Outdated
var ctx; | ||
|
||
this.server._contexts.some(function(elem) { | ||
for (var i = 0; i < contexts.length; i++) { | ||
const elem = contexts[i]; | ||
if (elem[0].test(servername)) { | ||
ctx = elem[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.
No need for the ctx variable at all. You could call the callback here and return and call the callback below with undefined (if that is possible - otherwise that is obsolete).
Nice work! |
5e3ed18
to
b70f34b
Compare
b70f34b
to
d53bb6a
Compare
Thanks for the detailed feedback! Made all the requested changes. (Sorry for the commit spam, went back and forth on the error message thing above but decided to leave because we don't have a test.) |
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. Please check the coverage once again though to be sure there is no uncovered code.
lib/_tls_wrap.js
Outdated
|
||
// TLS socket is using a `net.Server` instead of a tls.TLSServer. | ||
// Some TLS properties like `server._sharedCreds` will not be present | ||
if (!ctx) | ||
return cb(null); | ||
return requestOCSPDone(socket); |
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 could still be combined with the upper if (!ctx)
statement. It might also be interesting to see what socket._handle.sni_context
and socket.server._sharedCreds
returns. If we know it is either a object or null / undefined, we could make the check explicit (e.g. if (ctx === null)
).
if (!ctx) {
ctx = socket.server._sharedCreds;
if (!ctx)
return requestOCSPDone(socket);
}
Writing more explicit JS does make a nice difference with Turbofan.
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, I don't think we're guaranteed them to be anything in particular because user modules do have access and TLS has previously been broken by making these assumptions elsewhere. (Can't find the PR & issue in question right now but can link later.)
I will update to make it a nested if statement though. Thanks!
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.
Nothing stands out as being problematic. Would like @mcollina to take a look
Ping @mcollina |
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 am not very comfortable with this change landing in 8 right now as we are getting closer to LTS. It might be fine, but I would prefer it to get some proper testing under the 9 line before we backport.
This would need some CITGM runs to check if there are any known regressions.
I have added some questions and feedbacks, once those are satisfied you can dismiss this review (I am traveling until the 10th of October).
if (owner._newSessionPending) { | ||
owner._securePending = true; | ||
return; | ||
} |
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.
was this added here? Or was it moved from somewhere else?
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 moved from elsewhere. _finalInit
has two paths: onhandshakedone
(server) or being called directly (client). But _newSessionPending
can only happen on the server because it's called from onnewsession
which only runs on the server.
} | ||
|
||
|
||
function loadSession(self, hello, cb) { | ||
function loadSession(hello) { |
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 callback here is removed. It's very hard to track these changes, so I might be losing something, but it seems that the callback called self._handle.setOCSPResponse(response)
which lost doing the refactoring.
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 cb
for loadSession
is self._handle.endParser()
, which has instead been inlined.
lib/_tls_wrap.js
Outdated
get: function get() { | ||
return res.reading; | ||
get: () => { | ||
return this[kRes].reading; |
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 think there is a need to use this[kRes]
here, as we are closing upon res anyway.
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 was added because I think otherwise it's a cyclic reference, no? Also see this commit apapirovski@f7620fb
res._parent = handle; | ||
res._parentWrap = wrap; | ||
res._secureContext = context; | ||
res.reading = handle.reading; | ||
this[kRes] = 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.
I do not think this is a good idea, is it possible to remove it without allocating any more closures?
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 that I can tell. Doing it this way is more performant based on my testing but can revert.
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.
Can you please benchmark with and without? it does not seem to be needed, but I would be happy to be wrong.
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.
Sorry, what I mean is: I can't remove the closure without using this[kRes]
. If we're ok with having a closure then this is fine. The performance difference is minor.
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 closure?
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 cyclic reference still exists, as the closures below are closing over res
. The actual solution is to move the whole Object.defineProperty
to an outside function that is not passed res
to. Then this[kRes]
is correct.
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 might be going crazy here, sorry for dragging this on but...
this[kRes] = res;
Object.defineProperty(handle, 'reading', {
get: () => {
return this[kRes].reading;
},
set: (value) => {
this[kRes].reading = value;
}
});
this.on('close', onSocketCloseDestroySSL);
Then we unset this[kRes] and res is no longer referenced by anything. I could be completely nuts though, been a long day. That said, what you're saying makes sense so it could be:
const res = tls_wrap.wrap(handle._externalStream,
context.context,
!!options.isServer);
res._parent = handle;
res._parentWrap = wrap;
res._secureContext = context;
res.reading = handle.reading;
this[kRes] = res;
defineHandleReading(this, handle);
this.on('close', onSocketCloseDestroySSL);
return res;
};
function defineHandleReading(socket, handle) {
Object.defineProperty(handle, 'reading', {
get: () => {
return socket[kRes].reading;
},
set: (value) => {
socket[kRes].reading = value;
}
});
}
function onSocketCloseDestroySSL() {
// Make sure we are not doing it on OpenSSL's stack
setImmediate(destroySSL, this);
this[kRes] = null;
}
Not sure how much that actually improves things though since extra function call and all.
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 would have to wait for the handle to be collected to be able to free res
, while in this way it could be freed earlier. It was the spirit of https://github.com/apapirovski/node/blame/7a953929fef5a7892d4697ed73e6fd314055beee/lib/_tls_wrap.js#L404-L408, which makes the getter and setter refer to nothing, and it clear res
from those closures as well.
The function call can likely be inlined by V8, so there should not be drop in performance but verify.
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 think this is the most controversial part here. As the diff is definitely not easy to read here I would suggest to remove this optimization for now and you could open another PR for that or at least have a individual commit for it. Both would make the review a lot easier.
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've updated it based on @mcollina's feedback. I think it should be good now?
if (this._newSessionPending) { | ||
this._securePending = true; | ||
return; | ||
} |
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.
are we sure about removing this check? This function might be called outside of this module context.
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.
See one of the comments above regarding 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.
This does not answer my question. This function might be called from outside this module.
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.
Well, I mean, I suppose someone could reimplement the whole logic around _newSessionPending
in user-land. But right now that _newSessionPending
is set within onnewsession
which only runs on the server and so it was moved into that branch instead. The reason this was added was to support async sessions, as far as I can tell and this is the code in question:
function onnewsession(key, session) {
[removed]
owner._newSessionPending = false;
if (owner._securePending)
owner._finishInit();
owner._securePending = false;
};
owner._newSessionPending = true;
if (!owner.server.emit('newSession', key, session, done))
done();
}
Here's the commit in question: apapirovski@75ea11f
const kErrorEmitted = Symbol('error-emitted'); | ||
const kHandshakeTimeout = Symbol('handshake-timeout'); | ||
const kRes = Symbol('res'); | ||
const kSNICallback = Symbol('snicallback'); |
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 might want to define the new symbols in the constructor, assigning them to null
is ok, so you would not alter the shape of the object for every request.
As a side note, these types of refactoring are very hard to review. It would have been better if it was split in different commits to track down from where stuff was moved. |
@mcollina If you would prefer me to open a separate PR for different parts of this, I can probably do that. It's a bit tricky because not all of it is an identifiable unit but can figure something out. |
I might be overcautious, but it would be better if other @nodejs/tsc members review this. |
lib/_tls_wrap.js
Outdated
|
||
this.on('close', onSocketCloseDestroySSL); | ||
|
||
return res; | ||
}; | ||
|
||
function defineHandleReading(socket, handle) { |
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.
Would you mind adding a comment that links to f7620fb, explaining why it is being done in this 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.
Updated, I think the commit explains it well (and it shows all the relevant parts) but let me know if you prefer a full outline of it within the code.
685c255
to
c0f7f2c
Compare
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
Thanks for the review @mcollina — sorry about the size of change. 😓 |
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, if it works
CI run seemed clean twice, maybe we could kick off a CITGM? |
Landed in 5723b5d |
Limit the number of closures created and functions called throughout the lifecycle of TLSSocket, Server & connect. PR-URL: #15575 Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Fedor Indutny <fedor.indutny@gmail.com>
This is not landing cleanly on 8.x. Could you please backport |
I think @mcollina had objections to landing this on 8.x. Still the case? |
Limit the number of closures created and functions called throughout the lifecycle of TLSSocket, Server & connect. PR-URL: nodejs/node#15575 Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Fedor Indutny <fedor.indutny@gmail.com>
ping @mcollina |
@MylesBorins @apapirovski I would prefer this to land on 9, brew there for a bit, and then be backported. There is a non-null chance that this introduces regressions, and I am concerned on landing this right before 8 goes LTS. |
@mcollina I've thrown the baking label on here :D |
@mcollina @MylesBorins - is this still something that should be backported to v8.x now it has had time to bake? |
I'm +1 for backporting this if it lands cleanish. |
While working on
http2
I spent some time digging aroundtls
and I noticed that there were a whole lot of unnecessary closures and function calls throughout. I decided to go through and try to eliminate as many of them as possible. Turns out some of these didn't even need much changed (such as all the _handle events because the _handle already knows which TLSSocket it belongs to).I tried to limit the churn as much as possible but there definitely is some of it. I've avoided making unnecessary variable adjustments from var to let, const except where the line in question (or related block) was already changing. I also limited all my changes to performance and kept all the logic the same. While the existing tests cover a very solid amount of this module, I didn't want to potentially introduce bugs in uncovered edge cases.
It's a bit hard to get a good measure of performance for these changes as so much of this performance is being limited elsewhere but I created a basic benchmark for TLSSocket creation which clocked in at (and do note that this is limited by the C++ and other code, there's a note re: JS specific numbers further down)
The
tls-connect
benchmark was not much help because it seemed limited by my system's ability to allocate new sockets rather than the actual performance of the code. I couldn't even run a reliable benchmark comparing master to master itself (with 200 runs, I got results anywhere from -10 to 10% with almost no certainty), let alone my code to master.I also used vegeta to hit a basic TLS server and ran a benchmark for what the max handled requests per second would be at full saturation. With these changes I got a reliable 4% increase across a total of million requests or so.
Also, running the node profiler on the new code indicates that the JS code throughout TLSSocket takes about 50% less time and the JS code within
tlsConnectionListener
takes about 35% less time (this is across 40 runs oftls-connect.js
and 10m session withvegeta
). It's a bit hard to run an actual benchmark on it though because so much of it is tied to other code.I realize this is quite a lot to review so thanks for any and all feedback! Let me know if there's anything I can do to make this more digestible.
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
tls