Delegating Tasks to Web Workers - Creating the Basic Game - HTML5 Games: Creating Fun with HTML5, CSS3, and WebGL (2012)

HTML5 Games: Creating Fun with HTML5, CSS3, and WebGL (2012)

part 2

Creating the Basic Game

Chapter 5

Delegating Tasks to Web Workers

in this chapter

• Introducing Web Workers

• Describing major API functions

• Looking at usage examples

• Creating a worker-based board module

In this chapter, I show you how to use Web Workers, another cool feature to come out of WHATWG. I begin by describing what workers can and cannot do, their limitations, and the functions and objects available to them.

I then move on to a few simple examples and show you how to move a CPU-intensive task to a worker to keep the browser responsive to user interaction.

Finally, I show you how to use Web Workers to create a worker-based version of the board module you implemented in Chapter 4.

Working with Web Workers

JavaScript is single threaded by design. You cannot run multiple scripts in parallel; anything you ask the browser to do is processed in a serial manner. When you use functions such as setTimeout() and setInterval(), you might feel as though you are spawning separate threads that run independently from the main JavaScript thread, but in reality, the functions they call are pushed onto the same event loop that the main thread uses. One drawback to this is that you cannot have any function that blocks the execution and expect the browser to behave. For example, theXMLHttpRequest object used in Ajax has both synchronous and asynchronous modes. The asynchronous mode is by far the most used because synchronous requests tie up the thread, blocking any further execution until the request has finished. This includes any interaction with the page, which appears all but frozen.

Web Workers go a long way toward solving this problem by introducing functionality that resembles threads. With Web Workers, you can load scripts and make them run in the background, independent from the main thread. A script that runs in a worker cannot affect or lock up the main thread, which means you can now do CPU-intensive processing while still allowing the user to keep using the game or application.

note.eps

In this book, I often refer to “worker threads.” The Web Workers specification defines the functionality of workers as being “thread-like” because implementations don’t necessarily have to use actual OS-level threads. The implementation of Web Workers is up to the browser vendors.

Even though they are often mentioned in the same sentence as other HTML5 features, Web Workers are not part of the HTML5 specification but have their own specification. The full Web Workers specification is available at the WHATWG web site, www.whatwg.org/specs/web-workers/current-work/

Limitations in workers

You need to be aware of some important limitations to what a worker can do. In many cases, however, these constraints do not pose any problems as long as your code is encapsulated well and you keep your data isolated.

No shared state

One of the first issues to be aware of is the separation of state and data. No data from the parent thread is accessible from the worker thread, and vice versa. Any changes to the data of either thread must happen through the messaging API. Although this limitation might seem like a nuisance at first, it is also a tremendous help in avoiding nasty concurrency problems. If workers could freely manipulate the variables in the parent thread, the risk of running into problems such as deadlocks and race conditions would make using workers much more complex. With the relatively low entry bar to the world of web development, the decision to limit flexibility in return for simplicity and security makes sense.

No DOM access

As you might have guessed, this separation of worker and parent thread also means that workers have no access to the DOM. The window and document objects simply don’t exist in the scope of the worker, and any attempt to access them only results in errors.

This lack of DOM access doesn’t mean that the worker cannot send messages to the main thread that are then used to change the DOM. The messaging API doesn’t restrict you from defining your own message protocol for updating certain DOM elements or exchanging information about the elements in the document. However, if you plan to do heavy DOM manipulation, perhaps it is better to reconsider if the functionality really belongs in a worker.

Where can you use workers?

Unfortunately, support for Web Workers is not ubiquitous yet. Although Firefox, Safari, Chrome, and Opera all support them on the desktop, Microsoft has yet to implement them in Internet Explorer. We can only hope that Internet Explorer 10, which is still in development, will include Web Workers when it is released.

Lack of support is worse on mobile platforms. Neither Android 2.3 nor iOS 4.3 supports Web Workers, and it is not clear when or if either one will include this feature.

Using polyfills doesn’t really make sense either. Any fallback solutions would have to fake the concurrent processing, and thus nothing would be gained. With this information in mind, you should be aware that the best way to handle this situation is to make your application’s use of Web Workers optional.

What workers can do

