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.
Promise<boolean>), and I even tried a variation of this code usingthen()rather thanawait. Same results, not surprisingly.async/awaitdoes not magically make your synchronous code asynchronous. Theforloop is executed, let's say, "on the foreground" and it takes time to complete (about 350 ms on my computer). The browser is busy running theforloop, it does not do anything else until it completes.setTimeout()is the only thing that introduces asynchronous behaviour but the "async" activity of the browser in this case is to wait for the timeout to complete. Use web workers to run the time consuming calculations in the background.