-
Notifications
You must be signed in to change notification settings - Fork 29.9k
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
timers: clean up for readability #17279
Conversation
764ec52
to
faa30f3
Compare
lib/timers.js
Outdated
|
||
this[async_id_symbol] = ++async_id_fields[kAsyncIdCounter]; | ||
this[trigger_async_id_symbol] = initTriggerId(); | ||
if (async_hook_fields[kInit] > 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.
Please use braces for multi-line bodies.
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 just relocated code. Happy to adjust though.
lib/timers.js
Outdated
} | ||
return this; | ||
} | ||
} | ||
|
||
function setTimeout(callback, after, arg1, arg2, arg3) { |
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.
Did you benchmark using rest args here?
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, still a no go.
lib/timers.js
Outdated
break; | ||
default: | ||
args = [arg1, arg2, arg3]; | ||
for (i = 5; i < arguments.length; i++) |
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 know this wasn't the case previously, but can you also add braces here please?
lib/timers.js
Outdated
@@ -47,6 +47,8 @@ const { kInit, kDestroy, kAsyncIdCounter } = async_wrap.constants; | |||
const async_id_symbol = Symbol('asyncId'); | |||
const trigger_async_id_symbol = Symbol('triggerAsyncId'); | |||
|
|||
const functionApply = Function.prototype.apply; |
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 not sure we can safely do this. This was kind of explicitly avoided in 98609fc. If we do this, it means we may not pick up a monkey-patched version of apply()
during runtime.
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, so the question here is whether we want to pick up a monkey patched version of apply or not? I mean, my instinct says 'no' (and part of the reason for this adjustment) because realistically the internals could change again in the future to where apply
is not even used anymore.
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.
/cc @nodejs/tsc
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 the answer here might actually be different and that is, use Reflect.apply
— assuming performance holds up.
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.
Switched to Reflect.apply
which seems correct here. Quick test revealed no differences in performance. Open to other thoughts on this though.
lib/timers.js
Outdated
this[async_id_symbol], 'Immediate', this[trigger_async_id_symbol], this | ||
); | ||
class Immediate { | ||
constructor(args) { |
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 if we allow the _onImmediate
value to be passed to the constructor to save a step?
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 the comment right below... that's what I wanted to do but it runs 3x slower on our benchmarks. I have not figured out what exactly causes it to plummet.
It's also why it was like this before 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.
Ah ok, if I had to guess I would say it might be the different types (Promise
vs function). *shrug*
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.
But then it's fine for Timeout (and it was in its constructor originally)... so I have no clue. Maybe @bmeurer can shed some light on this for us?
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.
My guess here is that this trigger function tracking on the hidden class. Have you tried passing it to constructor, but setting the field _onImmediate
to null
before and only then store the 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.
Will try, thanks for the tip!
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.
That did it!
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.
Hey 👋
I know that it's a pretty old PR, but I was studying this code, and these lines caught my attention:
// this must be set to null first to avoid function tracking
// on the hidden class, revisit in V8 versions after 6.2
this._onTimeout = null;
this._onTimeout = callback;
Why in these cases the hidden class optimization is spoiling the performance instead of improving?
Is it still happen?
3903245
to
118e205
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.
Seems reasonable, certainly makes the code nicer.
One comment to be addressed.
Some history here. I was kinda not for this back in #12745, although I suppose this is not quite as aggressive.
My comment from then still stands, this may be rather difficult to backport and backport fixes from - would be able to check how much work that might be?
Also, @TimothyGu's comment from last time may still be relevant and may make this semver-major.
Is changing to ES6 classes backwards compatible? The prototype methods will no longer be enumerable after such a change.
lib/timers.js
Outdated
@@ -424,80 +426,148 @@ exports.enroll = function(item, msecs) { | |||
* DOM-style timers | |||
*/ | |||
|
|||
class Timeout { | |||
constructor(callback, after, args, isRepeat) { |
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 args
and isRepeat
go into an options object? It is foreseeable that: a) we may expose the constructor, and b) that we may want to also have an unrefed
option.
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.
They could but then we're creating an extra object just to pass them to the constructor (which just reassigns them anyway). I would maybe leave that change to when we actually do decide to expose as it will have a small performance implication? Just thinking out loud here.
} | ||
} | ||
else | ||
Reflect.apply(timer._onTimeout, timer, args); |
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 performs well? It is certainly much nicer. have any idea how this might be on older versions of Node?
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, it's now equivalent to what was there before.
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.
Forgot to address the second part, I don't think we can switch over on older Node.js versions hence the labels on this PR. That said, I'm planning to run the benchmarks on v8.x to confirm.
lib/timers.js
Outdated
Timeout.prototype.close = function() { | ||
this._onTimeout = null; | ||
if (this._handle) { | ||
// Fewer checks may be possible, but these cover everything. |
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.
Did you check this in detail? If not, can you please preserve the comment?
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 we can do less checks there (I followed the breadcrumb trail as much as I could) but happy to put the comment back.
(Also, I did remove an unnecessary this
check.)
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.
Ok sounds good then.
lib/timers.js
Outdated
this._idleNext = null; | ||
this._idlePrev = null; | ||
// this must be set to null first to avoid function tracking | ||
// on the hidden class, revisit in V8 versions after 6.2 |
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 a thing? ... Interesting.
Yeah, I'm open to hearing other people's thoughts on this so we can try to come to a consensus. Also, it would be fairly easy to convert the Since we're no longer using helper functions to create the Timeout & Immediate instances (meaning those classes are instantiated in a few places), I thought having them as ES6 classes made it slightly more readable and clear than having them as functions that are actually classes. But yeah, it's not too painful to change back and preserve the other changes in this PR. |
744284e
to
40adcde
Compare
Ran a Benchmark on the CI just to make sure and here are the results:
No clue what's going on with the |
Do you think you could at least split it into two commits, other improvements first? That would make things easier. |
@Fishrock123 Yeah, I should be able to split this up over the weekend. |
8709eb3
to
e094ee2
Compare
I've changed my mind, I don't think this really needs ES6 classes. All the 'real' changes from this PR can exist without them. PTAL @mscdex & @Fishrock123. |
New benchmark results, was able to resolve all regressions and we've got a major bump on one benchmark:
(And just to be clear, I'm not trying to get performance boosts here. Just making sure we're not regressing while improving readability of this code.) |
Remove micro-optimizations that no longer yield any benefits, restructure timers & immediates to be a bit more straightforward. Adjust timers benchmarks to run long enough to offer meaningful data.
e094ee2
to
eff64e1
Compare
@Fishrock123 @mscdex Any objections to this landing? Not using ES6 classes any longer so this is strictly readability refactoring. |
function cb2() {} | ||
|
||
process.on('exit', function() { | ||
bench.end(iterations / 1e6); |
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 loving this... It's async, so it could add noise, also having the callbacks have side effects improves the chances they are not optimized away.
I'd rather have duplicate cb()
code, or have cb
just count++
and cb2
test for exit.
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.
Addressed with duplicate code.
|
||
for (var i = 0; i < iterations; i++) { | ||
setTimeout(cb, 1); | ||
setTimeout(i % 2 ? cb : cb2, 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.
Suggestion: add a comment that this sets up n*1E6
timeout objects which will all get called together on next uv tick.
Which BTW mean this mesures:
- setup
n*1E6
timeouts - end tick - exit JS
- poll in uv for 1ms
- reenter JS
- call
n*1E6
callbacks
I wish we could exclude steps 2-4 from the measurement 🤔
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.
Added a comment.
Regarding backporting. https://github.com/nodejs/node/commits/v6.x/lib/timers.js shows 8 commits in 2017. So if used to estimate churn, indicates this is a very stable module. |
} | ||
} | ||
else | ||
Reflect.apply(timer._onTimeout, timer, args); |
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.
How well does this compare with timer._onTimeout(...args)
?
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 much faster, I don't have the numbers anymore but I tested that version first. In general, rest params in arguments perform reasonably well in most situations but spread syntax is still pretty slow.
Landed in d8bc4f2 |
Remove micro-optimizations that no longer yield any benefits, restructure timers & immediates to be a bit more straightforward. Adjust timers benchmarks to run long enough to offer meaningful data. PR-URL: #17279 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Refael Ackermann <[email protected]> Reviewed-By: Timothy Gu <[email protected]>
|
I think this should land cleanly once #17064 gets back-ported. |
Remove micro-optimizations that no longer yield any benefits, restructure timers & immediates to be a bit more straightforward. Adjust timers benchmarks to run long enough to offer meaningful data. PR-URL: #17279 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Refael Ackermann <[email protected]> Reviewed-By: Timothy Gu <[email protected]>
Remove micro-optimizations that no longer yield any benefits, restructure timers & immediates to be a bit more straightforward. Adjust timers benchmarks to run long enough to offer meaningful data. PR-URL: #17279 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Refael Ackermann <[email protected]> Reviewed-By: Timothy Gu <[email protected]>
Just doing a little bit of clean up in timers to — hopefully — make it a bit easier to grok.
I ran a benchmark locally and the performance seemed to remain equivalent but will start a Benchmark CI immediately to confirm.
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
benchmark, timers