I was writing some code where a user would click a button and the time of the next locally-visible solar eclipse would be computed.
This can be a very time-consuming calculation. So I wrote the calculation code with a couple of nested async functions, breaking all of the computations into bite-size pieces, allowing execution to yield frequently so that the user's web browser wouldn't lock up during the calculation.
Here's a grossly simplified test case of the timing issues involved, without the astronomical complexity (available on CodePen):
window.CP.PenTimer.MAX_TIME_IN_LOOP_WO_EXIT = 15000;
async function workWorkWork(msg) {
return new Promise(async (resolve) => {
const start = performance.now();
while (performance.now() < start + 5000) {
await new Promise((innerResolve) => {
let t = 0;
for (let i = 0; i < 1e7; ++i) t += Math.sin(Math.random());
console.log(msg || t);
// innerResolve(); // What I thought should have been sufficient
setTimeout(innerResolve); // What I had to do instead
});
}
resolve();
});
}
let busy = false;
function doStart() {
if (busy)
return;
const button = document.getElementById('my-button');
button.innerText = 'Started';
button.style.background = 'red';
busy = true;
(async () => {
setTimeout(() => (button.innerText = 'Busy'), 1000);
workWorkWork('other work');
await workWorkWork();
busy = false;
button.innerText = 'Start';
button.style.background = 'unset';
})();
}
After the user clicked the button, I wanted to wait for one second to go by before showing a busy indicator. The busy indicator was activated using a setTimeout called just before await-ing the calculation.
To my surprise, my the UI responded almost as if the async/await was blocking code. The whole calculation finished after several seconds, and then the busy indicator appeared, way too late.
Console output demonstrates that if I start up two calls of workWorkWork simultaneously, their output is interleaved in the web console, so each execution instance is properly yielding time to the other.
But the async function code doesn't yield any time to the setTimeout that was queued up before the function was called. It doesn't even let the DOM update with changes made before the async call and before the setTimeout.
The only way to get the setTimeout callback to run in a timely manner appears to be using other setTimeout calls within my async functions.
My first encounter with this problem was inside Angular code, and I figured I might be seeing an Angular-specific problem with the Angular Zone.js craziness, but I've now replicated the same problem in plain-ol' JavaScript.
Is this the behavior I should have expected? Is this documented anywhere?
Googling various combinations an permutations of async, await, setTimeout, Promise, etc., yields a lot of material which unfortunately has nothing to do with what I've run into here.
Correct behavior after clicking Start:
Button turns red, text changes to "Started".
About one second later, text changes to "Busy"
About four more seconds later, red background disappears and text changes back to "Start".
Bad behavior after clicking Start, without extra setTimeout:
Button makes no immediate change of text or color
About five seconds later, text changes to "Busy", and gets stuck that way.
Fulfilled promises are served via a microtask queue which gets handled BEFORE timer events. await works off promises so it also goes before timer events.
So, if both a timer event and a promise fulfillment are both waiting to run their handlers, then the promise will get served first.
Here's some more info on the microtask queue or more specifically the PromiseJobs queue.
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