Okay, before we begin, let me come clean and admit that the title of this article is a little sensationalist! JavaScript doesn’t really have multi-threading capabilities, and there’s nothing a JavaScript programmer can do to change that. In all browsers—apart from Google Chrome—JavaScript runs in a single execution thread, and that’s just how it is.
However, what we can do is simulate multi-threading, insofar that it gives rise to one of the benefits of a multi-threaded environment: it allows us to run extremely intensive code. This is code which would otherwise freeze-up the browser and generate one of those “unresponsive script” warnings in Firefox.
Time Waits for No One
It all hinges on the use of asynchronous timers. When we run repetitive code inside an asynchronous timer, we’re giving the browser’s script interpreter time to process each iteration.
Effectively, a piece of code inside a for
iterator is asking the interpreter to do everything straight away: “run this code n times as fast as possible.” However the same code inside an asynchronous timer is breaking the code up into small, discreet chunks; that is, “run this code once as fast possible,”—then wait—then “run this code once as fast as possible”, and so on, n times.
The trick is that the code inside each iteration is small and simple enough for the interpreter to process it completely within the speed of the timer, be it 100 or 5,000 milliseconds. If that requirement is met, then it doesn’t matter how intense the overall code is, because we’re not asking for it to be run all at once.
How Intense is “Too Intense”?
Normally, if I were writing a script that proved to be too intensive, I would look at re-engineering it; such a significant slowdown usually indicates a problem with the code, or a deeper problem with the design of an application.
But sometimes it doesn’t. Sometimes there’s simply no way to avoid the intensity of a particular operation, short of not doing it in JavaScript at all.
That might be the best solution in a given case; perhaps some processing in an application needs to be moved to the server side, where it has more processing power to work with, generally, and a genuinely threaded execution environment (a web server).
But eventually you may find a situation where that’s just not an option—where JavaScript simply must be able to do something, or be damned. That’s the situation I found myself in when developing my Firefox extension, Dust-Me Selectors.
The core of that extension is the ability to test CSS selectors that apply to a page, to see if they’re actually being used. The essence of this is a set of evaluations using the matchAll()
method from Dean Edwards’ base2:
for(var i=0; i
(contentdoc, selectors[i]).length > 0)
{
used ++;
}
else
{
unused ++;
}
}
Straightforward enough, for sure. But matchAll()
itself is pretty intense, having—as it does—to parse and evaluate any CSS1 or CSS2 selector, then walk the entire DOM tree looking for matches; and the extension does that for each individual selector, of which there may be several thousand. That process, on the surface so simple, could be so intensive that the whole browser freezes while it’s happening. And this is what we find.
Locking up the browser is obviously not an option, so if this is to work at all, we must find a way of making it run without error.
A Simple Test Case
Let’s demonstrate the problem with a simple test case involving two levels of iteration; the inner level is deliberately too intensive so we can create the race conditions, while the outer level is fairly short so that it simulates the main code. This is what we have:
function process()
{
var above = 0, below = 0;
for(var i=0; i<200000;> 1)
{
above ++;
}
else
{
below ++;
}
}
}
function test1()
{
var result1 = document.getElementById('result1');
var start = new Date().getTime();
for(var i=0; i<200; value =" 'time=" i="'" value =" 'time=">
We kick off our test, and get our output, from a simple form (this is test code, not production, so forgive me for resorting to using inline event handlers):
Now let’s run that code in Firefox (in this case, Firefox 3 on a 2GHz MacBook) … and as expected, the browser UI freezes while it’s running (making it impossible, for example, to press refresh and abandon the process). After about 90 iterations, Firefox produces an “unresponsive script” warning dialog.
If we allow it to continue, after another 90 iterations Firefox produces the same dialog again.
Safari 3 and Internet Explorer 6 behave similarly in this respect, with a frozen UI and a threshold at which a warning dialog is produced. In Opera there is no such dialog—it just continues to run the code until it’s done—but the browser UI is similarly frozen until the task is complete.
Clearly we can’t run code like that in practice. So let’s re-factor it and use an asynchronous timer for the outer loop:
function test2()
{
var result2 = document.getElementById('result2');
var start = new Date().getTime();
var i = 0, limit = 200, busy = false;
var processor = setInterval(function()
{
if(!busy)
{
busy = true;
result2.value = 'time=' +
(new Date().getTime() - start) + ' [i=' + i + ']';
process();
if(++i == limit)
{
clearInterval(processor);
result2.value = 'time=' +
(new Date().getTime() - start) + ' [done]';
}
busy = false;
}
}, 100);
}
Now let’s run it in again … and this time we receive completely different results. The code takes a while to complete, sure, but it runs successfully all the way to the end, without the UI freezing and without warnings about excessively slow scripting.
(The busy
flag is used to prevent timer instances from colliding. If we’re already in the middle of a sub-process when the next iteration comes around, we simply just wait for the following iteration, thereby ensuring that only one sub-process is running at a time.)
So you see, although the work we can do on the inner process is still minimal, the number of times we can run that process is now unlimited: we can run the outer loop basically forever, and the browser will never freeze.
That’s much more like it—we can use this in the wild.
You’re Crazy!
I can hear the objectors already. In fact, I could be one myself: why would you do this—what kind of crazy person insists on pushing JavaScript to all these places it was never designed to go? Your code is just too intense. This is the wrong tool for the job. If you have to jump through these kinds of hoops then the design of your application is fundamentally wrong.
I’ve already mentioned one example where I had to find a way for heavy scripting to work; it was either that, or the whole idea had to be abandoned. If you’re not convinced by that answer, then the rest of the article may not appeal to you either.
But if you are—or at least, if you’re open to being convinced, here’s another example that really nails it home: using JavaScript to write games where you can play against the computer.(coming soon)
0 comments
Post a Comment