JavaScript Timer Congestion
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
Using Timers Effectively by Neil Thomas
High Performance JavaScript by Nicholas C. Zakas
How JavaScript Timers Work by John Resig
Understanding JavaScript Timers by Angus Croll
setTimeout Patterns in JavaScript by Yours Truly