Почему MessagePort.postMessage приводит к сбою Firefox?

#javascript #web-worker #postmessage

#javascript #веб-работник #postmessage

Вопрос:

Я отправил отчет о сбое в Firefox, но я также хотел бы убедиться, что я не написал что-то неправильное или запрещенное спецификацией.

В приведенном ниже фрагменте:

  • Основной процесс создает веб-работника
  • Основной процесс создает MessageChannel
  • Основной процесс отправляет port2 веб-работнику
  • Веб-работник подтверждает получение порта
  • Основной процесс отправляет сообщение по каналу, чтобы запросить фактическую работу
  • Веб-работник создает 1000 ArrayBuffer сек и передает их обратно

Как указано в названии, этот код практически каждый раз приводит к сбою 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> 

Ответ №1:

Это определенно ошибка, и вы ничего не делаете «против спецификаций», нет, ваш код «должен» работать.

Вы очень хорошо справились с этой проблемой, по моему опыту, они обрабатываются быстрее, чем просто отчеты о сбоях, и действительно, это уже исправлено через три дня.

Кстати, я сделал более простое воспроизведение, в котором не используется 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> 


При этом, вероятно, вы можете обойти эту ошибку.

  • Эта ошибка касается только сообщений MessageChannel. Может быть, вы могли бы переписать свой код, чтобы продолжать использовать сообщения работника вместо этого:
 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 );
}; 

  • Передача такого количества массивных буферов в любом случае звучит немного странно. Я не уверен, зачем вам это нужно, но один из способов, пока SharedArrayBuffers не вернутся в игру, — это перенести один большой массивный буфер и создать из него множество «подмассивов».
    Таким образом, вы можете продолжать работать над ними, как если быэто было много небольших массивов, в то время как на самом деле все еще существует один базовый ArrayBuffer, и 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 ] );
} 

Комментарии:

1. Спасибо за репро и все предложения! Спасибо, что сообщили об ошибке, я должен был упомянуть в теме, что в итоге я сам сообщил об этом, я приношу извинения за это (вот почему ваш отчет был закрыт).

2. Я думаю, что вариант с одним буфером был бы лучшим решением, но нам довольно сложно принять его прямо сейчас (но именно так я создал прототип своего первого «исправления» для нашего варианта использования). Кроме того, использование Worker.postMessage — это не то, что мы можем легко сделать, но я заметил, что он, похоже, защищен. Еще раз спасибо!