【问题标题】:Why is MessagePort.postMessage crashing Firefox?为什么 MessagePort.postMessage 会导致 Firefox 崩溃?
【发布时间】:2020-11-27 13:36:58
【问题描述】:

我提交了a crash report to Firefox,但我也想确保我没有写错或规范禁止的内容。

在下面的sn-p中:

  • 主进程创建一个 web worker
  • 主进程创建MessageChannel
  • 主进程发送port2给web worker
  • Web Worker 确认接收到端口
  • 主进程在通道上发送消息请求实际工作
  • 网络工作者创建 1000 个ArrayBuffers 并将它们传回

如标题所述,这段代码几乎每次都会让 Firefox 崩溃。我尝试发送不同数量的缓冲区,看起来分水岭约为 170 个缓冲区。具体来说,我的印象是,在 171 之前,Firefox 不会崩溃,但在 171 到 174 之间,事情会变得很奇怪(因为窗口变得没有响应,工作人员没有返回任何东西)并且在 175 时它总是崩溃。

我的代码是错误的还是 Firefox 的错误/限制?

Chrome、Edge 和 Safari 似乎对代码没问题。

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>

【问题讨论】:

    标签: javascript web-worker postmessage


    【解决方案1】:

    这绝对是一个错误,你没有做任何“违反规范”的事情,不,你的代码“应该”工作。

    您在打开 this issue 时做得非常好,根据我的经验,这些问题得到处理的速度比崩溃报告要快,而且实际上三天后就已经修复了。

    顺便说一句,我做了一个不使用 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 );
    };
    &lt;button id="button"&gt;Crash Firefox Tab&lt;/button&gt;

    话虽如此,您可能可以解决此错误。

    • 此错误仅涉及 MessageChannel 的 MessagePorts。也许您可以重写代码以便继续使用 Worker 的 MessagePorts:

    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 );
    };
    • 无论如何,传输那么多 ArrayBuffers 听起来有点奇怪。我不知道为什么需要这样做,但在 SharedArrayBuffers 出现之前,有一种方法可以解决back to play 就是转移一个大的ArrayBuffer,并从中创建多个"sub-arrays"
      通过这种方式,您可以继续处理它们,就好像它们是许多小数组一样,而实际上仍然有一个底层 ArrayBuffer 并且在两个 env 之间执行许多 IO 时不必启动 GC。 我没有真正检查过,但我认为这比在任何浏览器中传输那么多小缓冲区要快。

    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 )
      );
    }
    • 如果你真的需要那么多ArrayBuffers,你可以在每个线程中创建副本并使用一个大ArrayBuffer仅用于传输,从那个大的 ArrayBuffer 中填充较小的。这意味着您将不断地在内存中存储三次数据,但之后不会再创建新数据,并且不必启动 GC。

    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 ] );
    }

    【讨论】:

    • 感谢您的复制和所有建议!感谢您报告该错误,我应该在帖子中提到我最终自己报告了它,对此我深表歉意(这就是您的报告被关闭的原因)。
    • 我认为单缓冲区选项是最好的选择,但我们现在很难采用(但这正是我为我们的用例设计第一个“修复”原型的方式) .同样使用 Worker.postMessage 也不是我们可以轻松做到的,但我确实注意到它似乎是免疫的。再次感谢!
    猜你喜欢
    • 1970-01-01
    • 2021-06-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-03
    • 1970-01-01
    • 1970-01-01
    • 2013-01-27
    相关资源
    最近更新 更多