Skip to content

NLESC-JCER/cpp2wasm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Guide to make C++ available as a web application

Some chapters have been rewritten into blogs at https://github.com/NLESC-JCER/run-cpp-on-web

CI Markdown Link Checker Entangled DOI Quality Gate Status

In this guide, we will describe 5 ways to make your C++ code available as a web application or web wervice

  1. Web service using Common Gateway Interface,
  2. Python web service using pybind11 and OpenAPI,
  3. Python web application using Flask and Celery,
  4. JavaScript web service using Emscripten and Fastify,
  5. JavaScript web application using web worker and React

This guide was written and tested on Linux operating system. The required dependencies to run this guide are described in the INSTALL.md document. If you want to contribute to the guide see CONTRIBUTING.md. The repo contains the files that can be made from the code snippets in this guide. The code snippets can be entangled to files using any of these methods.

A web application is meant for consumption by humans using HTML pages and a web service is meant for consumption by machines or other programs. A web service will accept and return machine readable documents like JSON (JavaScript Object Notation) documents. JSON is a open standard file format and data interchange format that is human-readable.

Example program: Newton-Raphson root finding

The Newton-Raphson root finding algorithm will be the use case. The algorithm is explained in this video series. The code we are using came from geeksforgeeks.org.

Let's first define the mathematical equation and its derivative, which we need in order to find the root.

The equation and its derivative which we will use in this guide are $x^3 - x^2 + 2$ and $3x^2 - 2x$ respectively. The root is the value (x-coordinate) which makes the mathematical equation (y-coordinate) equal to 0. In this equation the root is -1.

Plotted equation

// this C++ code snippet is store as cli/algebra.hpp

namespace algebra
{

// An example equation is x^3 - x^2  + 2
double equation(double x)
{
  return x * x * x - x * x + 2;
}

// Derivative of the above equation which is 3*x^2 - 2*x
double derivative(double x)
{
  return 3 * x * x - 2 * x;
}

} // namespace algebra

Next, we define the interface (C++ class).

// this C++ snippet is stored as cli/newtonraphson.hpp
#ifndef H_NEWTONRAPHSON_H
#define H_NEWTONRAPHSON_H

#include <string>

namespace rootfinding {
  class NewtonRaphson {
    public:
      NewtonRaphson(double tolerancein);
      double solve(double xin);
    private:
      double tolerance;
  };
}

#endif

In this C++ class, solve function will be performing the root finding task. We now need to define the algorithm so that solve function does what it supposed to do.

The implementation of the algorithm is

// this C++ code snippet is later referred to as <<algorithm>>
#include "newtonraphson.hpp"
#include "algebra.hpp"
#include <math.h>

using namespace algebra;

namespace rootfinding
{

NewtonRaphson::NewtonRaphson(double tolerancein) : tolerance(tolerancein) {}

// Function to find the root
double NewtonRaphson::solve(double xin)
{
  double x = xin;
  double delta_x = equation(x) / derivative(x);

  while (fabs(delta_x) >= tolerance)
  {
    delta_x = equation(x) / derivative(x);

    // x_new = x_old - f(x) / f'(x)
    x = x - delta_x;
  }
  return x;
};


} // namespace rootfinding

We are now ready to call the algorithm in a simple CLI program, as follows

// this C++ snippet is stored as cli/newtonraphson.cpp
#include<bits/stdc++.h>
#include <iomanip>

<<algorithm>>

// Driver program to test above
int main()
{
  double x0 = -20; // Initial values assumed
  double epsilon = 0.001;
  rootfinding::NewtonRaphson finder(epsilon);
  double x1 = finder.solve(x0);

  std::cout << std::fixed;
  std::cout << std::setprecision(6);
  std::cout << "The value of the root is : " << x1 << std::endl;

  return 0;
}

The program can be compiled with

g++ cli/cli-newtonraphson.cpp -o cli/newtonraphson.exe

and it can be run with

./cli/newtonraphson.exe

It should return the following

The value of the root is : -1.000000

Web service using Common Gateway Interface

cgi

Pros Cons
❤️ Very few moving parts, just C++ and Apache web server ⛔ Complicated Apache web server configuration
❤️ Proven technology ⛔ Not suitable for long initialization or calculations

The classic way to run programs when accessing a url is to use the Common Gateway Interface (CGI). In the Apache httpd web server you can configure a directory as a ScriptAlias, when visiting a file inside that directory the file will be executed. The executable can read the request body from the stdin and the response must be printed to the stdout. A response should consist of a content type such as application/json or text/html, followed by the content itself. For the web service, we parse and assemble JSON documents using the nlohmann/json.hpp library.

We start writing the CGI script by importing the JSON library and starting the main function.

// this C++ snippet is stored as cgi/cgi-newtonraphson.hpp
#include <string>
#include <iostream>
#include <nlohmann/json.hpp>

<<algorithm>>

int main(int argc, char *argv[])
{

We should parse the JSON request body from the stdin to get the epsilon and guess values.

  // this C++ snippet is appended to cgi/cgi-newtonraphson.hpp
  nlohmann::json request(nlohmann::json::parse(std::cin));
  double epsilon = request["epsilon"];
  double guess = request["guess"];

The root can be found with

  // this C++ snippet is appended to cgi/cgi-newtonraphson.hpp
  rootfinding::NewtonRaphson finder(epsilon);
  double root = finder.solve(guess);

And lastly, return a JSON document with the result

  // this C++ snippet is appended to cgi/cgi-newtonraphson.hpp
  nlohmann::json response;
  response["root"] = root;
  std::cout << "Content-type: application/json" << std::endl << std::endl;
  std::cout << response.dump(2) << std::endl;
  return 0;
}

This can be compiled with

g++ -Icgi/deps/ -Icli/ cgi/cgi-newtonraphson.cpp -o cgi/apache2/cgi-bin/newtonraphson

The CGI script can be tested from the command line

echo '{"guess":-20, "epsilon":0.001}' | cgi/apache2/cgi-bin/newtonraphson

It should output

Content-type: application/json

{
  "root": -1.0000001181322415
}

To host the cgi/apache2/cgi-bin/newtonraphson executable as http://localhost:8080/cgi-bin/newtonraphson CGI script we have to configure Apache like so

# this Apache2 configuration snippet is stored as cgi/apache2/apache2.conf
ServerName 127.0.0.1
Listen 8080
LoadModule mpm_event_module /usr/lib/apache2/modules/mod_mpm_event.so
LoadModule authz_core_module /usr/lib/apache2/modules/mod_authz_core.so
LoadModule alias_module /usr/lib/apache2/modules/mod_alias.so
LoadModule cgi_module /usr/lib/apache2/modules/mod_cgi.so
ErrorLog httpd_error_log
PidFile httpd.pid

ScriptAlias "/cgi-bin/" "cgi-bin/"

Start Apache httpd web server using

/usr/sbin/apache2 -X -d ./cgi/apache2

To test the CGI script we can not use a web browser, but need to use http client like curl. Because, a web browser uses the GET http request method and text/html as content type, but the CGI script requires a POST http request method and JSON string as request body. The curl command with a POST request can be run in another shell with

