Last active
March 30, 2024 18:11
-
-
Save JamesTheAwesomeDude/523fff3a2a1e68fd7c3dde96cf2ec4f0 to your computer and use it in GitHub Desktop.
Asynchronously execute a long-running synchronous Javascript function, even from file://
This file contains 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
function offmainthread(f, options) { | |
// Converts long-running synchronous CPU-bound | |
// function or eval-able string | |
// into an asynchronous WebWorker-based function | |
// to avoid blocking the main thread. | |
// DOES NOT CURRENTLY SUPPORT WASM FUNCTIONS. | |
"use strict"; | |
options = options || {}; | |
try { | |
void Worker; | |
} catch (error) { | |
// Worker interface not available | |
console.warn(error); | |
async function g() { | |
await new Promise(resolve => setTimeout(resolve)); | |
return f(...arguments); | |
} | |
Object.defineProperties(g, { | |
name: { | |
value: f.name, | |
writable: false | |
}, | |
length: { | |
value: f.length, | |
writable: false | |
} | |
}); | |
return g; | |
} | |
const src_worker = [ | |
"self.addEventListener('message', async function onmessage(event) {\n", | |
"\"use strict\"; // Not affecting the user-provided function\n", | |
"var result, {data: {arguments: call_arguments}, ports: [result_port]} = event;\n", | |
"try {\n", | |
"result = await f(...call_arguments);\n", | |
"result_port.postMessage({result}, {transfer: needsTransfer([result, ...call_arguments])});\n", | |
"} catch (error) {\n", | |
"result_port.postMessage({error});\n", | |
"} finally { self.close(); }\n", | |
"}, {once: true});\n", | |
"var f = (\n", | |
f, | |
"\n);\n", | |
"if (!(typeof f === 'function')) {const result = f; console.warn('Doesn\\'t look like the worker has a function. Maybe the expression did its work at start-up time? Returning the value %o directly for next call.', result); f = (...call_arguments) => {if (call_arguments.length > 0) console.warn('Ignoring arguments: %o', call_arguments); return result;};}\n", | |
needsTransfer | |
]; | |
async function g() { | |
// This creates one worker per _invocation_. | |
// I could have written it to create one worker per _function_, instead, | |
// but that would introduce several challenges: | |
// 1. The RPC-like interface required to support multiple calls to a single worker would drastically increase the interface and code complexity. | |
// 2. Idle workers count towards the cap, which is undesirable. | |
// 3. Setting up an onlining/offlining service to mitigate (2) would also drastically increase the code complexity. | |
// 4. If you want a persistent single-worker daemon that handles multiple function calls, you can still use this to make one! | |
// rpc_client_port = (rpc_daemon => {const {port1, port2} = new MessageChannel(); offmainthread(rpc_daemon)(port2); return port1;})(async function rpc_daemon(rpc_server_port) {;;;}); | |
// TODO: support wasm exported functions? | |
const w = new Worker(src_worker_url); | |
const result = new Promise((resolve, reject) => { | |
const {port1, port2} = new MessageChannel(); | |
port1.onmessageerror = event => void reject( | |
new InternalError('MessagePort:messageerror', {cause: event}) | |
); | |
port1.onmessage = event => { | |
if (event.data.error) | |
reject(event.data.error); | |
else | |
resolve(event.data.result); | |
}; | |
w.postMessage({ | |
arguments: Array.from(arguments) | |
}, { | |
transfer: needsTransfer([port2, ...arguments]) | |
}); | |
}); | |
result.finally(() => void w.terminate()); | |
return result; | |
} | |
Object.defineProperties(g, { | |
name: { | |
value: f.name, | |
writable: false | |
}, | |
length: { | |
value: f.length, | |
writable: false | |
} | |
}); | |
var src_worker_url; | |
try { | |
void FinalizationRegistry; | |
} catch (error) { | |
// FinalizationRegistry interface not available | |
console.warn(error); | |
src_worker_url = `data:application/javascript,${encodeURIComponent(src_worker.join(String()))}`; | |
return g; | |
} | |
const gc_reg = new FinalizationRegistry(u => void URL.revokeObjectURL(u)); | |
src_worker_url = URL.createObjectURL(new Blob(src_worker, {type: 'application/javascript'})); | |
gc_reg.register(g, src_worker_url); | |
return g; | |
} | |
function needsTransfer(obj, t = null, seen = null) { | |
t = t || Object.assign([ | |
/* https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects#supported_objects */ | |
'ArrayBuffer', | |
'MessagePort', | |
'ReadableStream', | |
'WritableStream', | |
'TransformStream', | |
'WebTransportReceiveStream', | |
'WebTransportSendStream', | |
'AudioData', | |
'ImageBitmap', | |
'VideoFrame', | |
'OffscreenCanvas', | |
'RTCDataChannel', | |
].map((()=>{"use strict"; if (!(this)) throw new InternalError(); return (clsName => this[clsName]);})()).filter(cls => cls), { | |
[Symbol.hasInstance](obj) { | |
return this.some(cls => obj instanceof cls); | |
} | |
}); | |
seen = seen || new WeakSet(); | |
if (typeof obj !== 'object' || seen.has(obj)) | |
return []; | |
else { | |
seen.add(obj); | |
if (obj instanceof t) | |
return [obj]; | |
else | |
return Object.entries(obj).flatMap(([, obj_]) => needsTransfer(obj_, t, seen)); | |
} | |
} | |
/* Example: | |
var invert_arrbuf = offmainthread(function invert_arrbuf(buf) { | |
// Takes in an ArrayBuffer | |
// and synchronously inverts every bit of it in-place | |
// (it also returns it anyway, for convenience) | |
let view = new Uint8Array(buf); | |
let length = view.length; | |
if (length > Number.MAX_SAFE_INTEGER) throw new RangeError(`File too large`, { | |
cause: { | |
length, | |
max: Number.MAX_SAFE_INTEGER | |
} | |
}); | |
for (let i = 0; i < length; i++) { | |
view[i] ^= -1; | |
} | |
return buf; | |
}); | |
x = new ArrayBuffer(500 * 1024 ** 2); | |
(new DataView(x)).setUint8(3, 186); | |
console.info("Pre call", new Uint8Array(x)); | |
p = invert_arrbuf(x); | |
console.info("Post call... (main thread is not being blocked!)"); | |
result = await p; | |
console.info("Post return", new Uint8Array(result)); | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment