Regular timers in JavaScript (setInteval
& setTimeout
) have two problems:
They aren't as much accuracy as you can get in JS. If you set it like this:
setInterval(fn, 1000);
You can't be sure that it will be called every 1000 ms. It'll be called 1000 + 1-10ms or even more if some heavy tasks were before the tick.
This is normal behaviour. The bad thing about it is that these delays will accumulate.
Throttling when tab is inactive. When you switch tabs for a while, timers that are in a invisible tab will tick at a higher frequency because of CPU saving. It's a reasonable thing, but killer for multiplayer gaming.
Both problems can be solved by webworker and some variables. That's why I've built a class for it and need a review.
My class also provide anti-blocking mechanism. When there was heavy tasks before timer tick and we need to deal with lag - timer will make up all lost ticks but it can be "forever" loop. To prevent this you can set the max ms for "making lost ticks" and after them will be a break for other code/events/etc and back to work.
module.exports = UberTimer = function UberTimer(callback, interval, blockThreadMaxMs) {
this.callback = callback;
this.interval = interval;
this.lastTickTime = -1;
this.blocking = {};
this.blocking.maxTime = blockThreadMaxMs;
this.blocking.startTime = -1;
}
////////////////////////////////////////////////////////////////////////
UberTimer.worker = null;
UberTimer.instances = [];
UberTimer.__startWorker = function() {
if ( UberTimer.worker ) return;
var blobURL = URL.createObjectURL( new Blob([ '(', (
function() {
setInterval(() => self.postMessage(0), 1);
}
).toString(), ')()' ], { type: 'application/javascript' } ) );
UberTimer.worker = new Worker(blobURL);
UberTimer.worker.onmessage = UberTimer.__tick;
}
UberTimer.__stopWorker = function() {
UberTimer.worker.terminate();
UberTimer.worker = null;
}
UberTimer.__tick = function(timer) {
for ( var timer of UberTimer.instances )
timer.__workerTick();
}
UberTimer.create = function(callback, interval, blockThreadMaxMs=100) {
var timer = new UberTimer(callback, interval, blockThreadMaxMs);
UberTimer.instances.push(timer);
UberTimer.__startWorker();
return timer;
}
UberTimer.release = function(timer) {
var i = UberTimer.instances.indexOf(timer)
if ( i ) UberTimer.instances.splice(i, 1);
if ( UberTimer.instances.length == 0 )
UberTimer.__stopWorker();
}
////////////////////////////////////////////////////////////////////////
UberTimer.prototype.__makeCall = function() {
this.callback();
this.lastTickTime += this.interval;
}
UberTimer.prototype.__workerTick = function() {
if ( this.lastTickTime == -1 ) return;
var now = performance.now();
if ( this.blocking.startTime == -1 )
this.blocking.startTime = now;
var elapsedTime = now - this.lastTickTime;
if ( elapsedTime >= this.interval ) {
this.__makeCall();
var blockingTime = now - this.blocking.startTime;
if ( blockingTime < this.blocking.maxTime )
this.__workerTick();
else
this.blocking.startTime = -1;
}
}
UberTimer.prototype.start = function() {
this.__makeCall();
this.lastTickTime = performance.now();
}
UberTimer.prototype.stop = function() {
this.lastTickTime = -1;
}
Usage:
var timer = UberTimer.create(callback, intevalInMs, optional: maxBlockingTime);
timer.start();
timer.stop();
UberTimer.release(timer);