Parallel mandelbrot set visualization with WebAssembly, Raynon and Rust

Each color band represents the work of a particular thread

I built an interactive Mandelbrot set visualizer in Rust that paralellizes computation with Rayon and compiles to WebAssembly to run in your browser. Explore the Demo Here (Works on a recent version of Chrome.)

Or follow one of the direct links into particular areas of the mandelbrot world map below:

Near the weist of mandelbrot

Upper East side

Zoomed in from the one above

Rayon in WebAssembly

Rayon’s core abstraction is the ability to spawn parallel computations and join them in the end to aggregate their results. On top of these rayon provides very convenient work-stealing parallel collections that make parallelising iterator-based code very easy.

There is a catch though: rayon usually relies on native threads provided by the std but these APIs are stubbed out in wasm32-unknown-unknown target since, as the target name indicates, the host envirionment information is missing and we cannot assume anything about its threading APIs.

We can solve this by manually providing multithreading primitives to rayon. This is is supported in recent versions by a handler that helps to set the function called for spawning new threads.

That gives a mechanism for multithreading but WebAssembly still does not have threads: we can have the new spawn handler to run everything in a single webAssembly module but that would defeat the purpose of using rayon in the first place.

hack threads into rayon

Instead, we can use a hack to provide threads to WebAssembly. Most modern browsers now support Web Workers API which act more like separate processes than threads but will still do the job. The idea is to spawn several such Workers that act as a thread pool for rayon. These threads in the pool simply wait for incoming messages, interpret the first message as a WebAssembly module, deploy it, and on the following messages invoke functions from the already deployed WebAssembly module. This uses very new and fragile APIs and raycast-parallel in wasm_bindgen crate is the only working demonstration of the method which is what I am relying on.

The code that is gluing javascript to mandelbrot calculation logic in Rust is in lib.rs Note that unlike native rayon parallelism, these threads/Workers are greedily instantiated which is less efficient and makes it more important to get the concurrency parameter right(setting it higher than what we need will instantiate threads that are not used).

Then we spawn all of multithreading logic to one worker that will then use rayon’s parallelism to send work to all the others. This helps to keep the main thread free and interactive for the user.

thread_pool.install is rayon’s way of saying instructing a close to use a custom pool instead of the native OS one (in this case using the native OS one would crash since rust’s std is stubbed out in wasm32-unknown-unknown)

Then we create a Rust Future that resolves as soon as the channel has data on it. We convert this Future to a JS promise and pass to javascript to resolve and print the result just as it would do with a regular native promise.

Pass more complex structures as a result

The above demonstrates how a value can be computed on several threads and passed back to front-end javascript by computing the result of simple dot product (result is f64)

However, we do not have to limit ourselves to computing such primitive results. We can pass more complex JS objects using web_sys library. For example, we can compute the frame buffer of some rendering task and pass it to javascript as ImageData object that can directly be rendered in a canvas element. In the next section we will do exactly that.

Sharing memory among workers

Copying buffers from workers to the main thread would defeat the purpose of parallelism and would probably be slower than the equivalent sequential program. For that reason the code represents the destination canvas data as a shared array buffer. This feature, however, is very new and still not available in stable Mozilla Firefox that is why the demo requires a recent Google Chrome.