Is there a faster way to yield to Javascript event loop than setTimeout(0)?

流过昼夜 提交于 2020-05-25 08:26:51

问题


I am trying to write a web worker that performs an interruptible computation. The only way to do that (other than Worker.terminate()) that I know is to periodically yield to the message loop so it can check if there are any new messages. For example this web worker calculates the sum of the integers from 0 to data, but if you send it a new message while the calculation is in progress it will cancel the calculation and start a new one.

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);
}

This works but it is appallingly slow. On average each iteration of the while loop takes 4 ms on my machine! That is a pretty huge overhead if you want cancellation to happen quickly.

Why is this so slow? And is there a faster way to do this?


回答1:


Yes, the message queue will have higher importance than timeouts one, and will thus fire at higher frequency.

You can bind to that queue quite easily with the 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 );
}
<pre id="log"></pre>

Now, you may also want to batch several rounds of the same operation per event loop.




回答2:


Why is this so slow?

Chrome (Blink) actually sets the minimum timeout to 4 ms:

// 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);

Edit: If you read further in the code, that minimum is only used if the nesting level is more than 5, however it does still set the minimum to 1 ms in all cases:

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

Apparently the WHATWG and W3C specs disagree about whether the minimum of 4 ms should always apply or only apply above a certain nesting level, but the WHATWG spec is the one that matters for HTML and it seems like Chrome has implemented that.

I'm not sure why my measurements indicate it still takes 4 ms though.


is there a faster way to do this?

Based on Kaiido's great idea to use another message channel you can do something like this:


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);
}

I'm not totally happy with this code, but it does seem to work and is waaay faster. Each loop takes around 0.04 ms on my machine.



来源:https://stackoverflow.com/questions/61338780/is-there-a-faster-way-to-yield-to-javascript-event-loop-than-settimeout0

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!