curl --request POST \
  --data '{"guess":-20, "epsilon":0.001}' \
  --header "Content-Type: application/json" \
  http://localhost:8080/cgi-bin/newtonraphson

Should return the following JSON document as a response

{
  "root":-1.0000001181322415
}

Instead of curl, we could use any http client in any language to consume the web service.

The problem with CGI scripts is when the program does some initialization, you have to wait for it on each visit. It is better to do the initialization once when the web service is starting up.

Python web service

openapi

Pros Cons
❤️ Python is a very popular language and has a large ecosystem ⛔ Pure Python is slower than C++
❤️ Web service is easy to discover and effortlessly documented with OpenAPI specification ⛔ Exception thrown from C++ has number instead of message

Writing a web service in C++ is possible, but other languages like Python are better equipped. Python has a big community making web applications, which resulted in a big ecosystem of web frameworks, template engines, tutorials.

Python packages can be installed using a package manager (pip) from the Python Package Index. It is customary to work with virtual environments to isolate the dependencies for a certain application and not pollute the global OS paths.

Accessing C++ functions with pybind11

To make a web application in Python, the C++ functions need to be called somehow. Python can call functions in a C++ library if its functions use Python.h datatypes. This requires a lot of boilerplate and conversions, several tools are out there that make the boilerplate/conversions much simpler. The tool we chose to use is pybind11 as it is currently (May 2020) actively maintained and is a header-only library.

To use pybind11, it must installed with pip

pip install pybind11

pybind11 requires bindings to expose C++ constants/functions/enumerations/classes to Python. The bindings are implemented by using the C++ PYBIND11_MODULE macro to configure what will be exposed to Python. The bindings can be compiled to a shared library called newtonraphsonpy*.so which can be imported into Python.

For example, the bindings of newtonraphson.hpp:NewtonRaphson class would look like:

// this C++ snippet is stored as openapi/py-newtonraphson.cpp
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

<<algorithm>>

namespace py = pybind11;

PYBIND11_MODULE(newtonraphsonpy, m) {
    py::class_<rootfinding::NewtonRaphson>(m, "NewtonRaphson")
        .def(py::init<double>(), py::arg("epsilon"))
        .def("solve",
             &rootfinding::NewtonRaphson::solve,
             py::arg("guess"),
             "Find root starting from initial guess"
        )
    ;
}

Compile with

g++ -O3 -Wall -shared -std=c++14 -fPIC -Icli/ `python3 -m pybind11 --includes` \
openapi/py-newtonraphson.cpp -o openapi/newtonraphsonpy`python3-config --extension-suffix`

In Python it can be used like so:

# this Python snippet is stored as openapi/example.py
from newtonraphsonpy import NewtonRaphson

finder = NewtonRaphson(epsilon=0.001)
root = finder.solve(guess=-20)
print ("{0:.6f}".format(root))

The Python example can be run with

python openapi/example.py

It will output something like

-1.0000001181322415

Now that the C++ functions can be called from Python it is time to call the function from a web service.

OpenAPI web service using connexion

A web service has a number of paths or urls to which requests can be sent and responses received. The interface can be defined with the OpenAPI specification (previously known as Swagger). The OpenAPI specification defines how requests and responses should look. The OpenAPI specifiation can either be generated by the web service provider or be a static document or contract. The contract-first approach allows for both consumer and provider to come to an agreement on the contract and work more or less independently on implementation. We will use the contract-first approach for our root finding web service example.

To make a web service which adheres to the OpenAPI specification contract, it is possible to generate a skeleton using the generator. Each time the contract changes, the generator must be re-run. The generator uses the Python based web framework Connexion. For the Python based root finding web service, Connexion was used as the web framework as it maps each path+method combination in the contract to a Python function and will handle the validation and serialization. The OpenAPI web service can be tested with Swagger UI, which facilitates browsing through the available paths, trying them out by constructing a request, and showing the curl command which can be used to call the web service. Swagger UI comes bundled with the Connexion framework.

OpenAPI uses JSON schema to describe the structure of the request body and responses.

The request body we want to accept is

{
  "epsilon": 0.001,
  "guess": -20
}

The JSON schema for the request body is

{
  "type": "object",
  "description": "this JSON document is later referred to as <<request-schema>>",
  "properties": {
    "epsilon": {
      "title": "Epsilon",
      "type": "number",
      "minimum": 0
    },
    "guess": {
      "title": "Initial guess",
      "type": "number"
    }
  },
  "required": [
    "epsilon",
    "guess"
  ],
  "additionalProperties": false
}

The response body we want the web service to return is

{
  "root": -1.00
}

The JSON schema for the response body is

{
  "type": "object",
  "description": "this JSON document is later referred to as <<response-schema>>",
  "properties": {
      "root": {
        "title": "Root",
        "type": "number"
      }
  },
  "required": [
      "root"
  ],
  "additionalProperties": false
}

The OpenAPI specification for the web service is:

# this yaml snippet is stored as openapi/openapi.yaml
openapi: 3.0.0
info:
  title: Root finder
  license:
    name: Apache-2.0
    url: https://www.apache.org/licenses/LICENSE-2.0.html
  version: 0.1.0
paths:
  /api/newtonraphson:
    post:
      description: Perform root finding with the Newton Raphson algorithm
      operationId: api.calculate
      requestBody:
        content:
          'application/json':
            schema:
              $ref: '#/components/schemas/NRRequest'
            example:
              epsilon: 0.001
              guess: -20
      responses:
        '200':
          description: The found root
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NRResponse'
components:
  schemas:
    NRRequest:
      <<request-schema>>
    NRResponse:
      <<response-schema>>

The webservice consists of a single path (/api/newtonraphson) with a POST method which receives a request and returns a response. The request and response specifications are specified under #/components/schemas.

The operation identifier (operationId) in the specification gets translated by Connexion to a Python method that will be called when the path is requested. Connexion calls the function with the JSON parsed request body.

# this Python snippet is stored as openapi/api.py
def calculate(body):
  epsilon = body['epsilon']
  guess = body['guess']
  from newtonraphsonpy import NewtonRaphson
  finder = NewtonRaphson(epsilon)
  root = finder.solve(guess)
  return {'root': root}

To provide the calculate method as a web service we must install Connexion Python library (with the Swagger UI for later testing)

pip install connexion[swagger-ui]

To run the web service we have to to tell Connexion which specification it should expose.

# this Python snippet is stored as openapi/webservice.py
import connexion

app = connexion.App(__name__)
app.add_api('openapi.yaml', validate_responses=True)
app.run(port=8080)

The web service can be started with

python openapi/webservice.py

We can try out the web service using the Swagger UI at http://localhost:8080/ui/, or by running a curl command like

curl --request POST \
  --header "accept: application/json" \
  --header "Content-Type: application/json" \
  --data '{"epsilon":0.001,"guess":-20}' \
  http://localhost:8080/api/newtonraphson

Python web application using Flask

flask

