Thursday, December 21, 2017

Originally, the specification for WebAssembly called for explicit caching of a compiled WebAssembly module to HTML5 IndexedDB.

Firefox and Edge added support for serialization to IndexedDB but, after some discussion between the WebAssembly Community Group, browser makers, and others, it has been decided that it will be best for browsers to implicitly cache WebAssembly modules instead.

The content of this article should not be used and any explicit caching already in place in your code should be removed.

This article is a continuation of a series 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 dig into an important topic.

One of the main reasons why we would want to use a WebAssembly module in the first place is for the performance improvements that it brings but we haven't yet made use of one key performance item.

Typically, when you go to a webpage that has a JavaScript file, the browser will cache it so that it doesn't have to download it again. The next time you go to that same webpage, if the file is in the browser's cache, it will load that rather than pull it from the server which saves time.

If you've been watching your network traffic, while working with the WebAssembly examples so far, you may have noticed that the wasm file is requested every time your page is loaded which isn't desired if the module hasn't changed.

HTML5 IndexedDB

WebAssembly modules were created with the ability to cache the compiled module in mind but the trick is that the caching is something that needs to be done explicitly by us.

In JavaScript, modules are cached to IndexedDB.

Up until this point, with all of my WebAssembly testing, I've simply been using the file system and double clicking on the html file to test things in the browser.

For security reasons, however, browsers will not allow websites to access IndexedDB from a local file which means we need to set up a server of some sort. I'm a developer on Windows so I'm going to use IIS.

IIS has a whitelist of file extensions that it will allow a website to provide and .wasm was not in my list which resulted in a 404 error when the page tried to fetch the wasm file.

Adding .wasm to the list in IIS is fairly simple:

Open up Internet Information Services (IIS) Manager(found in Control Panel, Administrative Tools)

You can set this at the root, Default Web Site, or at the individual Application level

Double click on the MIME Types link

Click the Add... link on the Actions pane to the right

Enter wasm for the file name extension

Enter application/wasm for the MIME type

Click OK

Caching

So far, we've been working with the module's instance but the result object from the WebAssembly.instantiate call also returns a module object which is the compiled module that we can cache in an IndexedDB database:

// Request the wasm file from the server and compile it...
fetch(sWasmURI).then(response =>

response.arrayBuffer()

).then(bytes =>

WebAssembly.instantiate(bytes, g_importObject)

).then(results => {

// We've been working with the .instance object so farobjModuleInstance = results.instance;

// The results object also holds a .module object which is// what we can cache:// results.module

});

When we retrieve the compiled module from the cache, we will need to pass it to WebAssembly.instantiate but there are a couple of differences compared to when we download the file.

The first difference is that we don't need to do a fetch or set up an arrayBuffer.

The second difference is that the return object from the instantiate object is the instance itself.

WebAssembly.instantiate(objModule, g_importObject).then(instance =>

g_objModuleInstance = instance

);

The following is some example code that shows how you can work with IndexedDB to cache and load WebAssembly modules:

// We've set things up in such a way so that each wasm file can// have a version and we only clear the items from the cache if// the version doesn't matchvar g_sTestWasmURI = "test.wasm";
var g_sTestWasmVersion = "1.0.0";

// Check to see if the module is cached and, if so, use that.// Otherwise, download the module and cache it.GetCompiledModuleFromIndexedDB(g_sTestWasmURI, g_sTestWasmVersion);

function GetCompiledModuleFromIndexedDB(sWasmURI, sWasmVersion) {

// If we successfully opened the database then...OpenDB(DB_NAME, DB_VERSION, HandleUpgradeDB).then(dbConnection => {

// If we successfully obtained the requested record then...GetRecordFromObjectStore(dbConnection, DB_OBJSTORE_MODULES, sWasmURI).then(objRecord => {

// If the version stored for this module doesn't match the// version we need then the module cached is out of date...if (objRecord.WasmVersion !== sWasmVersion) {

// Have the record deleted and then fetch the proper fileDeleteRecordFromObjectStore(dbConnection, DB_OBJSTORE_MODULES, sWasmURI).then(result => {

LoadWebAssemblyFromFile(dbConnection, sWasmURI, sWasmVersion);

});

}
else { // The cached record is the version we need...

// Have the module instantiated.//// NOTE: Unlike when we pass in the bytes to instantiate// in the LoadWebAssemblyFromFile method below, 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(objRecord.WasmModule, g_importObject).then(instance =>

g_objModuleInstance = instance // Hold onto the module's instance so that we can reuse it

);

}

}, sErrorMsg => { // Error in GetRecordFromObjectStore...

// We weren't able to pull the module from cache (most// likely because it doesn't exist yet - hasn't been cached// yet). Log the error and then fetch the file.console.log(sErrorMsg);
LoadWebAssemblyFromFile(dbConnection, sWasmURI, sWasmVersion);

});

}, sErrorMsg => { // Error in OpenDB...

// Log the error and then fetch the file (won't be able to// cache it in this case because we don't have a database// connection to work with)console.log(sErrorMsg);
LoadWebAssemblyFromFile(null, sWasmURI, sWasmVersion);

});

}

// Called by indexeddb if the database was just created or if the// database version was changedfunction HandleUpgradeDB(evt) {

// Request the wasm file from the server and compile it...fetch(sWasmURI).then(response =>

response.arrayBuffer()

).then(bytes =>

WebAssembly.instantiate(bytes, g_importObject)

).then(results => {

// Hold onto the module's instance so that we can reuse itg_objModuleInstance = results.instance;

// Only do the following if we have a database connection// object (this method will be passed a null if we failed to load// the module from cache due to an error when trying to open// the database)if (dbConnection !== null) {

// WARNING: Not all browsers that support WebAssembly// also support the ability to store the module in IndexedDB// (seems to work fine in Edge 16 and in Firefox but it// doesn't work for me in Chrome 63)try {

// Cache the compiled module so that we don't have to// pull the file from the server again unless we change// the module's version number.SaveRecordToObjectStore(dbConnection, DB_OBJSTORE_MODULES, objRecord);

}
catch (ex) {

console.log(`Unable to save the WebAssembly module to IndexedDB:${ex.message}`);

// This event handler will only be called if we're creating the database// for the first time or if we're upgrading the database to a new// version (this will be triggered before the onsuccess event handler// above if it does get called). Let the calling code handle upgrading// the database if needed to keep this file as generic as possible.dbRequest.onupgradeneeded = fncUpgradeDB;

// If we have a record then...(we have to check because there// won't be a record if the database was just created)var objRecord = evt.target.result;
if (objRecord) { fncResolve(objRecord); }
else { fncReject(`The record '${sRecordID}' was not found in the object store '${sObjectStoreName}'`); }

The following is the C code and command line needed to turn the C 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

Summary

Even though we tried to keep the code as clean as possible, this article was a bit more involved because of everything that is involved when working with IndexedDB databases.

Because this article was focused around WebAssembly caching, we only dug into IndexedDB as deep as was needed.

Fortunately, I had the privilege of writing a DZone Refcard on HTML5 IndexedDB a little while ago that you're more than welcome to check out if you would like more information on the technology.

Edit made on Monday, January 1st, 2018: Changed the MIME Type from application/octet-stream to application/wasm after discovering the WebAssembly meeting notes from October 23rd, 2017 proposing the MIME Type be submitted to IANA to make it official.