Quick Summary of how JavaScript Timers Work

There are two functions that browsers expose to the JavaScript environment for asynchronously executing functions after a a specified amount of time in milliseconds: window.setTimeout and window.setInterval. Timeouts occur once, intervals repeat. However, JavaScript is single-threaded (with the exception of Web Workers) and so if a timer is set, but then the process blocks, the callback can be delayed or even fail to ever be fired. For example, the "hello" message will never be logged below.

window.setTimeout(function () {
    console.log("hello");
}, 1000);
while (true) ;

The thread within which the JavaScript interpreter runs is also shared by the browser's UI and rendering systems. This means that not only will the above message never be logged, but that the browser will be unresponsive and appear to be frozen to the user.

For more in depth discussions about the details of how JavaScript timers work, see the references.

Timer Congestion

It also follows from the single threaded nature of JavaScript that only one timeout can fire its callback at any given moment in time. If another piece of JavaScript is already executing when a timer is scheduled to run, the timer's callback is queued to run when the interpreter is done with the code it is currently executing.

It is possible to set so many timers expiring at about the same time that they just keep queuing up one after another; never giving the browser's UI a moment to update. I am going to refer to this as "timer congestion". I first read about this phenomena in Nicholas C. Zakas' book High Performance JavaScript (HPJS from now on). It was mentioned only in passing, however I found the idea fascinating. HPJS refers to research conducted by Neil Thomas while he was working on the mobile version of GMail.

Neil shared his research and the process that led him to do that research in his article Using Timers Effectively.

When I first started working on the new version of Gmail for mobile, the application used only a couple of timers. As we continued adding more and more features, the number of timers grew. We were curious about the performance implications: would 10 concurrent timers make the app feel slow? How about 100? How would the performance of many low-frequency timers compare to a single high-frequency timer?

He summarized his results as follows.

With low-frequency timers - timers with a delay of one second or more - we could create many timers without significantly degrading performance on either [an Android G1 or iPhone 3G]. Even with 100 timers scheduled, our app was not noticeably less responsive. With high-frequency timers, however, the story was exactly the opposite. A few timers firing every 100-200 ms was sufficient to make our UI feel sluggish.

Thomas goes on to explain why they decided to hardcode their callback functions inline, rather than create a registration-based system:

Keep in mind that this code is going to execute many times every second. Looping over an array of registered callbacks might be slightly "cleaner" code, but it's critical that this function execute as quickly as possible. Hardcoding the function calls also makes it really easy to keep track of all the work that is being done within the timer.

However, by sacrificing the accuracy of when timers fire, it is possible to attain the clean architecture Thomas refers to and also guarantee that many timers will not cause the page to feel sluggish.

First, restrict yourself to a single recursive timeout loop which you can register tasks with. Use window.setTimeout instead of window.setInterval to ensure that the browser has enough time to update between every single iteration. This is necessary because window.setInterval(callback, ms) tries to fire its callback every n milliseconds rather than providing n milliseconds between each time it fires. Thus, if your callback takes longer than n milliseconds to execute, it will be repeatedly executed back to back, never allowing the browser a few milliseconds to update the UI.

Second, while looping through the tasks registered with the timeout loop, keep track of how long the loop has been running. Before the loop has been running long enough to negatively affect the user's experience, yield to the browser with window.setTimeout and then pick up the loop where you left off after the browser has had a chance to update the UI.

By doing these two things, when you have a large number of tasks firing their callbacks concurrently they will not make the browser feel sluggish like many timers expiring at the same time would. Instead, the tasks will only be pushed back and executed at a later time than they had been scheduled for to ensure that the browser can respond to user interaction. In extreme cases of congestion, it would be possible for a task pushed back for so long that it will effectively never be called. Even in this worst case scenario, it is better than the alternative: an unresponsive browser.

Introducing Chronos

Chronos is an implementation of the techniques described above which also mirrors the HTML 5 WindowTimers interface.

interface WindowTimers {
  long setTimeout(in any handler, in optional any timeout, in any... args);
  void clearTimeout(in long handle);
  long setInterval(in any handler, in optional any timeout, in any... args);
  void clearInterval(in long handle);
};

This means that to switch existing code from using window.setTimout and friends, all you need to do is include Chronos on the page and replace window.setTimeout with chronos.setTimeout, etc to take advantage of what Chronos has to offer.

Putting Chronos to the Test

I have put together a test page for Chronos where you can define how many concurrent timers to execute on the page, how long each one should block for, and whether to use Chronos, or the native timer functions. I have tested the page on my MacBook Pro (Firefox 3.6 and 4 beta, Safari 5, and Chrome 9), as well as on my Motorolla Droid 2 running Android 2.2. In all cases, the browser's UI updated more smoothly when using Chronos than without whenever there was enough of a workload for any type of sluggishness to occur.

References