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: enable timers to be used as primitives #19683

Closed
wants to merge 7 commits into from

Conversation

bmeck
Copy link
Member

@bmeck bmeck commented Mar 29, 2018

For web compatibility this allows timers to be stored as the key
of an Object property and be passed back to corresponding method to
clear the timer.

Note that this is using the async_id_symbol to get a numeric ID to correspond with timers.

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines

For web compatibility this allows timers to be stored as the key
of an Object property and be passed back to corresponding method to
clear the timer.
@nodejs-github-bot nodejs-github-bot added the timers Issues and PRs related to the timers subsystem / setImmediate, setInterval, setTimeout. label Mar 29, 2018
lib/timers.js Outdated

// Schedule or re-schedule a timer.
// The item must have been enroll()'d first.
const active = exports.active = function(item) {
KNOWN_TIMERS[item[async_id_symbol]] = item;
Copy link
Member

Choose a reason for hiding this comment

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

Can we do the caching in [@@toPrimitive]()? I doubt this is a very frequently used feature, so it might be good if we could avoid the storage overhead for the common case?

Copy link
Member Author

Choose a reason for hiding this comment

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

seems fine to change that.

Copy link
Member Author

Choose a reason for hiding this comment

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

not sure how we could easily avoid the delete though when clearing without doing some funny stuff. the cache should swap into dictionary mode really fast though.

@mscdex
Copy link
Contributor

mscdex commented Mar 29, 2018

Did you check benchmarks?

@bmeck
Copy link
Member Author

bmeck commented Mar 29, 2018

@mscdex I did not. I would be very surprised if it has any significant impact.

Copy link
Member

@addaleax addaleax left a comment

Choose a reason for hiding this comment

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

LGTM if the benchmarks are happy

This is semver-minor, right? Should we document this?

lib/timers.js Outdated
@@ -497,6 +505,12 @@ exports.setInterval = function(callback, repeat, arg1, arg2, arg3) {
};

exports.clearInterval = function(timer) {
if (typeof timer === 'number' || typeof timer === 'string') {
if (timer in KNOWN_TIMERS) {
clearInterval(KNOWN_TIMERS[timer]);
Copy link
Member

Choose a reason for hiding this comment

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

I think we may want to avoid accessing global.clearInterval here … maybe doing something like we do for clearTimeout, i.e. const clearInterval = exports.clearInterval = … is a good idea here too?

Copy link
Member Author

Choose a reason for hiding this comment

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

ah yes, that would be a bug if that happened. I'm not 100% sure on why these functions are not named looking at them...

@addaleax addaleax added the semver-minor PRs that contain new features and should be released in the next minor version. label Mar 29, 2018
@bmeck
Copy link
Member Author

bmeck commented Mar 29, 2018

I ran ./node benchmark/run.js timers but am not seeing anything changing within margin of error.

@Trott
Copy link
Member

Trott commented Mar 29, 2018

@bmeck You may already know this, but in case not: If you have a binary compiled from master called node-old and a binary compiled with your changes called node-new, you can have it do the statistical computation for you with something like:

node benchmark/compare.js --old node-old --new node-new timers > my-benchmark.csv

...followed by...

cat my-benchmark.csv | Rscript compare.R

See https://github.com/nodejs/node/blob/master/doc/guides/writing-and-running-benchmarks.md#comparing-nodejs-versions for more information.

@Trott
Copy link
Member

Trott commented Mar 29, 2018

(Edited above to include correct command...)

Copy link
Member

@addaleax addaleax left a comment

Choose a reason for hiding this comment

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

@@ -90,6 +90,12 @@ of the Node.js application.

Returns a reference to the `Timeout`.

### timeout[Symbol.toPrimitive]()

When coercing a `Timeout` to a primitive, a primitive will be generated that can be used with the appropriate function that can be used to clear the `Timeout`. This allows enhanced compatibility with browser `setTimeout`, and `setInterval` implementations.
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Please wrap at 80 characters :)

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

lib/timers.js Outdated
const clearInterval = exports.clearInterval = function(timer) {
if (typeof timer === 'number' || typeof timer === 'string') {
if (timer in KNOWN_TIMERS) {
clearInterval(KNOWN_TIMERS[timer]);
Copy link
Member

Choose a reason for hiding this comment

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

Would timer = KNOWN_TIMERS[timer] work? This would avoid a recursion.

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

@@ -90,6 +90,12 @@ of the Node.js application.

Returns a reference to the `Timeout`.

### timeout[Symbol.toPrimitive]()

When coercing a `Timeout` to a primitive, a primitive will be generated that can be used with the appropriate function that can be used to clear the `Timeout`. This allows enhanced compatibility with browser `setTimeout`, and `setInterval` implementations.
Copy link
Member

Choose a reason for hiding this comment

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

Wrap at 80 cols.

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed


When coercing a `Timeout` to a primitive, a primitive will be generated that can be used with the appropriate function that can be used to clear the `Timeout`. This allows enhanced compatibility with browser `setTimeout`, and `setInterval` implementations.

Returns a `number`.
Copy link
Member

Choose a reason for hiding this comment

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

Put

* Returns: {integer}

right below the heading.

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

lib/timers.js Outdated
@@ -145,6 +145,8 @@ const kRefed = Symbol('refed');
const refedLists = Object.create(null);
const unrefedLists = Object.create(null);

const KNOWN_TIMERS = Object.create(null);

Copy link
Member

Choose a reason for hiding this comment

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

Extraneous line.

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

@@ -521,6 +535,11 @@ function unrefdHandle(timer, now) {
return true;
}

Timeout.prototype[Symbol.toPrimitive] = function() {
Copy link
Member

Choose a reason for hiding this comment

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

Why @@toPrimitive rather than valueOf?

Copy link
Member Author

Choose a reason for hiding this comment

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

see https://codepen.io/bradleymeck/pen/eMMpdR?editors=0012 , .valueOf is not safe for using as a key. toPrimitive handles both string and number coercion.

@addaleax
Copy link
Member

There's one significant drop showing in the benchmark CI:

                                          confidence improvement accuracy (*)    (**)   (***)
[...]
timers/timers-cancel-pooled.js millions=5        ***    -48.32 %      ±11.22% ±15.03% ±19.77%

I guess we have to make a call deciding whether we're okay with that?

Copy link
Contributor

@mscdex mscdex left a comment

Choose a reason for hiding this comment

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

-1 if we're going to see a performance regression like that

@bmeck
Copy link
Member Author

bmeck commented Mar 30, 2018

It is probably from the uncondition delete in the cancel. We could replace timer.close when toPrimitive is called and make that more conditional at the cost of some odd things possibly showing up if people replace timer.close with a custom function and don't expect us to wrap it. I'll munge with this a bit and see if we can fix up that cliff. Not sure why my test wasn't showing it.

lib/timers.js Outdated
delete KNOWN_TIMERS[this[async_id_symbol]];
this.close = $close;
this.close();
}
Copy link
Member

Choose a reason for hiding this comment

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

This won't pass linting due to missing semicolon. Also, can the function be factored out?

Copy link
Member Author

@bmeck bmeck Mar 30, 2018

Choose a reason for hiding this comment

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

fixed / it cannot be factored out since it closes over $close.

Copy link
Member

Choose a reason for hiding this comment

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

$close is effectively always equal to Timeout.prototype.close, right?

Copy link
Member Author

@bmeck bmeck Mar 30, 2018

Choose a reason for hiding this comment

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

since it is public and mutable, that is not certain.

Copy link
Member

Choose a reason for hiding this comment

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

@bmeck I think it’s fine to just ignore that. You could still run into it anyway, when .close is changed after the [@@toPrimitive]() call…

Copy link
Member Author

Choose a reason for hiding this comment

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

if they replace it they are probably wrapping it, I'm already ignoring all the ways to avoid getter/setters but don't feel comfortable with completely removing .close this seems like one of the metrics people could be profiling handle lifetimes.

lib/timers.js Outdated
@@ -496,7 +504,15 @@ exports.setInterval = function(callback, repeat, arg1, arg2, arg3) {
return timeout;
};

exports.clearInterval = function(timer) {
const clearInterval = exports.clearInterval = function(timer) {
Copy link
Member

Choose a reason for hiding this comment

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

Why the addition of clearInterval variable? Doesn't seem used anywhere.

Copy link
Member Author

Choose a reason for hiding this comment

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

was added in e32b969 as a bug fix to previous iteration

lib/timers.js Outdated
if (timer in KNOWN_TIMERS) {
timer = KNOWN_TIMERS[timer];
}
else {
Copy link
Member

Choose a reason for hiding this comment

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

This fails linting.

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

@bmeck
Copy link
Member Author

bmeck commented Apr 2, 2018

@mscdex with the most change can you recheck your perf concerns?

@mscdex
Copy link
Contributor

mscdex commented Apr 3, 2018

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.

So - this does seem more useful than I thought. (With a request, like a session, you might need to store it in e.g. Redis.)

I'm going to guess the perf drop is due to some polymorphism? Maybe we should run turbolizer against it. I'm not yet going to unblock this yet but I wonder if the perf hit is really that much overall.

We should stress HTTP and see if cancel ticks go up significantly, I suppose.


assert.strictEqual(Number.isNaN(+timeout1), false);
assert.strictEqual(Number.isNaN(+timeout2), false);

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you check that +timeout1 === timeout1[Symbol.toPrimitive]()?

@Fishrock123
Copy link
Contributor

@bmeck I have a suggestion to make, if I may:

We can still get the functionality here while delaying perf concerns in the following way:

  • Keep the toPrimitive
  • Remove the id detection / polymorphism from existing timers methods
  • Add a getTimerById (or similar) to get a timer object by the primitive id

This has the following advantages:

If you're still interested, I would gladly review that kind of update. If not, mind if I take the existing commits and adjust?

@bmeck
Copy link
Member Author

bmeck commented Apr 25, 2018 via email

@apapirovski
Copy link
Member

@bmeck if you still want to pursue this, instead of doing the more expensive in check — you could just set a flag when a timer is converted to a number. In addition, the check within clearTimeout could be reversed. First check whether timer is true & has _onTimeout, then do the typeof check (get the timer, etc.) and call clearTimeout again with the result.

@Fishrock123
Copy link
Contributor

I'd love to see how @apapirovski's suggestions work out.

@apapirovski
Copy link
Member

@bmeck Do you want to continue on this or can I take over basing on top of your commit (with credit preserved)? I like the idea but I think I've got some solid ideas on how to make it faster, as expressed above.

@bmeck
Copy link
Member Author

bmeck commented Jun 1, 2018

@apapirovski please take it over, your work is quite exciting :)

@apapirovski
Copy link
Member

@bmeck thanks! I'll pull in your commit and build on top of it. 👍 I'll preserve the credit when/if I open a PR, depending on how the perf works out.

@apapirovski
Copy link
Member

I'll close this out since there's a more up-to-date / rebased version. Hopefully we can sort out the remaining issues to land it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
semver-minor PRs that contain new features and should be released in the next minor version. 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.

8 participants