Created
September 10, 2018 17:13
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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