Asynchronous Processing Unit (APU)

Version 2.1 — 16th November 2011

The Asynchronous Processing Unit (APU) is a fast and highly-controllable abstraction for performing intensive computation in JavaScript, without freezing-up the browser.

Abstract illustration for this script: a stylized Rubik's cube.

It's basically a threading trick using asynchronous timers, but it scores over earlier, less-formal implementations of this idea (including my own), in two significant ways:

  • it performs multiple iterations per timer loop — producing dramatically faster results, compared with a pure implementation that equates a single iteration to a single timer loop.
  • it's highly controllable and its instances can be nested — because the timer loops are explicitly controlled by internal callbacks, rather than being intervals that run unchecked once begun.

The APU abstraction satisfies a need that Web Workers can't — because they have no DOM, no access to the parent document or its environment, and (with the notable exception of XMLHttpRequest) almost no accessible host objects. But APUs are just ordinary code, with the same access to the DOM and environment as any other code, and the same ability to extend or limit their scope as any other object.

The APU works in all current browsers, with no known bugs or incompatibilities.

I've been tinkering with this abstraction for several years, and it's crucial functionality in both versions of Dust-Me Selectors, as well being used in all five versions of CodeBurner. But it's only recently got to a stable and user-friendly point, where it can be released as a standalone abstraction.

Using APUs there is no limit to the depth and complexity of computation you can do — providing it can be broken-down into discreet, iterative steps. If each of those steps alone is not enough to freeze-up the browser, then the number of steps it takes to produce the end result is insignificant — it could be hundreds-of-thousands, or billions, occupying the browser for an indefinite length of time, without ever locking-up its thread!

It's not a perfect solution, of course — you can't have parallel instances, and it can't return — but nonetheless it fills a niche that would otherwise be empty, making it possible to do stuff in JavaScript that would otherwise be unfeasible.

Get the script

Download the zipfile [6K] and unpack it, then use the code as you need it — either including the script file as it is, or pasting the codebase into yours.

The script contains a single function — the APU constructor and its prototypes:

function APU(chunksize, oninstance, oncomplete, onabort)
{
    this.argument = function(value, fallback)
    {
        return (typeof value == 'number' && value > 0 ? parseInt(value, 10) : fallback);
    };
    
    var 
    apu             = this, 
    timer           = null,
    chunk           = 0,
    chunksize       = this.argument(chunksize, 1);
    
    this.i          = 0;
    this.stopped    = false;
    
    this.call = function(fn)
    {
        if(typeof fn == 'function')
        {
            fn.call(this, this.i);
        }
    };

    
    this.docomplete = function()
    {
        this.call(oncomplete);
    };

    this.doinstance = function(speed)
    {
        if(apu.i == 0)
        { 
            this.call(oninstance);
        }
        else if((++ chunk) == chunksize)
        {
            chunk = 0; 
            timer = window.setTimeout(function()
            {
                apu.call(oninstance);
        
            }, apu.argument(speed, 10));
        }
        else
        { 
            this.call(oninstance);
        }
    };

    this.doabort = function()
    {
        window.clearTimeout(timer);
        
        this.call(onabort);
    };
}
APU.prototype =
{
    start : function()
    {
        this.i = 0;
        this.stopped = false;
        
        this.doinstance();
    },
    next : function(increment, speed)
    {
        if(this.stopped) { return; }

        this.i += this.argument(increment, 1);
        this.doinstance(speed);
    },
    complete : function()
    {
        if(this.stopped) { return; }

        this.stopped = true;
        this.docomplete();
    },
    abort : function()
    {
        if(this.stopped) { return; }

        this.stopped = true;
        this.doabort();
    }
};

Using the APU

The basic idea is that the APU encloses iterative code — code that you would otherwise put in a for or while loop. You enclose the code in a callback function, and the APU will then call that function repeatedly — passing an incremental counter variable to each iteration.

But that iterative process is itself contained within an asynchronous timer, and the timer also loops around repeatedly — after each group of n iterations, the timer loops around once, then performs another group of iterations, then another timer ... and so on.

The number of iterations in each group is referred to as the chunksize, and it's the combination of chunks and timers that's the key to how the APU works — asynchronous timers prevent the browser thread from locking-up, while the fact that a whole bunch of iterations are performed within each timer, speeds it up.

The APU Constructor

To use the APU you must first create a new instance, using the constructor function, which takes two required and two optional arguments:

var processor = new APU(
    50,                     //chunksize                 [REQUIRED Number (Integer)]
    function(i){ ... },     //oninstance callback       [REQUIRED Function]
    function(i){ ... },     //oncomplete callback       [OPTIONAL Function]
    function(i){ ... }      //onabort callback          [OPTIONAL Function]
    );
