#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 — это не то, что мы можем легко сделать, но я заметил, что он, похоже, защищен. Еще раз спасибо!