Skip to content

Instantly share code, notes, and snippets.

@JamesTheAwesomeDude
Last active March 30, 2024 18:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save JamesTheAwesomeDude/523fff3a2a1e68fd7c3dde96cf2ec4f0 to your computer and use it in GitHub Desktop.
Save JamesTheAwesomeDude/523fff3a2a1e68fd7c3dde96cf2ec4f0 to your computer and use it in GitHub Desktop.
Asynchronously execute a long-running synchronous Javascript function, even from file://
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