Now, let’s look at what you can actually do with workers. In general, you can do anything you would otherwise do with JavaScript. Creating a worker simply loads a script and executes it in a background thread. You are not limited in terms of JavaScript capability. In addition to pure JavaScript, you also have access to a few extras that are nice to have.

First, there’s a new function called importScripts(). You provide this function with a list of paths that point to scripts that you want to load. The function is variadic, which means that you must specify the paths as individual arguments:

importScripts(“script1.js”, “script2.js”, “script3.js”, ...);

Because the files are loaded synchronously, importScripts() doesn’t return until all the scripts finish loading. After each script is done loading, it is executed within the same scope as the calling worker. This means that the script can use any variables and functions declared in the worker and the worker can subsequently use any variables and functions introduced by the imported script. Overall, this enables you to separate your worker code easily into discrete modules.

The script th at created the worker can terminate the worker when it is no longer needed, but the worker itself can also choose to exit. A worker can self-terminate by calling the global close() function.

Timeouts and intervals

The timer functions that you know from the window object are all available in worker threads, which means that you are free to use the following functions:

• setTimeout()

• clearTimeout()

• setInterval()

• clearInterval()

If the worker is terminated, all timers are automatically cleared.

WebSockets and Ajax workers

You can also use the XMLHttpRequest object to create and process background Ajax requests. XMLHttpRequest can be useful if you need, for example, a background worker that continuously pings the server for updates that are then relayed to the main thread via the messaging API. Because blocking in a worker thread does not affect the main UI thread, you can even do synchronous requests — something that is usually a bad idea in non-worker code.

WebSockets support in worker threads is a bit of a gray area. For example, Chrome supports this combination, whereas Firefox lets you use WebSockets only in the main JavaScript thread, thus limiting the number of users who can benefit from it. For now, therefore, it is better to leave any WebSockets code to the main thread.

Using Workers

To create worker threads, you use the Worker() constructor:

var worker = new Worker(“myworker.js”);

You can create workers only from scripts that have the same origin as the creating script. That means you cannot refer to scripts on other domains. The script must also be loaded using the same scheme. That is, the script must not use https: if the HTML page uses the http: scheme, and so on. Additionally, you cannot create workers from a script running locally.

When you are done using the worker, make sure you call the terminate() method to free up memory and avoid old, lingering workers:

worker.terminate();

You can create more than one worker thread. You can even create more workers that use the same script. Some tasks are well suited for parallelization, and with computers sporting more and more CPU cores, dividing intensive tasks between a few workers can potentially give a nice performance boost. However, workers are not meant to be created in large numbers due to the overhead cost in the setup process, so try to keep the number of workers down to a handful or two.

Sending messages

Workers and their parent threads communicate through a common messaging API. Data is passed back and forth as strings, but that doesn’t mean you can send only string messages. If you send any complex structure such as an array or object, it is automatically converted to JSON format. For this reason, you can build fairly advanced messages but note that some things, such as DOM elements, cannot be converted to JSON.

In the creating thread, call the postMessage() method on the worker with the data that you want to send. Here are a few examples:

// send a string

worker.postMessage(“Hello worker!”);

// send an array

worker.postMessage([0, 1, 2, 3]);

// send an object literal

worker.postMessage({

command : “pollServer”,

timeout : 1000

});

In the same way, simply call postMessage() to send messages from the worker thread to the main thread:

// send a string to main thread

postMessage(“Hello, I’m ready to work!”);

Receiving messages

Messages can be intercepted by listening for the message event. In the parent thread, the event is fired on the worker object; whereas in the worker object, it is fired on the global object.

To listen for messages from the worker, attach a handler to the message event:

worker.addEventListener(“message”, function(event) {

// message received from worker thread

}, false);

Similarly, in the worker thread, listen for the message event on the global object:

addEventListener(“message”, function(event) {

// message received from main thread

}, false);

In both cases, you can find the message data in the data property on the event object. It is automatically decoded from JSON, so the structure of the data is intact.

Catching errors

If an error occurs in a worker thread, you might want to know about it in the main thread so that you can display a message, create a log entry, and so forth. In addition to the message event, worker objects also emit an error event, which is fired if an error happens that is not caught in the worker thread. When the event fires, it is too late to do anything about the error. The worker has already stopped whatever it was doing, but at least you can be informed that the error occurred.

