Feel free to comment and annotate via the Hypothes.is sidebar on the right. 👉

Introduction

I’ve recently become intrigued by WebAssembly, a new technology recently adopted by all the major web browsers for fast program execution. WebAssembly is essentially a binary set of instructions that can be generated by compiling programs in languages like C++, and then run like JavaScript. Because the WebAssembly format is compact, does not need to be parsed, and is designed for optimal compiling in browsers, it loads and runs very fast, at speeds approaching native compiled programs.1

Why might WebAssembly be relevant for the R ecosystem? R already has a lot of interaction with code in the browser via htmlwidgets. htmlwidgets execute JavaScript generated by R functions in the browser, either in pure HTML documents or in Shiny apps where there is two-way communication between JavaScript and R. WebAssembly could make the browser-side of this execution faster.

What I find most interesting, though is that WebAssembly can be compiled from the same C or C++ code that is compiled and run through R directly2. That means that we can write an algorithm once, compile it into both native and WebAssembly instructions, and then run it in either R or in the browser.

Why would you want to do this? Well, one might want to use the same algorithm as part of R-focused model-building and fitting, and then again in simulating or predicting from that already-fit model in the browser. The former task may be very compute intensive and require an R development environment and a lot of computer power, but the latter task may be incorporated into a user interface in the browser and require much less power, though still benefit from efficient execution. htmlwidgets can link these two tasks, generating browser-based programs based on parameters passed from R.

You see this paradigm in things like kerasjs, which can make predictions from Keras models in the browser with no server back-end. Fitting the Keras models, though, requires the full machinery of Keras and modeling interfaces like the R Keras package .

In my work, I am interested in stochastic simulation models for epidemic processes such as disease outbreaks. These simulations can run very fast individually, but most useful outputs require many repeated simulations. Fitting them to data is a complex, compute-intensive task for which I generally use iterated filtering methods in R and C++. A fit model could be usefully deployed as a web page, though, so that practitioners could see simulations, and explore parameter sensitivities or intervention scenarios.

This repo is a proof-of-concept exercise in running an algorithm written in C++ in both R and the browser. Below I go through the steps I took to build a minimal package, and then demonstrate the outputs and performance.

Writing the core C++ simulator

First, I wrote a standalone C++ simulation function. All this does is simulate a Poisson process: counting random, discrete events in a fixed time period. It’s effectively just a random number generator, but I picked it because it’s the simplest component of my ultimate goal, a general Gillespie algorithm simulator and its various accelerated flavors. This goes in src/ssa.h

#include <random>
#include <chrono>
int ssa(double rate, double max_time, unsigned seed = 0)
{

  //since this is pure C++ that may have multiple targets we do not
  //use the convenient Rcpp interface to R's RNG generators
  if(seed == 0) {
   seed = std::chrono::system_clock::now().time_since_epoch().count();
  }
  std::mt19937 gen(seed);
  std::exponential_distribution<double> jump(rate);

  double time = 0;
  int steps = 0;
  while(time < max_time) {
    time += jump(gen);
    steps += 1;
  }
  return steps;
}

Note that, since this code will not only run under R, I’m using C++ random number generator rather than Rcpp’s convenient interface to the ones in the R library. In fact, there’s no R or Rcpp-specific code or headers here at all.

Making wrappers for both R and JavaScript

Next, I wrote a tiny wrapper for this function so as to export it to R via Rcpp as rssa(), and put it in src/ssa.cpp.

#include <Rcpp.h>
#include "ssa.h"

//' Rcpp Stochastic Simulation Algorithm
//' @export
// [[Rcpp::export(rng = false)]]
int rssa(double rate, double max_time, unsigned seed = 0) {
  return ssa(rate, max_time, seed);
}

Then I wrote another wrapper, this one to export the same function to WebAssembly and JavaScript via emscripten, which is the dominant compiler for this. As this code will eventually be part of an htmlwidget, I put it in. inst/htmlwidgets/lib/emssa/emssa.cpp:

// quick_example.cpp
#include <emscripten/bind.h>
#include  "../../../../src/ssa.h"  //there' probably a better way to do this

using namespace emscripten;

EMSCRIPTEN_BINDINGS(my_module) {
  function("ssa", &ssa);
}

The EMPSCIPTEN_BINDINGS() function here takes the ssa function defined in src/ssa.h and tells empscripten to make it visible in the compiled WebAssembly/JS module it creates.

Compiling C++ to WebAssembly

I ran emscripten’s em++ compiler to compile the C++ to a WebAssembly module that can be loaded in the browser. Rather that figure out how to get emscripten installed, I just used a docker image to run it. I put this command in configure, which probably isn’t CRAN-friendly but conveniently runs it before building the package:

#!/bin/sh

docker run --rm -v "$(pwd):/src" trzeci/emscripten em++ \
  inst/htmlwidgets/lib/emssa/emssa.cpp \
  -o inst/htmlwidgets/lib/emssa/emssa.js \
  --bind \
  -s WASM=1 \
  -s SINGLE_FILE=1 \
  -O3  -std=c++11 \
  -s "EXTRA_EXPORTED_RUNTIME_METHODS=['addOnPostRun']"

Some notes on this beast of a command:

  • The first line runs em++ from the docker image, mounting the project directory into the Docker container’s src/ directory, where compilation happens.
  • The --bind argument tells emscripten to use set up bindings to the functions exported in emssa.cpp
  • -s WASM=1 compiles to WebAssembly, and s SINGLE_FILE=1 tells empscripten to inline the compiled WebAssembly (.wasm) file into a .js JavaScript wrapper as a data URI.3
  • -O3 and -std=c++11 are C++ compilation settings.
  • -s "EXTRA_EXPORTED_RUNTIME_METHODS=['addOnPostRun']" gives us this extra method to use in JavaScript code that calls the module. You’ll see what it’s for when I build the htmlwidget.

After compilation, there’s an emssa.js file in inst/htmlwidgets/lib/emssa. If you inspect it, you’ll see that wasmBinaryFile is defined in it as a data URI.

Making the htmlwidget

As the very handy htmlwidgets intro explains, I need three files to turn a JavaScript library into an R htmlwidget: the dependencies file, the R binding, and the JavaScript binding. Conveniently, htmlwidgets::scaffoldWidget("wassa") sets up template files for me. First, I edited the dependencies file, inst/htmlwidgets/wassa.yaml, to depend on the newly created emssa.js file:

dependencies:
  - name: emssa
    version: 0.0.1
    src: htmlwidgets/lib/emssa
    script:
      - emssa.js

Next, I created the R binding in R/wassa.R. This is a function that creates the htmlwidget. It takes the same arguments as the R and C++ simulator functions, and passes them to the JavaScript widget as the parameter list x.4 The sizingPolicy part just affects how the widget is displayed.

#' Web Assembly Stochastic Simulation Algorithm
#' @import htmlwidgets
#' @export
wassa <- function(rate, max_time, seed = 0) {
  x = list(rate = rate, max_time = max_time, seed = seed)
  htmlwidgets::createWidget(name = 'wassa',  x = x, width="100%", height="100%",
                            sizingPolicy = htmlwidgets::sizingPolicy(
                              viewer.fill = FALSE
                            ))
}

Finally, I created the JavaScript binding in inst/htmlwidgets/wassa.js. My JS skills are atrocious, but eventually I got this together:

HTMLWidgets.widget({
  name: 'wassa',
  type: 'output',
  factory: function(el, width, height) {
    return {
      renderValue: function(x) {
      Module.addOnPostRun(() => {  // Run this stuff after the wasm module loads
          var start = window.performance.now();  //start timing
          var result = Module.ssa(x.rate, x.max_time, x.seed); //run our function
          var time = window.performance.now() - start;  //stop timing
          el.innerHTML = '<strong>Output: ' + result +  '<br />' + //print results
                         'Time Elapsed: ' + Math.round(time) + ' milliseconds</strong>';
          el.setAttribute("style", "margin:5px; padding:5px; border: thin solid #0000FF;");
        });
      },
      resize: function(width, height) {}
    };
  }
});

This is based on the scaffoldWidget() template. The stuff under factory: generates the htmlwidget itself. When the widget runs the emssa.js dependency, it export a Module object that contains the compiled functions as well as some helpers. Module.addOnPostRun, which I chose to export in the compile step, tells the browser to wait until this the module fully loaded and compiled to run the code. Once that happens, I run the exported compiled function Module.ssa, using the parameters we passed the widget from R in x variable. I wrap this function in some commands to measure run time, then print the output and timing to the window inside a blue box.

Building and running

With all these pieces in place, along with a few other essential package bits generated by various usethis and devtools functions, I ran devtools::document() and devtools::install(), and now we can see the results.

First, here’s the rssa() function which is the Rcpp wrapper for the C++ code:

library(wassa)
rssa(1000, 1)
#> [1] 1020

Now, htmlwidget version. If everything works right, you’ll see the widget output in a blue box5:

wassa(1000, 1)

It works (for me, at least)! My C++ code is running in the browser. If you refresh the page, you’ll see that the widget output in the blue box changes each time. No server is needed - these calculations take place on your machine.

To make sure that both the R and widget versions of the code are doing the same thing, I can run the functions with the same random seed and check that outputs are the same:

rssa(1000, 1, seed=10)
#> [1] 1037
wassa(1000, 1, seed=10)

đź‘Ť !

How does performance compare? I run the function at a higher rate, which will require more iterations, and benchmark:

library(microbenchmark)
microbenchmark(rssa(1000000, 1), times = 200, unit = 'ms')
#> Unit: milliseconds
#>            expr      min       lq     mean   median       uq      max
#>  rssa(1e+06, 1) 30.04584 31.76275 36.48469 33.92648 36.54217 127.1864
#>  neval
#>    200
wassa(1000000, 1)

Not bad! If you refresh a few times, you’ll see the WebAssembly version runs in 130%-150% of the time it takes to run the compiled native version. At least, that’s the case on my laptop. What you see is a comparison between the native compiled code on my machine, and the WebAssembly running on your machine, via the engine in your particular browser. Note that mileage may vary for native speed, too, not just by machine but also by the compiler setting used to build the R package. In my case, the C++ code was compiled with clang 9.0.0 and the -O3 flag, the max level of optimization, just as empscripten was.


  1. I am sure I am getting some of this wrong. Lin Clark has an excellent series of cartoon explanations of WebAssembly.↩

  2. Technically you could do this with asm.js as well before WebAssembly came along.↩

  3. Without SINGLE_FILE, emscripten will create a binary WebAssembly .wasm file and a separate JavaScript wrapper to load it. This is actually the faster way to go. .wasm binaries load fast and can be compiled while streaming. However, R’s htmlwidgets inline all assets as base64-encoded data URIs anyway, and don’t do well with stand-alone binary attachments. In the end, though, this should on affect load time, not run time speed of our function.↩

  4. scaffoldWidget() also creates some Shiny-specific things that I’m dropping for now.↩

  5. You need an up-to-date browser to run WebAssembly, but the four major browsers all had it running as of the end of Feb 2017. The widget should also work in the latest RStudio Viewer pane, though sometimes it fails for reasons I haven’t quite figured out.↩