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?
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With