How to thread JavaScript
Posted by in Programming Software Engineering onI recently saw a question about how I would go about using Promises to "loop over a lot of data". It was an interesting question, because I often use long-running functions as a way to evaluate a system... but it never occurred to me to see if I could use (read: abuse) the system to make those functions run faster. This question got me to ask myself...
How would I create a promise in JavaScript to handle a really long loop?
I assumed that my goal is to avoid locking up the browser window while it is doing something that takes a lot of processing power. Simple enough, and there are a lot of tools out there to do that... but how do I combine it with Promises? I started with a pretty vanilla long-running function: iterate through a lot of values and add them all up. No particular reason for htis, you could use any long-running function, and an ideal-solution would be able to handle more than one.
function sum(e) {
let result = 0;
for (let i = 0; i < e; ++i) {
result += i;
}
return result;
}
Give this a big enough number, and it'll iterate over it for a pretty long time. Especially on low-end hardware.
I asked myself, “how could I use a Promise to speed this up?” If we weren’t working in JavaScript, I would say “let’s use threads”, but since it’s JavaScript I would normally say “break the loop up into chunks and spread them up with timeouts.” This is boring, but I've done something similar before in Node. Let's see how it works out:
function to_sum(e) {
return new Promise((resolve, reject) => {
const CHUNK_SIZE = 0xFFFF;
let result = 0;
let chunk = 0;
const partialSum = () => {
let i = 0;
for (
i = chunk * CHUNK_SIZE;
i < ++chunk * CHUNK_SIZE && i < e;
++i
) {
result += i;
}
if (i < e) {
setTimeout(partialSum, 0);
} else {
resolve(result);
}
};
partialSum();
});
}
By my accounts, this should work, but it doesn’t. That was a little bit frustrating for me. This code stalls, when I was hoping that it would just print values and move on. Boo.
to_sum(0xFFFF_FF).then((e) => console.log("result:", e))
to_sum(0xFFFF_F).then((e) => console.log("result:", e))
to_sum(0xFFFF_FFF).then((e) => console.log("result:", e))
console.log("Foo");
So… being the stubborn ass that I am, I decided to try something a little… unconventional.
I decided to warp the rules of time and space and bend the power of WebWorkers to my will. Warning: this is going to get really ugly:
function thread(fn) {
// Convert my function into a WebWorker-compatible blob URL
// while also preserving a nice, simple interface that
// lets us plug-and-play with values.
var url = URL.createObjectURL(new Blob([`
onmessage = function(e) {
const fn = (${fn});
postMessage(fn(e.data));
}`], { type: 'text/javascript' }));
// Return a factory function that creates Promises, but
// each Promise starts up a new worker, runs the function,
// returns the result, and kills the worker... all while
// maintaining a nice, simple interface for interactions.
return value => new Promise((resolve, reject) => {
var worker = new Worker(url);
worker.onmessage = (e) => {
worker.terminate();
resolve(e.data);
};
worker.onerror = (e) => {
worker.terminate();
reject(e);
};
worker.postMessage(value);
});
}
const th_sum = thread(sum);
But it tests just beautifully:
th_sum(0xFFFF_FF).then((e) => console.log("result:", e))
th_sum(0xFFFF_F).then((e) => console.log("result:", e))
th_sum(0xFFFF_FFF).then((e) => console.log("result:", e))
console.log("Foo");
In fact, this did exactly what I hoped it would.
I think this is going to be a little bit touchy, though. I didn’t have a chance to test this thoroughly, but I was able to make it work with arrays, numbers, and recursion. I think I’m satisfied with it enough to call it a successful proof-of-concept!
In practice, I would probably recommend just pre-writing the web worker and loading it normally instead of figuring out how to convert it into a blob/string/etc. And, to be fair, the conclusion revealed nothing we didn't already know: you can speed your browser up with WebWorkers.
But I also found that there's a nice, convenient way to combine WebWorkers with Promises to make them "feel" like a normal exchange of values. I'm definitely going to use that on my own projects in the future.