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: setInterval interval includes cb duration #14815

Closed
wants to merge 1 commit into from

Conversation

zhangzifa
Copy link
Contributor

@zhangzifa zhangzifa commented Aug 14, 2017

setInterval callback should be scheduled on the interval

Fixes: #7346

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
Affected core subsystem(s)

@nodejs-github-bot nodejs-github-bot added the timers Issues and PRs related to the timers subsystem / setImmediate, setInterval, setTimeout. label Aug 14, 2017
@zhangzifa
Copy link
Contributor Author

@Fishrock123 Could you help view this?
@mitsos1os Please check if this fulfills your requirement.

require('../common');
const assert = require('assert');

const sleep = function(ms) {
Copy link
Member

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

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

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 {

@Trott
Copy link
Member

Trott commented Aug 14, 2017

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

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

@Trott
Copy link
Member

Trott commented Aug 14, 2017

Surprisingly (to me at least), there does not seem to be any benchmarks for setInterval() in the Node.js code base.

@zhangzifa
Copy link
Contributor Author

thanks @Trott for your comments.

@benjamingr
Copy link
Member

@Trott I recall @mscdex did some interesting work on timers a while back and had benchmarks.

Copy link
Member

@benjamingr benjamingr left a 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)

@bnoordhuis
Copy link
Member

How does this behave with an interval of n ms and a callback that takes > n ms?

@zhangzifa
Copy link
Contributor Author

@bnoordhuis
When callback takes > n ms, the real interval is the duration of callback.


let cntr = 0;
let first, second;
setInterval(function() {
Copy link
Member

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

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

Copy link
Member

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

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

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

@Fishrock123
Copy link
Member

Fishrock123 commented Aug 14, 2017

When callback takes > n ms, the real interval is the duration of callback.

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.

@Fishrock123 Fishrock123 self-assigned this Aug 14, 2017
let cntr = 0;
let first, second;
setInterval(function() {
sleep(100);

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();

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

@zhangzifa
Copy link
Contributor Author

@Fishrock123 & @bnoordhuis

When callback takes > n ms, the real interval is the duration of callback.

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

Your concern is important, I updated the code a bit to fix it.

@andrasq
Copy link

andrasq commented Aug 15, 2017

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

// invoke handler() on a preset scheduling interval
funcion setExactInterval( handler, intervalMs ) {
  var running = false;
  function runHandler( ) {
    running = true;
    try {
      handler();
      running = false;
    } catch (err) {
      running = false;
      throw err;
    }
  }
  return setInterval(function(){ if (!running) setImmediate(runHandler) }, intervalMs);
}

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.

Copy link
Member

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

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

Copy link
Contributor Author

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.

@zhangzifa
Copy link
Contributor Author

hello @andrasq

Is this breaking? It changes the expected behavior of interval timers, and how they should be reasoned about.

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 setInterval behavior: Schedules a timeout to run handler every timeout milliseconds. Any arguments are passed straight through to the handler.

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

@Fishrock123 Fishrock123 Aug 15, 2017

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

Copy link
Contributor Author

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.

@Fishrock123
Copy link
Member

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.

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.

@BridgeAR
Copy link
Member

BridgeAR commented Sep 3, 2017

Ping @Fishrock123

@BridgeAR
Copy link
Member

@Fishrock123 PTAL

@BridgeAR
Copy link
Member

@nodejs/tsc PTAL

@mscdex
Copy link
Contributor

mscdex commented Sep 19, 2017

Has anyone checked with how browsers handle this? We should align with how they do it.

@BridgeAR BridgeAR dismissed Fishrock123’s stale review September 22, 2017 21:20

Dismissing the review due to no response. PTAL

@Fishrock123
Copy link
Member

Fishrock123 commented Nov 23, 2017

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 Date.now() with performance.now().)

@addaleax addaleax removed the author ready PRs that have at least one approval, no pending requests for changes, and a CI started. label Nov 28, 2017
@targos
Copy link
Member

targos commented Nov 29, 2017

+1 on the change. We should check CITGM before merging.

@targos
Copy link
Member

targos commented Nov 29, 2017

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

@mhdawson
Copy link
Member

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.

Copy link
Member

@mhdawson mhdawson left a comment

Choose a reason for hiding this comment

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

LGTM

@apapirovski
Copy link
Member

Behavior in this PR sounds right and since it is closer to browser behavior it is also good from a compatibility front.

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.

@apapirovski
Copy link
Member

apapirovski commented Nov 29, 2017

Relevant issue on WHATWG: whatwg/html#3151

To sum up: it seems likely that the spec is changed to follow the current browser behaviour.

@apapirovski
Copy link
Member

const msecs = item._idleTimeout;
if (msecs < 0 || msecs === undefined) return;

item._idleStart = TimerWrap.now();
if (typeof start === 'number') {
Copy link
Member

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?

Copy link
Member

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.

@Trott
Copy link
Member

Trott commented Dec 1, 2017

Should the tsc-agenda flag be removed? @mhdawson

@mhdawson mhdawson removed the tsc-agenda Issues and PRs to discuss during the meetings of the TSC. label Dec 1, 2017
@mhdawson
Copy link
Member

mhdawson commented Dec 1, 2017

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.

@BridgeAR
Copy link
Member

BridgeAR commented Dec 6, 2017

@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
@Trott
Copy link
Member

Trott commented Dec 15, 2017

@BridgeAR @zhangzifa I rebased and force pushed. The one merge conflict was simple to resolve. PTAL.

CI: https://ci.nodejs.org/job/node-test-pull-request/12136/

if (!args)
timer._onTimeout();
else
Reflect.apply(timer._onTimeout, timer, args);
if (timer._repeat)
rearm(timer);
rearm(timer, start);
Copy link
Member

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

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.

@BridgeAR
Copy link
Member

BridgeAR commented Jan 5, 2018

@apapirovski what do you think about landing this right now and just improve the code afterwards in a separate PR?

@BridgeAR
Copy link
Member

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.

@BridgeAR BridgeAR closed this Jan 12, 2018
BridgeAR pushed a commit to BridgeAR/node that referenced this pull request Jan 12, 2018
setInterval callback should be scheduled on the interval

Fixes: nodejs#7346

PR-URL: nodejs#14815
Fixes: nodejs#7346
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
@apapirovski
Copy link
Member

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/

@jasnell
Copy link
Member

jasnell commented Jan 14, 2018

Ouch. Let's revert if a quick fix cannot be found

@Trott
Copy link
Member

Trott commented Jan 14, 2018

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.

@Trott
Copy link
Member

Trott commented Jan 14, 2018

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

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

setInterval interval includes duration of callback