web workers vs. the crazy flies

Here’s a low tech demo of the power of web workers. A hundred flies will swarm randomly. Ones that get too high get sleepy, ones that sink too low get re-caffeinated and ones that cover the least distance will perish.

Click for demo (IE not supported)

Sourcecode is gisted on GitHub

I should start by saying this was not trivial to write. As we will see the web workers API is disarmingly simple but the tripwires are many. The biggest issue is the lack of useful debug support because the global worker object exists in a vacuum.

The basics

Your browser can be serviced by one or more web workers. A worker will perform non-DOM related tasks in a separate thread. This means worker processes are performed asynchronously with respect to the browser (in fact the worker has no access to the browser’s window object, the equivalent global object being self which references the worker). The implications are exciting. Lengthy computational tasks can be undertaken with no effect on browser responsiveness.

A web worker is a .js file which you set as an attribute of a worker object.

var worker = new Worker("buzzWorker.js");

The browser and the worker speak the same language. Messages are sent and received using postMessage and onMessage respectively

//on the browser
worker.onmessage = function(e){
    updateThing(e.data);
}

 var invokeWorker = function(action) {
    worker.postMessage({
        'action': action,
        'things': things
    });
}
//on the worker
{
    //....
    updates.maxDy = 2;
    updates.symbol = '*';
    postMessage(updates);
}

var onmessage = function(e){
    things = e.data.things;
    actions[e.data.action]();
}

By these means data and instructions can be passed to and fro between browser and worker.

Data streaming is by value not by reference. The data is serialized in transit and rebuilt as a new but (hopefully) identical object on the other side. In theory any serialize-able non-DOM object can be streamed. Mozilla, Chrome 5 and Safari 5 support posting of complex objects to workers (thanks to Rick Waldron and Charles Lehner for pointing out bugs in my code which webkit browsers objected to)

IE8 has no web worker support.

The app

The crazy flies app makes use of a web worker to analyze and acting upon the latest data pattern (i.e. which flies are where) while the browser focuses on buzzing them around the screen as fast as possible. Each fly is an instance of a Thing object which recursively moves itself around the screen in a random fashion. Every second the browser posts four instructions to the worker:

intervals[0] = window.setInterval(invokeWorker.curry('updatePaths'),1000);
intervals[1] = window.setInterval(invokeWorker.curry('makeHighestSleepy'),1000),
intervals[2] = window.setInterval(invokeWorker.curry('makeLowestBuzzy'),1000);
intervals[3] = window.setInterval(invokeWorker.curry('killSlowest'),1000);

The first instruction updates the approximate total distance traveled by each living Thing. The other three perform further analysis on the state of Things and then send back the appropriate data to the browser so that it can modify the flies.

The web worker does make a difference. Every second it uses Pythagoras’ theorem to increment the net path-length of each fly and every second it is sorting arrays three ways to find the highest lowest and least traveled flies. When I prototyped the app I first had all processes running in the browser. It limped along with a lengthy freeze every second. In contrast, with a web worker employed the scene plays out seamlessly on my computer (though the fluency may vary based on your processing speed).

Conclusion

JavaScript web workers are in their infancy and the use cases are limited (top of my wish-list is worker partitioning of the browser itself so that DOM events could run in separate threads). Browser support varies from patchy to non-existent and debugging is tough. It’s too early to claim web workers as an industrial strength solution but the outlook is promising, and in the meantime they’re fun to mess around with.

