From 978d44f5c903cba89fc5c835eea8dfb38a3a8ce2 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Thu, 11 May 2023 14:53:21 -0700 Subject: [PATCH] Use HandleAllocator for wasm workers. NFC Similar to what we have been doing to other APIs. For example, see \#19337 and #19054 --- src/library.js | 12 ++--- src/library_wasm_worker.js | 58 ++++++++++++++-------- src/library_webaudio.js | 6 ++- test/code_size/hello_wasm_worker_wasm.js | 46 ++++++++++------- test/code_size/hello_wasm_worker_wasm.json | 8 +-- test/wasm_worker/terminate_wasm_worker.c | 4 ++ 6 files changed, 82 insertions(+), 52 deletions(-) diff --git a/src/library.js b/src/library.js index 75fc483ff08b3..183d6eddb82c4 100644 --- a/src/library.js +++ b/src/library.js @@ -3661,21 +3661,21 @@ mergeInto(LibraryManager.library, { // Reserve slot 0 so that 0 is always an invalid handle this.allocated = [undefined]; this.freelist = []; - this.get = function(id) { + // Hack to save on codesize: use arrow functions here even though it + // means that `this` gets statically bound to the constrcuted object. + this.get = (id) => { #if ASSERTIONS assert(this.allocated[id] !== undefined, 'invalid handle: ' + id); #endif return this.allocated[id]; }; - this.has = function(id) { - return this.allocated[id] !== undefined; - }; - this.allocate = function(handle) { + this.has = (id) => this.allocated[id] !== undefined; + this.allocate = (handle) => { var id = this.freelist.pop() || this.allocated.length; this.allocated[id] = handle; return id; }; - this.free = function(id) { + this.free = (id) => { #if ASSERTIONS assert(this.allocated[id] !== undefined); #endif diff --git a/src/library_wasm_worker.js b/src/library_wasm_worker.js index 6b0a47b4e5d3a..9cd32578ba0ce 100644 --- a/src/library_wasm_worker.js +++ b/src/library_wasm_worker.js @@ -30,8 +30,8 @@ mergeInto(LibraryManager.library, { - $_wasmWorkers: {}, - $_wasmWorkersID: 1, + $_wasmWorkers__deps: ['$HandleAllocator'], + $_wasmWorkers: "new HandleAllocator();", // Starting up a Wasm Worker is an asynchronous operation, hence if the parent thread performs any // postMessage()-based wasm function calls s to the Worker, they must be delayed until the async @@ -105,17 +105,19 @@ mergeInto(LibraryManager.library, { $_wasmWorkerBlobUrl: "URL.createObjectURL(new Blob(['onmessage=function(d){onmessage=null;d=d.data;{{{ captureModuleArg() }}}{{{ instantiateWasm() }}}importScripts(d.js);{{{ instantiateModule() }}}d.wasm=d.mem=d.js=0;}'],{type:'application/javascript'}))", #endif - _emscripten_create_wasm_worker__deps: ['$_wasmWorkers', '$_wasmWorkersID', '$_wasmWorkerAppendToQueue', '$_wasmWorkerRunPostMessage' + _emscripten_create_wasm_worker__deps: ['$_wasmWorkers', '$_wasmWorkerAppendToQueue', + '$_wasmWorkerRunPostMessage', #if WASM_WORKERS == 2 - , '$_wasmWorkerBlobUrl' + '$_wasmWorkerBlobUrl', #endif ], - _emscripten_create_wasm_worker__postset: 'if (ENVIRONMENT_IS_WASM_WORKER) {\n' - + '_wasmWorkers[0] = this;\n' - + 'addEventListener("message", _wasmWorkerAppendToQueue);\n' - + '}\n', + _emscripten_create_wasm_worker__postset: ` + if (ENVIRONMENT_IS_WASM_WORKER) { + _wasmWorkers.allocated[0] = this; + addEventListener('message', _wasmWorkerAppendToQueue); + }`, _emscripten_create_wasm_worker: function(stackLowestAddress, stackSize) { - let worker = _wasmWorkers[_wasmWorkersID] = new Worker( + let worker = new Worker( #if WASM_WORKERS == 2 // WASM_WORKERS=2 mode embeds .ww.js file contents into the main .js file as a Blob URL. (convenient, but not CSP security safe, since this is eval-like) _wasmWorkerBlobUrl #elif MINIMAL_RUNTIME // MINIMAL_RUNTIME has a structure where the .ww.js file is loaded from the main HTML file in parallel to all other files for best performance @@ -124,9 +126,13 @@ mergeInto(LibraryManager.library, { locateFile('{{{ WASM_WORKER_FILE }}}') #endif ); + var id = _wasmWorkers.allocate(worker); +#if RUNTIME_DEBUG + dbg('new wasm worker -> ' + id); +#endif // Craft the Module object for the Wasm Worker scope: worker.postMessage({ - '$ww': _wasmWorkersID, // Signal with a non-zero value that this Worker will be a Wasm Worker, and not the main browser thread. + '$ww': id, // Signal with a non-zero value that this Worker will be a Wasm Worker, and not the main browser thread. #if MINIMAL_RUNTIME 'wasm': Module['wasm'], 'js': Module['js'], @@ -140,16 +146,19 @@ mergeInto(LibraryManager.library, { 'sz': stackSize, // sz = stack size }); worker.onmessage = _wasmWorkerRunPostMessage; - return _wasmWorkersID++; + return id; }, emscripten_terminate_wasm_worker: function(id) { #if ASSERTIONS assert(id != 0, 'emscripten_terminate_wasm_worker() cannot be called with id=0!'); #endif - if (_wasmWorkers[id]) { - _wasmWorkers[id].terminate(); - delete _wasmWorkers[id]; + if (_wasmWorkers.has(id)) { +#if RUNTIME_DEBUG + dbg('terminating wasm worker -> ' + id); +#endif + _wasmWorkers.get(id).terminate(); + _wasmWorkers.free(id); } }, @@ -157,10 +166,15 @@ mergeInto(LibraryManager.library, { #if ASSERTIONS assert(!ENVIRONMENT_IS_WASM_WORKER, 'emscripten_terminate_all_wasm_workers() cannot be called from a Wasm Worker: only the main browser thread has visibility to terminate all Workers!'); #endif - Object.values(_wasmWorkers).forEach((worker) => { - worker.terminate(); +#if RUNTIME_DEBUG + dbg('emscripten_terminate_all_wasm_workers'); +#endif + Object.values(_wasmWorkers.allocated).forEach((worker) => { + if (typeof worker !== 'undefined') { + worker.terminate(); + } }); - _wasmWorkers = {}; + _wasmWorkers = new HandleAllocator(); }, emscripten_current_thread_is_wasm_worker: function() { @@ -176,12 +190,12 @@ mergeInto(LibraryManager.library, { }, emscripten_wasm_worker_post_function_v: function(id, funcPtr) { - _wasmWorkers[id].postMessage({'_wsc': funcPtr, 'x': [] }); // "WaSm Call" + _wasmWorkers.get(id).postMessage({'_wsc': funcPtr, 'x': [] }); // "WaSm Call" }, $_wasmWorkerPostFunction1__sig: 'vipd', $_wasmWorkerPostFunction1: function(id, funcPtr, arg0) { - _wasmWorkers[id].postMessage({'_wsc': funcPtr, 'x': [arg0] }); // "WaSm Call" + _wasmWorkers.get(id).postMessage({'_wsc': funcPtr, 'x': [arg0] }); // "WaSm Call" }, emscripten_wasm_worker_post_function_vi: '$_wasmWorkerPostFunction1', @@ -189,14 +203,14 @@ mergeInto(LibraryManager.library, { $_wasmWorkerPostFunction2__sig: 'vipdd', $_wasmWorkerPostFunction2: function(id, funcPtr, arg0, arg1) { - _wasmWorkers[id].postMessage({'_wsc': funcPtr, 'x': [arg0, arg1] }); // "WaSm Call" + _wasmWorkers.get(id).postMessage({'_wsc': funcPtr, 'x': [arg0, arg1] }); // "WaSm Call" }, emscripten_wasm_worker_post_function_vii: '$_wasmWorkerPostFunction2', emscripten_wasm_worker_post_function_vdd: '$_wasmWorkerPostFunction2', $_wasmWorkerPostFunction3__sig: 'vipddd', $_wasmWorkerPostFunction3: function(id, funcPtr, arg0, arg1, arg2) { - _wasmWorkers[id].postMessage({'_wsc': funcPtr, 'x': [arg0, arg1, arg2] }); // "WaSm Call" + _wasmWorkers.get(id).postMessage({'_wsc': funcPtr, 'x': [arg0, arg1, arg2] }); // "WaSm Call" }, emscripten_wasm_worker_post_function_viii: '$_wasmWorkerPostFunction3', emscripten_wasm_worker_post_function_vddd: '$_wasmWorkerPostFunction3', @@ -210,7 +224,7 @@ mergeInto(LibraryManager.library, { assert(UTF8ToString(sigPtr)[0] != 'v', 'Do NOT specify the return argument in the signature string for a call to emscripten_wasm_worker_post_function_sig(), just pass the function arguments.'); assert(varargs); #endif - _wasmWorkers[id].postMessage({'_wsc': funcPtr, 'x': readEmAsmArgs(sigPtr, varargs) }); + _wasmWorkers.get(id).postMessage({'_wsc': funcPtr, 'x': readEmAsmArgs(sigPtr, varargs) }); }, $atomicWaitStates: "['ok', 'not-equal', 'timed-out']", diff --git a/src/library_webaudio.js b/src/library_webaudio.js index aaf28d386fb45..bcefd3b684006 100644 --- a/src/library_webaudio.js +++ b/src/library_webaudio.js @@ -123,7 +123,7 @@ let LibraryWebAudio = { #if AUDIO_WORKLET emscripten_start_wasm_audio_worklet_thread_async__deps: [ - '$_wasmWorkersID', + '$_wasmWorkers', '$_EmAudioDispatchProcessorCallback'], emscripten_start_wasm_audio_worklet_thread_async: function(contextHandle, stackLowestAddress, stackSize, callback, userData) { @@ -173,9 +173,11 @@ let LibraryWebAudio = { #if WEBAUDIO_DEBUG console.log(`emscripten_start_wasm_audio_worklet_thread_async() addModule('audioworklet.js') completed`); #endif + // Assign the loaded AudioWorkletGlobalScope a Wasm Worker ID so that it can utilized its own TLS slots, and it is recognized to not be the main browser thread. + var id = _wasmWorkers.allocate({}); audioWorklet.bootstrapMessage = new AudioWorkletNode(audioContext, 'message', { processorOptions: { - '$ww': _wasmWorkersID++, // Assign the loaded AudioWorkletGlobalScope a Wasm Worker ID so that it can utilized its own TLS slots, and it is recognized to not be the main browser thread. + '$ww': id, #if MINIMAL_RUNTIME 'wasm': Module['wasm'], 'mem': wasmMemory, diff --git a/test/code_size/hello_wasm_worker_wasm.js b/test/code_size/hello_wasm_worker_wasm.js index 77a6b5a4d9824..1d9133f1f4435 100644 --- a/test/code_size/hello_wasm_worker_wasm.js +++ b/test/code_size/hello_wasm_worker_wasm.js @@ -1,42 +1,52 @@ -var b = Module, c = b.$ww, f, e = b.mem || new WebAssembly.Memory({ +var b = Module, d = b.$ww, f, e = b.mem || new WebAssembly.Memory({ initial: 256, maximum: 256, shared: !0 -}), g = e.buffer, h = [], m = {}, n = 1, p, q; +}), g = e.buffer, h = [], n = new function() { + this.j = [ void 0 ]; + this.m = []; + this.get = a => this.j[a]; + this.has = a => void 0 !== this.j[a]; + this.l = a => { + var c = this.m.pop() || this.j.length; + this.j[c] = a; + return c; + }; +}, p, q; function k(a) { a = a.data; - let d = a._wsc; - d && f.get(d)(...a.x); + let c = a._wsc; + c && f.get(c)(...a.x); } -function l(a) { +function m(a) { h.push(a); } -c && (m[0] = this, addEventListener("message", l)); +d && (n.j[0] = this, addEventListener("message", m)); WebAssembly.instantiate(b.wasm, { a: { - b: function(a, d) { - let r = m[n] = new Worker(b.$wb); - r.postMessage({ - $ww: n, + b: function(a, c) { + let l = new Worker(b.$wb), r = n.l(l); + l.postMessage({ + $ww: r, wasm: b.wasm, js: b.js, mem: e, sb: a, - sz: d + sz: c }); - r.onmessage = k; - return n++; + l.onmessage = k; + return r; }, c: function() { return !1; }, - d: function(a, d) { - m[a].postMessage({ - _wsc: d, + d: function(a, c) { + n.get(a).postMessage({ + _wsc: c, x: [] }); }, @@ -50,7 +60,7 @@ WebAssembly.instantiate(b.wasm, { p = a.g; q = a.i; f = a.h; - c ? (a = b, q(a.sb, a.sz), removeEventListener("message", l), h = h.forEach(k), + d ? (a = b, q(a.sb, a.sz), removeEventListener("message", m), h = h.forEach(k), addEventListener("message", k)) : a.f(); - c || p(); + d || p(); })); \ No newline at end of file diff --git a/test/code_size/hello_wasm_worker_wasm.json b/test/code_size/hello_wasm_worker_wasm.json index b8f284b88c113..ecaa701a5c8ba 100644 --- a/test/code_size/hello_wasm_worker_wasm.json +++ b/test/code_size/hello_wasm_worker_wasm.json @@ -1,10 +1,10 @@ { "a.html": 737, "a.html.gz": 433, - "a.js": 715, - "a.js.gz": 465, + "a.js": 878, + "a.js.gz": 533, "a.wasm": 1862, "a.wasm.gz": 1040, - "total": 3314, - "total_gz": 1938 + "total": 3477, + "total_gz": 2006 } diff --git a/test/wasm_worker/terminate_wasm_worker.c b/test/wasm_worker/terminate_wasm_worker.c index ce3989bced614..5b77b4f0d4059 100644 --- a/test/wasm_worker/terminate_wasm_worker.c +++ b/test/wasm_worker/terminate_wasm_worker.c @@ -45,11 +45,15 @@ char stack[1024]; int should_throw(void(*func)()) { int threw = EM_ASM_INT({ + var oldAbort = abort; + abort = () => { throw 'abort' }; try { dynCall('v', $0); } catch(e) { console.error('Threw an exception like expected: ' + e); return 1; + } finally { + abort = oldAbort; } console.error('Function was expected to throw, but did not!'); return 0;