In response to a comment, I’ve added two new features to Asynchronizer: for...in loops, and “clumps.” I also added some safeguards against two certain types of accidental misuse. The download URL hasn’t changed.
Modeling for...in
For this part, I’ll assume that you know what a for...in loop is, and that you’re familiar with Asynchronizer. Here’s a for...in loop, followed by its equivalent using Asynchronizer’s for_in_() method:
var a = new Asynchronizer(),
colors = {
'red': '#FF0000',
'green': '#00FF00',
'blue': '#0000FF'
},
name;
// synchronous
for (name in colors) {
writeln(name + ': ' + colors[name]);
}
writeln('done');
// asynchronous
a.for_in_(
colors,
function($, name) {
writeln(name + ': ' + colors[name]);
return $.continue_();
},
function() {
writeln('done');
}
);
It requires three arguments:
obj- The object over which to iterate.
statements-
This function contains the body of the loop, and is called once for every property of
obj. callback- This function is called when the loop is complete.
The statements function receives two arguments. The first is the same type of argument passed to the other loops’ statements functions, with break_ and continue_ methods. The second contains the name of the current property of obj.
Note: because it’s a function parameter, the variable name in the example above has different scope inside the asynchronous loop. To use the name variable that already exists outside the loop, you could do something like this instead:
a.for_in_(
colors,
function($, $$) {
name = $$;
write(name + ': ' + colors[name]);
return $.continue_();
},
function() {
write('done');
}
);
In addition to these three required arguments, for_in_ can take an optional fourth argument, which brings us to…
Clumps
A limitation of the first version of Asynchronizer was that each loop iteration was put into the event queue separately. For most loops, a single iteration completes in a very small amount of time, and so there was a lot of overhead in doing each one as a separate call to setTimeout.
This produced a Catch-22: any loop that could benefit from Asynchronizer would be rendered prohibitively slow in Asynchronizer. So, I added the ability to “clump” several iterations together into one dispatch. Let’s make our good old exclamation mark application even better by clumping it:
(Note: I’m only going to demonstrate clumping using a for_ loop, but it works the same way in while_ and for_in_ loops too.)
var a = new Asynchronizer();
var i;
a.for_(
function(){i = 0;},
function(){return i < 100000;},
function(){i++;},
function($){
write('! ');
return $.continue_();
},
function(){
write('done!');
},
{clump: 100}
);
Two things have changed. You might have noticed the first change in the for_in_ example above: Instead of just calling $.continue_(), we’re returning its result. This same change goes for $.break_(). If you’re interested in why, check out the source code for Asynchronizer. I’ve added a lot of comments in this version.
I recommend that you use return in non-clumped loops as well (though it is not technically necessary), for the sake of consistency.
The second difference is that for_ takes an optional sixth argument: an object with a clump property, whose value indicates how many iterations of the loop to perform at a time. Its default value is zero, which specifies that the loop not be clumped, and that each iteration be sent off as a separate event. This way, far fewer timeouts are used, and the operation completes in much less time.
In the example above, I used a clump size that divides the size of the loop evenly (100000 ÷ 100 = 1000, with no remainder), but that isn’t a requirement. If it doesn’t divide evenly, that just means that the final clump with be the size of the remainder. If I used a clump size of 138, for example, then the first 724 clumps would each perform 138 iterations, and the last clump would perform 88 iterations.
Dynamic Clumping
The problem with the above is picking a clump size. If it’s too small, the operation takes longer than it needs to, but if it’s too big, the browser feels unresponsive. One loop might do well with one size clump, and another loop that does something else might need a different sized clump. Worse, two computers or browsers might take different amounts of time to execute the same number of iterations of the same loop.
For that reason, you can let Asynchronizer dynamically adjust the clump size for the best balance between speed and responsiveness, by using the string 'dynamic' as the value of the clump property.
{clump: 'dynamic'}
If you find that you don’t agree with Asynchronizer’s idea of the proper balance, you can adjust it. Dynamic clumping works by measuring the time spent on the previous clump, and adjusting the number of iterations per clump, so that the next clump can run for a particular amount of time.
The default amount of time per dynamic clump is 50 milliseconds, which is simply the number that I found to work the best in all the browsers I tested in, on moderately new hardware. You can override that value when you construct the Asynchronizer, by passing it an object that has a dynamicClumpDuration property:
var a = new Asynchronizer({dynamicClumpDuration: 30});
Incidentally, another property you can specify here is timeoutDelay (regardless of whether you’re using clumps), which is the number of milliseconds to delay each timeout. The default is zero. Setting a long delay, such as 1000, can be helpful for debugging.
Caveat Clumptor
Synchronous loops can’t contain asynchronous loops, because the outside synchronous loop won’t wait for the inside asynchronous one to finish. Since clumped loops are hybrids—simultaneously asynchronous and synchronous—this compounds the constraints on how you can use them.
A clumped loop:
- can be placed inside an asynchronous loop,
- can contain a synchronous loop,
- cannot be placed inside a synchronous loop,
- cannot contain an asynchronous loop, and therefore,
- cannot contain another clumped loop.
Asynchronizer will throw an exception if you place a clumped loop in either of the last two contexts listed above (as long as the loops both use the same Asynchronizer), but it has no way of knowing about outer synchronous loops.
2 Responses
That limitation on nesting clumped loops is unfortunate. Is it in your control to make the outer loop stop clumping if there's an inner async loop? That seems like it would simplify cases where my inner loop might sometimes try some clumping, but other times it might not. I get the feeling that this kind of complexity is why the twisted and mochikit libraries use deferreds. Deferreds chain very gracefully between different parts of a program.
Also, what's with the -1? This isn't C :) You can use a string like "dynamic" as the token for selecting the dynamic algorithm. It'll be equally hard to learn, but easier to remember, and much easier to read. Consider the day you invent a new auto-clump-size algorithm-- I hope you weren't going to call it "-2".
The nesting of clumps is definitely something I'll investigate further. Unless I'm overlooking something, it should be possible to implement the next time I get to work on this.
I did debate the -1 thing, but I guess you're right about the memorability. And with this change, I can offer instant gratification.