Skip to content
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

Closed
wants to merge 3 commits into from

Conversation

apapirovski
Copy link
Member

@apapirovski apapirovski commented Nov 23, 2017

Just doing a little bit of clean up in timers to — hopefully —  make it a bit easier to grok.

  • Remove micro-optimizations that no longer yield any benefits
  • Simplify Timeout & Immediate to just run all their setup code in the constructor instead of needing extra helper functions
  • Simplify Timeout to just have a single argument that determines whether it's repeating or not, instead of having two separate helper functions that differ in only one line
  • Adjust timers benchmarks to run long enough to offer meaningful data

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), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • commit message follows commit guidelines
Affected core subsystem(s)

benchmark, timers

@apapirovski apapirovski added timers Issues and PRs related to the timers subsystem / setImmediate, setInterval, setTimeout. dont-land-on-v4.x labels Nov 23, 2017
@apapirovski apapirovski force-pushed the patch-timers-perf branch 2 times, most recently from 764ec52 to faa30f3 Compare November 23, 2017 19:15
lib/timers.js Outdated

this[async_id_symbol] = ++async_id_fields[kAsyncIdCounter];
this[trigger_async_id_symbol] = initTriggerId();
if (async_hook_fields[kInit] > 0)
Copy link
Contributor

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.

Copy link
Member Author

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) {
Copy link
Contributor

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?

Copy link
Member Author

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++)
Copy link
Contributor

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;
Copy link
Contributor

@mscdex mscdex Nov 23, 2017

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.

Copy link
Member Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/cc @nodejs/tsc

Copy link
Member Author

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.

Copy link
Member Author

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) {
Copy link
Contributor

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?

Copy link
Member Author

@apapirovski apapirovski Nov 23, 2017

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.

Copy link
Contributor

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*

Copy link
Member Author

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?

Copy link
Member

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?

Copy link
Member Author

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!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That did it!

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?

Copy link
Contributor

@Fishrock123 Fishrock123 left a 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) {
Copy link
Contributor

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.

Copy link
Member Author

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);
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Member Author

@apapirovski apapirovski Nov 25, 2017

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.
Copy link
Contributor

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?

Copy link
Member Author

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.)

Copy link
Contributor

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
Copy link
Contributor

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.

@apapirovski
Copy link
Member Author

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?

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 Timeout class back to how it was and still preserve all of the other relevant changes in this PR.

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.

@apapirovski
Copy link
Member Author

Ran a Benchmark on the CI just to make sure and here are the results:

 timers/immediate.js type="breadth1" thousands=5000     -2.63 %        *** 5.634607e-05
 timers/immediate.js type="breadth4" thousands=5000      0.49 %            7.652080e-01
 timers/immediate.js type="breadth" thousands=5000       2.51 %        *** 9.290645e-05
 timers/immediate.js type="clear" thousands=5000         0.93 %            3.366699e-01
 timers/immediate.js type="depth1" thousands=5000       -1.27 %          * 1.191555e-02
 timers/immediate.js type="depth" thousands=5000        -4.23 %        *** 7.135043e-16
 timers/set-immediate-breadth-args.js millions=5         3.29 %            6.900801e-02
 timers/set-immediate-breadth.js millions=10             2.18 %        *** 6.599080e-06
 timers/set-immediate-depth-args.js millions=5          -0.48 %            3.170515e-01
 timers/timers-breadth.js thousands=5000                 1.25 %         ** 2.818523e-03
 timers/timers-cancel-pooled.js millions=5               0.62 %            5.948548e-01
 timers/timers-cancel-unpooled.js millions=1            97.49 %        *** 2.412041e-45
 timers/timers-depth.js thousands=1                     -0.01 %            9.215241e-01
 timers/timers-insert-pooled.js millions=5               2.49 %        *** 1.611240e-05
 timers/timers-insert-unpooled.js millions=1             1.53 %            2.240269e-01
 timers/timers-timeout-pooled.js millions=10           -22.55 %        *** 4.849235e-27

No clue what's going on with the timers-timeout-pooled.js, while I'm pretty sure timers-cancel-unpooled.js is hitting the "function tracking" issue discussed above and the new version fixes that. I would like to fix timers-timeout-pooled.js before landing this although I'm not convinced that benchmark is 100% representative.

