Wednesday, December 27, 2017

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:

// 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 =>

// The WebAssembly module instance that we'll be working withvar 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) {

// Call the add method in the WebAssembly module and pass the// result back to the main threadvar 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()}.`);

// 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:

// 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) {

// 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()}.`);

// 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 blobvar bInlineWorker = new Blob([document.getElementById("scriptWorker").textContent]);
var sBlobURL = window.URL.createObjectURL(bInlineWorker);

// 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 =>

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.

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: