I am trying to use async / await to do long computations on JavaScript on a browser. Should the code be changed or any improvements be done? It seems using promises may make it complicated. I also have two related questions at the end of the question.
It was doing 60000!
(factorial), and the answer is too long to show on screen, so the number of digits of the answer in binary is being shown. (showing the number of digits of the answer in decimal took too long for the conversion).
The iterative version is:
(some note: the computation won't start until 3 seconds later, to show how the UI looks like without the computation).
(function() {
let startTimeOfProgram = performance.now(),
timer = 3,
element = $("#status-display-content"),
elementTimer = $("#timer");
setInterval(() => { elementTimer.html((Math.round((performance.now() - startTimeOfProgram) / 10) / 100).toFixed(2)) }, 33);
function updateState() {
element.html(timer--).css({ transition: "none" }).css({ opacity: 1 });
if (timer < 0) timer = 3;
// need to be next cycle because cannot change
// transition and have it animated all in the same cycle
setTimeout(function() {
element.css({ transition: "opacity 1s" }).css({ opacity: 0 }).on("transitionend", () => { updateState(); element.off("transitionend"); });
}, 0);
}
updateState();
function binaryLength(n) {
return n.toString(2).length;
}
const occasionalSleeper = (function() {
let lastSleepingTime = performance.now();
return function() {
if (performance.now() - lastSleepingTime > 33) {
lastSleepingTime = performance.now();
return new Promise(resolve => setTimeout(resolve, 0));
} else {
return Promise.resolve();
}
}
}());
async function asyncBigIntFactorial(n) {
let start = performance.now();
n = BigInt(n);
if (n <= 1n) return 1n;
let result = 1n;
for (let i = 2n; i <= n; i++) {
await occasionalSleeper();
result *= i;
}
console.log("Time taken", (performance.now() - start) / 1000);
return result;
}
setTimeout(function() {
let startTimeOfComputation = performance.now();
asyncBigIntFactorial(60000).then(result => {
$("#calculation-result")
.html(`Number of digits of answer in binary: ${binaryLength(result)}<br>Time it took: ${Math.round((performance.now() - startTimeOfComputation) / 10) / 100} seconds`);
});
}, 3000);
}());
#status-display {
background-color: #000;
color: green;
font: 111px Arial, sans-serif;
width: 150px;
height: 150px;
border-radius: 21%;
text-align: center;
line-height: 150px;
}
#timer {
width: 150px;
text-align: center;
font: 15px Arial, sans-serif;
}
#status-display-content {
transition: opacity 1s
}
#calculation-result {
margin-left: .3em;
}
#left-panel, #calculation-result {
display: inline-block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="left-panel">
<div id="status-display">
<div id="status-display-content">
</div>
</div>
<div id="timer"></div>
</div>
<div id="calculation-result"></div>
It is also on: http://jsfiddle.net/rxdpbvku/
It can be contrasted with the version with the await occasionalSleeper()
commented out, with the UI not responsive during the computation: http://jsfiddle.net/fhL2gqpn/
Basically, it is using an occasionalSleeper()
and call await occasionalSleeper()
during the computation.
const occasionalSleeper = (function() {
let lastSleepingTime = performance.now();
return function() {
if (performance.now() - lastSleepingTime > 33) {
lastSleepingTime = performance.now();
return new Promise(resolve => setTimeout(resolve, 0));
} else {
return Promise.resolve();
}
}
}());
and the calculation part is:
async function asyncBigIntFactorial(n) {
let start = performance.now();
n = BigInt(n);
if (n <= 1n) return 1n;
let result = 1n;
for (let i = 2n; i <= n; i++) {
await occasionalSleeper();
result *= i;
}
console.log("Time taken", (performance.now() - start) / 1000);
return result;
}
The recursive version is:
(function() {
let startTimeOfProgram = performance.now(),
timer = 3,
element = $("#status-display-content"),
elementTimer = $("#timer");
setInterval(() => { elementTimer.html((Math.round((performance.now() - startTimeOfProgram) / 10) / 100).toFixed(2)) }, 33);
function updateState() {
element.html(timer--).css({ transition: "none" }).css({ opacity: 1 });
if (timer < 0) timer = 3;
// need to be next cycle because cannot change
// transition and have it animated all in the same cycle
setTimeout(function() {
element.css({ transition: "opacity 1s" }).css({ opacity: 0 }).on("transitionend", () => { updateState(); element.off("transitionend"); });
}, 0);
}
updateState();
function binaryLength(n) {
return n.toString(2).length;
}
const occasionalSleeper = (function() {
let lastSleepingTime = performance.now();
return function() {
if (performance.now() - lastSleepingTime > 33) {
lastSleepingTime = performance.now();
return new Promise(resolve => setTimeout(resolve, 0));
} else {
return Promise.resolve();
}
}
}());
async function asyncBigIntFactorial(n) {
let start = performance.now();
async function factorialHelper(n) {
n = BigInt(n);
if (n <= 1n) return 1n;
await occasionalSleeper();
let simplerAnswer = factorialHelper(n - 1n);
return simplerAnswer.then(async function(a) {
await occasionalSleeper();
return n * a;
});
}
let result = factorialHelper(n);
console.log("Time taken", (performance.now() - start) / 1000);
return result;
}
setTimeout(function() {
let startTimeOfComputation = performance.now();
asyncBigIntFactorial(60000).then(result => {
$("#calculation-result")
.html(`Number of digits of answer in binary: ${binaryLength(result)}<br>Time it took: ${Math.round((performance.now() - startTimeOfComputation) / 10) / 100} seconds`);
});
}, 3000);
}());
#status-display {
background-color: #000;
color: green;
font: 111px Arial, sans-serif;
width: 150px;
height: 150px;
border-radius: 21%;
text-align: center;
line-height: 150px;
}
#timer {
width: 150px;
text-align: center;
font: 15px Arial, sans-serif;
}
#status-display-content {
transition: opacity 1s
}
#calculation-result {
margin-left: .3em;
}
#left-panel, #calculation-result {
display: inline-block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="left-panel">
<div id="status-display">
<div id="status-display-content">
</div>
</div>
<div id="timer"></div>
</div>
<div id="calculation-result"></div>
It is also on: http://jsfiddle.net/rxdpbvku/
The recursive version reported finishing almost instantaneously, because at that time only the promise was created.
Some observations:
If inside of:
return simplerAnswer.then(async function(a) {
await occasionalSleeper();
return n * a;
});
if the await occasionalSleeper()
is commented out (or if that function is not an async function), it seems the final unwrapping of the promises will cause it to pause the UI. (I think it is the final unwrapping of 60000 promises). JSFiddle: http://jsfiddle.net/y0827r16/
Another observation is, if it is a real recursion, calling itself 60,000 times deep will cause a stack overflow. But using async / await, it won't. Why is that? Is it because it is like a table of resolving or unwrapping promises, so a table of 60,000 entries is not a problem at all.
I also have a question is that if inside any async
function, if such occasionalSleeper()
is added automatically by the async standard, then won't it make the programmer not have to worry where to add them?