<input type = "file" id = "el">
<code id = "out"></code>
const el = document.getElementById('el');
const out = document.getElementById('out');
el.addEventListener('change', async () => {
const file = el.files?.[0];
if (file) {
const reader = file.stream().getReader();
out.innerText = JSON.stringify({ fileSize: file.size });
let received = 0;
while (true) {
const chunk = await reader.read();
if (chunk.done) break;
// chunk.value.forEach((it) => it + 1);
received += chunk.value.byteLength;
out.innerText = JSON.stringify({
fileSize: file.size,
process: `${received} / ${file.size} (${((received / file.size) * 100).toFixed(2)}%)`,
});
}
}
});
Приведенный выше код работает хорошо, <code>
будет показывать прогресс в режиме реального времени.
Но если я добавлю строку chunk.value.forEach((it) => it + 1);
, то основной поток как бы блокируется, страница перестает отвечать до завершения обработки файла. (Тест в Edge 125)
Я могу использовать requestAnimationFrame
, чтобы это исправить.
Но почему это происходит, есть ли способ лучше, чем requestAnimationFrame
?
----редактироватьchunk.value.forEach((it) => it + 1);
— это упрощение реального кода. Я хочу вычислить md5 файла.
Браузер, похоже, ограничивает размер каждого фрагмента, чтобы каждый цикл длился около 16 мс. Дополнительный код практически не влияет на время и размер фрагмента.
while(true) {
const start = Date.now();
// ...
console.info('chunk', Date.now() - start, chunk.value.byteLength);
}
// chunk 7 524288
// chunk 17 1572864
// chunk 25 2097152
// chunk 16 2097152
// chunk 18 2097152
// chunk 15 2097152
// chunk 16 2097152
// ...
Даже если нет, то нередко можно получить фрагменты очень больших размеров. И вы повторяете каждый байт в фрагменте. Например, если файловая система возвращает фрагменты по 10 МБ каждый. Это означает, что в каждом цикле while вы повторяете цикл forEach 10 миллионов раз. Он не блокируется полностью, но вы можете оценить, как он блокируется в 99,999% случаев (зацикливание 10 миллионов раз) и разблокируется в 0,001% случаев (зацикливание цикла while).
But if I add the line
- вы понимаете, что добавлять эту строку совершенно бесполезно - она ничего не делает
Что вы на самом деле пытаетесь сделать этой строкой? Это упрощение реального кода?
Боюсь, такое поведение на самом деле соответствует спецификациям, даже если оно приводит к ужасным впечатлениям...
Шаги Pull контроллера ReadableStream
могут синхронно возвращать уже поставленные в очередь фрагменты. Таким образом, если ваш обратный вызов в реакции read()
выполняется дольше, чем файловая система занимает постановку в очередь новых фрагментов, браузер никогда не вернет управление обратно в цикл событий, и вы попадете в цикл микрозадач, что в значительной степени блокирует пользовательский интерфейс.
Интересно, что Firefox, похоже, где-то ставит задачу в очередь, поскольку они не блокируют пользовательский интерфейс, в отличие от Chrome. Возможно, на уровне спецификаций можно было бы обосновать это стандартом. (Однако Chrome демонстрирует поведение блокировки в течение довольно долгого времени, по крайней мере, M84, поэтому его нельзя рассматривать как такую большую проблему...).
На данный момент, чтобы избежать этого, вы можете поставить задачу в очередь из обратного вызова. Для этого быстрее всего было бы использовать все еще экспериментальный метод Scheduler.postTask() с приоритетом «блокировка пользователей»,
await scheduler.postTask(() => {}, { priority: "user-blocking" });
а для браузеров, которые до сих пор не поддерживают этот метод, вы можете пропатчить его с помощью MessageChannel()
{
const { port1, port2 } = new MessageChannel();
globalThis.scheduler ??= {
postTask(cb, options) {
return new Promise((resolve, reject) => {
port1.addEventListener("message", () => {
try { resolve(cb()); } catch(err) { reject(err); }
}, { once: true });
port2.postMessage("");
port1.start();
});
}
};
}
// monkey-patch scheduler.postTask(cb, { priority: "user-blocking" })
{
const { port1, port2 } = new MessageChannel();
globalThis.scheduler ??= {
postTask(cb, options) {
return new Promise((resolve, reject) => {
port1.addEventListener("message", () => {
try { resolve(cb()); } catch(err) { reject(err); }
}, { once: true });
port2.postMessage("");
port1.start();
});
}
};
}
scheduler.postTask(() => {}).then(() => console.info("yep"))
const el = document.getElementById('el');
const out = document.getElementById('out');
el.addEventListener('change', async () => {
const file = el.files?.[0];
if (file) {
const reader = file.stream().getReader();
out.innerText = JSON.stringify({ fileSize: file.size });
let received = 0;
while (true) {
const chunk = await reader.read();
if (chunk.done) break;
// Lock for 100ms
t1 = performance.now();
while(performance.now() - t1 < 100) {}
await scheduler.postTask(() => {}, { priority: "user-blocking" });
received += chunk.value.byteLength;
out.innerText = JSON.stringify({
fileSize: file.size,
process: `${received} / ${file.size} (${((received / file.size) * 100).toFixed(2)}%)`,
});
}
}
});
<input type = "file" id = "el">
<code id = "out"></code>
и отправить туда ваш файл, потому что даже если, поставив задачу в очередь в цикле, мы дадим циклу событий немного передохнуть, он все равно будет с трудом справляться со всеми обновлениями пользовательского интерфейса при таком небольшом количестве доступного времени. Обратите внимание, что отправка объекта File
(или Blob
) в рабочий контекст не копирует данные, поэтому вам не нужно беспокоиться об использовании памяти для этого.
Ps: Я открыл BUG 355256389, посмотрим, как пойдет.
Вы повторяете каждый байт фрагмента, и потенциально драйвер файловой системы возвращает только один гигантский фрагмент (например, один фрагмент размером 1 ГБ). Иногда это нормально для файлов в зависимости от реализации, хотя входные данные, полученные из сети (например, если вы создали поток из URL-адреса), обычно делятся по размеру MTU.