Wednesday, June 11, 2014

Wrapping Functions In Web Workers

It's commonplace, now that JavaScript has matured, and is continuing to mature, to implement heavy-duty data-crunching operations in the front-end. For instance, rather than calling an API to reduce a large set, it's easier to just reduce it in the browser. The wins are that you don't have to support an API to do this, and there's no network overhead. However, there's a new kind of overhead in that the browser will eat up valuable seconds while performing these long-running tasks. And since JavaScript code is single-threaded, these tasks diminish DOM responsiveness.

The trouble I have with web workers is the awkwardness of getting one started. You have to either load an external script into the worker, or you can inline the worker. You then need to enter the message-passing realm of web workers, just to get things started, and to get any return values. None of this is ideal for me, I've already got a simple function that does some work. In certain contexts, I want it to run in the background, in it's own thread, and to not disrupt anything that's happening in the DOM.

So, I figured I would make a wrapper utility. Something that takes a simple JavaScript function and "wraps" it in a web worker. Additionally, I though it would make sense if the wrapped function, when called, returned a promise. For this I'm just using jQuery Deferred instances. Here's what the wrapper utility looks like.

function worker( func ) {
    return (function() {
        
        var blob = URL.createObjectURL( new Blob( [ "\
            onmessage = function(){\
                postMessage((" + func.toString() + ")());\
            };"])),
            
            worker = new Worker( blob ),
            deferred = new $.Deferred();
        
        worker.onmessage = function( e ) {
            deferred.resolve( e.data );    
        };
        
        worker.postMessage();
        return deferred;

    });
}

The worker() utility takes a function object and wraps it with a new function — whose job is to inject the passed-in function into a web worker. The other job of this wrapper function is to return the deferred object, so that the caller can listen to done events, and get the function result. The web worker itself is created inline by constructing a blob URL from a function string. The function string is the code that the web worker will actually execute. So the idea is to take the string version of the passed-in function, and wrap it with another function that's invoked when the web worker onmessage event is triggered. We get the result out of the worker thread, back into the main thread, by calling postMessage() with the result of our passed-in function.

At this point, we've taken care of building up the web worker code using some inline code string manipulation trickery. This is probably the most difficult part of the whole thing. Now we just have to start the web worker, and tie it to the deferred that this function returns.

To do that, we just have to instantiate the web worker, and deferred instances — the worker gets passed the blob URL we just created. Then, we tie the worker to the deferred using the onmessage event handler. When this handler fires, it resolves the deferred with data retuned from the worker. Lastly, the worker is started with a simple call to postMessage(), and the deferred is returned.

Here is what code that uses the worker() utility looks like.

// "doWork" is some function that takes some time
// to execute, and returns a value
var asyncWork = worker( doWork );

// "asyncWork" returns a deferred instead of
// the value returned by "doWork" - that value
// is available as "result" in the done callback
asyncWork().done(function( result ) {
    $( '<p/>' ).text( result )
        .appendTo( 'body' );

// Code here is run immediately instead of waiting
// for "doWork" to complete...

No comments :

Post a Comment