Plotting Fractals in WebAssembly

Previous Top Next
6: Zooming In 7: WebAssembly and Web Workers  
7.3 Create the Web Worker 7.4 Adapt the Main Thread Coding  
7.4.2 Split WebAssembly Coding 7.4.3 Create Web Workers 7.4.4 Send/Receive Web Worker Messages

7.4.3 Create Web Workers

The coding in the main thread is no longer responsible for directly calling the WebAssembly function to plot a fractal image. Instead, the main thread needs to implement coding that does the following:

  1. Creates as many Web Workers as required
  2. Attaches an onmessage event handler to each worker instance
  3. Send a message to each worker instance whenever a fractal image needs to be plotted
  4. Listen for the worker’s completion message
  5. When all the workers have finished the current image calculation, the region of shared memory containing the image is transferred to the canvas element for display

Notice that the function below has been written as an event handler. This means it accepts an argument evt that, if present, is assumed to be an event object. This is because not only is this function called directly from the start() function (without being passed any arguments), but is it also attached to the input event of the workers slider UI element. Therefore, when the slider is moved, this function will be called, but now an event object is passed. Hence the test for if (evt)...

<GOTCHA>
You might think that if a slider range input element can only have numeric values, then the evt.target.value property ought to return a number…

Nope! evt.target.value returns a string… 🤔

If you want the numeric representation of this value, then call evt.target.valueAsNumber
</GOTCHA>

The function below shows only the essential parts of the code. Other statements for adjusting the UI are present but have not been shown here:

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Build required number of worker threads
const rebuild_workers = evt => {
  if (evt) {
    WORKERS = evt.target.valueAsNumber
    $id("workers_txt").innerHTML = WORKERS
  }

  // If the number of workers has changed, terminate any existing workers then creating new ones
  if (worker_list.length !== WORKERS) {
    worker_list.map(w => w.terminate() )
    worker_list.length = 0

    plot_time.start = window.performance.now()

    // Create new set of workers
    for (let i=0; i<WORKERS; i++) {
      // snip UI stuff...

      let w = new Worker('worker.js')

      // Respond to messages received from the worker
      w.onmessage = worker_msg_handler

      // Initialise worker thread
      w.postMessage(gen_worker_msg('init', i, 'mb', 0, 0, host_fns))

      worker_list.push(w)
    }

    // snip more UI stuff...
  }
}

After each worker instance is created, we do the following:

  1. Attach a message handler function in order to process inbound messages from that worker
  2. Send a message to the worker by calling its postMessage function. The actual message content is generated by the gen_worker_msg helper function.

This helper function is responsible for creating a message object that will become the value of the data property received by the worker thread.

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Generate a worker message object
const gen_worker_msg = (p_action, p_worker_id, p_fractal, p_zx, p_zy, p_host_fns) =>
  p_fractal === "mb"
  ? { action : p_action,
      payload : {
        host_fns : p_host_fns || {},
        fractal : {
          name          : p_fractal,
          width         : CANVAS_WIDTH,
          height        : CANVAS_HEIGHT,
          origin_x      : X_ORIGIN,
          origin_y      : Y_ORIGIN,
          zx            : 0.0,
          zy            : 0.0,
          ppu           : PPU,
          is_mandelbrot : true,
          img_offset    : mImageStart,
        },
        max_iters : MAX_ITERS,
        worker_id : p_worker_id || 0,
      },
    }
  : { action : p_action,
      payload : {
        host_fns : p_host_fns || {},
        fractal : {
          name          : p_fractal,
          width         : CANVAS_WIDTH,
          height        : CANVAS_HEIGHT,
          origin_x      : 0.0,
          origin_y      : 0.0,
          zx            : p_zx,
          zy            : p_zy,
          ppu           : MIN_PPU,
          is_mandelbrot : false,
          img_offset    : jImageStart,
        },
        max_iters : MAX_ITERS,
        worker_id : p_worker_id || 0,
      },
    }