Pros Cons
❤️ Minimalist and modular framework ⛔ Needs additional packages for extra functionality
❤️ A lot of examples and good documentation ⛔ Lots of moving parts: web service + worker + Redis queue

The Python standard library ships with a HTTP server which is very low level. A web framework is an abstraction layer for making writing web applications more pleasant. To write our web application we will use the Flask web framework. Flask was chosen as it minimalistic and has a large active community.

The Flask Python library can be installed with

pip install flask

We'll use the shared library that the openapi example also uses:

cd flask && ln -s ../openapi/newtonraphsonpy`python3-config --extension-suffix` . && cd -

Our web application will have 2 pages:

  1. a page with form and submit button,
  2. and a page which shows the result of the calculation.

Each page is available on a different url. In Flask the way urls are mapped to Python function is done by adding a route decorator (@app.route) to the function.

The first page with the form and submit button is defined as a function returning a HTML form.

# this Python code snippet is later referred to as <<py-form>>
@app.route('/', methods=['GET'])
def form():
  return '''<!doctype html>
    <form method="POST">
      <label for="epsilon">Epsilon</label>
      <input type="number" name="epsilon" value="0.001">
      <label for="guess">Guess</label>
      <input type="number" name="guess" value="-20">
      <button type="submit">Submit</button>
    </form>'''

The form will be submitted to the '/' path with the POST method. In the handler of this route we want to perform the calculation and return the result HTML page. To get the submitted values we use the Flask global request object. To construct the returned HTML we use f-strings to replace the variable names with the variable values.

# this Python code snippet is later referred to as <<py-calculate>>
@app.route('/', methods=['POST'])
def calculate():
  epsilon = float(request.form['epsilon'])
  guess = float(request.form['guess'])

  from newtonraphsonpy import NewtonRaphson
  finder = NewtonRaphson(epsilon)
  root = finder.solve(guess)

  return f'''<!doctype html>
    <p>With epsilon of {epsilon} and a guess of {guess} the found root is {root}.</p>'''
  # this Python code snippet is appended to <<py-calculate>>

Putting it all together in

# this Python snippet is stored as flask/webapp.py
from flask import Flask, request
app = Flask(__name__)

<<py-form>>

<<py-calculate>>

app.run(port=5001)

And running it with

python flask/webapp.py

To test we can visit http://localhost:5001 fill the form and press submit to get the result.

The form should look like

Flask form

After pressing submit the result should look like

Flask result

Long-running tasks with Celery

When performing a long calculation (more than 30 seconds), the end-user requires feedback of the progress. In a normal request/response cycle, feedback is only returned in the response. To give feedback during the calculation, the computation must be offloaded to a task queue. In Python, a commonly used task queue is celery. While the calculation is running on some worker it is possible to have a progress page which can check in the queue what the progress is of the calculation.

Our Celery powered web application will have 3 pages:

  1. a page with a form and a submit button,
  2. a page to show the progress of the calculation,
  3. and a page which shows the result of the calculation. Each calculation will have it's own progress and result page.

Celery needs a broker for a queue and result storage. We'll use redis in a Docker container as Celery broker, because it's simple to setup. Redis can be started with the following command

docker run --rm -d -p 6379:6379 --name some-redis redis

To use Celery we must install the redis flavored version with

pip install celery[redis]

Let's set up a method that can be submitted to the Celery task queue. First configure Celery to use the Redis database.

# this Python code snippet is later referred to as <<celery-config>>
from celery import Celery
capp = Celery('tasks', broker='redis://localhost:6379', backend='redis://localhost:6379')

When a method is decorated with the Celery task decorator then it can be submitted to the Celery task queue. We'll add some sleeps to demonstrate what would happen with a long running calculation. We'll also tell Celery about in which step the calculation is; later, we can display this step to the user.

# this Python snippet is stored as flask/tasks.py
import time

<<celery-config>>

@capp.task(bind=True)
def calculate(self, epsilon, guess):
  if not self.request.called_directly:
    self.update_state(state='INITIALIZING')
  time.sleep(5)
  from newtonraphsonpy import NewtonRaphson
  finder = NewtonRaphson(epsilon)
  if not self.request.called_directly:
    self.update_state(state='FINDING')
  time.sleep(5)
  root = finder.solve(guess)
  return {'root': root, 'guess': guess, 'epsilon':epsilon}

Instead of running the calculation when the submit button is pressed, we will submit the calculation task to the task queue by using the .delay() function. The submission will return a job identifier we can use later to get the status and result of the job. The web browser will redirect to a url with the job identifier in it.

# this Python code snippet is later referred to as <<py-submit>>
@app.route('/', methods=['POST'])
def submit():
  epsilon = float(request.form['epsilon'])
  guess = float(request.form['guess'])
  from tasks import calculate
  job = calculate.delay(epsilon, guess)
  return redirect(url_for('result', jobid=job.id))

The last method is to ask the Celery task queue what the status is of the job and return the result when it is succesful.

# this Python code snippet is later referred to as <<py-result>>
@app.route('/result/<jobid>')
def result(jobid):
  from tasks import capp
  job = capp.AsyncResult(jobid)
  job.maybe_throw()
  if job.successful():
    result = job.get()
    epsilon = result['epsilon']
    guess = result['guess']
    root = result['root']
    return f'''<!doctype html>
      <p>With epsilon of {epsilon} and a guess of {guess} the found root is {root}.</p>'''
  else:
    return f'''<!doctype html>
      <p>{job.status}<p>'''

Putting it all together

# this Python snippet is stored as flask/webapp-celery.py
from flask import Flask, render_template, request, redirect, url_for

app = Flask(__name__)

<<py-form>>

<<py-submit>>

<<py-result>>

if __name__ == '__main__':
  app.run(port=5000)

Start the web application like before with

python flask/webapp-celery.py

Tasks will be run by the Celery worker. The worker can be started with

PYTHONPATH=flask celery worker -A tasks

(The PYTHONPATH environment variable is set so the Celery worker can find the tasks.py and newtonraphsonpy.*.so files in the flask/ directory)

To test the web service

  1. Go to http://localhost:5000,

    Celery form

  2. Submit form,

  3. Refresh result page until progress states are replaced with result.

    Celery sleep initializing

    Celery sleep finding

    Celery result

The redis server can be shut down with

docker stop some-redis

JavaScript web service

wasm

Pros Cons
❤️ JavaScript is a powerful language which runs on many different platforms including mobile devices ⛔ OpenAPI spec and JSON schema are slightly out of sync
❤️ Same language on server as in web browser ⛔ Requires server infrastructure for calculations

JavaScript is the de facto programming language for web browsers. The JavaScript engine in the Chrome browser called V8 has been wrapped in a runtime engine called Node.js which can execute JavaScript code outside the browser.

Accessing C++ functions with Emscripten

For a long time web browsers could only execute non-JavaScript code using plugins like Flash. Later, tools where made that could transpile non-JavaScript code to JavaScript, but the performance was less than running native code. To run code as fast as native code, the WebAssembly language was developed. WebAssembly is a low-level, Assembly-like language with a compact binary format. The binary format is stored as a WebAssembly module or *.wasm file, which can be loaded by all modern web browsers and by Node.js on the server.

