Есть ли более быстрый способ получить Javascript событие l oop, чем setTimeout (0)? - PullRequest
3 голосов
/ 21 апреля 2020

Я пытаюсь написать веб-работника, который выполняет прерываемые вычисления. Единственный способ сделать это (кроме Worker.terminate()), который я знаю, - периодически возвращаться к сообщению l oop, чтобы он мог проверять наличие новых сообщений. Например, этот веб-работник вычисляет сумму целых чисел от 0 до data, но если вы отправите ему новое сообщение во время выполнения вычисления, оно отменит вычисление и начнет новое.

let currentTask = {
  cancelled: false,
}

onmessage = event => {
  // Cancel the current task if there is one.
  currentTask.cancelled = true;

  // Make a new task (this takes advantage of objects being references in Javascript).
  currentTask = {
    cancelled: false,
  };
  performComputation(currentTask, event.data);
}

// Wait for setTimeout(0) to complete, so that the event loop can receive any pending messages.
function yieldToMacrotasks() {
  return new Promise((resolve) => setTimeout(resolve));
}

async function performComputation(task, data) {
  let total = 0;

  while (data !== 0) {
    // Do a little bit of computation.
    total += data;
    --data;

    // Yield to the event loop.
    await yieldToMacrotasks();

    // Check if this task has been superceded by another one.
    if (task.cancelled) {
      return;
    }
  }

  // Return the result.
  postMessage(total);
}

Это работает, но ужасно медленно. В среднем каждая итерация while l oop занимает 4 мс на моей машине! Это довольно большие издержки, если вы хотите, чтобы отмена происходила быстро.

Почему это так медленно? И есть ли более быстрый способ сделать это?

Ответы [ 2 ]

2 голосов
/ 21 апреля 2020

Да, очередь message будет иметь более высокую важность, чем timeout one, и, следовательно, будет срабатывать с более высокой частотой.

Вы можете легко привязаться к этой очереди с помощью MessageChannel API :

let i = 0;
let j = 0;
const channel = new MessageChannel();
channel.port1.onmessage = messageLoop;

function messageLoop() {
  i++;
  // loop
  channel.port2.postMessage("");
}
function timeoutLoop() {
  j++;
  setTimeout( timeoutLoop );
}

messageLoop();
timeoutLoop();

// just to log
requestAnimationFrame( display );
function display() {
  log.textContent = "message: " + i + '\n' +
                    "timeout: " + j;
  requestAnimationFrame( display );
}

Теперь вы можете также хотеть пакетировать несколько раундов одной и той же операции за событие l oop.

0 голосов
/ 21 апреля 2020

Почему это так медленно?

Chrome (Мигание) на самом деле устанавливает минимальное время ожидания 4 мс :

// Chromium uses a minimum timer interval of 4ms. We'd like to go
// lower; however, there are poorly coded websites out there which do
// create CPU-spinning loops.  Using 4ms prevents the CPU from
// spinning too busily and provides a balance between CPU spinning and
// the smallest possible interval timer.
static constexpr base::TimeDelta kMinimumInterval =
    base::TimeDelta::FromMilliseconds(4);

Редактировать: Если вы читаете дальше в коде, этот минимум используется только в том случае, если уровень вложенности больше 5, однако он все равно устанавливает минимум на 1 мс во всех случаях:

  base::TimeDelta interval_milliseconds =
      std::max(base::TimeDelta::FromMilliseconds(1), interval);
  if (interval_milliseconds < kMinimumInterval &&
      nesting_level_ >= kMaxTimerNestingLevel)
    interval_milliseconds = kMinimumInterval;

Видимо, Спецификации WHATWG и W3 C расходятся во мнениях относительно того, должно ли минимальное значение 4 мсек всегда применяться или применяться только выше определенного уровня вложенности, но значение WHATWG spe c имеет значение для HTML и похоже, что Chrome реализовал это.

Я не уверен, почему мои измерения показывают, что это все еще занимает 4 мс.


есть более быстрый способ сделать это?

Основываясь на прекрасной идее Kaiido использовать другой канал сообщений, вы можете сделать что-то вроде этого:


let currentTask = {
  cancelled: false,
}

onmessage = event => {
  currentTask.cancelled = true;
  currentTask = {
    cancelled: false,
  };
  performComputation(currentTask, event.data);
}

async function performComputation(task, data) {
  let total = 0;

  let promiseResolver;

  const channel = new MessageChannel();
  channel.port2.onmessage = event => {
    promiseResolver();
  };

  while (data !== 0) {
    // Do a little bit of computation.
    total += data;
    --data;

    // Yield to the event loop.
    const promise = new Promise(resolve => {
      promiseResolver = resolve;
    });
    channel.port1.postMessage(null);
    await promise;

    // Check if this task has been superceded by another one.
    if (task.cancelled) {
      return;
    }
  }

  // Return the result.
  postMessage(total);
}

Я не совсем доволен этим кодом, но это кажется, работает и работает waaay быстрее. Каждый l oop занимает около 0,04 мс на моей машине.

...