setInterval timing slowly drifts away from staying accurate
Asked Answered
D

6

37

It seems that when I setInterval for 1000ms, it actually fires the function every 1001ms or so. This results in a slow temporal drift the longer its running.

var start;
var f = function() {
    if (!start) start = new Date().getTime();
    var diff = new Date().getTime() - start;
    var drift = diff % 1000;
    $('<li>').text(drift + "ms").appendTo('#results');
};

setInterval(f, 1000);

When run this shows the inaccuracy immediately.

  • 0ms
  • 1ms
  • 2ms
  • 3ms
  • 4ms
  • 5ms
  • 5ms
  • 7ms
  • 8ms
  • 9ms
  • 9ms
  • 10ms

See it for yourself: http://jsfiddle.net/zryNf/

So is there a more accurate way to keep time? or a way to make setInterval behave with more accuracy?

Dickenson answered 17/11, 2011 at 20:8 Comment(9)
You could use a "setTimeout()" approach where your handler explicitly resets its next interval, figuring in the error, but really you can't depend on serious accuracy in a browser.Delapaz
I wonder, whats the usecase here. Interesting observationPosition
The interpreted nature of javascript, plus browser differences are going to make accuracy difficult at this level.Kirkland
You might test to see if a very-long-running function body increases your drift. Does setInterval account for elapsed time in the function before scheduling the next?Dielle
making the the function take longer doesn't seem to change it much: jsfiddle.net/zryNf/7Dickenson
Answering my own comment: yes, setInterval does attempt to account for function run time: jsfiddle.net/zryNf/8Dielle
Related question https://mcmap.net/q/144959/-will-setinterval-driftDineen
Solution to this: https://mcmap.net/q/144958/-is-there-a-more-accurate-way-to-create-a-javascript-timer-than-settimeoutMccarley
are you sure that the problem isn't that your function is taking 1ms to run? Anyway i ran your fiddle, and i did not get your results. I got results that didn't hit exactly on zero, but some of them were actually early.Moretta
D
21

I think I may have figured out a solution. I figured, if you can measure it you can compensate for it, right?

http://jsfiddle.net/zryNf/9/

var start;
var nextAt;

var f = function() {
    if (!start) {
        start = new Date().getTime();
        nextAt = start;
    }
    nextAt += 1000;

    var drift = (new Date().getTime() - start) % 1000;    
    $('<li>').text(drift + "ms").appendTo('#results');

    setTimeout(f, nextAt - new Date().getTime());
};

f();

result varies a bit but here's a recent run:

0ms
7ms
2ms
1ms
1ms
1ms
2ms
1ms
1ms
1ms

So if it gets called 1ms, 2ms or even 10ms later than it should the next call is scheduled to compensate for that. As long as inaccuracy is only per call, but the clock should never lose time, then this should work well.


And now I wrapped this up a global accurateInterval function which is a near drop in replacement for setInterval. https://gist.github.com/1d99b3cd81d610ac7351

Dickenson answered 17/11, 2011 at 20:51 Comment(8)
One warning: daylight savings time. You're either going to get one frame lagging for an hour ("spring forward") or frames firing as fast as possible (negative timeout intervals) for a full hour ("fall back").Dielle
Also, note that new Date().getTime() - start is equivalent to new Date - start.Dielle
Daylight savings time is an interesting edge case for sure. But at least for my needs such conditions are rare enough, and the consequences of that result not severe enough, for this not to really be a concern.Dickenson
@Dielle note: It's a full hour of frame time, not of real time, when it's sprinting forwards.Heisel
i have used the git. it works using coffee format (time, func) but if you try using 'normal' format (func, time) it spazzes out and ticks over at around 100ms instead of 1000ms. still looking for a reason.Arrowworm
the only thing i could do to fix the issue was to either use coffee formatting, or remove the format conversion if statement and switch the vars around. it also refuses to console.debug no matter what i try, rather odd.Arrowworm
I don't think daylight savings time matters here. new Date().getTime() returns ms since the UNIX epoch, which always marches forwards uniformly (except of course for leap seconds...).Faustofaustus
I recommend substituting (new Date()).getTime() with performance.now()! Should get even better accuracy that way, and DST (and other locale-related) issue disappear entirely!Muzzy
P
9

with a bit of googleing, you will see thatsetInterval and settimeout both will not execute the code at the exact specified time you tell it. with setInterval(f,1000); it will wait AT LEAST 1000MS before it executes, it will NOT wait exactly 1000MS. Other processes are also waiting for their turn to use the CPU, which causes delays. If you need an accurate timer that times at 1 second. I would use a shorter interval, like 50MS and compare it to the start time. I wouldnt go under 50MS though because browsers have a minimum interval

here are a few references:

