Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WebKit setInterval and system time change

I have encountered following issue when creating simple task: displaying html clock by using WebKit engine. Additional requirement were to handle system time change and that it should work on Windows. I have used setInterval to achive this but it seems to freeze browser after I change system time backward. For me it looks like WebKit issue. It is easy to reproduce on safari by running this simple code:

<p id="date"></p>
setInterval(SetTime, 1000);
function SetTime() {
document.getElementById('date').textContent=new Date();
}

After that I have made another approach with recursive setTimeout call. Same effect.

(function loop() {
document.getElementById('date').textContent=new Date();
setTimeout(loop, 1000);
})();

Any ideas why is that happening and how to go around this?

like image 805
Marek Grochowski Avatar asked Apr 11 '14 13:04

Marek Grochowski


People also ask

Why is setInterval not accurate?

Why are setTimeout and setInterval not accurate? To answer this question, you need to understand that there is a mechanism called event loop in the JavaScript host environment (browser or Node. js). It is necessary for front-end developers to understand this mechanism.

Does setInterval affect performance?

This is unlikely to make much of a difference though, and as has been mentioned, using setInterval with long intervals (a second is big, 4ms is small) is unlikely to have any major effects.

Does setInterval run immediately?

Method 1: Calling the function once before executing setInterval: The function can simply be invoked once before using the setInterval function. This will execute the function once immediately and then the setInterval() function can be set with the required callback.

What is the minimum time we can give as a parameter to the setInterval method?

To mitigate the potential impact this can have on performance, once intervals are nested beyond five levels deep, the browser will automatically enforce a 4 ms minimum value for the interval. Attempts to specify a value less than 4 ms in deeply-nested calls to setInterval() will be pinned to 4 ms.


1 Answers

This is almost definitely an issue with WebKit.

The Problem

When you use setTimeout, you create a 'timer' with two properties:

  • A timestamp, set to now + the specified delay
  • A callback to fire once once the system time is greater than the timestamp.

You can imagine a naïve implementation of setTimeout looking something like this:

var timers = [];
function setTimeout(callback, delay) {
    var id = timers.length;
    timers[id] = {
        callback: callback,
        timestamp: Date.now() + delay
    }
    return id;
}

This would simply create a timer and add it to a list. Then, on each tick, the JS runtime would check these timers and execute the callbacks for those that have fired:

var now = Date.now();
for(var id in timers) {
    var timer = timers[id];
    if(timer && timer.timestamp < now) {
        callback();
        delete timers[id];
    }
}

Given this implementation, imagine now changing the system time (i.e. Date.now() in the examples above) to a value in the past -- the timer's timestamp will still be set relative to the previous system time (i.e. in the future).

The same issue exists with setInterval, which (assuming sane code in WebKit) will be implemented using setTimeout.

Unfortunately, this means that any invocation of setTimeout or setInterval is going to suffer.

The Solution

As an alternative, you can use the lovely window.requestAnimationFrame method to perform a callback on each tick. I haven't tested this at at all, but it should continue to fire on each tick, regardless of the system time.

As a bonus, each time it fires the callback, you get passed the current timestamp as a parameter. By keeping track of the previous timestamp passed to your callback, you could easily detect backwards system time changes:

var lastTimestamp;
var onNextFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame;

var checkForPast = function(timestamp) {
    if(lastTimestamp && timestamp < lastTimestamp) {
        console.error('System time moved into the past! I should probably handle this');
    }
    lastTimestamp = timestamp;
    onNextFrame(checkForPast);
};

onNextFrame(checkForPast);

Conclusions

This might not be great news for you, but you should probably rewrite your entire application to use requestAnimationFrame anyway - it seems much more suited to your needs:

var onNextFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame;
var dateElem = document.getElementById('date');

var updateDate = function(timestamp) {
    dateElem.textContent = new Date();
    onNextFrame(updateDate);
};
onNextFrame(updateDate);
like image 50
Jim O'Brien Avatar answered Oct 18 '22 02:10

Jim O'Brien