24 thoughts on “web workers vs. the crazy flies

  1. nice investigation 🙂
    the spec didnt said that we can pass other stuff than strings, which sounds stupid, but I guess it was to make things easier for the implementors, but I guess that passing full JSON strings would work … at the price of CPU hog 😦
    FF did go further by allowing passing arrays. That sounds good but are you sure other browsers have plans for this ?

    I would have like to play this demo without the Web Workers, to see the lag in action

    and you found a smart way to pass in methods to execute 🙂

    var onmessage = function(e){
    things = e.data.things;
    actions[e.data.action]();
    }

    well done

    1. Thanks for the nice comments!

      >>FF did go further by allowing passing arrays. That sounds good but are you sure other browsers have plans for this ?

      According to the spec:

      interface Worker : AbstractWorker {
        void terminate();
      
        void postMessage(in any message, in optional MessagePortArray ports);
                 attribute Function onmessage;
      };
      1. ok, my bad
        they changed the spec to follow the one of Mozilla, I’m pretty sure 🙂

        no plan for having the same demo working without workers, to see the browser lag difference ?

      2. I will try. Also want to try to get this to work with webkit and I need to start planning next blog. And I’ve got my day job too 😉

        (actually I think you may be right – I did read somewhere that they updated the spec after Mozilla went one better)

  2. Angus, I got the demo to work in Chrome without modifying anything. It just worked (6.0.398.0 (46652) Ubuntu).

    Nice example!

    I wasn’t aware that you could pass in non-serialized objects either.

    1. Hey Nick, thats good to know 🙂 (you saw asterisks and zeds and the gorey death scenes too?)

      I’m on 6.0.437.3 and it does nothing. You’re using a mac right? Maybe works on mac + safari 5 too. I know its only a matter of time until the webkit browsers come up to speed on the spec. (btw just tested on Opera 10.53, belatedly – no dice)

      1. No gory death scenes or asterisks, actually. Just the “%” flies buzzing around. Too bad. I am actually on Ubuntu Lucid Lynx.

        Maybe its just me, but Opera seems completely irrelevant now-days. (Was it ever relevant?) I get about 1% of visitors to my sites using Opera…

      2. Ok so its NOT working on chrome. The buzzing %s are just the browser. The web worker updates them based on its analysis.

        re. Opera. Yes and No – the only reason they still interest me is they make a hell of an effort to support web specs (although sometimes very badly). I was told they fully implement every HTML 5 tag including all the new input tags

      3. Karmic 64 bits and Chrome (6.0.466.0 dev) here. The %, *, and z all show up, with red % (just before death). No crashes, works great, runs to the end.

        Nice work.

  3. I’m reading this from my mobile device, so can’t test for myself, but complex messages are supported in chrome 5, safari 5 and opera 10.6 beta – I have published research illustrating this.

    1. Rick – yep you’re right. I’ll update my text accordingly. The next question in how complex. In chrome I’m getting a

      NOT_SUPPORTED_ERR: DOM Exception 9

      Which suggests it doesn’t like the fact each Array member has a property which points to a DOM element. If I have enough time I will try putting the elements in a hash which I can reference with a string

      Thanks for the catch!

      1. I’ve had a chance to play around with this and I can see now why you’ve reported that it doesn’t work in anything but FireFox – you’re attempting to pass non-thread safe content (a DOM Element and a function) when the spec clearly dictates that this is not allowed and will not be allowed (so there is no “catching up” that the other browsers have to do). Why it works in FF is because FF strips out the non thread safe contents of your objects in the “things” array when it serializes the message arguments – the webkit implementation does not do this. When this happens, FF processes silently, but incorrectly. It SHOULD throw an error. Basically, message sent through postMessage() are given the JSON scrub (http://www.json.org/) so anything that can be evaluated and restructured as valid JSON will be allowed. postMessage() must conform to the HTML5 Communication specification, notably this:

        http://www.w3.org/TR/2009/WD-html5-20090423/comms.html#posting-messages

        Follow that to the “Safe passing of structured data” spec:

        http://www.w3.org/TR/html5/infrastructure.html#structured-clone

        As for “How complex”:

        http://weblog.bocoup.com/javascript-web-workers-chrome-5-now-supports-complex-messages

      2. Thanks, yeah I’m already onto the DOM element issue. BTW I don’t pass a function – just a string reference to a hash of functions on the worker

        Edit: Now removed all DOM objects refs from message. No errors in chrome or safari but not working either. Perhaps its now a race condition that can be solved by putting callbacks on the web worker functions.

  4. “BTW I don’t pass a function – just a string reference to a hash of functions on the worker”

    When I inspect each object in the array on the console it has “start” which is being identified as a function. That’s where I got that from.

  5. I forked it and got it working in Safari. .gist table { margin-bottom: 0; } This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters Show hidden characters <head> <title>swarm!</title> http://buzzWorker.js <style> .thing { position:absolute; } .dying { color:#ff0000; font-weight:bold; } </style> </head> <body> <script> /////////////////////////////////////////////////// // Thing object /////////////////////////////////////////////////// var things = [], thingMap = {}, elemMap = {}; var Thing = function(left, top, id) { this.id = id; this.minDx = -3; this.maxDx = 3; this.minDy = -3; this.maxDy = 3; this.x = this.xOld = left; this.y = this.yOld = top; this.pxTravelled = 0; elemMap[id] = createThingElem(left, top); } Thing.prototype.start = function() { var thisElem = elemMap[this.id]; var thing = this; var move = function() { thing.x = bounded(thing.x + scaledRandomInt(thing.minDx,thing.maxDx),200,600); thing.y = bounded(thing.y + scaledRandomInt(thing.minDy,thing.maxDy),100,500); if (!thing.dead) { setTimeout(move, 1); } thisElem.style.left = thing.x; thisElem.style.top = thing.y; }; move(); } var createThingElem = function(left, top) { var elem = document.createElement("DIV"); elem.innerHTML = "%"; elem.className = "thing"; document.body.appendChild(elem); elem.style.left = this.x; elem.style.top = this.y; return elem; } //////////////////////////////////////////////// // utils //////////////////////////////////////////////// var toArray = function(arr) { return Array.prototype.slice.call(arr); } var removeFromArray = function(arr, val) { for (var i=0; i<arr.length; i++) { if (arr[i] === val) { arr.splice(i, 1); } } } Function.prototype.curry = function() { if (arguments.length<1) { return this; //nothing to curry with – return function } var __method = this; var args = toArray(arguments); return function() { return __method.apply(this, args.concat(toArray(arguments))); } } var scaledRandomInt = function(min, max) { return Math.round(min + Math.random()*(max-min)); } var bounded = function(value, lo, hi) { return Math.max(Math.min(value,hi),lo); } /////////////////////////////////////////// // response to worker ////////////////////////////////////////// var updateThing = function(updateData) { updateThis = thingMap[updateData.id]; updateElem = elemMap[updateData.id]; delete updateData.id; if (updateData.kill) { removeFromArray(things, updateThis); //biggest drag on browser performance updateElem.className += " dying"; window.setTimeout(function() {updateElem.style.display = "none"; updateThis.dead = true}, 150); if (!things.length) { window.setTimeout(clearIntervals,50); alert("the end"); } return; } for (var prop in updateData) { updateThis[prop] = updateData[prop]; } if (updateThis.symbol) { updateElem.innerHTML = updateThis.symbol; } } /////////////////////////////////////////// // worker ////////////////////////////////////////// var worker = new Worker("buzzWorker.js"); worker.onmessage = function(e){ updateThing(e.data); } var invokeWorker = function(action) { worker.postMessage({ 'action': action, 'things': things }); } var intervals = []; intervals[0] = window.setInterval(invokeWorker.curry('updatePaths'),1000); intervals[1] = window.setInterval(invokeWorker.curry('makeHighestSleepy'),1000), intervals[2] = window.setInterval(invokeWorker.curry('makeLowestBuzzy'),1000); intervals[3] = window.setInterval(invokeWorker.curry('killSlowest'),1000); var clearIntervals = function() { for (var i=0;i<intervals.length;i++) { window.clearInterval(intervals[i]); } } /////////////////////////////////////////////////// // init /////////////////////////////////////////////////// var init = function(numberOfThings) { var i = -1; while (i++ < numberOfThings) { things[i] = new Thing(400, 300, i); things[i].start(); thingMap[things[i].id] = things[i]; } }; init(100); </script> </body> view raw buzz.html hosted with ❤ by GitHub This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters Show hidden characters var things; var updates; var getDistance = function(x1,x2,y1,y2) { return Math.sqrt(Math.pow(Math.abs(x1-x2),2) + Math.pow(Math.abs(y1-y2),2)); } var actions = { makeHighestSleepy: function(){ var highest = things.sort(function(a, b){ return a.y – b.y }); updates = {}; updates.id = highest[0].id; updates.minDy = -2; updates.maxDy = 3; updates.symbol = 'z'; postMessage(updates); }, makeLowestBuzzy: function(){ var lowest = things.sort(function(a, b){ return b.y – a.y }); updates = {}; updates.id = lowest[0].id; updates.minDy = -3; updates.maxDy = 2; updates.symbol = '*'; postMessage(updates); }, killSlowest: function(){ var slowest = things.sort(function(a, b){ return a.pxTravelled – b.pxTravelled }); updates = {}; updates.id = slowest[0].id; updates.kill = true; postMessage(updates); }, updatePaths: function(){ for (var i = things.length-1; i; i–) { var t = things[i]; t.pxTravelled += getDistance(t.xOld, t.x, t.yOld, t.y); t.xOld = t.x; t.yOld = t.y; } } } onmessage = function(e){ things = e.data.things; actions[e.data.action](); } view raw buzzWorker.js hosted with ❤ by GitHub The problem wasn’t actually passing complex messages, but two other things: The onmessage function in the worker had to be in the global scope, otherwise it was never called. So I just took off the var keyword. The sort functions should use subtraction to compare numbers, not > or b, or a – b instead of a < b.
    1. Charles, many thanks. Works in Chrome too now. It was my dumb var in front of onmessage that was the issue (force of habit!). I’ve updated the demo and the post. Great catch!

  6. When passing messages to and from workers, I typically include a ‘type’ property in the anonymous objects carrying the payload, like this:

    x.postMessage({‘type’: ‘debug’, ‘data’: { ‘error’: …, … }})

    or

    x.postMessage({‘type’: someclass.MSG_MSGTYPE, ‘data’: { ‘greeting’: ‘hello’}})

    …or something. That way, I can key off the type value to route messages to appropriate callbacks. This is particularly handy for debugging. You can catch errors in the worker and post a message back to the worker’s parent where you can do what you want with the error information in the payload.

Leave a comment