WebAssembly in Action

Author of the book "WebAssembly in Action"
Save 40% with the code: ggallantbl
The book's original source code can be downloaded from the Manning website and GitHub. The GitHub repository includes an updated-code branch that has been adjusted to work with the latest version of Emscripten (currently version 3.1.44).
Showing posts with label postMessage. Show all posts
Showing posts with label postMessage. Show all posts

Wednesday, December 27, 2017

WebAssembly - Web Workers


This is a continuation of a series of articles exploring how we can build and work with WebAssembly modules using Emscripten. The previous articles are not required reading to understand what we're going to cover today but, if you're curious, you can find them here:
Today we're going to continue using a bare-bones WebAssembly module (no Emscripten built-in helper methods) just to keep things as clear as possible as we examine HTML5 Web Workers.

Web Workers allow JavaScript code to run in a separate thread from the browser window's UI thread. This allows long-running scripts to do their processing without interfering with the responsiveness of the browser's UI.

Because Web Workers are running in a separate thread from the UI, the worker threads have no access to the DOM or other UI functionality. In addition, Web Workers are not intended to be used in large numbers and are expected to be long-lived since they have a high start-up performance cost and high memory cost per worker instance.

WebAssembly modules can be loaded in either the UI thread or in a Web Worker. You would then take the compiled module and pass that to another thread, or even another window, by using the postMessage method.

Since you will only be passing the compiled module to the other thread, you can call WebAssembly.compile rather than WebAssembly.instantiate to get just the module.

The following is some code that will request the wasm file from the server, compile it into a module, and then pass the compiled module to a Web Worker:

fetch("test.wasm").then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.compile(bytes)
).then(WasmModule =>
g_WebWorker.postMessage(WasmModule)
);

The code above will also work in a Web Worker if you wanted to do this in reverse (load and compile the module in a Web Worker and then pass the module to the UI thread). The only difference would be that you would use self.postMessage as opposed to g_WebWorker.postMessge in the example above.

The following is some example code that creates a Web Worker, loads the wasm file, and passes the compiled module to the Web Worker:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<input type="button" value="Test" onclick="OnClickTest();" />

<script type="text/javascript">
// Create a Web Worker (separate thread) that we'll pass the
// WebAssembly module to.
var g_WebWorker = new Worker("WebWorker.js");
g_WebWorker.onerror = function (evt) {
console.log(`Error from Web Worker: ${evt.message}`);
}
g_WebWorker.onmessage = function (evt) {
alert(`Message from the Web Worker:\n\n ${evt.data}`);
}


// Request the wasm file from the server and compile it...(Typically
// we would call 'WebAssembly.instantiate' which compiles and
// instantiates the module. In this case, however, we just want the
// compiled module which will be passed to the Web Worker. The
// Web Worker will be responsible for instantiating the module.)
fetch("test.wasm").then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.compile(bytes)
).then(WasmModule =>
g_WebWorker.postMessage({ "MessagePurpose": "CompiledModule", "WasmModule": WasmModule })
);

function OnClickTest() {
// Ask the Web Worker to add two values
g_WebWorker.postMessage({ "MessagePurpose": "AddValues", "Val1": 1, "Val2": 2 });
}
</script>
</body>
</html>

The following is the content of our WebWorker.js file:

var g_importObject = {
'env': {
'memoryBase': 0,
'tableBase': 0,
'memory': new WebAssembly.Memory({ initial: 256 }),
'table': new WebAssembly.Table({ initial: 0, element: 'anyfunc' })
}
};

// The WebAssembly module instance that we'll be working with
var g_objInstance = null;