chunksize [REQUIRED Number (Integer)]

A Number which specifies the number of process iterations (or chunks) that will be performed within each timer loop, of whatever iterative code the APU is running (as defined in the oninstance callback). This value must be an integer greater than zero; floats will be rounded, and invalid values default to 1.

For example, let's say you had an array of 365 dates, and each of those dates had to be parsed in some way. Using an APU with a chunksize of 50, your parsing code will iterate 50 times, then the pause of a single setTimeout will happen, then another 50 iterations ... and so on until you get to the final, residual 15 iterations.

The value you define here should be guided by the complexity of the work — the more it has to do within each iteration, the lower the chunksize may need to be to avoid locking-up the browser. A value of 50 is a good place to start; but if the code is particularly heavy it might be better to start with 1, then experiment with higher values for as long as the browser is still responsive!

oninstance callback [REQUIRED Function]

A Function that encloses the primary iterative code you want the APU to run.

Each time this function is called it's passed a counter variable, which starts at 0 then increments by 1 after each iteration (by default).

The contents of this function represent the smallest iterative step your code can be reduced to. So if you find that the browser is unresponsive even with a chunksize of 1, then you're doing too much work within oninstance. In that situation, you would need to break it down into smaller iterative steps; once they're small enough not to lock-up the browser anymore, then you can try to increase the chunksize again, to make it run faster.

You control when an APU iterates, using the next() control method — with which you can also override the counter increment and timer speed.

oncomplete callback [OPTIONAL Function]

A Function that encloses any additional code you want to run when the APU's work is complete.

This function is also passed the counter variable, with whatever residual value it has by then — so if the counter was being incremented by 1 and the APU did 100 iterations in total, then the residual value passed to oncomplete will also be 100.

You control when an APU completes, using the complete() control method.

onabort callback [OPTIONAL Function]

A Function that encloses any additional code you want to run if the APU is aborted prematurely.

This function is also passed the counter variable, the value of which will obviously depend on when it's called, and what its increments were.

You control when and if an APU aborts prematurely, using the abort() control method.

Control Methods

Once constructed, an APU will still not run on its own — it must be explicitly started, incremented and completed; you can also abort it at any point, should that prove necessary.

To acheive this, the APU provides four control methods:

start()

The start() method is used to start an APU. Typical usage is to create a new processor and start it immediately:

var processor = new APU(1, function(i)
{
    alert("counter = " + i);
});

processor.start();

When you use the start() method, the first iteration of the code within oninstance will be called immediately, with no preceding timer instance even if the chunksize is 1. The counter value passed to this first iteration will always be 0.

You may not actually need to create that reference variable. If you want to be able to control an APU externally — such as starting it from an event, or aborting it from another process; or controlling a parent instance from a nested instance — then you will need a reference to do so. But if not, you can dispense with the reference and chain the start() command directly onto the end of the instantiation:

new APU(1, function(i)
{
    alert("counter = " + i);

}).start();

From within a single APU, all the other control methods can be referred to using "this".

complete()

The complete() method is used to complete an APU once its work is done. Typical usage is to define a condition at the very beginning of your oninstance code, that determines whether or not it's time to complete:

var processor = new APU(1, function(i)
{
    if(i == data.length)
    {
        this.complete();
    }
    else
    {
        alert("counter = " + i);
    }
});

processor.start();

Although the expression in an ordinary for loop is evaluated before the iterative code, with an APU the counter is incremented after each iteration. So evaluating the complete() condition at the start of oninstance is logically equivalent (and produces the same residual value).

If the evaluation above were written in a for loop, it would be like this:

for(var i = 0; i != data.length; i ++)

Though in practise it would usually be written like this:

for(var i = 0; i < data.length; i ++)

Which is not exactly the same, of course, but amounts to the same thing in most cases (ie. where nothing else affects the value of i). By the same thinking then, the original evaluation could also have been written like this:

if(!(i < data.length))
{
    this.complete();
}

Alternatively ... if you'd rather evaluate complete() at the end of your oninstance code, you can adjust the condition to suit that location:

function(i)
{
    alert("counter = " + i);

    if(i == (data.length - 1))
    {
        this.complete();
    }
}

In that example there would be ten iterations, completing at the end of the 10th. By contrast, in the earlier example there are eleven iterations, it's just that the 11th is interrupted before it does anything.

But more importantly, the placement of complete() affects the residual counter value passed to oncomplete — in the earlier example the counter is incremented one extra time, compared with the latter, hence the residual value will be one-higher.

I guess the thing to do is whatever makes for the simplest logic in each case. There are no hard and fast rules, only my conventions, and I'm sure you have your own ideas about the way you like to code!

