diff --git a/src/library_pthread.js b/src/library_pthread.js index da8a2b2670ba4..b920e442da2ad 100644 --- a/src/library_pthread.js +++ b/src/library_pthread.js @@ -14,11 +14,6 @@ var LibraryPThread = { schedPolicy: 0/*SCHED_OTHER*/, schedPrio: 0 }, - // Workers that have been created but uninitialized. These have already been - // parsed, but the wasm file has not yet been loaded, making it - // distinct from the unusedWorkers below. These workers will be created before - // loading wasm on the main thread. - preallocatedWorkers: [], // Contains all Workers that are idle/unused and not currently hosting an executing pthread. // Unused Workers can either be pooled up before page startup, but also when a pthread quits, its hosting // Worker is not terminated, but is returned to this pool as an optimization so that starting the next thread is faster. @@ -34,14 +29,26 @@ var LibraryPThread = { _emscripten_register_main_browser_thread_id(PThread.mainThreadBlock); }, initMainThreadBlock: function() { - if (ENVIRONMENT_IS_PTHREAD) return undefined; + if (ENVIRONMENT_IS_PTHREAD) return; #if PTHREAD_POOL_SIZE > 0 - var requestedPoolSize = {{{ PTHREAD_POOL_SIZE }}}; -#if PTHREADS_DEBUG - out('Preallocating ' + requestedPoolSize + ' workers.'); + // Start loading up the Worker pool, if requested. + for(var i = 0; i < {{{PTHREAD_POOL_SIZE}}}; ++i) { + PThread.allocateUnusedWorker(); + } #endif - PThread.preallocatedWorkers = PThread.createNewWorkers(requestedPoolSize); + + // In asm.js we do not need to wait for Wasm Module to compile on the main thread, so can load + // up each Worker immediately. (in asm.js mode ignore PTHREAD_POOL_DELAY_LOAD altogether for + // simplicity, as multithreading performance optimizations are not interesting there) +#if !WASM && PTHREAD_POOL_SIZE > 0 + addOnPreRun(function() { addRunDependency('pthreads'); }); + var numWorkersToLoad = PThread.unusedWorkers.length; + PThread.unusedWorkers.forEach(function(w) { PThread.loadWasmModuleToWorker(w, function() { + // PTHREAD_POOL_DELAY_LOAD==0: we wanted to synchronously wait until the Worker pool + // has loaded up. If all Workers have finished loading up the Wasm Module, proceed with main() + if (!--numWorkersToLoad) removeRunDependency('pthreads'); + })}); #endif PThread.mainThreadBlock = {{{ makeStaticAlloc(C_STRUCTS.pthread.__size__) }}}; @@ -217,15 +224,6 @@ var LibraryPThread = { } PThread.pthreads = {}; - for (var i = 0; i < PThread.preallocatedWorkers.length; ++i) { - var worker = PThread.preallocatedWorkers[i]; -#if ASSERTIONS - assert(!worker.pthread); // This Worker should not be hosting a pthread at this time. -#endif - worker.terminate(); - } - PThread.preallocatedWorkers = []; - for (var i = 0; i < PThread.unusedWorkers.length; ++i) { var worker = PThread.unusedWorkers[i]; #if ASSERTIONS @@ -281,190 +279,148 @@ var LibraryPThread = { #endif }, - // Allocates the given amount of new web workers and stores them in the pool of unused workers. + // Loads the WebAssembly module into the given list of Workers. // onFinishedLoading: A callback function that will be called once all of the workers have been initialized and are - // ready to host pthreads. Optional. This is used to mitigate bug https://bugzilla.mozilla.org/show_bug.cgi?id=1049079 - allocateUnusedWorkers: function(numWorkers, onFinishedLoading) { - if (typeof SharedArrayBuffer === 'undefined') return; // No multithreading support, no-op. -#if PTHREADS_DEBUG - out('Allocating ' + numWorkers + ' workers for a pthread spawn pool.'); + // ready to host pthreads. + loadWasmModuleToWorker: function(worker, onFinishedLoading) { + worker.onmessage = function(e) { + var d = e['data']; + var cmd = d['cmd']; + // Sometimes we need to backproxy events to the calling thread (e.g. HTML5 DOM events handlers such as emscripten_set_mousemove_callback()), so keep track in a globally accessible variable about the thread that initiated the proxying. + if (worker.pthread) PThread.currentProxiedOperationCallerThread = worker.pthread.threadInfoStruct; + + // If this message is intended to a recipient that is not the main thread, forward it to the target thread. + if (d['targetThread'] && d['targetThread'] != _pthread_self()) { + var thread = PThread.pthreads[d.targetThread]; + if (thread) { + thread.worker.postMessage(e.data, d['transferList']); + } else { + console.error('Internal error! Worker sent a message "' + cmd + '" to target pthread ' + d['targetThread'] + ', but that thread no longer exists!'); + } + PThread.currentProxiedOperationCallerThread = undefined; + return; + } + + if (cmd === 'processQueuedMainThreadWork') { + // TODO: Must post message to main Emscripten thread in PROXY_TO_WORKER mode. + _emscripten_main_thread_process_queued_calls(); + } else if (cmd === 'spawnThread') { + __spawn_thread(e.data); + } else if (cmd === 'cleanupThread') { + __cleanup_thread(d['thread']); + } else if (cmd === 'killThread') { + __kill_thread(d['thread']); + } else if (cmd === 'cancelThread') { + __cancel_thread(d['thread']); + } else if (cmd === 'loaded') { + worker.loaded = true; + if (onFinishedLoading) onFinishedLoading(worker); + // If this Worker is already pending to start running a thread, launch the thread now + if (worker.runPthread) { + worker.runPthread(); + delete worker.runPthread; + } + } else if (cmd === 'print') { + out('Thread ' + d['threadId'] + ': ' + d['text']); + } else if (cmd === 'printErr') { + err('Thread ' + d['threadId'] + ': ' + d['text']); + } else if (cmd === 'alert') { + alert('Thread ' + d['threadId'] + ': ' + d['text']); + } else if (cmd === 'exit') { + var detached = worker.pthread && Atomics.load(HEAPU32, (worker.pthread.thread + {{{ C_STRUCTS.pthread.detached }}}) >> 2); + if (detached) { + PThread.returnWorkerToPool(worker); + } +#if EXIT_RUNTIME // If building with -s EXIT_RUNTIME=0, no thread will post this message, so don't even compile it in. + } else if (cmd === 'exitProcess') { + // A pthread has requested to exit the whole application process (runtime). + noExitRuntime = false; + try { + exit(d['returnCode']); + } catch (e) { + if (e instanceof ExitStatus) return; + throw e; + } #endif + } else if (cmd === 'cancelDone') { + PThread.returnWorkerToPool(worker); + } else if (cmd === 'objectTransfer') { + PThread.receiveObjectTransfer(e.data); + } else if (e.data.target === 'setimmediate') { + worker.postMessage(e.data); // Worker wants to postMessage() to itself to implement setImmediate() emulation. + } else { + err("worker sent an unknown command " + cmd); + } + PThread.currentProxiedOperationCallerThread = undefined; + }; - var workers = []; + worker.onerror = function(e) { + err('pthread sent an error! ' + e.filename + ':' + e.lineno + ': ' + e.message); + }; - var numWorkersToCreate = numWorkers; - if (PThread.preallocatedWorkers.length > 0) { - var workersUsed = Math.min(PThread.preallocatedWorkers.length, numWorkers); -#if PTHREADS_DEBUG - out('Using ' + workersUsed + ' preallocated workers'); -#endif - workers = workers.concat(PThread.preallocatedWorkers.splice(0, workersUsed)); - numWorkersToCreate -= workersUsed; - } - if (numWorkersToCreate > 0) { - workers = workers.concat(PThread.createNewWorkers(numWorkersToCreate)); +#if ENVIRONMENT_MAY_BE_NODE + if (ENVIRONMENT_HAS_NODE) { + worker.on('message', function(data) { + worker.onmessage({ data: data }); + }); + worker.on('error', function(data) { + worker.onerror(data); + }); + worker.on('exit', function(data) { + console.log('worker exited - TODO: update the worker queue?'); + }); } +#endif - // Add the listeners. - PThread.attachListenerToWorkers(workers, onFinishedLoading); - - // Load the wasm module into the worker. - for (var i = 0; i < numWorkers; ++i) { - var worker = workers[i]; - - // Ask the new worker to load up the Emscripten-compiled page. This is a heavy operation. - worker.postMessage({ - 'cmd': 'load', - // If the application main .js file was loaded from a Blob, then it is not possible - // to access the URL of the current script that could be passed to a Web Worker so that - // it could load up the same file. In that case, developer must either deliver the Blob - // object in Module['mainScriptUrlOrBlob'], or a URL to it, so that pthread Workers can - // independently load up the same main application file. - 'urlOrBlob': Module['mainScriptUrlOrBlob'] || _scriptDir, +#if ASSERTIONS && WASM + assert(wasmMemory, 'WebAssembly memory should have been loaded by now!'); + assert(wasmModule, 'WebAssembly Module should have been loaded by now!'); +#endif + + // Ask the new worker to load up the Emscripten-compiled page. This is a heavy operation. + worker.postMessage({ + 'cmd': 'load', + // If the application main .js file was loaded from a Blob, then it is not possible + // to access the URL of the current script that could be passed to a Web Worker so that + // it could load up the same file. In that case, developer must either deliver the Blob + // object in Module['mainScriptUrlOrBlob'], or a URL to it, so that pthread Workers can + // independently load up the same main application file. + 'urlOrBlob': Module['mainScriptUrlOrBlob'] || _scriptDir, #if WASM - 'wasmMemory': wasmMemory, - 'wasmModule': wasmModule, + 'wasmMemory': wasmMemory, + 'wasmModule': wasmModule, #if LOAD_SOURCE_MAP - 'wasmSourceMap': wasmSourceMap, + 'wasmSourceMap': wasmSourceMap, #endif #if USE_OFFSET_CONVERTER - 'wasmOffsetConverter': wasmOffsetConverter, + 'wasmOffsetConverter': wasmOffsetConverter, #endif #else - 'buffer': HEAPU8.buffer, - 'asmJsUrlOrBlob': Module["asmJsUrlOrBlob"], + 'buffer': HEAPU8.buffer, + 'asmJsUrlOrBlob': Module["asmJsUrlOrBlob"], #endif - 'DYNAMIC_BASE': DYNAMIC_BASE, - 'DYNAMICTOP_PTR': DYNAMICTOP_PTR - }); - PThread.unusedWorkers.push(worker); - } + 'DYNAMIC_BASE': DYNAMIC_BASE, + 'DYNAMICTOP_PTR': DYNAMICTOP_PTR + }); }, - // Attaches the listeners to the given workers. If onFinishedLoading is provided, - // will call that function when all workers have been loaded. It is assumed that no worker - // is yet loaded. - attachListenerToWorkers: function(workers, onFinishedLoading) { - var numWorkersLoaded = 0; - var numWorkers = workers.length; - for (var i = 0; i < numWorkers; ++i) { - var worker = workers[i]; - (function(worker) { - worker.onmessage = function(e) { - var d = e['data']; - var cmd = d['cmd']; - // Sometimes we need to backproxy events to the calling thread (e.g. HTML5 DOM events handlers such as emscripten_set_mousemove_callback()), so keep track in a globally accessible variable about the thread that initiated the proxying. - if (worker.pthread) PThread.currentProxiedOperationCallerThread = worker.pthread.threadInfoStruct; - - // If this message is intended to a recipient that is not the main thread, forward it to the target thread. - if (d['targetThread'] && d['targetThread'] != _pthread_self()) { - var thread = PThread.pthreads[d.targetThread]; - if (thread) { - thread.worker.postMessage(e.data, d['transferList']); - } else { - console.error('Internal error! Worker sent a message "' + cmd + '" to target pthread ' + d['targetThread'] + ', but that thread no longer exists!'); - } - PThread.currentProxiedOperationCallerThread = undefined; - return; - } - - if (cmd === 'processQueuedMainThreadWork') { - // TODO: Must post message to main Emscripten thread in PROXY_TO_WORKER mode. - _emscripten_main_thread_process_queued_calls(); - } else if (cmd === 'spawnThread') { - __spawn_thread(e.data); - } else if (cmd === 'cleanupThread') { - __cleanup_thread(d['thread']); - } else if (cmd === 'killThread') { - __kill_thread(d['thread']); - } else if (cmd === 'cancelThread') { - __cancel_thread(d['thread']); - } else if (cmd === 'loaded') { - worker.loaded = true; - // If this Worker is already pending to start running a thread, launch the thread now - if (worker.runPthread) { - worker.runPthread(); - delete worker.runPthread; - } - ++numWorkersLoaded; - if (numWorkersLoaded === numWorkers && onFinishedLoading) { - onFinishedLoading(); - } - } else if (cmd === 'print') { - out('Thread ' + d['threadId'] + ': ' + d['text']); - } else if (cmd === 'printErr') { - err('Thread ' + d['threadId'] + ': ' + d['text']); - } else if (cmd === 'alert') { - alert('Thread ' + d['threadId'] + ': ' + d['text']); - } else if (cmd === 'exit') { - var detached = worker.pthread && Atomics.load(HEAPU32, (worker.pthread.thread + {{{ C_STRUCTS.pthread.detached }}}) >> 2); - if (detached) { - PThread.returnWorkerToPool(worker); - } -#if EXIT_RUNTIME // If building with -s EXIT_RUNTIME=0, no thread will post this message, so don't even compile it in. - } else if (cmd === 'exitProcess') { - // A pthread has requested to exit the whole application process (runtime). - noExitRuntime = false; - try { - exit(d['returnCode']); - } catch (e) { - if (e instanceof ExitStatus) return; - throw e; - } -#endif - } else if (cmd === 'cancelDone') { - PThread.returnWorkerToPool(worker); - } else if (cmd === 'objectTransfer') { - PThread.receiveObjectTransfer(e.data); - } else if (e.data.target === 'setimmediate') { - worker.postMessage(e.data); // Worker wants to postMessage() to itself to implement setImmediate() emulation. - } else { - err("worker sent an unknown command " + cmd); - } - PThread.currentProxiedOperationCallerThread = undefined; - }; - - worker.onerror = function(e) { - err('pthread sent an error! ' + e.filename + ':' + e.lineno + ': ' + e.message); - }; - -#if ENVIRONMENT_MAY_BE_NODE - if (ENVIRONMENT_HAS_NODE) { - worker.on('message', function(data) { - worker.onmessage({ data: data }); - }); - worker.on('error', function(data) { - worker.onerror(data); - }); - worker.on('exit', function(data) { - console.log('worker exited - TODO: update the worker queue?'); - }); - } -#endif - }(worker)); - } // for each worker - }, - - createNewWorkers: function(numWorkers) { - // Creates new workers with the discovered pthread worker file. - if (typeof SharedArrayBuffer === 'undefined') return []; // No multithreading support, no-op. -#if PTHREADS_DEBUG - out('Creating ' + numWorkers + ' workers.'); -#endif - var pthreadMainJs = "{{{ PTHREAD_WORKER_FILE }}}"; + // Creates a new web Worker and places it in the unused worker pool to wait for its use. + allocateUnusedWorker: function() { // Allow HTML module to configure the location where the 'worker.js' file will be loaded from, // via Module.locateFile() function. If not specified, then the default URL 'worker.js' relative // to the main html file is loaded. - pthreadMainJs = locateFile(pthreadMainJs); - var newWorkers = []; - for (var i = 0; i < numWorkers; ++i) { - newWorkers.push(new Worker(pthreadMainJs)); - } - return newWorkers; + var pthreadMainJs = locateFile('{{{ PTHREAD_WORKER_FILE }}}'); +#if PTHREADS_DEBUG + out('Allocating a new web worker from ' + pthreadMainJs); +#endif + PThread.unusedWorkers.push(new Worker(pthreadMainJs)); }, getNewWorker: function() { - if (PThread.unusedWorkers.length == 0) PThread.allocateUnusedWorkers(1); + if (PThread.unusedWorkers.length == 0) { + PThread.allocateUnusedWorker(); + PThread.loadWasmModuleToWorker(PThread.unusedWorkers[0]); + } if (PThread.unusedWorkers.length > 0) return PThread.unusedWorkers.pop(); else return null; }, diff --git a/src/preamble.js b/src/preamble.js index a0248497cd5eb..9f27c50047ece 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -781,11 +781,6 @@ var memoryInitializer = null; #include "memoryprofiler.js" #endif -#if PTHREAD_POOL_SIZE > 0 && PTHREAD_POOL_DELAY_LOAD != 1 -// To work around https://bugzilla.mozilla.org/show_bug.cgi?id=1049079, warm up a worker pool before starting up the application. -if (!ENVIRONMENT_IS_PTHREAD) addOnPreRun(function() { if (typeof SharedArrayBuffer !== 'undefined') { addRunDependency('pthreads'); PThread.allocateUnusedWorkers({{{PTHREAD_POOL_SIZE}}}, function() { removeRunDependency('pthreads'); }); }}); -#endif - #if ASSERTIONS && !('$FS' in addedLibraryItems) && !ASMFS // show errors on likely calls to FS when it was not included var FS = { @@ -920,11 +915,27 @@ function createWasm() { #endif #endif #if USE_PTHREADS - // Keep a reference to the compiled module so we can post it to the workers. + // We now have the Wasm module loaded up, keep a reference to the compiled module so we can post it to the workers. wasmModule = module; // Instantiation is synchronous in pthreads and we assert on run dependencies. - if (!ENVIRONMENT_IS_PTHREAD) removeRunDependency('wasm-instantiate'); -#else + if (!ENVIRONMENT_IS_PTHREAD) { +#if PTHREAD_POOL_SIZE > 0 + var numWorkersToLoad = PThread.unusedWorkers.length; + PThread.unusedWorkers.forEach(function(w) { PThread.loadWasmModuleToWorker(w, function() { +#if !PTHREAD_POOL_DELAY_LOAD + // PTHREAD_POOL_DELAY_LOAD==0: we wanted to synchronously wait until the Worker pool + // has loaded up. If all Workers have finished loading up the Wasm Module, proceed with main() + if (!--numWorkersToLoad) removeRunDependency('wasm-instantiate'); +#endif + })}); +#endif +#if PTHREAD_POOL_DELAY_LOAD || PTHREAD_POOL_SIZE == 0 + // PTHREAD_POOL_DELAY_LOAD==1 (or no preloaded pool in use): do not wait up for the Workers to + // instantiate the Wasm module, but proceed with main() immediately. + removeRunDependency('wasm-instantiate'); +#endif + } +#else // singlethreaded build: removeRunDependency('wasm-instantiate'); #endif } diff --git a/src/settings.js b/src/settings.js index b8bc41151bdfd..b684424493ffe 100644 --- a/src/settings.js +++ b/src/settings.js @@ -1379,15 +1379,33 @@ var IN_TEST_HARNESS = 0; // [compile+link] - affects user code at compile and system libraries at link var USE_PTHREADS = 0; -// PTHREAD_POOL_SIZE specifies the number of web workers that are created -// before the main runtime is initialized. If 0, workers are created on -// demand. If PTHREAD_POOL_DELAY_LOAD = 0, then the workers will be fully -// loaded (available for use) prior to the main runtime being initialized. If -// PTHREAD_POOL_DELAY_LOAD = 1, then the workers will only be created and -// have their runtimes loaded on demand after the main runtime is initialized. -// Note that this means that the workers cannot be joined from the main thread -// unless PROXY_TO_PTHREAD is used. +// In web browsers, Workers cannot be created while the main browser thread +// is executing JS/Wasm code, but the main thread must regularly yield back +// to the browser event loop for Worker initialization to occur. +// This means that pthread_create() is essentially an asynchronous operation +// when called from the main browser thread, and the main thread must +// repeatedly yield back to the JS event loop in order for the thread to +// actually start. +// If your application needs to be able to synchronously create new threads, +// you can pre-create a pthread pool by specifying -s PTHREAD_POOL_SIZE=x, +// in which case the specified number of Workers will be preloaded into a pool +// before the application starts, and that many threads can then be available +// for synchronous creation. +// [link] - affects generated JS runtime code at link time var PTHREAD_POOL_SIZE = 0; + +// If your application does not need the ability to synchronously create +// threads, but it would still like to opportunistically speed up initial thread +// startup time by prewarming a pool of Workers, you can specify the size of +// the pool with -s PTHREAD_POOL_SIZE=x, but then also specify +// -s PTHREAD_POOL_DELAY_LOAD=1, which will cause the runtime to not wait up at +// startup for the Worker pool to finish loading. Instead, the runtime will +// immediately start up and the Worker pool will asynchronously spin up in +// parallel on the background. This can shorten the time that pthread_create() +// calls take to actually start a thread, but without actually slowing down +// main application startup speed. If PTHREAD_POOL_DELAY_LOAD=0 (default), +// then the runtime will wait for the pool to start up before running main(). +// [link] - affects generated JS runtime code at link time var PTHREAD_POOL_DELAY_LOAD = 0; // If not explicitly specified, this is the stack size to use for newly created diff --git a/tests/pthread/test_pthread_file_io.cpp b/tests/pthread/test_pthread_file_io.cpp index 982564f7dc53a..dfebc7eefb254 100644 --- a/tests/pthread/test_pthread_file_io.cpp +++ b/tests/pthread/test_pthread_file_io.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include diff --git a/tests/pthread/test_pthread_preallocates_workers.cpp b/tests/pthread/test_pthread_preallocates_workers.cpp index 2040f356c0784..e284ef93c5bf6 100644 --- a/tests/pthread/test_pthread_preallocates_workers.cpp +++ b/tests/pthread/test_pthread_preallocates_workers.cpp @@ -47,15 +47,13 @@ int main() // This test should be run with a prewarmed pool of size 4. None // of the threads are allocated yet. - assert(EM_ASM_INT(return PThread.preallocatedWorkers.length) == 4); - assert(EM_ASM_INT(return PThread.unusedWorkers.length) == 0); + assert(EM_ASM_INT(return PThread.unusedWorkers.length) == 4); assert(EM_ASM_INT(return PThread.runningWorkers.length) == 0); CreateThread(0); // We have one running thread, allocated on demand. - assert(EM_ASM_INT(return PThread.preallocatedWorkers.length) == 3); - assert(EM_ASM_INT(return PThread.unusedWorkers.length) == 0); + assert(EM_ASM_INT(return PThread.unusedWorkers.length) == 3); assert(EM_ASM_INT(return PThread.runningWorkers.length) == 1); for (int i = 1; i < 5; ++i) {