// Listen for messages from the main thread. Because all messages to this
// thread come through this method, we need a way to know what is being
// asked of us which is why we included the MessagePurpose property.
self.onmessage = function (evt) {
// If we've been asked to call the module's Add method then...
var objData = evt.data;
var sMessagePurpose = objData.MessagePurpose;
if (sMessagePurpose === "AddValues") {

// Call the add method in the WebAssembly module and pass the
// result back to the main thread
var iResult = g_objInstance.exports._add(objData.Val1, objData.Val2);
self.postMessage(`This is the Web Worker...The result of ${objData.Val1.toString()} + ${objData.Val2.toString()} is ${iResult.toString()}.`);

} // If we've been passed a compiled WebAssembly module then...
else if (sMessagePurpose === "CompiledModule") {

// NOTE: Unlike when we pass in the bytes to instantiate, we don't
// have a separate 'instance' and 'modules' object returned in this
// case since we started out with the module object. We're only
// passed back the instance in this case.
WebAssembly.instantiate(objData.WasmModule, g_importObject).then(instance =>
g_objInstance = instance // Hold onto the module's instance so that we can reuse it
);

}
}

The following is the C code as well as the command line needed to turn the code into a WebAssembly module for today's article:

int add(int x, int y){ return x + y; }

emcc test.c -s WASM=1 -s SIDE_MODULE=1 -O1 -o test.wasm


Inline Web Workers

While reading about using Web Workers, I ran across one comment about how this adds yet another network request (one for the wasm file and now another for the Web Worker's JavaScript file).

The following may not work for every situation, especially if the Web Worker's code is large or complex, but there is a way to create an inline Web Worker by using a Blob object.

You can pass a Blob object a string of JavaScript but I found that using a literal string was difficult to work with and that having a section in the HTML felt more natural which is why we're using a custom Script tag in the sample code below.

The following is an example of how you can create an inline Web Worker:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<input type="button" value="Test" onclick="OnClickTest();" />

<script id="scriptWorker" type="javascript/worker">
var g_importObject = {
'env': {
'memoryBase': 0,
'tableBase': 0,
'memory': new WebAssembly.Memory({ initial: 256 }),
'table': new WebAssembly.Table({ initial: 0, element: 'anyfunc' })
}
};

// The WebAssembly module instance that we'll be working with
var g_objInstance = null;

// Listen for messages from the main thread. Because all
// messages to this thread come through this method, we need a
// way to know what is being asked of us which is why we included
// the MessagePurpose property.
self.onmessage = function (evt) {
// If we've been asked to call the module's Add method then...
var objData = evt.data;
var sMessagePurpose = objData.MessagePurpose;
if (sMessagePurpose === "AddValues") {

// Call the add method in the WebAssembly module and pass
// the result back to the main thread
var iResult = g_objInstance.exports._add(objData.Val1, objData.Val2);
self.postMessage(`This is the Web Worker...The result of ${objData.Val1.toString()} + ${objData.Val2.toString()} is ${iResult.toString()}.`);

}// If we've been passed a compiled WebAssembly module then...
else if (sMessagePurpose === "CompiledModule") {

// NOTE: Unlike when we pass in the bytes to instantiate, we
// don't have a separate 'instance' and 'modules' object
// returned in this case since we started out with the module
// object. We're only passed back the instance in this case.
WebAssembly.instantiate(objData.WasmModule, g_importObject).then(instance =>
g_objInstance = instance // Hold onto the module's instance so that we can reuse it
);

}
}
</script>

<script type="text/javascript">
// Load the text from our special Script tag into a Blob and then
// grab the URI from the blob
var bInlineWorker = new Blob([document.getElementById("scriptWorker").textContent]);
var sBlobURL = window.URL.createObjectURL(bInlineWorker);

// Create a Web Worker (separate thread) that we'll pass the
// WebAssembly module to.
var g_WebWorker = new Worker(sBlobURL);
g_WebWorker.onerror = function (evt) {
console.log(`Error from Web Worker: ${evt.message}`);
}
g_WebWorker.onmessage = function (evt) {
alert(`Message from the Web Worker:\n\n ${evt.data}`);
}


// Request the wasm file from the server and compile it...(Typically
// we would call 'WebAssembly.instantiate' which compiles and
// instantiates the module. In this case, however, we just want the
// compiled module which will be passed to the Web Worker. The
// Web Worker will be responsible for instantiating the module.)
fetch("test.wasm").then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.compile(bytes)
).then(WasmModule =>
g_WebWorker.postMessage({ "MessagePurpose": "CompiledModule", "WasmModule": WasmModule })
);