next()

The next() method is used to iterate the APU. Typical usage is to put this command at the end of your oninstance code:

var processor = new APU(1, function(i)
{
    if(i == data.length)
    {
        this.complete();
    }
    else
    {
        alert("counter = " + i);

        this.next();
    }
});

processor.start();

And with that, we now have the minimum code required for a functional APU — it starts, iterates over some code, then completes.

For a demo of this example, see :: Basic Implementation Demo

The next() function also has two optional arguments, allowing you to override the counter increment and/or the timer speed:

counter increment [OPTIONAL Number (Integer)]

A Number which specifies by how much to increment the counter this iteration, thereby controlling the value that will be passed to the next oninstance. This value must be an integer greater than zero; floats will be rounded, and invalid values default to 1.

So for example, if you wanted to increment the counter by 3 instead of 1, simply pass that number as the first argument to next() — the counter will then go 0, 3, 6 ... and so on:

this.next(3);

Of course you don't have to pass the same value every time — you might need to increment the counter by different amounts at different times, or perhaps only override the default in certain circumstances. This ability may be essential when working with non-contiguously numbered data:

function(i)
{
    alert(data[i]);
    
    if(i == limit)
    {
        this.complete();
    }
    else
    {
        var n = 1;
        while(typeof data[i + n] == "undefined")
        {
            n ++;
        }
        this.next(n);
    }    
}

What we're effectively doing there is defining the counter manually, for every iteration after zero, and working it out by skipping-over undefined data indices. But we need to avoid a situation where the while loop evaluates a counter that's now higher than the highest index in the data — or it'll get stuck and run forever. So we need to know the highest index (shown above in the limit var) and use that value for the complete() condition.

For a demo of this example, see :: Non-Contiguous Numbering Demo

timer speed [OPTIONAL Number (Integer)]

A Number which specifies the speed of the next timer loop (in milliseconds). This value must be an integer greater than zero; floats will be rounded, and invalid values default to 10.

As we noted earlier, the timers are there as a threading trick to avoid locking-up the browser — they're not really designed for performance animation and so the actual speed of the timer is not usually significant. But it does create a pause in execution, so other things being equal, we want it as fast as possible.

Now in practise, most browsers can't or won't run a timer faster than 10ms, and if we set a speed faster than that it could actually come out slower than that, as the browser strains to try to keep up. Since the whole point of the APU is to reduce computational stress, it would be self-defeating to reduce it in one way only to increase it again in another.

So if you find that your APU is not performing as fast as you want, you should look to increase the chunksize rather than the timer speed; or just get it to do less work!

But there may be situations where you need to define a specific — and much slower — speed for the timer, to purposely delay execution in order to wait for something else; or just for testing or debugging. Combined with a chunksize of 1, this argument effectively allows you to create a controlled asynchronous timer loop, the speed of which can also be overriden on a per-loop basis.

abort()

The abort() method can be used to prematurely abort an APU before it's finished. There's no typical usage for this method, it all depends on when and why you need to abort the process.

When you use the abort() method the processor doesn't stop immediately, it merely sets a flag that the timer won't proceed past — so aborting an APU will make it stop after the current set of chunks. This of course implies that it will stop immediately if the chunksize is 1.

In the example below a random-number is used for the abort() condition, effectively implementing a 1-in-5 chance that it will abort before each iteration:

function(i)
{
    if(i == data.length)
    {
        this.complete();
    }
    else if(Math.ceil(Math.random() * 5) == 1)
    {
        this.abort();
    }
    else
    {
        alert("counter = " + i);

        this.next();
    }
}

For a demo of this example, see :: Randomly Aborting Demo

More likely in practise, an external process might be the trigger to abort an APU. In Dust-Me Selectors, for example, a tabbed-dialog displays a list of unused CSS selectors, output which is compiled and built using an APU. But if the user selects a different tab before it's finished compiling, the APU is aborted and the compilation stops. In that example then, it's a user-event on the tabs that triggers abort():

tabs.addEventListener("click", function(e)
{
    compiler.abort();

    tabSelect(e.target);

}, false);

The onabort callback is used in turn, to close the output and add a message that says it's incomplete.

top

Get the script

BSD License → Terms of use

Categories...

Components

Parts for other scripting:

Website gadgets

Bits of site functionality:

Usability widgets

Local network apps

Web-applications for your home or office network:

Game and novelties

Our internal search engine is currently offline, undergoing some configuration changes in preparation for a major site overhaul. In the meantime, you can still search this site using Google Custom Search.


In this area

Main areas


[brothercake] a round peg in a square hole, that still fits