worker.addEventListener(“error”, function(error) {

alert(“Worker error: “ + error);

}, false);

Shared workers

The type of worker I just described is called a dedicated worker. The Web Workers specification also defines another type: the shared worker. Shared workers differ from dedicated workers in that they can have multiple connections. They are not bound to one HTML page. If you have multiple HTML pages from the same origin running in the same browser, these pages can all access any shared workers created by one of the pages.

To create a shared worker, use the SharedWorker() constructor. In addition to the script path, this constructor also takes a second, optional name parameter. If the name parameter is not given, an empty string is used for the name. If you attempt to create a shared worker and one has already been created with the same script and name, a new connection to that worker is created instead of a brand new worker.

I don’t go into depth with shared workers here, and they are not used for the Jewel Warrior game. Let me just give you a short example to show how multiple pages can connect to and communicate with the same worker. Listing 5.1 shows the test HTML page that creates a shared worker.

note.eps

Chrome and Safari support both types of workers, but Firefox does not yet support shared workers.

Listing 5.1 Shared Worker Test Page

<!DOCTYPE HTML>

<html>

<textarea cols=80 rows=20 id=”output”></textarea>

<script>

var worker = new SharedWorker(“shared-worker.js”,”worker”);

worker.port.addEventListener(“message”, function(event) {

document.getElementById(“output”).value +=

event.data + “\r\n”;

}, false);

worker.port.start();

worker.port.postMessage(”Hello”);

</script>

</html>

Whenever the page receives a message from the worker, the message is printed in the output textarea. The worker is greeted with a “Hello” when the connection is established. Listing 5.2 shows the worker script.

Listing 5.2 The shared-worker.js Script

var c = 0;