function OnClickTest() {
// Ask the Web Worker to add two values
g_WebWorker.postMessage({ "MessagePurpose": "AddValues", "Val1": 1, "Val2": 2 });
}
</script>
</body>
</html>


Summary

One nice thing about Web Workers is that they also have access to IndexedDB which means the worker thread can handle caching too if you wish to work with WebAssembly modules entirely from the worker thread.

We didn't dig into caching a WebAssembly module in this article but, if you're interested, you can check out our previous article: WebAssembly - Caching to HTML5 IndexedDB

The focus of today's article with Web Workers was just around what was needed when working with WebAssembly modules. If you'd like to know more about Web Workers, I have several articles that may be of interest:


A New Book

I’ve been honored with an opportunity to write a book about WebAssembly.

If you enjoyed this article and would like to know more about WebAssembly, I welcome you to check out my book: WebAssembly in Action

Monday, August 2, 2010

An Introduction to HTML 5 Web Workers


Prior to HTML 5 Web Workers, all JavaScript code would run in the same thread as the UI of that browser window. The result was that all long-running scripts would cause the browser window to freeze until processing finished. If the script took long enough, the browser would prompt the user to see if he/she wanted to stop the unresponsive script.

Web workers allow for script to run in a separate thread from the browser window's UI. This allows long-running scripts to take place without interfering with the browser's user interface (the window stays responsive to input, like clicks and scrolling, even while processing).

As a result of running in a separate thread from the browser's UI the worker thread does not have direct access to the DOM or other UI objects.

If the worker thread needs the UI updated based on processing, the JavaScript that created the worker thread can set up a callback function to listen for messages from the worker. The main UI thread could then respond to the messages from the worker and do the proper UI modification.

One thing to be aware of with web workers is that they are not intended to be used in large numbers and are expected to be long-lived. The worker threads also have a high start-up performance cost as well as a high memory cost per worker instance.


Types of Web Workers

There are two types of web workers available: Dedicated Workers and Shared Workers.

Dedicated workers are linked to their creator. They are also a bit simpler to create and work with than shared workers.

Shared workers allow any script from the same origin/domain to obtain a reference to the worker and communicate with it.


Browser Support

Dedicated Web Workers are supported in Firefox 3.5+, Chrome 5+, and Safari 5+ (I've read that dedicated web workers are supported in Chrome 4 and Safari 4 as well but I haven't tested those browsers to know for sure).

Shared Web Workers are supported in Chrome 5.0+ and Safari 5.0+. I haven't tested Shared Web Worker support in Chrome 4 or Safari 4 so I don't know if those browsers support it or not.

Firefox does not appear to support Shared Web Workers (tested in 3.6.8 as well as 4.0 beta 2).

Internet Explorer does not yet support either type of web worker (tested in IE 8 as well as IE 9 Preview 3).


Test for Browser Features

Because web workers are a relatively new technology, not all browsers will support them. Even in a browser that supports one type of web worker, it might not support the other type (Firefox, for example, currently only supports dedicated web workers).

It is always best to test if a browser supports a particular feature before trying to use it.

The following is an example of how you would test if a browser supports Dedicated Web Workers or Shared Web Workers:

// Test if Dedicated Web Workers are available
if (window.Worker) { g_bDedicatedWorkersEnabled = true; }

// Test if Shared Web Workers are available
if (window.SharedWorker) { g_bSharedWorkersEnabled = true; }


Dedicated Web Workers

To create a dedicated web worker, you simply create a new instance of the Worker object passing in a string that specifies the location of the JavaScript file for the worker code. The following is an example of creating a dedicated worker instance:

var aWorker = new Worker("DedicatedWebWorker.js");

The worker object was designed to accept a string (location of the JavaScript file) rather than a function in order to prevent a developer from creating a closure that would allow the worker thread to gain direct access to the browser's DOM or other page elements.

