Search code examples
javascriptmultithreadinggoogle-chromepromiseweb-worker

Web workers inside promise causing crash


The expected workflow of my code is getting the data from getData.

getData calls the worker that will do ImageUtil.getHex on the input. ImageUtil.getHex is a heavy function that needs to iterate every pixel of an image area area, so that's why I want to create it runs in the background and done in multithreading. The function is also independent, which I guess is a good candidate to put in the worker.

This is the chunk of code that needs the getData function:

class Mosaic {
    // ...
  build() {
    for (let y = 0; y < canvas.height; y += options.tileHeight) {
      for (let x = 0; x < canvas.width; x += options.tileWidth) {
        //... some code here
        // imgContext: a context of a canvas containing the image
        let area = imgContext.getImageData(x, y, w, h);
        this.getData(area, x, y)
          .then((data) => {
              // do something with data
            });
        //... 
      }
    }
  // ...
  }

  // ...
}

this is the getData function:

getData(data, x, y) {
  return new Promise((resolve, reject) => {
    let worker = new Worker('js/worker.js');
    worker.onmessage = (e) => {
      let hex = e.data;
      let img = new Image();
      let loc = `image/${hex}`
      img.onload = (e) => {
        resolve({
          hex: hex,
          x: x,
          y: y
        });
      }
      img.src = loc;
    }
    worker.postMessage(data);
  }); 

js/worker.js

self.addEventListener('message', function(e) {
  let hex = ImageUtil.getHex(e.data); // another function
  self.postMessage(hex);
  self.close();
}, false);

class ImageUtil {
  static getHex(imgData) {
    let data = imgData.data;
    let r = 0,
        g = 0,
        b = 0,

    for (let i = 0; i < data.length; i += 4) {
      // count rgb here
    }

    let count = data.length / 4;
    r = ("0" + (Math.floor(r / count)).toString(16)).slice(-2);
    g = ("0" + (Math.floor(g / count)).toString(16)).slice(-2);
    b = ("0" + (Math.floor(b / count)).toString(16)).slice(-2);
    let hex = `${r}${g}${b}`;
    return hex;
  }
}

The problem is, when ran, it makes the browser crash. Even if it's not crashing, the performance is much slower than without using worker.

Steps to reproduce:

  1. The other part of the code that isn't mentioned here create a Mosaic object. Then, we call build() on that Mosaic object.
  2. Crash

I think I misunderstood the way of workers work. Is it the right way, and how do I fix the code so it won't crash anymore?

Thanks!


Solution

  • The issue is that you are calling makeTile within a nested for loop, where makeTile creates worker. You are creating 950 Worker instances. Each Worker instance calls postMessage. That is the reason the browser is crashing.

    You need to adjust you scripts to handle arrays of promises, instead of a single Promise. worker.js should be called once, not 950 times.

    You can create an array before the for loops, pass the data as an array to Promise.resolve()

      var arr = [];
      for (let y = 0; y < canvas.height; y += options.tileHeight) {
        for (let x = 0; x < canvas.width; x += options.tileWidth) {
          let areaData = imgContext
                         .getImageData(x, y, options.tileWidth, options.tileHeight);
          arr.push(Promise.resolve([areaData, x, y]))
        }
      };
    

    then after the for loops use Promise.all() to process the data

    Promise.all(arr.map(function(tile) {
       this.makeTile(/* tile data here */) // make necessary changes at `makeTile`
       .then(function(tiles) {
         // do stuff with `tiles` array
    
       })
    }))
    

    Move

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

    outside of makeTile(), or create logic so that the call is only made once.

    Similarly, at worker.js, adjust the script to handle an array of data, instead of a single value.

    When message event is fired at main thread, process the data as an array of values.

    The gist of the solution is to refactor your code base to handle arrays, and arrays of promises; both at main thread and at worker.js; with the object being to call worker.js at most once at change event of <input type="file"> element. Where a single message is posted to Worker, and single message event is expected from Worker. Then do stuff with the returned array containing the processed data.