Skip to content

Instantly share code, notes, and snippets.

@Prestaul
Created September 10, 2018 17:13
Show Gist options
  • Save Prestaul/438cfe44f989fd80aaed318f7f2c6e7a to your computer and use it in GitHub Desktop.
Save Prestaul/438cfe44f989fd80aaed318f7f2c6e7a to your computer and use it in GitHub Desktop.
A utility for creating a pool of web workers for batch processing outside of UI thread
// Used by both PooledWorker and WorkerPool
function deferred() {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return {
promise,
resolve,
reject
};
}
// PooledWorker wraps window.Worker in a promise-y interface
class PooledWorker {
constructor(src) {
this.worker = new Worker(src);
this.execution = null;
this.worker.addEventListener('message', ({ data }) => {
if (!this.execution) {
throw new Error('Received a message from an idle worker');
}
if (data.output) {
this.execution.resolve(data.output);
} else {
this.execution.reject(new Error(data.error || 'Invalid message received from worker'));
}
this.execution = null;
});
}
exec(input) {
this.execution = deferred();
this.worker.postMessage(input);
return this.execution.promise;
}
}
// Construct the source code for our worker
const workerScript = handler => `
const handler = ${handler.toString()};
const transfer = transferList => transferable => {
transferList.push(transferable);
return transferable;
};
self.addEventListener('message', async event => {
try {
const transferList = [];
const output = await handler(event.data, transfer(transferList));
self.postMessage({ output }, transferList);
} catch(error) {
self.postMessage({ error });
}
});
`;
const DEFAULT_OPTIONS = {
maxWorkers: navigator.hardwareConcurrency
};
// WorkerPool provides a promise-y interface for parallelizing a repeated task in web workers
class WorkerPool {
constructor(src, { maxWorkers } = DEFAULT_OPTIONS) {
if (typeof src !== 'function') {
throw new Error('Invalid parameter to WorkerPool.src');
}
const blob = new Blob([ workerScript(src) ], { type: 'text/javascript' });
const workerUrl = URL.createObjectURL(blob);
// Let's make most of our properties read-only and non-enumerable because changing them
// after initialization could result in strange behavior
Object.defineProperties(this, {
maxWorkers: { value: maxWorkers },
idleWorkers: { value: [] },
queue: { value: [] },
size: { value: 0, writable: true },
workerUrl: { value: workerUrl }
});
}
releaseWorker(worker) {
if (this.queue.length) {
const promisedWorker = this.queue.shift();
promisedWorker.resolve(worker);
} else {
this.idleWorkers.push(worker);
}
}
getWorker() {
if (this.idleWorkers.length) {
return this.idleWorkers.pop();
} else if(this.size < this.maxWorkers) {
const worker = new PooledWorker(this.workerUrl);
this.size++;
return worker;
}
const promisedWorker = deferred();
this.queue.push(promisedWorker);
return promisedWorker.promise;
}
async exec(input) {
// If we fail here then we have no worker to release and the exec promise will reject
let worker = await this.getWorker();
try {
return await worker.exec(input);
} finally {
// Ensure that we release any worker if an error occurs during task execution. `finally`
// does not swallow the error, so the exec promise will still reject
if (worker) {
this.releaseWorker(worker);
}
}
}
}
// Example pool for loading and decoding images in workers
const pool = new WorkerPool(async (payload, transferObject) => {
const response = await fetch(payload.imageURL);
const blob = await response.blob();
const bitmap = await createImageBitmap(blob);
return transferObject(bitmap);
return bitmap;
});
// Example usage of the pool
// Note: using Promise.all means that you don't get any images until all are loaded
await Promise.all([
pool.exec({ imageURL: 'https://blittle.github.io/chrome-dev-tools/sources/workers.png' }),
pool.exec({ imageURL: 'https://blittle.github.io/chrome-dev-tools/elements/event-listeners.png' }),
pool.exec({ imageURL: 'https://blittle.github.io/chrome-dev-tools/network/web-socket.png' }),
pool.exec({ imageURL: 'https://blittle.github.io/chrome-dev-tools/network/xhr-breakpoints.png' }),
pool.exec({ imageURL: 'https://blittle.github.io/chrome-dev-tools/network/copy-data.png' }),
pool.exec({ imageURL: 'https://blittle.github.io/chrome-dev-tools/sources/pretty-print.gif' }),
pool.exec({ imageURL: 'https://blittle.github.io/chrome-dev-tools/sources/edit-auto.gif' }),
pool.exec({ imageURL: 'https://blittle.github.io/chrome-dev-tools/elements/less.png' }),
pool.exec({ imageURL: 'https://blittle.github.io/chrome-dev-tools/elements/computed-properties.png' }),
pool.exec({ imageURL: 'https://blittle.github.io/chrome-dev-tools/sources/snippets.png' })
]);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment