Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 526
Timmmm Avatar asked Apr 21 '20 08:04

Timmmm


1 Answers

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.

Here are a few reasons why this method works:

  • Per specs, setTimeout will get throttled to a minimum of 4ms after the 5th level of call, that is after the fifth iteration of OP's loop.
    Message events are not subject to this limitation.

  • Some browsers will make the task initiated by setTimeout have a lower priority, in some cases.
    Namely, Firefox does that at page loading, so that scripts calling setTimeout at this moment don't block other events ; they do even create a task queue just for that.
    Even if still un-specced, it seems that at least in Chrome, message events have a "user-visible" priority, which means some UI events could come first, but that's about it. (Tested this using the up-coming scheduler.postTask() API in Chrome)

  • Most modern browsers will throttle default timeouts when the page is not visible, and this may even apply for Workers.
    Message events are not subject to this limitation.

  • As found by OP, Chrome does set a minimum of 1ms even for the first 5 calls.


But remember that if all these limitations have been put on setTimeout, it's because scheduling that many tasks at such a rate has a cost.

Use this only in a Worker thread!

Doing this in a Window context will throttle all the normal tasks the browser has to handle, but which they'll consider less important, like Network requests, Garbage Collection etc.
Also, posting a new task means that the event loop has to run at high frequency and will never idle, which means more energy consumption.

like image 97
Kaiido Avatar answered Sep 27 '22 18:09

Kaiido