2

If you create a very simple program that has a setInterval with 1 second delay, and you log the times its function is called, you will notice that the interval 'drifts'.

Basically, it actually takes (1,000ms + some amount of time) between each call.

For this program, it actually takes ~1,005ms between each call.

What causes the drift?

Is it taking 5ms to requeue setInterval?

Is it the length of the time it takes to run the function? (I doubt this, but having trouble concluding.)

Why does setInterval behave this way, and not just base itself on some clock time? (e.g. if you have 1,000ms delay and you started at time 3... just check if 1,003 then 2,003 and so on has elapsed?)

Example:

const startTime = new Date().valueOf();

function printElapsedTime(startTime) {
  console.log(new Date().valueOf() - startTime);
}

let intervalObj = setInterval(printElapsedTime, 1000, startTime);

Output: 1005 2010 3015 4020

So you are not sync'd to 1 second anymore. Since it drifts by about 5, after 100 runs it will be running a half second 'later' than expected.

This question discusses how to avoid this drift, but does not explain WHY this drift is happening. (As in it does not say that setInterval is recursively adding itself to the event queue after each call - which takes 3ms ... which is just a guess at the drift cause).

Don P
  • 60,113
  • 114
  • 300
  • 432
  • The linked answer does not explain why the drift exists. It simply gives fixes if you want to avoid the drift. – Don P Sep 03 '21 at 17:53
  • 7
    The spec only says the following: *"This API does not guarantee that timers will fire exactly on schedule. Delays due to CPU load, other tasks, etc, are to be expected."* https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers – Felix Kling Sep 03 '21 at 17:53
  • Maybe someone familiar with Chrome or Node's implementation can answer – Don P Sep 03 '21 at 17:55
  • 3
    There is more: Step 17 allows the algorithm to wait an arbitrary amount of time before queuing the callback (if I understand that right) *"This is intended to allow user agents to pad timeouts as needed to optimize the power usage of the device. For example, some processors have a low-power mode where the granularity of timers is reduced; on such platforms, user agents can slow timers down to fit this schedule instead of requiring the processor to use the more accurate mode with its associated higher power usage."* And if memory serves right, this happens in browser tabs that are not focused. – Felix Kling Sep 03 '21 at 18:00
  • There is nothing to answer: timeouts are only guaranteed to take _at least_ the amount of time you specify. If they take longer, that's spec-compliant. Which you then also need to pair with the fact that all modern browsers apply interval throttling, so you get the first few "as requested", after that the minimum interval becomes 4ms in the foreground and a whopping "many seconds" in the background. – Mike 'Pomax' Kamermans Sep 03 '21 at 18:01
  • Your intervals and timeouts will resolve after a minimum of the specified time but that depends on what else that thread is doing. It won't break some other execution in order to run your callback. Also, logging isn't free. – canon Sep 03 '21 at 18:03
  • @FelixKling - ah that's interesting – Don P Sep 03 '21 at 18:03
  • Thanks for the comments all - just to clarify, I understand the event loop, cost of logging, and that the timers are not guaranteed - apologies that my question isn't stating this clearly but I'm wondering *why* this is the case, as basing the call on clock time seems like an obvious solution - but I'm sure the implementers were considering something else (e.g. the power usage Felix mentions above) – Don P Sep 03 '21 at 18:04
  • Also https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers, steps 10 and 11, _(10) If timeout is less than 0, then set timeout to 0. (11) If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4._ so if you're calling a setInterval or setTimeout in code that was itself triggered that way (i.e. almost all timeout-based loop code) that'll bump up the "nesting level" and you hit the timeout throttle almost immediately. Though you won't hit that in this case (you'll just hit background throttling for blurred tabs) – Mike 'Pomax' Kamermans Sep 03 '21 at 18:05
  • Probably worth mentioning that for precision timing it is better to use the [Performance API](https://developer.mozilla.org/en-US/docs/Web/API/Performance) or if you are dealing with sound or animation use the relevant APIs (e.g. [setValueAtTime](https://developer.mozilla.org/en-US/docs/Web/API/AudioParam/setValueAtTime)). – Rúnar Berg Sep 03 '21 at 18:37

2 Answers2

1

While no Javascript running on a standard browser claims to be real-time (as pointed out in several comments) there are steps you an take to make things not get as out of hand as it appears the example in the question does (the errors being cumulative).

Just to start with an experiment I ran this on my Windows 10 Chrome:

const startTime = new Date().valueOf();

function printElapsedTime(startTime) {
  let curTime = new Date().valueOf();
  console.log(curTime - startTime);
}

let intervalObj = setInterval(printElapsedTime, 1000, startTime);
<div id="show">0</div>

This gave fairly consistent error each second and around the minute time you can see there was no cumulative drift:

enter image description here

However, using Firefox on the same system there was cumulative drift and this can be seen as pretty significant by the one minute mark:

enter image description here

So the question is, can anything be done to make it a bit better across browsers?

This snippet ditches setInterval and instead uses setTimeout on each invocation:

const startTime = new Date().valueOf();
let nextExpected = startTime + 1000;

function printElapsedTime(startTime) {
  let curTime = new Date().valueOf();
  console.log(curTime - startTime);
  let nextInterval = 1000 + nextExpected - curTime;
  setTimeout(printElapsedTime, nextInterval, startTime);
  nextExpected = curTime + nextInterval;
}

let intervalObj = setTimeout(printElapsedTime, 1000, startTime);
<div id="show">0</div>

On Firefox this gave:

enter image description here

There was no cumulative drift and the error around the one minute mark was no worse than earlier.

So, in attempt to actually answer the question:

  1. Computers do have other duties to attend to and cannot guarantee to process a timeout function at an exact time (though the spec requires them not to process before the interval has elapsed). In the given code in particular console.log will take time, settingup a new interval (in the final example) takes time, but the laptop/phone etc will also be dealing with lots of other stuff at the same time, housekeeping in the background, listening for interrupts etc etc.

  2. Different browsers seem to treat setInterval differently - the spec doesn't seem to say what if anything they should do about cumulative drift. From the experiments here it seems that Chrome/Edge at least on my Windows10 laptop does some mitigating which means the drift isn't cumulative whereas FF doesn't seem to adjust and the drift can be significant.

It would be interesting to know if others on different systems get equivalent results. Anyway, the basic message is don't rely on such timeouts, it is not a real time system.

A Haworth
  • 30,908
  • 4
  • 11
  • 14
0

Long story short, none of desktop operating systems is real-time os

https://en.m.wikipedia.org/wiki/Real-time_operating_system

Thus, executing a task like calling the callback function is not guaranteed in an exact time. The os does it’s best to juggle all the tasks, take care of power/resource constraints to optimize the performance as a whole. As a result, timings float around a little.

Interestingly, you get a consistent 5 ms shift. I have no explanation for that

Eriks Klotins
  • 4,042
  • 1
  • 12
  • 26