addEventListener(“connect”, function(event) {

var id = c++,

port = event.ports[0];

port.postMessage(”You are now connected as #” + id);

port.addEventListener(”message”, function(event) {

if (event.data == ”Hello”) {

port.postMessage(”And hello to you, #” + id);

}

}, false);

port.start();

}, false);

Shared workers do not have a global message event like dedicated workers do. Instead, they must listen for connect events that fire whenever a new page creates a connection to the worker. Communication between the worker and connecting threads happens via port objects that emitmessage events and expose the postMessage() method. Note also the port.start() function, which must be called before any messages can be received.

When the HTML page in Listing 5.1 is loaded, it connects to a worker. If you open another tab with the same or a similar page, it connects to the same worker, and you see the connection counter increasing.

A prime example

Let’s move on to another example that is just slightly more useful. Here, I use a dedicated worker to do some CPU-intensive processing, thereby freeing up the main thread.

Consider the problem of determining whether a number is a prime number. A prime is a number that has only two (natural number) divisors, 1 and itself. For example, 9 is not a prime because it can be divided by 3. On the other hand, 7 cannot be divided by any other number and is therefore a prime.

Listing 5.3 shows prime.js, a simple brute-force algorithm that returns a boolean value indicating whether a number, n, is a prime.

Listing 5.3 The Prime Checking Algorithm

function isPrime(n) {

if (n < 2) return false;

for (var i=2,m=Math.sqrt(n);i<=m;i++) {

if (n % i === 0) {

return false;

}

}

return true;

}

The smallest prime number is 2, so isPrime() returns false for anything less than 2. If a pair of divisors exists, one of the divisors must be smaller than or equal to the square root of n, so you need to test only numbers in the range [2, sqrt(n)] to determine the primality of n. So, for each number i from 2 to sqrt(n), you use the remainder operator (%) to test whether i can divide n. If it can, n is not a prime and isPrime() returns false. If the loop exits without finding a divisor, n is a prime, and the function returns true.

Creating the test page

Because the loop must run to the end if the n is a prime, any sufficiently large prime number makes isPrime() hog the CPU for a while, effectively locking down the main UI thread for that page.

To see that this is actually the case, create the prime.html test page shown in Listing 5.4.

Listing 5.4 The Non-worker Test Page

<!DOCTYPE HTML>

<html lang=”en-US”>

<head>

<meta charset=”UTF-8”>

<title>Prime Number</title>

<script src=”sizzle.js”></script>

<script src=”prime.js”></script>

</head>

<body>

Number (n): <input id=”number” value=”1125899839733759”>

<button id=”check”>Is n prime?</button><br/><br/>

<button id=”click-test”>Try to click me!</button>

<script>

var $ = Sizzle;

$(”#check”)[0].addEventListener(”click”, function() {

var n = parseInt($(”#number”)[0].value, 10),

res = isPrime(n);

if (res) {

alert(n + ” is a prime number”);

} else {

alert(n + ” is NOT a prime number”);

}

}, false);

$(”#click-test”)[0].addEventListener(”click”, function() {

alert(”Hello!”);

}, false);

</script>

</body>

</html>

This simple test page features an input field with a button. When you click the check button, the value of the number field is passed to isPrime(), and the result is displayed in a message box. The default number I specified in the input field is a prime and should take a while to check.

The second button, click-test, is there to test whether the UI responds. Try clicking this button while isPrime() is running. Nothing happens until after the isPrime() call finishes and the results are displayed.

Moving the task to a worker

If, instead, you move the isPrime() function to a worker thread, the main UI thread is kept free, and the UI remains as responsive as ever. Listing 5.5 shows the prime-worker.js script.

Listing 5.5 The Worker Script

importScripts(“prime.js”);

addEventListener(“message”, function(event) {

var res = isPrime(event.data);

postMessage(res);

}, false);

Notice how the prime.js file is reused by importing it with the importScripts() function. The message event handler simply passes along any data it receives to isPrime() and posts back the result to the main thread when it is done. The worker thread might be busy, but the main thread is not. Now change the click event handler in the HTML page as shown in Listing 5.6.

Listing 5.6 Communicating with the Worker

$(“#check”)[0].addEventListener(“click”, function() {

var n = parseInt($(“#number”)[0].value, 10),

worker = new Worker(“prime-worker.js”);

worker.addEventListener(“message”, function(event) {

if (event.data) {

alert(n + “ is a prime number”);

} else {

alert(n + “ is NOT a prime number”);

}

}, false);

worker.postMessage(n);

}, false);

When the check button is clicked, a new worker is created from the prime-worker.js script. The value of the number input field is converted to a number and posted to the worker using the postMessage() method. The message event handler waits for a response from the worker and pops up a message box with the result.

If you try clicking the test button now, you see that the UI still responds and the “Hello!” alert appears as soon as you click. All the intensive processing happens independently in the background and doesn’t affect the page.

Using Web Workers in Games

You have now seen some basic examples of how to use Web Workers, but how do they relate to games? When deciding what elements to delegate to worker threads, you must ask yourself a few questions.

First, do you need to move the game element to a separate worker thread? Does the game element do anything that could benefit from running independently?

Second, does the game element depend on having access to the DOM? Remember that worker threads cannot access the document, so any tasks that depend on the DOM must be done in the main thread.

Good candidates for workers include elements such as artificial intelligence (AI). Many games have entities that the player does not control and instead react to player actions or the environment in general. Processing the behavior of enemies and other entities is potentially a rather intensive task. This task doesn’t need to manipulate the page and could therefore be a candidate for a worker thread. Physics simulation is another example. Like AI, physics simulation can be demanding on the CPU and doesn’t necessarily need to run in the same thread as long as the data is kept synchronized.

Creating the worker module

In the case of Jewel Warrior, moving anything to a web worker is difficult to justify. Due to the relatively small 8x8 board, the board module that takes care of the game logic is fairly lightweight in terms of processing needs. It could easily run in a separate worker thread, though, so let’s try that. Because the module was designed with multiplayer support in mind, it already has an asynchronous interface, which makes it easy to adapt to the asynchronous nature of worker messaging. It is pure logic; it manipulates only its own internal data and does not need DOM access. Listing 5.7 shows the beginnings of the board.worker.js script.

Listing 5.7 Importing the Board Module

var jewel = {};

importScripts(“board.js”);

The board worker imports the regular board module. This lets you reuse the functionality already present in the board module. Because the module is created in the jewel namespace, an empty jewel object is created prior to importing the script. If the object doesn’t exist when the script is imported and executed, you get a runtime error.

When the board worker receives a message from the game, it needs to call the appropriate method on the imported board module and post back the results. The messages coming from the game to the worker use a custom message format described by the following object literal:

{

id : <number>,

command : <string>,

data : <any>

}

Here, the id property is an ID number uniquely identifying this message. The command property is a string that determines what the worker should do, and the data property contains any data needed to perform that task. All messages posted to the worker trigger a response message with the following format:

{

id : <number>,

data : <any>,

jewels : <array>

}

The id property is the ID number of the original message; data is the response data, if any. The jewels property contains a two-dimensional array that represents the current state of the jewel board. The board data is always attached, so the main thread can keep a local copy of the data for easy access. Listing 5.8 shows the message event handler in the board.worker.js script.

Listing 5.8 The Message Handler

addEventListener(“message”, function(event) {

var board = jewel.board,

message = event.data;

switch (message.command) {

case “initialize” :

jewel.settings = message.data;

board.initialize(callback);

break;

case “swap” :

board.swap(

message.data.x1,

message.data.y1,

message.data.x2,

message.data.y2,

callback

);

break;

}

function callback(data) {

postMessage({

id : message.id,

data : data,

jewels : board.getBoard()

});

}

}, false);

The worker supports two commands, initialize and swap, that are mapped to the corresponding methods on the board module. When the worker receives the initialize command, data must contain the settings object from the jewel namespace in the parent.

Remember that the board.initialize() function takes a callback function as its first and only argument. A special callback function is defined in the worker and passed to board.initialize() and any other asynchronous board methods. When the callback function is called, the dataparameter is sent to the main thread as a message, and it is then up to the main thread to handle the callback message.

Keeping the same interface

Now you can put this new worker module to use. The game should be able to run both with and without worker support, so ideally, you need a new worker-based board module that has the same interface as the non-worker board.js module. Any functions exposed in the board module must also exist in the worker board module with the same signatures; specifically, the functions initialize(), swap(), and getBoard(). The idea is that if those functions exist and follow the same logic, you can replace one module with the other, and the rest of the game is none the wiser. Listing 5.9 shows the initial worker-based board module with the initialize() function. Put the code in a new file called board.worker-interface.js.

Listing 5.9 The Worker Board Module

jewel.board = (function() {

var dom = jewel.dom,

settings,

worker;

function initialize(callback) {

settings = jewel.settings;

rows = settings.rows;

cols = settings.cols;

worker = new Worker(”scripts/board.worker.js”);

}

})();

Currently, initialize() just sets up a new worker object from the worker script. Notice that the callback function is not called from initialize(). The callback must not be called before the worker has done its job and posted the response message back to the board module.

Sending messages

The worker thread must be told to call the initialize() method on the real board module. To do so, you must send messages to the message event handler in Listing 5.8. The post() function in board.worker-interface.js that sends messages to the message event handler is shown in Listing 5.10.

Listing 5.10 Posting Messages to the Worker

jewel.board = (function() {

var dom = jewel.dom,

settings,

worker,

messageCount,

callbacks;

function initialize(callback) {

settings = jewel.settings;

rows = settings.rows;

cols = settings.cols;

messageCount = 0;

callbacks = [];

worker = new Worker(”scripts/board.worker.js”);

}

function post(command, data, callback) {

callbacks[messageCount] = callback;

worker.postMessage({

id : messageCount,

command : command,

data : data

});

messageCount++;

}

})();

The post() function takes three arguments—a command, the data for the command, and a callback function—that must be called when the response is received. To handle callbacks, you need to keep track of the messages posted to the worker. Each message is given a unique id; in this case, I chose a simple incrementing counter. Whenever a message is posted to the worker, the callback is saved in the callbacks array using the message id as index.

You can now use this post() function to create the swap() function. When swap() is called, it must post a “swap” message to the worker, providing it with the four coordinates. Listing 5.11 shows the worker-based swap() in board.worker-interface.js.

Listing 5.11 The Swap Message

jewel.board = (function() {

...

function swap(x1, y1, x2, y2, callback) {

post(“swap”, {

x1 : x1,

y1 : y1,

x2 : x2,

y2 : y2

}, callback);

}

})();

Handling responses

When the callback function is passed to post(), it is entered into the callbacks array so it can be fetched whenever the worker posts a response back to the main thread. The board module listens for responses by attaching a message event handler in initialize() as shown in Listing 5.12.

Listing 5.12 The Message Handler

jewel.board = (function() {

...

function messageHandler(event) {

// uncomment to log worker messages

// console.log(event.data);

var message = event.data;

jewels = message.jewels;

if (callbacks[message.id]) {

callbacks[message.id](message.data);

delete callbacks[message.id];

}

}

function initialize(callback) {

...

dom.bind(worker, “message”, messageHandler);

post(”initialize”, settings, callback);

}

})();

After the event handler is attached, the “initialize” message is sent to the worker. When the worker finishes setting up the board, it calls its own callback function, which posts a message back to the board module, which then calls the callback function originally passed to initialize().

The only function missing now is getBoard(), which you can copy verbatim from the board.js module. You can also copy the print() and getJewel() functions if you want to inspect the board data. The final step is to expose the methods at the end of the module, as Listing 5.13 shows.

Listing 5.13 Exposing the Public Methods

jewel.board = (function() {

...

return {

initialize : initialize,

swap : swap,

getBoard : getBoard,

print : print

};

})();

Loading the right module

Now you have two board modules: the one from Chapter 4 and the new one that delegates the work to a worker thread. Because workers are supported in only some browsers, the new board module must be loaded only if workers are available. If they are not, the game must fall back to the regular board module. Listing 5.14 shows the modifications to the loader.

Listing 5.14 Loading the Worker-based Board Module

// loading stage 2

if (Modernizr.standalone) {

Modernizr.load([

{

load : [

“scripts/screen.main-menu.js”

]

},{

test : Modernizr.webworkers,

yep : “scripts/board.worker-interface.js”,

nope : “scripts/board.js”

}

]);

}

Add a new test group to the loader after the standalone test. The new test uses Modernizr’s Web Worker detection to test for Web Worker support and loads the appropriate module. If you want to disable the Web Worker module, you can just set Modernizr.webworkers = false before Modernizr starts loading the files.

Preloading the worker module

When you use the Worker() constructor to create a new worker, the script is automatically pulled from the server. It would be nice if the file were already in the cache so the user didn’t have to wait for the additional HTTP request before the game could begin.

You could easily just add the script to the Modernizr.load() calls in the loader script. However, because the worker script uses functions that are not available outside the worker, most notably the importScripts() function, errors would occur. A simple fix is to test for the presence of that function and execute the rest of the code only if it exists. An arguably more elegant method would just make sure the script is only loaded and not executed. Yepnope, Modernizr’s built-in script loader, has some easy-to-use extension mechanisms that you can use for such purposes. A few examples are included in the downloadable package from http://yepnopejs.com/, one of which is in fact a preloading extension.

Prefixes are a neat feature in yepnope. They enable you to define a prefix that, if found in the script path, triggers some extra functionality. Listing 5.15 shows the yepnope preload extension added in the loader script.

Listing 5.15 Extending yepnope

...

// extend yepnope with preloading

yepnope.addPrefix(“preload”, function(resource) {

resource.noexec = true;

return resource;

});

// loading stage 1

Modernizr.load([

...

]);

This prefix lets you add “preload!” to the file paths passed to Modernizr.load(). If a file has the prefix, the script doesn’t execute. Listing 5.16 shows the worker script added to the loader with the preload prefix.

Listing 5.16 Preloading the Worker Module

// loading stage 2

if (Modernizr.standalone) {

Modernizr.load([

{

load : [

“scripts/screen.main-menu.js”

]

},{

test : Modernizr.webworkers,

yep : [

“scripts/board.worker-interface.js”,

“preload!scripts/board.worker.js”

],

nope : “scripts/board.js”

}

]);

}

The yep case is changed to an array of scripts, adding the worker script with the new preload prefix. Because of the preload prefix, this file is only loaded, not executed. The worker-specific code therefore cannot cause problems.

Summary

In this chapter, you saw how to use Web Workers to free up the main UI thread by delegating any CPU-intensive tasks to workers running in the background. You learned how to create worker objects and use the messaging API to send messages back and forth between the worker and parent script.

You also used that knowledge to implement a worker-based board module that uses the existing board logic but runs in a separate worker thread. I hope you have gained a better understanding of the possibilities that lie in Web Workers and how they can potentially change the way web applications are written as well as the amount of processing you can do in the browser.