Почему асинхронный цикл File.stream().getReader().read() может блокировать основной поток?

<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
// ...

Вы повторяете каждый байт фрагмента, и потенциально драйвер файловой системы возвращает только один гигантский фрагмент (например, один фрагмент размером 1 ГБ). Иногда это нормально для файлов в зависимости от реализации, хотя входные данные, полученные из сети (например, если вы создали поток из URL-адреса), обычно делятся по размеру MTU.

slebetman 24.07.2024 07:05

Даже если нет, то нередко можно получить фрагменты очень больших размеров. И вы повторяете каждый байт в фрагменте. Например, если файловая система возвращает фрагменты по 10 МБ каждый. Это означает, что в каждом цикле while вы повторяете цикл forEach 10 миллионов раз. Он не блокируется полностью, но вы можете оценить, как он блокируется в 99,999% случаев (зацикливание 10 миллионов раз) и разблокируется в 0,001% случаев (зацикливание цикла while).

slebetman 24.07.2024 07:12
But if I add the line - вы понимаете, что добавлять эту строку совершенно бесполезно - она ​​ничего не делает
Jaromanda X 24.07.2024 07:21

Что вы на самом деле пытаетесь сделать этой строкой? Это упрощение реального кода?

Barmar 24.07.2024 15:38
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
1
4
57
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Боюсь, такое поведение на самом деле соответствует спецификациям, даже если оно приводит к ужасным впечатлениям...

Шаги 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>

Но в вашем случае лучше всего использовать Web Worker,

и отправить туда ваш файл, потому что даже если, поставив задачу в очередь в цикле, мы дадим циклу событий немного передохнуть, он все равно будет с трудом справляться со всеми обновлениями пользовательского интерфейса при таком небольшом количестве доступного времени. Обратите внимание, что отправка объекта File (или Blob) в рабочий контекст не копирует данные, поэтому вам не нужно беспокоиться об использовании памяти для этого.


Ps: Я открыл BUG 355256389, посмотрим, как пойдет.

Другие вопросы по теме