Instead of writing code in the WebAssembly language, there are compilers that can take C++/C code and compile it to a WebAssembly module. Emscripten is the most popular C++ to WebAssembly compiler. Emscripten has been successfully used to port game engines like the Unreal engine to the browser making it possible to have complex 3D games in the browser without needing to install anything else than the web browser. To call C++ code (which has been compiled to a WebAssembly module) from JavaScript, a binding is required. The binding will map C++ constructs to their JavaScript equivalent and back. The binding called embind is declared in a C++ file which is included in the compilation.

The binding of the C++ code will be

// this C++ snippet is stored as webassembly/wasm-newtonraphson.cpp
#include <emscripten/bind.h>

<<algorithm>>

using namespace emscripten;

EMSCRIPTEN_BINDINGS(newtonraphsonwasm) {
  class_<rootfinding::NewtonRaphson>("NewtonRaphson")
    .constructor<double>()
    .function("solve", &rootfinding::NewtonRaphson::solve)
    ;
}

The algorithm and binding can be compiled into a WebAssembly module with the Emscripten compiler called emcc. The C++ headers are located in the cli/ directory so add it to the include path. To make live easier we configure the compile command to generate a webassembly/newtonraphsonwasm.js file which exports the createModule function. The createModule function loads and initializes the generated WebAssembly module called webassembly/newtonraphsonwasm.wasm for us. The last argument of the compiler is the C++ file with the emscripten bindings.

emcc -Icli/ -o webassembly/newtonraphsonwasm.js \
  -s MODULARIZE=1 -s EXPORT_NAME=createModule \
  --bind webassembly/wasm-newtonraphson.cpp

To use the WebAssembly module in Node.js we need to import it with

// this JavaScript snippet is later referred to as <<import-wasm>>
const createModule = require('./newtonraphsonwasm.js')

The createModule function returns a Promise. We use await to keep the flow flat instead a nested promise chain for easier reading. The module returned by the await call contains the NewtonRaphson class we defined in the emscripten bindings.

// this JavaScript snippet is later referred to as <<find-root-js>>
const module = await createModule()

We create an object from the module.NewtonRaphson class and find the root. We will define the epsilon and guess variables later when we call the code from the command line or from a web service or from a web application.

// this JavaScript snippet is appended to <<find-root-js>>
const finder = new module.NewtonRaphson(epsilon)
const root = finder.solve(guess)

Let's write a command line script to test the WebAssembly module. We get the epsilon and guess from the command line arguments, find the root with the WebAssembly module and print the result. We need to wrap in a async function as Node.js (version 12) does support a top level await.

// this JavaScript snippet stored as webassembly/cli.js
<<import-wasm>>

const main = async () => {
  const epsilon = parseFloat(process.argv[2])
  const guess = parseFloat(process.argv[3])
  <<find-root-js>>
  const msg = 'Given epsilon of %d and inital guess of %d the found root is %s'
  console.log(msg, epsilon, guess, root.toPrecision(3))
}
main()

Run the script with

node webassembly/cli.js 0.01 -20

Should output Given epsilon of 0.01 and inital guess of -20 the found root is -1.00.

In this chapter we executed the Newton-Raphson algorithm on the command line in JavaScript with Node.js by compiling the C++ code to a WebAssembly module with emscripten.

Web service using Fastify

Now that we can execute the C++ code from JavaScript we are ready to wrap it up in a web service. Node.js ships with a low level http server that can be used to write a web service, but we are going to use the Fastify web framework as it supports multiple routes, async/await and JSON schemas.

First we need to install Fastify with the Node.js package manager called npm. We will use --no-save option to skip saving the dependency in package.json as we are not publishing a package.

npm install --no-save fastify

The Fastify web framework can be imported with require.

// this JavaScript snippet is later referred to as <<import-wasm-fastify>>
const fastify = require('fastify')()

Let's start the web service file by importing Fastify and the WebAssembly module with

// this JavaScript snippet stored as webassembly/webservice.js
<<import-fastify>>
<<import-wasm>>

A handler function can be defined which will process a request body JSON object containing the epsilon and guess and returns the found root. We will later configure Fastify to call this method when visiting an url.

// this JavaScript snippet is later referred to as <<fastify-handler>>
const handler = async ({body}) => {
  const { epsilon, guess } = body
  <<find-root-js>>
  return { root }
}

Fastify can use JSON-schema to validate the incoming request and and outgoing response.

Define a Fastify route for a POST request to /api/newtonraphson url which calls the handler function. The request body must be validated against <<request-schema>> and the OK (code=200) response must be validated against <<response-schema>> as defined in the OpenAPI chapter. By defining schemas we implicitly tell the web service it should accept and return application/json as content type.

// this JavaScript snippet appended to webassembly/webservice.js
<<fastify-handler>>

fastify.route({
  url: '/api/newtonraphson',
  method: 'POST',
  schema: {
    body:
      <<request-schema>>
    ,
    response: {
      200:
        <<response-schema>>
    }
  },
  handler
})

Now that the route have been defined we can tell Fastify to listen on http://localhost:<port> for requests and die when an error is thrown.

// this JavaScript snippet is later referred to as <<fastify-listen>>
const main = async (port) => {
  try {
    const host = 'localhost'
    console.log('Server listening on http://%s:%d (Press CTRL+C to quit)', host, port)
    await fastify.listen(port, host)
  } catch (err) {
    console.log(err)
    process.exit(1)
  }
}

Let's listen on http://localhost:3000

// this JavaScript snippet is appended to webassembly/webservice.js
<<fastify-listen>>
main(3000)

Run the web service with

node webassembly/webservice.js

In another terminal test web service with

curl --request POST \
  --header "accept: application/json" \
  --header "Content-Type: application/json" \
  --data '{"epsilon":0.001,"guess":-20}' \
  http://localhost:3000/api/newtonraphson

Should return something like

{
  "root": -1.0000001181322415
}

To test the validation, call the web service with a typo in the epsilon field name

wget --content-on-error --quiet --output-document=- \
  --header="accept: application/json" \
  --header="Content-Type: application/json" \
  --post-data '{"epilon":0.001,"guess":-20}' \
  http://localhost:3000/api/newtonraphson

Should return an error like

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "body should have required property 'epsilon'"
}

OpenAPI web service using fastify-oas

The web service we made in the previous chapter can not tell us which urls or routes it has. We can use a OpenAPI specification for that. As Fastify routes already use JSON schemas for the request body and response body we can generate the OpenAPI specification with the fastify-oas plugin.

Install the plugin with

npm install --no-save fastify-oas

Same as before we need to import Fastify

// this JavaScript snippet is later referred to as <<fastify-openapi-plugin>>
<<import-fastify>>

We need to import the plugin

// this JavaScript snippet is appended to <<fastify-openapi-plugin>>
const oas = require('fastify-oas')

Next we need to register the plugin (oas) and configure it. Configure the plugin by setting the OpenAPI info fields and set all paths to consume/produce the application/json content type and set the urls of the web service. Lastly setting exposeRoute to true will make the plugin add the following routes:

