Skip to content

Instantly share code, notes, and snippets.

@SamMousa
Created August 31, 2020 11:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SamMousa/bcbd331d235b3b650977a919a0bade44 to your computer and use it in GitHub Desktop.
Save SamMousa/bcbd331d235b3b650977a919a0bade44 to your computer and use it in GitHub Desktop.
Execute work in web workers return result as promise
<!DOCTYPE html>
<html lang="en">
<template id="worker">
<script>
if (typeof self.document === 'undefined') {
self.addEventListener('message', async e => {
let id = e.data.id;
let data = e.data.data;
let result = await handleMessage(data);
self.postMessage({id, result});
});
let handlers = {
sleep: (data) => new Promise((resolve, reject) => setTimeout(() => resolve("Slept for " + data.duration + " ms"), data.duration)),
busywait: (data) => new Promise((resolve, reject) => {
// Busy wait is looping until enough time has passed
let end = Date.now() + data.duration;
while (true) {
if (Date.now() >= end) {
break;
}
}
resolve("Looped for " + data.duration + " ms");
}),
iterate: (data) => new Promise((resolve, reject) => {
// Busy wait is looping until enough time has passed
let start = Date.now();
for(let i = 0; i < data.iterations; i++) {
// Do something here
let p = [];
for (let j = 0; j < 1000; j++) {
p.push(Math.sin(90 * Math.PI / 180));
}
}
resolve("Iterated for " + (Date.now() - start).toString() + " ms");
})
};
async function handleMessage(data) {
if (typeof data !== 'object') {
return data;
}
return handlers[data.type](data);
}
}
</script>
</template>
<body>
<h1>Explanation</h1>
<pre>
Web workers allow you to use multithreading in the browser, not only does this allow you to use more processing power, it also makes sure the UI thread is never blocked.
This example creates a PromiseWorker class that uses a configured number of workers and uses a round-robin approach to executing tasks.
A task is defined as a simple JS object, handlers are defined inside the script with tag 'worker'
By wrapping the messaging inside an object that returns promises it becomes easy to work with the data:
let pw = new PromiseWorker(workerCount, workerUrl);
let promise = pw.execute({type: iterate, iterations: 500000});
</pre>
<div style="position: sticky; top: 0px; padding: 10px; background-color: white;">
<button id="sleep">Sleep 1000ms</button>
<button id="sleep100">Sleep 100 x 1000ms</button>
<button id="busywait">Busywait 1000ms</button>
<button id="busywait5000">Busywait 5000ms</button>
<button id="busywait10_5000">Busywait 10 x 5000ms</button>
<button id="iterate500000">Iterate 500.000 times</button>
<button id="iterate10_500000">Iterate 10x 500.000 times</button>
<button id="addworker">Add worker</button>
<button id="removeworker">Remove worker</button>
<label id="workercount">Worker count</label>
</div>
<pre id="log" style="overflow: auto">
</pre>
</body>
<script>
let startMoment = Date.now();
class PromiseWorker {
workerUrl
counter = 0;
nextWorker = 0;
resolvers = {};
workers = [];
setWorkers(count) {
this.nextWorker = 0;
console.log("Setting worker count to", count, "from", this.workers.length);
// Decrease length
while(this.workers.length > count) {
this.workers.pop().terminate();
}
// Increase length
while (this.workers.length < count) {
let worker = new Worker(this.workerUrl);
worker.addEventListener('message', e => this.resultHandler(e.data.id, e.data.result));
this.workers.push(worker);
}
console.log("Workers array: ", this.workers.length);
}
resultHandler(id, result) {
let resolver = this.resolvers[id];
if (typeof resolver === 'undefined') {
console.warn('No resolver for ID', id);
}
delete this.resolvers[id];
resolver(result);
this.log("Resolving " + id + " with: " + JSON.stringify(result));
}
constructor(workerCount = 10, workerUrl)
{
this.workerUrl = workerUrl;
this.setWorkers(workerCount);
}
execute(data) {
let id = this.counter++;
this.nextWorker++;
if (this.nextWorker === this.workers.length) {
this.nextWorker = 0;
}
let worker = this.workers[this.nextWorker];
this.log("Worker " + this.nextWorker.toString() + ": Sending message (" + id + ")" + JSON.stringify(data));
let resolvers = this.resolvers;
return new Promise((resolve, reject) => {
resolvers[id] = resolve;
let message = {id, data}
worker.postMessage(message);
});
}
log(text) {
document.getElementById('log').textContent += (Date.now()-startMoment).toString().padStart(6, 0) + ": " + text + '\r\n';
}
}
// Promise generator
let workerCount = navigator.hardwareConcurrency;
let workerUrl = window.URL.createObjectURL(new Blob([document.getElementById('worker').content.firstElementChild.textContent]));
let pw = new PromiseWorker(workerCount, workerUrl);
pw.execute('test').then(console.log);
document.getElementById('sleep').addEventListener('click', () => pw.execute({type: 'sleep', duration: 1000}).then(console.log));
document.getElementById('sleep100').addEventListener('click', () => {
for (i = 0; i < 100; i++) {
pw.execute({type: 'sleep', duration: 1000}).then(console.log);
}
});
document.getElementById('busywait').addEventListener('click', () => pw.execute({type: 'busywait', duration: 1000}).then(console.log));
document.getElementById('busywait5000').addEventListener('click', () => pw.execute({type: 'busywait', duration: 5000}).then(console.log));
document.getElementById('iterate500000').addEventListener('click', () => pw.execute({type: 'iterate', iterations: 500000}).then(console.log));
document.getElementById('iterate10_500000').addEventListener('click', () => {
let promises = [];
let start = Date.now();
for(i = 0; i < 10; i++) {
promises.push(pw.execute({type: 'iterate', iterations: 500000}));
}
Promise.all(promises).then(() => console.log("10 times iterate 500.000", Date.now()-start, "ms"));
});
document.getElementById('busywait10_5000').addEventListener('click', () => {
let promises = [];
let start = Date.now();
for(i = 0; i < 10; i++) {
promises.push(pw.execute({type: 'busywait', duration: 5000}));
}
Promise.all(promises).then(() => console.log("10 times busywait 5000", Date.now()-start, "ms"));
});
document.getElementById('addworker').addEventListener('click', () => {
workerCount++;
pw.setWorkers(workerCount);
document.getElementById('workercount').textContent = workerCount;
});
document.getElementById('removeworker').addEventListener('click', () => {
if (workerCount <= 1) {
return;
}
workerCount--
pw.setWorkers(workerCount);
document.getElementById('workercount').textContent = workerCount;
});
</script>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment