-
Notifications
You must be signed in to change notification settings - Fork 30.2k
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: setInterval interval includes cb duration #14815
Conversation
@Fishrock123 Could you help view this? |
require('../common'); | ||
const assert = require('assert'); | ||
|
||
const sleep = function(ms) { |
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 linter will want this to be either an arrow function or else a function declaration. So either:
const sleep = (ms) => {
...or...
function sleep(ms) {
while (new Date().getTime() < st + ms); | ||
}; | ||
|
||
var cntr = 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.
Linter will flag this and the next line and want it to be let
instead of var
.
|
||
var cntr = 0; | ||
var first, second; | ||
setInterval(function(){ |
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.
Linter will expect a space before the {
Hi, @zhangzifa! Thanks for doing this! I was looking at that issue just yesterday. Glad someone is working to fix the issue! |
lib/timers.js
Outdated
const msecs = item._idleTimeout; | ||
if (msecs < 0 || msecs === undefined) return; | ||
|
||
item._idleStart = TimerWrap.now(); | ||
if (start) { |
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.
Because we know that start
will always be an integer, this is probably more performant as if (start !== 0)
.
Surprisingly (to me at least), there does not seem to be any benchmarks for |
thanks @Trott for your comments. |
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 approve is on the content of the PR.
I'd like a CITGM run on this one - and a CTC member signing off because it's a potentially breaking change. (Looks like Trott has that covered)
How does this behave with an interval of |
@bnoordhuis |
|
||
let cntr = 0; | ||
let first, second; | ||
setInterval(function() { |
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 could as well use an arrow function here. (Just a personal preference, feel free to ignore.)
} else if (cntr === 2) { | ||
second = new Date(); | ||
assert(Math.abs(second - first - 200) < 10); | ||
process.exit(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.
Is there any advantage of using process.exit(0)
instead of clearInterval()
?
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.
CI: https://ci.nodejs.org/job/node-test-pull-request/9653/
The change seems fine and looks ok as a patch to me.
I'm not so comfortable with the test, it looks like it should be in /pummel/
- perhaps we can override and hook into TimerWrap.now()
or use async-hooks(?) to check invocation order.
lib/timers.js
Outdated
@@ -465,6 +469,7 @@ function ontimeout(timer) { | |||
var callback = timer._onTimeout; | |||
if (typeof callback !== 'function') | |||
return promiseResolve(callback, args[0]); | |||
var start = TimerWrap.now(); |
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.
const
for new values please
I think @bnoordhuis might be asking if it will loop infinitely - that is will the next callback be called in the same timerslist iteration? (I'm uncertain offhand but will try to take a look too.) Edit: assigning myself so I don't forget. |
let cntr = 0; | ||
let first, second; | ||
setInterval(function() { | ||
sleep(100); |
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.
Should we use common.busyLoop
instead of defining another function that does something similar?
if (cntr === 1) { | ||
first = new Date(); | ||
} else if (cntr === 2) { | ||
second = new Date(); |
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.
In general I would suggest not using wall-clock time, and instead use a monotonic clock by e.g calling Timer.now()
.
Your concern is important, I updated the code a bit to fix it. |
Is this breaking? It changes the expected behavior of interval timers, and how they should be reasoned about. The intended behavior is already possible with a setImmediate and a mutex: the interval function can arrange for the handler to be called and return to schedule the next run still on the same tick, with the mutex preventing overlapping runs. Something like (arg passing omitted):
There is merit in considering an interval function that always fires on the exact scheduled time, but then it should always be on multiples of the interval, even the duration exceeds it. I.e., if the handler runs too long, the next scheduled time should be the earliest time point that was part of the original sequence, and not the first time point of a new, re-anchored or adjusted sequence. |
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 test doesn't appear to work locally. New CI: https://ci.nodejs.org/job/node-test-pull-request/9674/
Unfortunately using process.nextTick()
causes something wrong with AsyncHooks in the timerwrap.setInterval
test. I'm not sure why but perhaps something else is not quite right in the timers code with AsyncHooks?
if (duration >= timer._repeat) { | ||
// If callback duration >= timer._repeat, | ||
// add 1 ms to avoid blocking eventloop | ||
insert(timer, false, start + duration - timer._repeat + 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.
I think just wrapping insert()
in process.nextTick()
should be enough. If not, perhaps just adding +1ms will work fine. (Timer granularity is only 1-2ms anyways even on fast machines afaik.)
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.
Wrap insert
in process.nextTick
and fix the test.
hello @andrasq
I don't know if there is any spec/doc describes the expected behavior of interval timers for Node.js. I only found this for html, which tells BTW, this PR does not change the order of different timer with same interval. |
lib/timers.js
Outdated
process.nextTick(insert, timer, false, start + duration - timer._repeat); | ||
} else { | ||
insert(timer, false, start); | ||
} |
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 had meant the entire block here, I don't think the conditional or the +- start is necessary.
In an ideal world, replacing active(timer)
with process.nextTick(insert, timer, false, start)
would be all that were necessary. Unfortunately that causes a slight continuity break and the existing timers iteration code is unaware that a timer will be scheduled in the same slot but only after this iteration, causing the underlying TimerWrap to be re-created (and thus impacting AsyncHooks tests).
Imo, insert(timer, false, start + 1)
is the way to go. (Perhaps with a conditional check still.)
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, the conditional check is needed if callback > interval, otherwise, loop will be blocked.
I think this should maintain that, but with the caveat of an event turn break. We've maintained some consistency about not firing timers during scheduling in the past and also this prevents infinite loops. |
Ping @Fishrock123 |
@Fishrock123 PTAL |
@nodejs/tsc PTAL |
Has anyone checked with how browsers handle this? We should align with how they do it. |
Dismissing the review due to no response. PTAL
FWIW, the following test appears to "pass" in Chrome, FireFox, and Safari. I think that indicates that browsers follow the behavior in this PR? 'use strict';
function busyLoop(time) {
const startTime = Date.now();
const stopTime = startTime + time;
while (Date.now() < stopTime) {}
}
let cntr = 0;
let first, second;
const t = setInterval(() => {
busyLoop(50);
cntr++;
if (cntr === 1) {
first = Date.now();
} else if (cntr === 2) {
second = Date.now();
if (Math.abs(second - first - 100) < 10) {
throw new Error('Assertion failed!');
}
clearInterval(t);
}
}); (Still passes if you replace |
+1 on the change. We should check CITGM before merging. |
The HTML standard specs timers here: https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers I'm not sure I read it correctly, but it seems that browsers should behave like Node is behaving without this change (schedule the next task after execution - step 5.3)? |
We discussed in the TSC meeting this week. Behavior in this PR sounds right and since it is closer to browser behavior it is also good from a compatibility front. Main concern is about breaking existing code. There was an additional question if it should queue if multiple intervals are missed. It would be good to check browser behavior and queue if that is what happens there, but that can be handled in a later PR. The net is that this can go forward provided we get a CITGM run to validate we don't see unexpected breakage of existing modules. |
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
Should we check if there's any movement from the browser side to potentially re-implement this to follow the spec? It would be unfortunate to end up in a situation where we have a number of releases where we mirror the browsers only to then have them adjust it and for Node have to go back to doing it the old way. |
Relevant issue on WHATWG: whatwg/html#3151 To sum up: it seems likely that the spec is changed to follow the current browser behaviour. |
Also, Chrome drift correction code: https://cs.chromium.org/chromium/src/third_party/WebKit/Source/platform/Timer.cpp?rcl=e6d900fb6ed08dbd3a048899f38962ee75f4d8d0&l=162 |
const msecs = item._idleTimeout; | ||
if (msecs < 0 || msecs === undefined) return; | ||
|
||
item._idleStart = TimerWrap.now(); | ||
if (typeof start === 'number') { |
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.
Could this just check for start === undefined
instead of typeof
?
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, that should be sufficient.
Should the |
Removed tsc-agenda, @apapirovski thanks for following up on the spec side. Its good to know that the spec will line up with the direction we are taking. |
@zhangzifa this seems to be finally in a state it could land. It only needs another rebase. |
setInterval callback should be scheduled on the interval Fixes: nodejs#7346
@BridgeAR @zhangzifa I rebased and force pushed. The one merge conflict was simple to resolve. PTAL. |
if (!args) | ||
timer._onTimeout(); | ||
else | ||
Reflect.apply(timer._onTimeout, timer, args); | ||
if (timer._repeat) | ||
rearm(timer); | ||
rearm(timer, start); |
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 is TimerWrap.now()
not called here? It is the only used spot in the function, so it does not have to be called if timer._repeat
is falsy.
const msecs = item._idleTimeout; | ||
if (msecs < 0 || msecs === undefined) return; | ||
|
||
item._idleStart = TimerWrap.now(); | ||
if (typeof start === 'number') { |
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, that should be sufficient.
@apapirovski what do you think about landing this right now and just improve the code afterwards in a separate PR? |
Landed in 1385e1b I went ahead and landed this without any further changes as I think we can work on it again and improve it later on but it would be good to have this already as is. |
setInterval callback should be scheduled on the interval Fixes: nodejs#7346 PR-URL: nodejs#14815 Fixes: nodejs#7346 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Jeremiah Senkpiel <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Ruben Bridgewater <[email protected]> Reviewed-By: Michael Dawson <[email protected]>
This should've had another CI run before landing, OS X is consistently failing on CI now. https://ci.nodejs.org/job/node-test-commit-osx/ |
Ouch. Let's revert if a quick fix cannot be found |
The test has an arbitrary 10ms window of accuracy after an interval runs twice. A possibly-improved approach (but still imperfect) might be to run an interval more than that--perhaps 10 times? or 16 if you like powers of 2?--and make the check a bit more generous. |
(But I'm totally +100 on a revert, whether or not a quick fix can be found. Revert, then re-apply with fix is fine by me.) |
setInterval callback should be scheduled on the interval
Fixes: #7346
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)