// this JavaScript snippet is appended to <<fastify-openapi-plugin>>
fastify.register(oas, {
  swagger: {
    info: {
      title: 'Root finder',
      license: {
        name: 'Apache-2.0',
        url: 'https://www.apache.org/licenses/LICENSE-2.0.html'
      },
      version: '0.1.0'
    },
    consumes: ['application/json'],
    produces: ['application/json'],
    servers: [{
      url: 'http://localhost:3001'
    }, {
      url: 'http://localhost:3002'
    }]
  },
  exposeRoute: true
})

In the route we would like to define example values. The JSON schema we defined for the request body in the OpeAPI chapter does not allow an example field, but the OpenAPI specifaction does. So we inject the example here.

// this JavaScript snippet is later referred to as <<fastify-openapi-route>>
const requestSchemaWithExample =
  <<request-schema>>
requestSchemaWithExample.example = {
  epsilon: 0.001,
  guess: -20
}

We need to define a route using the same handler as before and the schemas with example request body.

// this JavaScript snippet is appended to <<fastify-openapi-route>>
fastify.route({
  url: '/api/newtonraphson',
  method: 'POST',
  schema: {
    body: requestSchemaWithExample,
    response: {
      200:
        <<response-schema>>
    }
  },
  handler
})

Let's load the WebAssembly module, add the plugin, add the handler and add the route to webassembly/openapi.js file with

// this JavaScript snippet is stored as webassembly/openapi.js
<<import-wasm>>

<<fastify-openapi-plugin>>

<<fastify-handler>>

<<fastify-openapi-route>>

Next we listen on http://localhost:3001.

// this JavaScript snippet is appended to webassembly/openapi.js
<<fastify-listen>>
main(3001)

Run the web service with

node webassembly/openapi.js

The OpenAPI specification is generated in JSON and YAML format. Try the web service out by visiting the Swagger UI.

Or try it out in another terminal with curl using

curl --request POST \
  --header "Content-Type: application/json" \
  --header "accept: application/json" \
  --data '{"guess":-20, "epsilon":0.001}' \
  http://localhost:3001/api/newtonraphson

Long running task with worker threads

The web service we made in the prevous chapter will block any other requests while the algorithm solving is running. This is due to the inner workings of Node.js. Node.js uses a single threaded event loop, so while an event is being handled Node.js is busy. Node.js uses callbacks and promises to handle long IO tasks efficiently.

To use the CPU in parallel Node.js has worker threads. We don't want to start a new thread each time a request is recieved to perform the calculation, we want to use a pool of waiting threads. So each request will be computed by a thread from the pool. Node.js gives use the low level primitives to create a thread. A thread pool implementation is explained in the Node.js documentation, we could copy it here or use an existing package. On npmjs I found the node-worker-threads-pool package which is relativly similar to the version in the Node.js documentation, it is active and has a good number of stars/downloads compared to the other search results.

Let's use node-worker-threads-pool for our thread pool.

Install the pool package with

npm install --no-save node-worker-threads-pool

Let's create a static pool of 4 threads which runs the task defined in ./webassembly/task.js as a worker thread.

// this JavaScript snippet stored as webassembly/webservice-threaded.js
const { StaticPool } = require('node-worker-threads-pool')

const pool = new StaticPool({
  size: 4,
  task: './webassembly/task.js'
});

The web service handler has to call pool.exec() to perform the calculation in the worker thread and wait for the result. By using await the main event loop of Node.js is free to do other work while the work is being done in the worker thread.

// this JavaScript snippet later referred to as <<fastify-handler-threaded>>

const handler = async ({body}) => {
  const { epsilon, guess } = body
  const root = await pool.exec({epsilon, guess})
  return { root }
}

The pool.exec({epsilon, guess}) will cause an emit of an event with 'message' as name and {epsilon, guess} as argument in the task.

In the task, each time we get a 'message' event on the parentPort we want to perform the calculation.

// this JavaScript snippet appended to webassembly/task.js
<<import-wasm>>
const { parentPort } = require('worker_threads')

parentPort.on('message', async ({epsilon, guess}) => {

We must wait for the WebAsemmbly module to be initialized.

  // this JavaScript snippet appended to webassembly/task.js
  const { NewtonRaphson } = await createModule()

Now we can find the root.

  // this JavaScript snippet appended to webassembly/task.js
  const finder = new NewtonRaphson(epsilon)
  const root = finder.solve(guess)

And send the result back to the web service handler by posting a message to the port of the parent thread.

  // this JavaScript snippet appended to webassembly/task.js
  parentPort.postMessage(root)
})

Similar to the previous chapter we register the OpenAPI plugin, define a route and listen on http://localhost:3002

// this JavaScript snippet is appended to webassembly/webservice-threaded.js
<<fastify-openapi-plugin>>

<<fastify-handler-threaded>>

<<fastify-openapi-route>>

<<fastify-listen>>
main(3002)

Run the web service with

node webassembly/webservice-threaded.js

Test with

curl --request POST \
  --header "Content-Type: application/json" \
  --header "accept: application/json" \
  --data '{"guess":-20, "epsilon":0.001}' \
  http://localhost:3002/api/newtonraphson

Or goto Swagger UI to try it out. Do not forget to switch to the http://localhost:3002 server in the servers pull down.

In this chapter we created a web service which

  1. was written in JavaScript and executed with Node.js
  2. uses Emscripten to compile the C++ algorithm to WebAssembly module
  3. uses Fastify web framework to define routes
  4. validates requests and responses with a JSON schemas
  5. generates an OpenAPI specfication
  6. performs the calculation in a worker thread from a thread pool

JavaScript web application

react

Pros Cons
❤️ No server infrastucture required except file hosting ⛔ Big learning curve
❤️ Ecosystem allows for building application with few lines ⛔ Requires modern web browser

In the Web application section, a common approach is to render an entire HTML page even if a subset of elements requires a change. With the advances in the web browser (JavaScript) engines including methods to fetch JSON documents from a web service, it has become possible to address this shortcoming. The so-called Single Page Applications (SPA) enable changes to be made in a part of the page without rendering the entire page. To ease SPA development, a number of frameworks have been developed. The most popular front-end web frameworks are (as of June 2020):

Their pros and cons are summarized here.

For Newton-Raphson web application, we selected React because of its small API and its use of functional programming.

The C++ algorithm is compiled into a wasm file using bindings. When a calculation form is submitted in the React application a web worker loads the wasm file, starts the calculation, renders the result. With this architecture the application only needs cheap static file hosting to host the HTML, js and wasm files. The calculation will be done in the web browser on the end users machine instead of a server.

Using WebAssembly module in web browser

We reuse the WebAssembly module we created in previous chapter.

The WebAssembly module must be loaded and initialized by calling the createModule function and waiting for the JavaScript promise to resolve.

// this JavaScript snippet is later referred to as <<wasm-promise>>
createModule().then((module) => {
  <<wasm-calculate>>
  <<render-answer>>
});

