Gmail for Mobile HTML5 Series: Using Timers Effectively
On April 7th, Google
launched a new version of Gmail for mobile for iPhone and Android-powered devices. We shared
the behind-the-scenes story through this blog and decided to share more of what we've learned in a brief series
of follow-up blog posts. This week, I'll talk about two kinds of timers Javascript provides
and how you can put them to good use in your own web applications.
Javascript Timer API
Timers provide a way to schedule
a function to run at some later point in time. The two functions used to create timers are:
var id = setTimeout(fn, delay)
creates a timer
which calls the specified function once after the given delay.
var id = setInterval(fn, delay)
is similar, but
the function is called continually until the timer is canceled.
Each type of timer has a corresponding
clear
method (e.g.,
clearTimeout(id)
) that stops a timer while it is running.
There are many great resources
online already that document the nitty-gritty details of this API. That's not our focus here.
Instead, I want to talk about specific ways in which you can put these timers to work for you
in your web application.
Let's Do the Time Warp Again
It's important to keep in mind that just because you ask for a
timer with a certain delay doesn't mean it will fire
precisely that many
milliseconds later. On current browsers, all Javascript code executes within a single thread.
This means all your timers have to contend for execution time not only with each other, but
also with all the other Javascript code in your application. If another block of code is in
the middle of executing when a timer should fire, the timer will be delayed until that block
of code is done. (Help is on the way: HTML5's
Web
Workers will allow web applications to spawn workers that run scripts in
parallel.)
Let's look at a
concrete example of what this means in practice. I added a timer to Gmail for mobile that is
supposed to fire every 200 ms. Each time it fires, it records the time between consecutive
timer ticks. Here are the results after 100 ticks:
The dashed blue line represents the requested timer interval, 200
ms. Notice that the median was almost 50% higher than requested, at 276 ms; the time between
ticks varied from 98 ms to almost 4 seconds, with an average delay of 493 ms.
This highlights the fact that the interval at
which your timer actually fires may differ greatly from the requested delay. In fact, the time
between ticks is typically highly variable. It will fluctuate based on what else is happening
in your application and on the device. Don't rely on your timer being precise!
Buy One Timer, Get One Free?
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?
To answer these questions,
we conducted a few experiments. We injected some extra code into a development build of Gmail
that created a lot of different timers, each of which just performed some simple calculations.
Then we fired up the app on an iPhone 3G and Android G1 (both running the latest version of
their respective firmware) and observed the performance. Note that although we could have just
created a separate test page for this, we chose to inject the code right into our app so we
could see how fast it would be to read and process mail with all those timers running.
Here's what we learned. With
low-frequency timers — timers with a delay of one second or more — we could create many timers
without significantly degrading performance on either device. 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.
This led us to take a mixed approach with the
timers we use in our application. For timers that have a reasonably long delay, we just freely
create new timers wherever they are needed. However, for timers that need to execute many
times each second, we consolidate all of the work into a single global high-frequency timer.
One High-Frequency Timer
to Rule Them All
A
global high-frequency timer strikes a balance between needing several different functions to
execute frequently and application performance. The idea is simple: create a single timer in a
central class that calls as many functions as required. Ours looks something like this:
var
highFrequencyTimerId_ = window.setInterval(globalTimerCallback, 100);
globalTimerCallback = function() {
navigationManager.checkHash();
spinnerManager.spin();
detectWakeFromSleep_();
};
Why did we choose to
hardcode the various function calls in globalTimerCallback()
rather
than implementing a more generic callback registration interface? 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.
To Die, To Sleep; To Sleep, Perchance to Tick
One neat application of a high-frequency timer
is to detect when the device has been woken from sleep. When your application is put to sleep
— either because the device is put into sleep mode or because the user has navigated away from
the browser to another application — time, as perceived by your app, pauses. No timers fire
until the user navigates back to your app and it wakes up. By keeping a close eye on the
actual time that elapses between high-frequency timer ticks, you can detect when the app wakes
from sleep.
First, create a
high-frequency timer, as described above. In your timer, call a function that keeps track of
the time between ticks:
// The time, in ms, that must be "missed" before we
// assume the app was put
to sleep.
var THRESHOLD = 10000;
var lastTick_;
detectWakeFromSleep_ = function() {
var now = new Date().getTime();
var delta = now - this.lastTick_;
if (delta > THRESHOLD) {
//
The app probably just woke up after being asleep.
fetchUpdatedData();
}
lastTick_ = now;
};
Now your users will always have the latest data available when they return to
your app without having to manually perform a refresh!
I hope this helps you make the most of timers in your web application. Stay
tuned for the next post, where we'll discuss some cool tricks to spice up the text areas on
your forms.
By Neil Thomas, Software Engineering Intern, Google
Mobile