34

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?

Alex Wayne
  • 178,991
  • 47
  • 309
  • 337
  • 4
    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. – Pointy Nov 17 '11 at 20:11
  • I wonder, whats the usecase here. Interesting observation – meo Nov 17 '11 at 20:13
  • The interpreted nature of javascript, plus browser differences are going to make accuracy difficult at this level. – Jonathan M Nov 17 '11 at 20:14
  • 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? – Phrogz Nov 17 '11 at 20:16
  • making the the function take longer doesn't seem to change it much: http://jsfiddle.net/zryNf/7/ – Alex Wayne Nov 17 '11 at 20:23
  • Answering my own comment: yes, `setInterval` does attempt to account for function run time: http://jsfiddle.net/zryNf/8/ – Phrogz Nov 17 '11 at 20:24
  • 1
    Related question http://stackoverflow.com/questions/985670/will-setinterval-drift – Daniel Nov 17 '11 at 20:29
  • Solution to this: http://stackoverflow.com/a/9576841/2647345 – Luc Feb 07 '16 at 18:40
  • 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. – John Lord May 25 '21 at 19:03

6 Answers6

18

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

Alex Wayne
  • 178,991
  • 47
  • 309
  • 337
  • 2
    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"). – Phrogz Nov 17 '11 at 21:19
  • Also, note that `new Date().getTime() - start` is equivalent to `new Date - start`. – Phrogz Nov 17 '11 at 21:21
  • 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. – Alex Wayne Nov 17 '11 at 21:49
  • @Phrogz note: It's a full hour of frame time, not of real time, when it's sprinting forwards. – John Dvorak Jan 14 '14 at 07:50
  • 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. – RozzA Mar 05 '14 at 06:05
  • 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. – RozzA Mar 05 '14 at 06:11
  • 1
    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...). – joegoldbeck Feb 14 '17 at 16:47
  • 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! – Gershom Maes May 25 '21 at 17:55
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

Johnny Craig
  • 4,974
  • 2
  • 26
  • 27
  • 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). – Phrogz Nov 17 '11 at 20:29
  • Interesting, but a super high tick rate seems like a brute force solution. – Alex Wayne Nov 17 '11 at 20:41
  • instead of using setInterval, try using a setTimeout but the next timeout time would be `1000 + drift` – Johnny Craig Nov 17 '11 at 20:46
  • 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 drifting – RozzA Mar 05 '14 at 05:31
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

mVChr
  • 49,587
  • 11
  • 107
  • 104
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.

Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • 3
    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. – Alex Wayne Nov 17 '11 at 20:43
  • @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. – Phrogz Nov 17 '11 at 20:45
  • Well at least this proves this is highly variable by JS engine implementation. – Alex Wayne Nov 17 '11 at 20:56
  • 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. – RozzA Mar 05 '14 at 05:12
  • 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) – RozzA Mar 05 '14 at 05:30
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)
A_A
  • 1,832
  • 2
  • 11
  • 17
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);
Edhowler
  • 715
  • 8
  • 17