The module variable contains the NewtonRaphson class we defined in the binding above.

The root finder can be called with

// this JavaScript snippet is before referred to as <<wasm-calculate>>
const epsilon = 0.001;
const finder = new module.NewtonRaphson(epsilon);
const guess = -20;
const root = finder.solve(guess);

To run the JavaScript in a web browser an HTML page is needed. To be able to use the createModule function, we will import the newtonraphsonwasm.js with a script tag.

<!doctype html>
<!-- this HTML page is stored as webassembly/example.html -->
<html lang="en">
  <head>
    <title>Example</title>
    <script type="text/javascript" src="newtonraphsonwasm.js"></script>
    <script>
      <<wasm-promise>>
    </script>
  </head>
  <body>
    <span id="answer"> </span>
  </body>
</html>

In order to display the value of root, we use an HTML element whose id is equal to answer. We can use document manipulation functions like getElementById and innerHTML to find this element in our web page and subsequently set its contents, like so:

document.getElementById('answer').innerHTML = root.toFixed(2);

The web browser can only load the newtonraphsonwasm.js file when hosted by a web server. Python ships with a built-in web server, we will use it to host all files of the repository on port 8000.

python3 -m http.server 8000

Visit http://localhost:8000/webassembly/example.html to see the result of the calculation. Embedded below is the example hosted on GitHub pages

https://nlesc-jcer.github.io/cpp2wasm/webassembly/example.html.

The result of root finding was calculated using the C++ algorithm compiled to a WebAssembly module, executed by some JavaScript and rendered on a HTML page.

Long-running tasks with web worker

Executing a long running C++ method will block the browser from running any other code like updating the user interface. In order to avoid this, the method can be run in the background using web workers. A web worker runs in its own thread and can be interacted with from JavaScript using messages.

We need to instantiate a web worker which we will implement later in webassembly/worker.js.

// this JavaScript snippet is later referred to as <<worker-consumer>>
const worker = new Worker('worker.js');

We need to send the worker a message with description for the work it should do.

// this JavaScript snippet is appended to <<worker-consumer>>
worker.postMessage({
  type: 'CALCULATE',
  payload: { epsilon: 0.001, guess: -20 }
});

In the web worker we need to listen for incoming messages.

// this JavaScript snippet is later referred to as <<worker-provider-onmessage>>
onmessage = function(message) {
  <<handle-message>>
};

Before we can handle the message we need to import the WebAssembly module.

// this JavaScript snippet is stored as webassembly/worker.js
importScripts('newtonraphsonwasm.js');

<<worker-provider-onmessage>>

We can handle the CALCULATE message only after the WebAssembly module is loaded and initialized.

// this JavaScript snippet is before referred to as <<handle-message>>
if (message.data.type === 'CALCULATE') {
  createModule().then((module) => {
    <<perform-calc-in-worker>>
    <<post-result>>
  });
}

Let's calculate the result (root) based on the payload parameters in the incoming message.

// this JavaScript snippet is before referred to as <<perform-calc-in-worker>>
const epsilon = message.data.payload.epsilon;
const finder = new module.NewtonRaphson(epsilon);
const guess = message.data.payload.guess;
const root = finder.solve(guess);

And send the result back to the web worker consumer as a outgoing message.

// this JavaScript snippet is before referred to as <<post-result>>
postMessage({
  type: 'RESULT',
  payload: {
    root: root
  }
});

Listen for messages from worker and when a result message is received put the result in the HTML page like we did before.

// this JavaScript snippet is appended to <<worker-consumer>>
worker.onmessage = function(message) {
  if (message.data.type === 'RESULT') {
    const root = message.data.payload.root;
    <<render-answer>>
  }
}

Like before we need a HTML page to run the JavaScript, but now we don't need to import the newtonraphsonwasm.js file here as it is imported in the worker.js file.

<!doctype html>
<!-- this HTML page is stored as webassembly/example-web-worker.html -->
<html lang="en">
  <head>
    <title>Example web worker</title>
    <script>
      <<worker-consumer>>
    </script>
  </head>
  <body>
    <span id="answer"> </span>
  </body>
</html>

Like before we also need to host the files in a web server with

python3 -m http.server 8000

Visit http://localhost:8000/webassembly/example-web-worker.html to see the result of the calculation. Embedded below is the example hosted on GitHub pages

<iframe width="100%" height="60" src="https://nlesc-jcer.github.io/cpp2wasm/webassembly/example-web-worker.html" /></iframe>

The result of root finding was calculated using the C++ algorithm compiled to a WebAssembly module, imported in a web worker (separate thread), executed by JavaScript with messages to/from the web worker and rendered on a HTML page.

React application

To render the React application we need a HTML element as a container. We will give it the identifier container which will use later when we implement the React application in the app.js file.

<!doctype html>
<!-- this HTML page is stored as react/example-app.html -->
<html lang="en">
  <head>
    <title>Example React application</title>
    <<imports>>
  </head>
  <div id="container"></div>

  <script type="text/babel" src="app.js"></script>
</html>

To use React we need to import the React library.

<!-- this HTML snippet is before and later referred to as <<imports>> -->
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>

A React application is constructed from React components. The simplest React component is a function which returns a HTML tag with a variable inside.

// this JavaScript snippet is later referred to as <<heading-component>>
function Heading() {
  const title = 'Root finding web application';
  return <h1>{title}</h1>
}

A component can be rendered using

ReactDOM.render(
  <Heading/>,
  document.getElementById('container')
);

The Heading React component would render to the following HTML.

<h1>Root finding web application</h1>;

The <h1>{title}</h1> looks like HTML, but is actually called JSX. A transformer like Babel can convert JSX to valid JavaScript code. The transformed Heading component will look like.

function Heading() {
  const title = 'Root finding web application';
  return React.createElement('h1', null, `{title}`);
}

JXS is syntactic sugar that makes React components easier to write and read. In the rest of the chapter, we will use JSX.

To transform JSX we need to import Babel.

<!-- this HTML snippet is appended to <<imports>> -->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

The code supplied here should not be used in production as converting JSX in the web browser is slow. It's better to use Create React App which gives you an infrastructure to perform the transformation offline.

The web application in our example should have a form with epsilon and guess input fields, as well as a submit button. The form in JSX can be written in the following way:

{ /* this JavaScript snippet is later referred to as <<react-form>> */ }
<form onSubmit={handleSubmit}>
  <label>
    Epsilon:
    <input name="epsilon" type="number" value={epsilon} onChange={onEpsilonChange}/>
  </label>
  <label>
    Initial guess:
    <input name="guess" type="number" value={guess} onChange={onGuessChange}/>
  </label>
  <input type="submit" value="Submit" />
</form>

The form tag has a onSubmit property, which is set to a function (handleSubmit) that will handle the form submission. The input tag has a value property to set the variable (epsilon and guess) and it also has onChange property to set the function (onEpsilonChange and onGuessChange) which will be triggered when the user changes the value.

Let's implement the value and onChange for the epsilon input. To store the value we will use the React useState hook.