"In order to understand how the timers work internally there's one important concept that needs to be explored: timer delay is not guaranteed. Since all JavaScript in a browser executes on a single thread asynchronous events (such as mouse clicks and timers) are only run when there's been an opening in the execution. This is best demonstrated with a diagram, like in the following:" taken from: http://css.dzone.com/news/how-javascript-timers-work

"Chrome and Chromium provide an interval that averages just over 41 milliseconds, enough of a difference for the second clock to be visibly slower in well under a minute. Safari comes in at just under 41ms, performing better than Chrome, but still not great. I took these readings under Windows XP, but Chrome actually performed worse under Windows 7 where the interval averaged around 46ms." taken from: http://www.goat1000.com/2011/03/23/how-accurate-is-window.setinterval.html

Pareu answered 17/11, 2011 at 20:26 Comment(4)
50ms is 20 fps; Chrome caps callbacks at 200fps, or 5ms; Firefox and IE will run at least as fast as 250fps (I'm not sure if they're limited at all).Dielle
Interesting, but a super high tick rate seems like a brute force solution.Dickenson
instead of using setInterval, try using a setTimeout but the next timeout time would be 1000 + driftPareu
in my research of this topic, setInterval does indeed try to make up lost time by firing early (in firefox22 anyway) but it still ends up driftingArrowworm
L
4

Here's another autocorrecting interval. The interval is set to a shorter time period and then it waits until it's at least a second later to fire. It won't always fire exactly 1000ms later (seems to range from 0-6ms delay), but it autocorrects and won't drift.

EDIT: Updated to use recalling setTimeout instead of setInterval otherwise it may do something odd after 1000 or so iterations.

var start, tick = 0;
var f = function() {
    if (!start) start = new Date().getTime();
    var now = new Date().getTime();
    if (now < start + tick*1000) {
        setTimeout(f, 0);
    } else {
        tick++;
        var diff = now - start;
        var drift = diff % 1000;
        $('<li>').text(drift + "ms").appendTo('#results');
        setTimeout(f, 990);
    }
};

setTimeout(f, 990);

Run demo

Ligialignaloes answered 17/11, 2011 at 21:8 Comment(0)
D
1

I don't see a drift nearly as large as your script is reporting:
http://jsfiddle.net/hqmLg/1/

I'm leaving that script running. Right now (Chrome, Win 7) I see:

240 calls in 240.005s is 0.99979 calls/second

Indeed, I've seen the drift go up to .007s and then down to .003s. I think your measurement technique is flawed.

In Firefox I see it drift even more strongly (+/- 8ms either direction) and then compensate in the next run. Most of the time I'm seeing "1.000000 calls/second" in Firefox.

Dielle answered 17/11, 2011 at 20:40 Comment(5)
I'm running Chrome beta 16.0.912.41 on Lion, and I see a much higher drift: 110 calls in 110.092s is 0.999164 calls/second. Almost a millisecond per call.Dickenson
@Squeegy Good data point; I've edited to clarify that I'm on Windows 7. Looks to me like Chrome drifts (sometimes backwards, mostly forwards) and doesn't attempt to compensate for it, but Firefox does.Dielle
Well at least this proves this is highly variable by JS engine implementation.Dickenson
this script does not count 'total' drift. it, like all the others, don't count negative drift as drift, rather, it make the positive drift seem less. in fact, i am seeing a drift of up to 150ms either way, running win7, ff22, a bunch of tabs and pandora radio.Arrowworm
To clarify, my intervals are firing anywhere from 900ms to 1150ms. firefox seems to try and make up for positive drift by firing early next tick, but it still lags and slowly drifts forwards (losing time)Arrowworm
P
0

You can use this function to keep the calls close to the expected schedule. It uses setTimeout and calculates the next call time based on the elapsed time.

function setApproxInterval(callback, interval) {
  let running = true
  const startTime = Date.now()

  const loop = (nthRun) => {
    const targetTime = nthRun * interval + startTime
    const timeout = targetTime - Date.now()
    setTimeout(() => {
      if (running) {
        callback()
        loop(nthRun + 1)
      }
    }, timeout)
  }

  loop(1)
  return () => running = false
}

function clearApproxInterval(stopInterval) {
  stopInterval()
}

// Example usage
const testStart = Date.now()
const interval = setApproxInterval(() => console.log(`${Date.now() - testStart}ms`), 1000)
setTimeout(() => clearApproxInterval(interval), 10000)
Purapurblind answered 11/9, 2020 at 11:12 Comment(0)
U
0

The innacuracy in setInterval or setTimeout can be easily reproduced by changing tabs on Google Chrome. In order to treat those cases, you might want to considere making a condition for when the user is in another tab.

setTimeout(function() {
    if (!document.hasFocus()) {
        //... do something different, because more than 1 second might have passed
    }
}, 1000);
Underlie answered 16/4, 2021 at 1:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.