Communication between the creator of the worker thread and the worker thread itself is achieved by means of postMessage calls. To pass a message to the worker thread you would do the following:

aWorker.postMessage("Hello from the main page");

In order for the creator of the worker thread to receive messages from the worker thread, you will need to set up a function to receive the 'onmessage' event. The following is an example of how you would attach a function to the onmessage event:

aWorker.onmessage = OnWorkerMessage;

function OnWorkerMessage(evt) {
alert("Worker's message: " + evt.data);
}

Within the JavaScript file of the worker thread (DedicatedWebWorker.js in our case) you would attach a function to the 'onmessage' event to receive messages from the thread that created the worker.

The following is an example of a worker thread attached to the onmessage event:

onmessage = OnCreatorMessage;

function OnCreatorMessage(evt) {
var sReturnMessage = ("Hello from the worker thread! This is what you sent my way: " + evt.data);

// Pass our message back to the creator's thread
postMessage(sReturnMessage);
}


Shared Web Workers

Shared web workers provide a way for any window, from the same origin/domain (SomeWebSite.com for example), to share the use of a worker thread.

Since worker threads take time to start initially and use a lot of memory per worker, being able to share the worker rather than creating one for each open window can improve performance. The trade off, however, is that the shared worker objects are a bit more involved to set up than dedicated workers are.

To create a shared web worker, you create an instance of the SharedWorker object and pass in a string indicating the location of the JavaScript file for the worker thread. The following is an example of creating a SharedWorker instance:

var aSharedWorker = new SharedWorker("SharedWorker.js");

To receive messages from the shared worker thread you attach to the 'onmessage' event but in a slightly different way compared to that of a dedicated web worker. In this case, you need to access the 'port' object of the shared worker instance and then attach to the port object's onmessage event as in the following example:

aSharedWorker.port.onmessage = OnWorkerMessage;

Rather than using the above method to attach to the onmessage event, you can also use the addEventListener function as in the following example:

// uses 'message' (not 'onmessage' as in previous examples) aSharedWorker.port.addEventListener("message", OnWorkerMessage, false);

// Required when using addEventListener
aSharedWorker.port.start();

The OnWorkerMessage function itself is shown below (no difference compared to how it worked with dedicated web workers):

function OnWorkerMessage(evt) {
alert("Worker's message: " + evt.data);
}

For each additional window that wants to connect with the shared worker, you simply repeat the above steps in each window.


Communication with the shared worker thread is achieved by means of postMessage calls on the port object. To pass a message to the shared worker thread you would do the following:

aSharedWorker.port.postMessage("Hello from Page 1");


Within the JavaScript file of the shared worker thread (in our case SharedWorker.js) you first attach to the 'onconnect' event which is called whenever a new object obtains a reference to the shared worker thread.

When the onconnect event fires you then attach to the onmessage event of the port passed in so that you can send/receive messages with the newly connected object.

// event triggered whenever an object attaches to this shared worker
onconnect = function(evt) {
// Get the port of the newly attached object. Attach to the onmessage
// event so that we can send/receive messages with the newly
// attached object

var port = evt.ports[0];
port.onmessage = function(e) { OnCallerMessage(e, port); }
}

function OnCallerMessage(evt, port) {
var sReturnMessage = ("Hello from the SharedWorker thread! This is what you sent my way: " + evt.data);

port.postMessage(sReturnMessage);
}


In Closing

I hope you found this post useful in understanding the basics of HTML 5 Web Workers.

For more detailed information the the HTML 5 Web Workers specification you can view it by clicking on the following link: http://dev.w3.org/html5/workers/


Update: If you are interested, I have now posted a follow-up to this article called 'A Deeper Look at HTML 5 Web Workers' which examines things like multiple shared workers, importing JavaScript files, and using the XMLHttpRequest object from within a worker thread.

New: I have recently written an article for DZone.com which combines the Web Worker information presented in my blog posts and introduces some new information like transferable objects, inline workers, and includes a link to a sample project stored on github.com. The article can be found here: http://refcardz.dzone.com/refcardz/html5-web-workers