// this JavaScript snippet is later referred to as <<react-state>>
const [epsilon, setEpsilon] = React.useState(0.001);

The argument of the useState function is the initial value. The epsilon variable contains the current value for epsilon and setEpsilon is a function to set epsilon to a new value.

The input tag in the form will call the onChange function with a event object. We need to extract the user input from the event and pass it to setEpsilon. The value should be a number, so we use Number() to cast the string from the event to a number.

// this JavaScript snippet is appended to <<react-state>>
function onEpsilonChange(event) {
  setEpsilon(Number(event.target.value));
}

We will follow the same steps for the guess input as well.

// this JavaScript snippet is appended to <<react-state>>
const [guess, setGuess] = React.useState(-20);

function onGuessChange(event) {
  setGuess(Number(event.target.value));
}

We are ready to implement the handleSubmit function which will process the form data. The function will get, similar to the onChange of the input tag, an event object. Normally when you submit a form the form fields will be send to the server, but we want to perform the calculation in the browser so we have to disable the default action with.

// this JavaScript snippet is later referred to as <<handle-submit>>
event.preventDefault();

Like we did in the previous chapter we have to construct a web worker.

// this JavaScript snippet is appended to <<handle-submit>>
const worker = new Worker('worker.js');

The worker.js is the same as in the previous chapter so we re-use it by

cd react && ln -s ../webassembly/worker.js . && cd -

We have to post a message to the worker with the values from the form.

// this JavaScript snippet is appended to <<handle-submit>>
worker.postMessage({
  type: 'CALCULATE',
  payload: { epsilon: epsilon, guess: guess }
});

We need a place to store the result of the calculation (root value), we will use useState function again. The initial value of the result is set to undefined as the result is only known after the calculation has been completed.

// this JavaScript snippet is appended to <<react-state>>
const [root, setRoot] = React.useState(undefined);

When the worker is done it will send a message back to the app. The app needs to store the result value (root) using setRoot. The worker will then be terminated because it did its job.

// this JavaScript snippet is appended to <<handle-submit>>
worker.onmessage = function(message) {
    if (message.data.type === 'RESULT') {
      const result = message.data.payload.root;
      setRoot(result);
      worker.terminate();
  }
};

To render the result we can use a React Component which has root as a property. When the calculation has not been done yet, it will render Not submitted. When the root property value is set then we will show it.

// this JavaScript snippet is later referred to as <<result-component>>
function Result(props) {
  const root = props.root;
  let message = 'Not submitted';
  if (root !== undefined) {
    message = 'Root = ' + root;
  }
  return <div id="answer">{message}</div>;
}

We can combine the heading, form and result components and all the states and handleSubmit function into the App React component.

<<heading-component>>
<<result-component>>

// this JavaScript snippet appenended to react/app.js
function App() {
  <<react-state>>

  function handleSubmit(event) {
    <<handle-submit>>
  }

  return (
    <div>
      <Heading/>
      <<react-form>>
      <Result root={root}/>
    </div>
  );
}

Finally we can render the App component to the HTML container with container as identifier.

// this JavaScript snippet appenended to react/app.js
ReactDOM.render(
  <App/>,
  document.getElementById('container')
);

Make sure that the App can find the WebAssembly files by

cd react && ln -s ../webassembly/newtonraphsonwasm.wasm . && cd -

and

cd react && ln -s ../webassembly/newtonraphsonwasm.js . && cd -

Like before, we also need to host the files in a web server with

python3 -m http.server 8000

Visit http://localhost:8000/react/example-app.html to see the root answer. Embedded below is the example app hosted on GitHub pages

<iframe width="100%" height="160" src="https://nlesc-jcer.github.io/cpp2wasm/react/example-app.html" /></iframe>

JSON schema powered form

The JSON schema can be used to generate a form. The form values will be validated against the schema. The most popular JSON schema form for React is react-jsonschema-form so we will write a web application with it.

In the OpenAPI chapter a request and response schema was defined. For the form we need the request schema is

// this JavaScript snippet is later referred to as <<jsonschema-app>>
const schema =
  <<request-schema>>
;

To render the application we need a HTML page. We will reuse the imports we did in the previous chapter.

<!doctype html>
<!-- this HTML page is stored as react/example-jsonschema-form.html -->
<html lang="en">
  <head>
    <title>Example JSON schema powered form</title>
    <<imports>>
  </head>
  <div id="container"></div>

  <script type="text/babel" src="jsonschema-app.js"></script>
</html>

To use the react-jsonschema-form React component we need to import it.

<!-- this HTML snippet is appended to <<imports>>  -->
<script src="https://unpkg.com/@rjsf/core/dist/react-jsonschema-form.js"></script>

The form component is exported as JSONSchemaForm.default and can be aliases to Form for easy use with

// this JavaScript snippet is appended to <<jsonschema-app>>
const Form = JSONSchemaForm.default;

The form by default uses the Bootstrap 3 theme. The theme injects class names into the HTML tags. The styles associated with the class names must be imported from the Bootstrap CSS file.

<!-- this HTML snippet is appended to <<imports>>  -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">

The schema defines a description which we want to replace in the form. This can be done by defining a uiSchema

const uiSchema = {
  "ui:description": "Find root using Newton-Raphson algorithm"
}

The values in the form must be initialized and updated whenever the form changes.

// this JavaScript snippet is appended to <<jsonschema-app>>
const [formData, setFormData] = React.useState({
  epsilon: 0.001,
  guess: -20
});

function handleChange(event) {
  setFormData(event.formData);
}

The form can be rendered with

{ /* this JavaScript snippet is later referred to as <<jsonschema-form>>  */}
<Form
  schema={schema}
  uiSchema={uiSchema}
  formData={formData}
  onChange={handleChange}
  onSubmit={handleSubmit}
/>

The handleSubmit function recieves the form input values and use the web worker we created earlier to perform the calculation and render the result.

// this JavaScript snippet is appended to <<jsonschema-app>>
const [root, setRoot] = React.useState(undefined);

function handleSubmit(submission, event) {
  event.preventDefault();
  const worker = new Worker('worker.js');
  worker.postMessage({
    type: 'CALCULATE',
    payload: submission.formData
  });
  worker.onmessage = function(message) {
      if (message.data.type === 'RESULT') {
        const result = message.data.payload.root;
        setRoot(result);
        worker.terminate();
    }
  };
}

The App component can be defined and rendered with.

// this JavaScript snippet stored as react/jsonschema-app.js
function App() {
  <<jsonschema-app>>

  return (
    <div>
      <Heading/>
      <<jsonschema-form>>
      <Result root={root}/>
    </div>
  );
}

ReactDOM.render(
  <App/>,
  document.getElementById('container')
);

The Heading and Result React component can be reused.

// this JavaScript snippet appended to react/jsonschema-app.js
<<heading-component>>
<<result-component>>

Like before we also need to host the files in a web server with

python3 -m http.server 8000

Visit http://localhost:8000/react/example-jsonschema-form.html to see the root answer. Embedded below is the example app hosted on GitHub pages

<iframe width="100%" height="320" src="https://nlesc-jcer.github.io/cpp2wasm/react/example-jsonschema-form.html" /></iframe>

If you enter a negative number in the epsilon field the form will become invalid with a error message.

Visualization with Vega-Lite

The plots in web application can be made using Vega-Lite. Vega-Lite is a JavaScript library which describes a plot using a JSON document. In Vega-Lite the JSON Document is called a specification and can be compile to a lower level Vega specifcation to be rendered.

To make an interesting plot we need more than one result. We are going to do a parameter sweep and measure how long each calculation takes.

Lets make a new JSON schema for the form in which we can set a max, min and step for epsilon.

// this JavaScript snippet is later referred to as <<jsonschema-app>>
const schema = {
  "type": "object",
  "properties": {
    "epsilon": {
      "title": "Epsilon",
      "type": "object",
      "properties": {
        "min": {
          "title": "Minimum",
          "type": "number",
          "minimum": 0,
          "default": 0.0001
        },
        "max": {
          "title": "Maximum",
          "type": "number",
          "minimum": 0,
          "default": 0.001
        },
        "step": {
          "title": "Step",
          "type": "number",
          "enum": [
            0.1,
            0.01,
            0.001,
            0.0001,
            0.00001,
            0.000001
          ],
          "default": 0.0001
        }
      },
      "required": ["min", "max", "step"],
      "additionalProperties": false
    },
    "guess": {
      "title": "Initial guess",
      "type": "number",
      "default": -20
    }
  },
  "required": ["epsilon", "guess"],
  "additionalProperties": false
};

Let's render the epsilon step field as a radio group

const uiSchema = {
  "epsilon": {
    "step": {
      "ui:widget": "radio",
      "ui:options": {
        "inline": true
      }
    }
  }
};

We need to rewrite the worker to perform a parameter sweep. The worker will recieve a payload like

{
  "epsilon": {
    "min": 0.0001,
    "max": 0.001,
    "step": 0.0001
  },
  "guess": -20
}

The worker will send back an array containing objects with the root result, the input parameters and the duration in milliseconds.

[{
  "epsilon": 0.0001,
  "guess": -20,
  "root": -1,
  "duration": 0.61
}]

To perform the sweep we will first unpack the payload.

// this JavaScript snippet is later referred to as <<calculate-sweep>>
const {min, max, step} = message.data.payload.epsilon;
const guess = message.data.payload.guess;

The result array needs to be initialized.

// this JavaScript snippet appended to <<calculate-sweep>>
const roots = [];

Lets use a classic for loop to iterate over requested the epsilons.

// this JavaScript snippet appended to <<calculate-sweep>>
for (let epsilon = min; epsilon <= max; epsilon += step) {

To measure the duration of a calculation we use the performance.now() method which returns a timestamp in milliseconds.

  // this JavaScript snippet appended to <<calculate-sweep>>
  const t0 = performance.now();
  const finder = new module.NewtonRaphson(epsilon);
  const root = finder.solve(guess);
  const duration = performance.now() - t0;

We append the root result object using shorthand property names to the result array.

  // this JavaScript snippet appended to <<calculate-sweep>>
  roots.push({
    epsilon,
    guess,
    root,
    duration
  });

To complete the sweep calculation we need to close the for loop and post the result.

  // this JavaScript snippet appended to <<calculate-sweep>>
}
postMessage({
  type: 'RESULT',
  payload: {
    roots
  }
});

The sweep calculation snippet (<<calculate-sweep>>) must be run in a new web worker called worker-sweep.js. Like before we need to wait for the WebAssembly module to be initialized before we can start the calculation.

// this JavaScript snippet stored as react/worker-sweep.js
importScripts('newtonraphsonwasm.js');

onmessage = function(message) {
  if (message.data.type === 'CALCULATE') {
    createModule().then((module) => {
      <<calculate-sweep>>
    });
  }
};

To handle the submit we will start a worker, send the form data to the worker, recieve the workers result and store it in the roots variable.

// this JavaScript snippet is appended to <<plot-app>>
const [roots, setRoots] = React.useState([]);

function handleSubmit(submission, event) {
  event.preventDefault();
  const worker = new Worker('worker-sweep.js');
  worker.postMessage({
    type: 'CALCULATE',
    payload: submission.formData
  });
  worker.onmessage = function(message) {
      if (message.data.type === 'RESULT') {
        const result = message.data.payload.roots;
        setRoots(result);
        worker.terminate();
    }
  };
}

Now that we got data, we are ready to plot. We use the Vega-Lite specification to declare the plot. The specification for a scatter plot of the epsilon against the duration looks like.

// this JavaScript snippet is later referred to as <<vega-lite-spec>>
const spec = {
  "$schema": "https://vega.github.io/schema/vega-lite/v4.json",
  "data": { "values": roots },
  "mark": "point",
  "encoding": {
    "x": { "field": "epsilon", "type": "quantitative" },
    "y": { "field": "duration", "type": "quantitative", "title": "Duration (ms)" }
  },
  "width": 800,
  "height": 600
};

To render the spec we use the vegaEmbed module. The Vega-Lite specification is a simplification of the Vega specification so wil first import vega then vega-lite and lastly vega-embed.

<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>

The vegaEmbed() function needs a DOM element to render the plot in. In React we must use the useRef hook to get a reference to a DOM element. As the DOM element needs time to initialize we need to use the useEffect hook to only embed the plot when the DOM element is ready. The Plot React component can be written as

// this JavaScript snippet is later referred to as <<plot-component>>
function Plot({roots}) {
  const container = React.useRef(null);

  function didUpdate() {
    if (container.current === null) {
      return;
    }
    <<vega-lite-spec>>
    vegaEmbed(container.current, spec);
  }
  const dependencies = [container, roots];
  React.useEffect(didUpdate, dependencies);

  return <div ref={container}/>;
}

The App component can be defined and rendered with.

// this JavaScript snippet stored as react/plot-app.js
<<heading-component>>

<<plot-component>>

function App() {
  const Form = JSONSchemaForm.default;
  const [formData, setFormData] = React.useState({

  });

  function handleChange(event) {
    setFormData(event.formData);
  }

  <<plot-app>>

  return (
    <div>
      <Heading/>
      <<jsonschema-form>>
      <Plot roots={roots}/>
    </div>
  );
}

ReactDOM.render(
  <App/>,
  document.getElementById('container')
);

The HTML page should look like

<!doctype html>
<!-- this HTML page is stored as react/plot-form.html -->
<html lang="en">
  <head>
    <title>Example plot</title>
    <<imports>>
  <head>
  <body>
    <div id="container"></div>

    <script type="text/babel" src="plot-app.js"></script>
  </body>
</html>

Like before we also need to host the files in a web server with

python3 -m http.server 8000

Visit http://localhost:8000/react/example-plot.html to see the epsilon/duration plot.

Embedded below is the example app hosted on GitHub pages

<iframe width="100%" height="1450" src="https://nlesc-jcer.github.io/cpp2wasm/react/example-plot.html" /></iframe>

After the submit button is pressed the plot should show that the first calculation took a bit longer then the rest.