@Fishrock123
Copy link
Contributor

Also, it would be fairly easy to convert the Timeout class back to how it was and still preserve all of the other relevant changes in this PR.

Do you think you could at least split it into two commits, other improvements first? That would make things easier.

@apapirovski
Copy link
Member Author

@Fishrock123 Yeah, I should be able to split this up over the weekend.

@apapirovski apapirovski force-pushed the patch-timers-perf branch 3 times, most recently from 8709eb3 to e094ee2 Compare November 25, 2017 13:43
@apapirovski
Copy link
Member Author

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.

@apapirovski
Copy link
Member Author

apapirovski commented Nov 25, 2017

@apapirovski
Copy link
Member Author

apapirovski commented Nov 25, 2017

New benchmark results, was able to resolve all regressions and we've got a major bump on one benchmark:

 timers/immediate.js type="breadth1" thousands=5000     -2.58 %        *** 6.584465e-07
 timers/immediate.js type="breadth4" thousands=5000     -0.37 %            8.223856e-01
 timers/immediate.js type="breadth" thousands=5000       2.12 %         ** 1.340324e-03
 timers/immediate.js type="clear" thousands=5000         2.02 %          * 3.461550e-02
 timers/immediate.js type="depth1" thousands=5000       -0.03 %            9.631820e-01
 timers/immediate.js type="depth" thousands=5000        -3.02 %        *** 1.611862e-07
 timers/set-immediate-breadth-args.js millions=5         2.42 %            2.093568e-01
 timers/set-immediate-breadth.js millions=10             1.83 %        *** 4.767013e-06
 timers/set-immediate-depth-args.js millions=5          -0.46 %            2.570641e-01
 timers/timers-breadth.js thousands=5000                 1.69 %        *** 3.210408e-05
 timers/timers-cancel-pooled.js millions=5               1.45 %            2.326928e-01
 timers/timers-cancel-unpooled.js millions=1            95.99 %        *** 8.813862e-41
 timers/timers-depth.js thousands=1                     -0.05 %            5.132490e-01
 timers/timers-insert-pooled.js millions=5               2.13 %        *** 1.252006e-05
 timers/timers-insert-unpooled.js millions=1             4.07 %         ** 4.065701e-03
 timers/timers-timeout-pooled.js millions=10             4.13 %            3.526779e-01

(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.
@apapirovski
Copy link
Member Author

@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);
Copy link
Contributor

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.

Copy link
Member Author

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);
Copy link
Contributor

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:

  1. setup n*1E6 timeouts
  2. end tick - exit JS
  3. poll in uv for 1ms
  4. reenter JS
  5. call n*1E6 callbacks

I wish we could exclude steps 2-4 from the measurement 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment.

@refack
Copy link
Contributor

refack commented Nov 28, 2017

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);
Copy link
Member

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)?

Copy link
Member Author

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.

@apapirovski
Copy link
Member Author

Landed in d8bc4f2

@apapirovski apapirovski deleted the patch-timers-perf branch November 28, 2017 21:33
apapirovski added a commit that referenced this pull request Nov 28, 2017
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 <jasnell@gmail.com>
Reviewed-By: Refael Ackermann <refack@gmail.com>
Reviewed-By: Timothy Gu <timothygu99@gmail.com>
@MylesBorins
Copy link
Contributor

MylesBorins commented Dec 11, 2017


edit: it landed after other PRs were backported

@apapirovski
Copy link
Member Author

I think this should land cleanly once #17064 gets back-ported.

MylesBorins pushed a commit that referenced this pull request Dec 12, 2017
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 <jasnell@gmail.com>
Reviewed-By: Refael Ackermann <refack@gmail.com>
Reviewed-By: Timothy Gu <timothygu99@gmail.com>
MylesBorins pushed a commit that referenced this pull request Dec 12, 2017
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 <jasnell@gmail.com>
Reviewed-By: Refael Ackermann <refack@gmail.com>
Reviewed-By: Timothy Gu <timothygu99@gmail.com>
@MylesBorins MylesBorins mentioned this pull request Dec 12, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
timers Issues and PRs related to the timers subsystem / setImmediate, setInterval, setTimeout.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants