29
JUN 2012

Posted by Nader at 04:55 PM EDT

7476 reads

Share this

Avoiding JavaScript setTimeout and setInterval Problems


When OnSIP's popularity exploded, much of the software needed work to accommodate the growing number of customers. Nader Zeid, Software Engineer at OnSIP, was tasked with improving the Admin Portal.  Customers with over 100+ users would be faced with unbearable load times when they logged into OnSIP's Admin Portal. The cause of this problem was due to the server being overloaded. To fix this dilemma, Nader began writing some JS code that would speed up OnSIP's website. Instead of the server having all the work to do, it made sense to make the browser chip in and do some work, too. In order to make this colossal JavaScript app run correctly Nader had to overcome many obstacles.


Nader Zeid:

I thought I’d take this blog opportunity to share with other JavaScript developers a technique to establish some determinacy in creating timed events with the setTimeout and setInterval functions. As mentioned in nice detail over here, the time interval argument of each of those functions really only establishes that the given function will execute after at least that amount of time. So a timed event can miss its target by literally any amount of time. In a large JavaScript application with several timed events, this phenomenon can disproportionately delay queued tasks or in the worst case lock the web page.


The problem:

The two images below display two runs of some example code. We print to the console recursively with setTimeout.




Notice that on multiple runs the output changes. Runs of each example function are apparently subject to different delays.



The solution:

We quite simply have to subject them to the same delays. We do this by setting exactly one global timed function and implementing a function queue such that when a function’s given time target is reached, we execute it.


var Scheduler = (function () {
  var tasks = [];
  var minimum = 10;
  var timeoutVar = null;
  var output = {
    add: function (func, context, timer, once) {
      var iTimer = parseInt(timer);
      context = context && typeof context === 'object' ? context : null;
      if(typeof func === 'function' && !isNaN(iTimer) && iTimer > 0) {
        tasks.push([func, context, iTimer, iTimer * minimum, once]);
      }
    },
    remove: function (func, context) {
      for(var i=0, l=tasks.length; i<l; i++) {
        if(tasks[i][0] === func && (tasks[i][1] === context || tasks[i][1] == null)) {
          tasks.splice(i, 1);
          return;
        }
      }
    },
    halt: function () {
      if(timeoutVar) {
        clearInterval(timeoutVar);
      }
    }
  };
  var schedule = function () {
    for(var i=0, l=tasks.length; i<l; i++) {
      if(tasks[i] instanceof Array) {
        tasks[i][3] -= minimum;
        if(tasks[i][3] < 0) {
          tasks[i][3] = tasks[i][2] * minimum;
          tasks[i][0].apply(tasks[i][1]);
          if(tasks[i][4]) {
            tasks.splice(i, 1);
          }
        }
      }
    }
  };
  timeoutVar = setInterval(schedule, minimum);
  return output;
})();

Overview:
Scheduler.add(func, context, timer, once)
  • func
    : the function to be added to the queue and executed.
  • context
    : the context in which the function is executed, as per
    func.apply(context)
    .
  • timer
    : the minimum time delay for execution. This is multiplied with
    minimum
    to give the time in milliseconds.
  • once
    : decides whether the function is executed once or repeatedly.
Scheduler.remove(func, context)
  • func
    : the function to be removed from the queue.
  • context
    : the context matching
    func
    .

Scheduler.halt()
Terminates the scheduler.


In use:
var x = 0;
var example1 = function () {
  console.log('TEST1');
  if(x < 10) {
    Scheduler.add(example1, null, 2, true);
    x++;
  }
}

var example2 = function () {
  console.log('TEST2');
  if(x < 10) {
    Scheduler.add(example2, null, 4, true);
    x++;
  }
}

example1();
example2();


Notes:

  • The crux of the solution is minimizing the number of calls to setTimeout and setInterval. This way the JavaScript engine only needs to maintain the timing of one event, thereby increasing reliability.

  • Since all functions are funneled into the same queue, the order of function execution is guaranteed.

  • We set a minimum time interval for the scheduler and force input to some multiple of that minimum. Using multiples this way maintains that the given time interval is the least amount of time needed to pass for the given function to be executed. Feel free to test other ideas.

  • The code in its current form works in all major browsers.

Be aware that this idea is far from original. Nearly all large-scale JavaScript frameworks implement a similar event loop to handle this ubiquitous problem. The benefit of this solution is that you get to see it and play with it yourself. =)