damix911
damix911

Reputation: 4453

Why is MessagePort.postMessage crashing Firefox?

I submitted a crash report to Firefox, but I would also like to make sure that I did not write something wrong or forbidden by the spec.

In the snippet below:

As stated in the title, this code crashes Firefox for me pretty much every time. I tried to send a different amount of buffers, and it seems like the watershed is at about ~170 buffers. Specifically, I had the impression that up to 171, Firefox does not crash, but between 171-174 things get weird (as in the window becomes unresponsive, of nothing comes back from the worker) and at 175 it always crashes.

Is my code wrong or is this a Firefox bug/limitation?

Chrome, Edge and Safari seem to be okay with the code.

addEventListener("load", () => {
  const workerSrc = document.getElementById("worker-src").innerText;
  const src = URL.createObjectURL(new Blob([workerSrc], { type: "application/javascript" }));

  const btn = document.createElement("button");
  btn.innerText = "Click me!";
  btn.addEventListener("click", () => {
    const worker = new Worker(src);
    const channel = new MessageChannel();

    channel.port1.addEventListener("message", (message) => {
      if (message.data.name === "messagePortResult") {
        channel.port1.postMessage({ name: "getBuffers" });
      } else if (message.data.name === "getBuffersResult") {
        console.log("This is what I got back from the worker: ", message.data.data);
      }
    });

    channel.port1.start();

    worker.postMessage({ name: "messagePort", port: channel.port2 }, [channel.port2]);
  });

  document.body.appendChild(btn);
});
<script id="worker-src" type="x-js/x-worker">
  let port = null;

  addEventListener("message", (message) => {
    if (message.data.name === "messagePort") {
      port = message.data.port;

      port.addEventListener("message", () => {
        const buffers = [];

        for (let i = 0; i < 1000; i++) {
          buffers.push(new ArrayBuffer(1024));
        }

        port.postMessage({ name: "getBuffersResult", data: buffers }, buffers);
      });

      port.start();

      port.postMessage({ name: "messagePortResult" });
    }
  });
</script>

Upvotes: 1

Views: 371

Answers (1)

Kaiido
Kaiido

Reputation: 137133

This is definitely a bug, and you are not doing anything "against the specs" no, your code "should" work.

You did very well opening this issue, in my experience these get treated faster than just crash reports and indeed it's already fixed after three days.

By the way, I made a simpler repro which doesn't use a Worker:

button.onclick = (evt) => {
  const { port1 } = new MessageChannel();
  const buffers = [];
  for( let i = 0; i<1000; i++ ) {
    buffers.push( new ArrayBuffer( 1024 ) );
  }
  port1.postMessage( buffers, buffers );
};
<button id="button">Crash Firefox Tab</button>


That being said, it is probably possible for you to workaround this bug.

  • This bug only concerns MessageChannel's MessagePorts. Maybe you could rewrite your code so that you keep using the Worker's MessagePorts instead:

const worker_content = `
  const buffers = [];
  for( let i = 0; i<1000; i++ ) {
    buffers.push( new ArrayBuffer( 1024 ) );  
  }
  postMessage( { data: buffers }, buffers );
`;
const worker_url = URL.createObjectURL( new Blob( [ worker_content ] ) );
worker = new Worker( worker_url );
worker.onmessage = (evt) => {
  console.log( "received", evt.data );
};

  • Transferring that many ArrayBuffers sounds a bit weird anyway. I'm not sure why you need to do this, but one way around until SharedArrayBuffers come back to play is to transfer a single big ArrayBuffer, and create many "sub-arrays" from it.
    This way you can keep working on these as if they were many small arrays, while there is actually still a single underlying ArrayBuffer and GC doesn't have to kick in when doing many IO between both envs.
    I didn't really check, but I'd assume this will be faster than transferring that many small buffers in any browser.

const nb_of_buffers = 1000;
const size_of_buffers = 1024;
const { port1, port2 } = new MessageChannel();
{
  // in your main thread
  port1.onmessage = (evt) => {
    const big_arr = evt.data;
    const size_of_array = size_of_buffers / big_arr.BYTES_PER_ELEMENT;
    const arrays = [];
    for( let i = 0; i < nb_of_buffers; i++) {
      const start = i * size_of_array;
      const end = start + size_of_array;
      arrays.push( big_arr.subarray( start, end ) );
    }
    console.log( "received %s arrays", arrays.length );
    console.log( "first array", arrays[ 0 ] );
    console.log( "last array", arrays[ arrays.length - 1 ] );
    console.log( "same buffer anyway?", arrays[ 0 ].buffer === arrays[ arrays.length - 1 ].buffer );
  };
}
{
  // in Worker
  const big_buffer = new ArrayBuffer( 1024 * 1000 );
  const big_arr = new Uint32Array( big_buffer );
  const size_of_array = size_of_buffers / big_arr.BYTES_PER_ELEMENT;
  const arrays = [];
  for( let i = 0; i < nb_of_buffers; i++) {
    const start = i * size_of_array;
    const end = start + size_of_array;
    const sub_array = big_arr.subarray( start, end );
    arrays.push( sub_array );
    sub_array.fill( i );
  }
  // transfer to main
  port2.postMessage( big_arr, [big_buffer] );
  console.log( "sub_arrays buffer got transferred?",
    arrays.every( arr => arr.buffer.byteLength === 0 )
  );
}

  • In case you really need that many ArrayBuffers, you could create copies in each thread and use a single big ArrayBuffer only for transferring, filling the smaller ones from that big ArrayBuffer. This means you'll constantly have the data stored thrice in memory, but no new data is ever created after, and GC doesn't have to kick in.

const nb_of_buffers = 1000;
const size_of_buffers = 1024;
const { port1, port2 } = new MessageChannel();
{
  // in your main thread
  const buffers = [];
  for( let i = 0; i < nb_of_buffers; i++) {
    buffers.push( new ArrayBuffer( size_of_buffers ) );
  }
  port1.onmessage = (evt) => {
    const transfer_arr = new Uint32Array( evt.data );
    // update the values of each small arrays
    buffers.forEach( (buf, index) => {
      const size_of_arr = size_of_buffers / transfer_arr.BYTES_PER_ELEMENT;
      const start = index * size_of_arr;
      const end = start + size_of_arr;
      const sub_array = transfer_arr.subarray( start, end );
      new Uint32Array( buf ).set( sub_array );
    } );
    console.log( "first array", new Uint32Array( buffers[ 0 ] ) );
    console.log( "last array", new Uint32Array( buffers[ buffers.length - 1 ] ) );
  };
}
{
  // in Worker
  const buffers = [];
  for( let i = 0; i < nb_of_buffers; i++) {
    const buf = new ArrayBuffer( size_of_buffers );
    buffers.push( buf );
    new Uint32Array( buf ).fill( i );
  }
  // copy inside big_buffer
  const big_buffer = new ArrayBuffer( size_of_buffers * nb_of_buffers );
  const big_array = new Uint32Array( big_buffer );
  buffers.forEach( (buf, index) => {
    const small_array = new Uint32Array( buf );
    const size_of_arr = size_of_buffers / small_array.BYTES_PER_ELEMENT;
    const start = index * size_of_arr;
    big_array.set( small_array, start );
  } );
  // transfer to main
  port2.postMessage( big_buffer, [ big_buffer ] );
}

Upvotes: 1

Related Questions