diff --git a/.eslintrc.yml b/.eslintrc.yml index 391ed268db98..e0d34de7d5d6 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -21,6 +21,7 @@ ignorePatterns: - "src/embind/" - "src/emrun_postjs.js" - "src/worker.js" + - "src/wasm_worker.js" - "src/wasm2js.js" - "src/webGLClient.js" - "src/webGLWorker.js" diff --git a/ChangeLog.md b/ChangeLog.md index a711f179fd9b..d7cc6d128c75 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -26,6 +26,10 @@ See docs/process.md for more on how version tagging works. - Fix deadlock in `munmap` that was introduced in 3.1.5. The deadlock would occur in multi-threaded programs when a partial unmap was requested (which emscripten does not support). (#16413) +- Added new compiler+linker option -sSHARED_MEMORY=1, which enables targeting + a shared WebAssembly.Memory. (#16419) +- Added new API "Wasm Workers", which is an alternative to pthreads for building + multithreaded applications, enabled via -sWASM_WORKERS=1 (#12833) 3.1.6 - 02/24/2022 ------------------ diff --git a/emcc.py b/emcc.py index eda5cc5ad8c8..edafcfa9aec3 100755 --- a/emcc.py +++ b/emcc.py @@ -833,6 +833,9 @@ def get_cflags(user_args): if settings.SHARED_MEMORY: cflags.append('-D__EMSCRIPTEN_SHARED_MEMORY__=1') + if settings.WASM_WORKERS: + cflags.append('-D__EMSCRIPTEN_WASM_WORKERS__=1') + if not settings.STRICT: # The preprocessor define EMSCRIPTEN is deprecated. Don't pass it to code # in strict mode. Code should use the define __EMSCRIPTEN__ instead. @@ -1424,11 +1427,17 @@ def phase_setup(options, state, newargs, user_settings): if settings.MAIN_MODULE or settings.SIDE_MODULE: settings.RELOCATABLE = 1 - if settings.USE_PTHREADS: + # Pthreads and Wasm Workers require targeting shared Wasm memory (SAB). + if settings.USE_PTHREADS or settings.WASM_WORKERS: settings.SHARED_MEMORY = 1 - if settings.SHARED_MEMORY and '-pthread' not in newargs: + if settings.USE_PTHREADS and '-pthread' not in newargs: newargs += ['-pthread'] + elif settings.SHARED_MEMORY: + if '-matomics' not in newargs: + newargs += ['-matomics'] + if '-mbulk-memory' not in newargs: + newargs += ['-mbulk-memory'] if 'DISABLE_EXCEPTION_CATCHING' in user_settings and 'EXCEPTION_CATCHING_ALLOWED' in user_settings: # If we get here then the user specified both DISABLE_EXCEPTION_CATCHING and EXCEPTION_CATCHING_ALLOWED @@ -2066,6 +2075,18 @@ def phase_linker_setup(options, state, newargs, user_settings): if settings.INCLUDE_FULL_LIBRARY and not settings.DISABLE_EXCEPTION_CATCHING: settings.EXPORTED_FUNCTIONS += ['_emscripten_format_exception', '_free'] + if settings.WASM_WORKERS: + # TODO: After #15982 is resolved, these dependencies can be declared in library_wasm_worker.js + # instead of having to record them here. + wasm_worker_imports = ['_emscripten_wasm_worker_initialize'] + settings.EXPORTED_FUNCTIONS += wasm_worker_imports + building.user_requested_exports.update(wasm_worker_imports) + settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['_wasm_worker_initializeRuntime'] + # set location of Wasm Worker bootstrap JS file + if settings.WASM_WORKERS == 1: + settings.WASM_WORKER_FILE = unsuffixed(os.path.basename(target)) + '.ww.js' + settings.JS_LIBRARIES.append((0, shared.path_from_root('src', 'library_wasm_worker.js'))) + if settings.FORCE_FILESYSTEM and not settings.MINIMAL_RUNTIME: # when the filesystem is forced, we export by default methods that filesystem usage # may need, including filesystem usage from standalone file packager output (i.e. @@ -2805,8 +2826,8 @@ def phase_final_emitting(options, state, target, wasm_target, memfile): # src = re.sub(r'\n+[ \n]*\n+', '\n', src) # write_file(final_js, src) + target_dir = os.path.dirname(os.path.abspath(target)) if settings.USE_PTHREADS: - target_dir = os.path.dirname(os.path.abspath(target)) worker_output = os.path.join(target_dir, settings.PTHREAD_WORKER_FILE) contents = shared.read_and_preprocess(utils.path_from_root('src/worker.js'), expand_macros=True) write_file(worker_output, contents) @@ -2816,6 +2837,17 @@ def phase_final_emitting(options, state, target, wasm_target, memfile): minified_worker = building.acorn_optimizer(worker_output, ['minifyWhitespace'], return_output=True) write_file(worker_output, minified_worker) + # Deploy the Wasm Worker bootstrap file as an output file (*.ww.js) + if settings.WASM_WORKERS == 1: + worker_output = os.path.join(target_dir, settings.WASM_WORKER_FILE) + with open(worker_output, 'w') as f: + f.write(shared.read_and_preprocess(shared.path_from_root('src', 'wasm_worker.js'), expand_macros=True)) + + # Minify the wasm_worker.js file in optimized builds + if (settings.OPT_LEVEL >= 1 or settings.SHRINK_LEVEL >= 1) and not settings.DEBUG_LEVEL: + minified_worker = building.acorn_optimizer(worker_output, ['minifyWhitespace'], return_output=True) + open(worker_output, 'w').write(minified_worker) + # track files that will need native eols generated_text_files_with_native_eols = [] diff --git a/site/source/_themes/emscripten_sphinx_rtd_theme/static/css/theme.css b/site/source/_themes/emscripten_sphinx_rtd_theme/static/css/theme.css index 07f99a3d7a82..a7b7eba2dc89 100644 --- a/site/source/_themes/emscripten_sphinx_rtd_theme/static/css/theme.css +++ b/site/source/_themes/emscripten_sphinx_rtd_theme/static/css/theme.css @@ -510,3 +510,4 @@ and (max-width : 480px) { } } +.cellborder { border: solid 1px black; } \ No newline at end of file diff --git a/site/source/docs/api_reference/index.rst b/site/source/docs/api_reference/index.rst index a60489f2ba82..4bd5b703e577 100644 --- a/site/source/docs/api_reference/index.rst +++ b/site/source/docs/api_reference/index.rst @@ -21,6 +21,9 @@ This section lists Emscripten's public API, organised by header file. At a very - :ref:`Fetch-API`: API for managing accesses to network XHR and IndexedDB. +- :ref:`wasm_workers`: + Enables writing multithreaded programs using a web-like API. + - :ref:`Module`: Global JavaScript object that can be used to control code execution and access exported methods. @@ -53,6 +56,7 @@ This section lists Emscripten's public API, organised by header file. At a very bind.h trace.h fiber.h + wasm_workers advanced-apis diff --git a/site/source/docs/api_reference/wasm_workers.rst b/site/source/docs/api_reference/wasm_workers.rst new file mode 100644 index 000000000000..7278f7539f4d --- /dev/null +++ b/site/source/docs/api_reference/wasm_workers.rst @@ -0,0 +1,356 @@ +.. _wasm_workers: + +================ +Wasm Workers API +================ + +The Wasm Workers API enables C/C++ code to leverage Web Workers and shared +WebAssembly.Memory (SharedArrayBuffer) to build multithreaded programs +via a direct web-like programming API. + +Quick Example +============= + +.. code-block:: cpp + + #include + #include + + void run_in_worker() + { + printf("Hello from wasm worker!\n"); + } + + int main() + { + emscripten_wasm_worker_t worker = emscripten_malloc_wasm_worker(/*stack size: */1024); + emscripten_wasm_worker_post_function_v(worker, run_in_worker); + } + +Build the code by passing the Emscripten flag ``-sWASM_WORKERS=1`` at both compile +and link steps. The example code creates a new Worker on the main browser thread, +which shares the same WebAssembly.Module and WebAssembly.Memory object. Then a +``postMessage()`` is passed to the Worker to ask it to execute the function +``run_in_worker()`` to print a string. + +For more complex threading scenarios, see the functions ``emscripten_create_wasm_worker_with_tls()`` +and ``emscripten_create_wasm_worker_no_tls()`` on how to manage the memory resources that +the thread will use. + +Introduction +============ + +In WebAssembly programs, the Memory object that contains the application state can be +shared across multiple Workers. This enables direct, high performance (and if explicit +care is not taken, racy!) access to synchronously share data state between multiple +Workers (shared state multithreading). + +Emscripten supports two multithreading APIs to leverage this web feature: + - POSIX Threads (Pthreads) API, and + - Wasm Workers API. + +The Pthreads API has a long history with native C programming and the POSIX standard, +while Wasm Workers API is unique to Emscripten compiler only. + +These two APIs provide largely the same feature set, but have important differences, +which this documentation seeks to explain to help decide which API one should target. + +Pthreads vs Wasm Workers: Which One to Use? +=========================================== + +The intended audience and use cases of these two multithreading APIs are slightly +different. + +The focus on Pthreads API is on portability and cross-platform compatibility. This API +is best used in scenarios where portability is most important, e.g. when a codebase is +cross-compiled to multiple platforms, like building both a native Linux x64 executable and an +Emscripten WebAssembly based web site. + +Pthreads API in Emscripten seeks to carefully emulate compatibility and the features that +the native Pthreads platforms already provide. This helps porting large C/C++ codebases +over to WebAssembly. + +Wasm Workers API on the other hand seeks to provide a "direct mapping" to the web +multithreading primitives as they exist on the web, and call it a day. If an application +is only developed to target WebAssembly, and portability is not a concern, then using Wasm +Workers can provide great benefits in the form of simpler compiled output, less complexity, +smaller code size and possibly better performance. + +However this benefit might not be an obvious win. The Pthreads API was designed to be useful +from the synchronous C/C++ language, whereas Web Workers are designed to be useful from +asynchronous JavaScript. WebAssembly C/C++ programs can find themselves somewhere in +the middle. + +Pthreads and Wasm Workers share several similarities: + + * Both can use emscripten_atomic_* Atomics API, + * Both can use GCC __sync_* Atomics API, + * Both can use C11 and C++11 Atomics APIs, + * Both types of threads have a local stack. + * Both types of threads have thread-local storage (TLS) support via ``thread_local`` (C++11), + ``_Thread_local`` (C11) and ``__thread`` (GNU11) keywords. + * Both types of threads support TLS via explicitly linked in Wasm globals (see + ``tests/wasm_worker/wasm_worker_tls_wasm_assembly.c/.S`` for example code) + * Both types of threads have a concept of a thread ID (``pthread_self()`` for pthreads, + ``emscripten_wasm_worker_self_id()`` for Wasm Workers) + * Both types of threads can perform an event-based and an infinite loop programming model. + * Both can use ``EM_ASM`` and ``EM_JS`` API to execute JS code on the calling thread. + * Both can call out to JS library functions (linked in with ``--js-library`` directive) to + execute JS code on the calling thread. + * Neither pthreads nor Wasm Workers can be used in conjunction with ``-sSINGLE_FILE=1`` linker flag. + +However, the differences are more notable. + +Pthreads can proxy JS functions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Only pthreads can use the ``MAIN_THREAD_EM_ASM*()`` and ``MAIN_THREAD_ASYNC_EM_ASM()`` functions and +the ``foo__proxy: 'sync'/'async'`` proxying directive in JS libraries. + +Wasm Workers on the other hand do not provide a built-in JS function proxying facility. Proxying a JS +function with Wasm Workers can be done by explicitly passing the address of that function to the +``emscripten_wasm_worker_post_function_*`` API. + +If you need to synchronously wait for the posted function to finish from within a Worker, use one of +the ``emscripten_wasm_worker_*()`` thread synchronization functions to sleep the calling thread until +the callee has finished the operation. + +Note that Wasm Workers cannot + +Pthreads have cancellation points +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +At the expense of performance and code size, pthreads implement a notion of **POSIX cancellation +points** (``pthread_cancel()``, ``pthread_testcancel()``). + +Wasm Workers are more lightweight and performant by not enabling that concept. + +Pthreads may start up synchronously - Wasm Workers always start asynchronously +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Creating new Workers can be slow. Spawning a Worker in JavaScript is an asynchronous operation. In order +to support synchronous pthread startup (for applications that need it) and to improve thread startup +performance, pthreads are hosted in a cached Emscripten runtime managed Worker pool. + +Wasm Workers omit this concept, and as result Wasm Workers will always start up asynchronously. +If you need to detect when a Wasm Worker has started up, post a ping-pong function and reply pair +manually between the Worker and its creator. If you need to spin up new threads quickly, consider +managing a pool of Wasm Workers yourself. + +Pthread topology is flat - Wasm Workers are hierarchical +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +On the web, if a Worker spawns a child Worker of its own, it will create a nested Worker hierarchy +that the main thread cannot directly access. To sidestep portability issues stemming from this kind +of topology, pthreads flatten the Worker creation chain under the hood so that only the main browser thread +ever spawns threads. + +Wasm Workers do not implement this kind of topology flattening, and creating a Wasm Worker in a +Wasm Worker will produce a nested Worker hierarchy. If you need to create Wasm Workers from within +a Wasm Worker, consider which type of hierarchy you would like, and if necessary, flatten the +hierarchy manually by posting the Worker creation over to the main thread yourself. + +Note that support for nested Workers varies across browsers. As of 02/2022, nested Workers are `not +supported in Safari `_. See `here +`_ for a polyfill. + +Pthreads can use the Wasm Worker synchronization API, but not vice versa +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The multithreading synchronization primitives offered in ``emscripten/wasm_worker.h`` +(``emscripten_lock_*``, ``emscripten_semaphore_*``, ``emscripten_condvar_*``) can be freely invoked +from within pthreads if one so wishes, but Wasm Workers cannot utilize any of the synchronization +functionality in the Pthread API (``pthread_mutex_*``, ``pthread_cond_``, ``pthread_rwlock_*``, etc), +since they lack the needed pthread runtime. + +Pthreads have a "thread main" function and atexit handlers +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The startup/execution model of pthreads is to start up executing a given thread entry point function. +When that function exits, the pthread will also (by default) quit, and the Worker hosting that pthread +will return to the Worker pool to wait for another thread to be created on it. + +Wasm Workers instead implement the direct web-like model, where a newly created Worker sits idle in its +event loop, waiting for functions to be posted to it. When those functions finish, the Worker will +return to its event loop, waiting to receive more functions (or worker scope web events) to execute. +A Wasm Worker will only quit with a call to ``emscripten_terminate_wasm_worker(worker_id)`` or +``emscripten_terminate_all_wasm_workers()``. + +Pthreads allow one to register thread exit handlers via ``pthread_atexit``, which will be called when +the thread quits. Wasm Workers do not have this concept. + +Pthreads have a per-thread incoming proxy message queue, Wasm Workers do not +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In order to enable flexible synchronous execution of code on other threads, and to implement support +APIs for example for MEMFS filesystem and Offscreen Framebuffer (WebGL emulated from a Worker) features, +main browser thread and each pthread have a system-backed "proxy message queue" to receive messages. + +This enables user code to call API functions ``emscripten_sync_run_in_main_thread*()``, +``emscripten_sync_run_in_main_runtime_thread()``, ``emscripten_async_run_in_main_runtime_thread()``, +``emscripten_dispatch_to_thread()``, etc. from ``emscripten/threading.h`` to perform proxied calls. + +Wasm Workers do not provide this functionality. If needed, such messaging should be implemented manually +by users via regular multithreaded synchronized programming techniques (mutexes, futexes, semaphores, etc.) + +Pthreads synchronize wallclock times +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Another portability aiding emulation feature that Pthreads provide is that the time values returned by +``emscripten_get_now()`` are synchronized to a common time base across all threads. + +Wasm Workers omit this concept, and it is recommended to use the function ``emscripten_performance_now()`` +for high performance timing in a Wasm Worker, and avoid comparing resulting values across Workers, or +manually synchronize them. + +Input events API backproxies only to pthreads +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The multithreaded input API provided in ``emscripten/html5.h`` only works with the pthread API. When +calling any of the functions ``emscripten_set_*_callback_on_thread()``, one can choose the target +pthread to be the recipient of the received events. + +With Wasm Workers, if desired, "backproxying" events from the main browser thread to a Wasm Worker +should be implemented manually e.g. by using the ``emscripten_wasm_worker_post_function_*()`` API family. + +However note that backproxying input events has a drawback that it prevents security sensitive operations, +like fullscreen requests, pointer locking and audio playback resuming, since handling the input event +is detached from the event callback context executing the initial operation. + +Pthread vs emscripten_lock implementation differences +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The mutex implementation from ``pthread_mutex_*`` has a few different creation options, one being a +"recursive" mutex. + +The lock implemented by ``emscripten_lock_*`` API is not recursive (and does not provide an option). + +Pthreads also offer a programming guard against a programming error that one thread would not release +a lock that is owned by another thread. ``emscripten_lock_*`` API does not track lock ownership. + +Memory requirements +^^^^^^^^^^^^^^^^^^^ + +Pthreads have a fixed dependency to dynamic memory allocation, and perform calls to ``malloc`` and ``free`` +to allocate thread specific data, stacks and TLS slots. + +With the exception of the helper function ``emscripten_malloc_wasm_worker()``, Wasm Workers are not dependent +on a dynamic memory allocator. Memory allocation needs are met by the caller at Worker creation time, and +can be statically placed if desired. + +Generated code size +^^^^^^^^^^^^^^^^^^^ + +The disk size overhead from pthreads is on the order of a few hundred KBs. Wasm Workers runtime on the other +hand is optimized for tiny deployments, just a few hundred bytes on disk. + +API Differences +^^^^^^^^^^^^^^^ + +To further understand the different APIs available between Pthreads and Wasm Workers, refer to the following +table. + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FeaturePthreadsWasm Workers
Thread terminationThread calls
pthread_exit(status)
or main thread calls
pthread_kill(code)
Worker cannot terminate itself, parent thread terminates by calling
emscripten_terminate_wasm_worker(worker)
Thread stackSpecify in pthread_attr_t structure.Manage thread stack area explicitly with
emscripten_create_wasm_worker_*_tls()
functions, or +
automatically allocate stack with
emscripten_malloc_wasm_worker()
API.
Thread Local Storage (TLS)Supported transparently.Supported either explicitly with
emscripten_create_wasm_worker_*_tls()
functions, or +
automatically via
emscripten_malloc_wasm_worker()
API.
Thread IDCreating a pthread obtains its ID. Call
pthread_self()
to acquire ID of calling thread.
Creating a Worker obtains its ID. Call
emscripten_wasm_worker_self_id()
acquire ID of calling thread.
High resolution timer``emscripten_get_now()````emscripten_performance_now()``
Synchronous blocking on main threadSynchronization primitives internally fall back to busy spin loops.Explicit spin vs sleep synchronization primitives.
Futex API
emscripten_futex_wait
emscripten_futex_wake
in emscripten/threading.h
emscripten_wasm_wait_i32
emscripten_wasm_wait_i64
emscripten_wasm_notify
in emscripten/wasm_workers.h
Asynchronous futex waitN/A
emscripten_atomic_wait_async()
emscripten_*_async_acquire()
However these are a difficult footgun, read WebAssembly/threads issue #176
C/C++ Function Proxyingemscripten/threading.h API for proxying function calls to other threads.Use emscripten_wasm_worker_post_function_*() API to message functions to other threads. These messages follow event queue semantics rather than proxy queue semantics.
Build flagsCompile and link with -pthreadCompile and link with -sWASM_WORKERS=1
Preprocessor directives__EMSCRIPTEN_SHARED_MEMORY__=1 and __EMSCRIPTEN_PTHREADS__=1 are active__EMSCRIPTEN_SHARED_MEMORY__=1 and __EMSCRIPTEN_WASM_WORKERS__=1 are active
JS library directivesUSE_PTHREADS=1 and SHARED_MEMORY=1 are activeUSE_PTHREADS=1, SHARED_MEMORY=1 and WASM_WORKERS=1 are active
Atomics APISupported, use any of __atomic_* API, __sync_* API or C++11 std::atomic API.
Nonrecursive mutex
pthread_mutex_*
emscripten_lock_*
Recursive mutex
pthread_mutex_*
N/A
SemaphoresN/A
emscripten_semaphore_*
Condition Variables
pthread_cond_*
emscripten_condvar_*
Read-Write locks
pthread_rwlock_*
N/A
Spinlocks
pthread_spin_*
emscripten_lock_busyspin*
WebGL Offscreen Framebuffer
Supported with -sOFFSCREEN_FRAMEBUFFER=1
Not supported.
+ +Limitations +=========== + +The following build options are not supported at the moment with Wasm Workers: + +- -sSINGLE_FILE=1 +- Dynamic linking (-sLINKABLE=1, -sMAIN_MODULE=1, -sSIDE_MODULE=1) +- -sPROXY_TO_WORKER=1 +- -sPROXY_TO_PTHREAD=1 + +Example Code +============ + +See the directory tests/wasm_workers/ for code examples on different Wasm Workers API functionality. diff --git a/src/jsifier.js b/src/jsifier.js index 17918dc2302e..ebcdc200529d 100644 --- a/src/jsifier.js +++ b/src/jsifier.js @@ -291,6 +291,14 @@ function ${name}(${args}) { if (ENVIRONMENT_IS_PTHREAD) return _emscripten_proxy_to_main_thread_js(${proxiedFunctionTable.length}, ${+sync}${args ? ', ' : ''}${args}); ${body} +}\n`); + } else if (WASM_WORKERS && ASSERTIONS) { + // In ASSERTIONS builds add runtime checks that proxied functions are not attempted to be called in Wasm Workers + // (since there is no automatic proxying architecture available) + contentText = modifyFunction(snippet, (name, args, body) => ` +function ${name}(${args}) { + assert(!ENVIRONMENT_IS_WASM_WORKER, "Attempted to call proxied function '${name}' in a Wasm Worker, but in Wasm Worker enabled builds, proxied function architecture is not available!"); + ${body} }\n`); } proxiedFunctionTable.push(finalName); diff --git a/src/library.js b/src/library.js index e2529814cd0f..10cfb26ab68a 100644 --- a/src/library.js +++ b/src/library.js @@ -2416,6 +2416,7 @@ LibraryManager.library = { #endif #if USE_PTHREADS // Pthreads need their clocks synchronized to the execution of the main thread, so give them a special form of the function. +// N.b. Wasm workers do not provide this kind of clock synchronization. "if (ENVIRONMENT_IS_PTHREAD) {\n" + " _emscripten_get_now = () => performance.now() - Module['__performance_now_clock_drift'];\n" + "} else " + diff --git a/src/library_pthread.js b/src/library_pthread.js index 7647abcd9356..73ccabe004ef 100644 --- a/src/library_pthread.js +++ b/src/library_pthread.js @@ -7,9 +7,9 @@ #if !USE_PTHREADS #error "Internal error! USE_PTHREADS should be enabled when including library_pthread.js." #endif -//#if !SHARED_MEMORY -//#error "Internal error! SHARED_MEMORY should be enabled when including library_pthread.js."" -//#endif +#if !SHARED_MEMORY +#error "Internal error! SHARED_MEMORY should be enabled when including library_pthread.js." +#endif #if USE_PTHREADS == 2 #error "USE_PTHREADS=2 is no longer supported" #endif diff --git a/src/library_pthread_stub.js b/src/library_pthread_stub.js index bb1de897bfd3..b18ef09eafb7 100644 --- a/src/library_pthread_stub.js +++ b/src/library_pthread_stub.js @@ -7,9 +7,9 @@ #if USE_PTHREADS #error "Internal error! USE_PTHREADS should not be enabled when including library_pthread_stub.js." #endif -//#if STANDALONE_WASM && SHARED_MEMORY -//#error "STANDALONE_WASM does not support shared memories yet" -//#endif +#if STANDALONE_WASM && SHARED_MEMORY +#error "STANDALONE_WASM does not support shared memories yet" +#endif var LibraryPThreadStub = { // =================================================================================== diff --git a/src/library_wasm_worker.js b/src/library_wasm_worker.js new file mode 100644 index 000000000000..8714a340fe84 --- /dev/null +++ b/src/library_wasm_worker.js @@ -0,0 +1,387 @@ +{{{ (function() { global.captureModuleArg = function() { return MODULARIZE ? '' : 'self.Module=d;'; }; return null; })(); }}} +{{{ (function() { global.instantiateModule = function() { return MODULARIZE ? `${EXPORT_NAME}(d);` : ''; }; return null; })(); }}} +{{{ (function() { global.instantiateWasm = function() { return MINIMAL_RUNTIME ? '' : 'd[`instantiateWasm`]=(i,r)=>{var n=new WebAssembly.Instance(d[`wasm`],i);r(n,d[`wasm`]);return n.exports};'; }; return null; })(); }}} + +#if WASM_WORKERS + +#if !SHARED_MEMORY +#error "Internal error! SHARED_MEMORY should be enabled when building with WASM_WORKERS" +#endif +#if SINGLE_FILE +#error "-sSINGLE_FILE is not supported with -sWASM_WORKERS!" +#endif +#if LINKABLE +#error "-sLINKABLE is not supported with -sWASM_WORKERS!" +#endif +#if SIDE_MODULE +#error "-sSIDE_MODULE is not supported with -sWASM_WORKERS!" +#endif +#if MAIN_MODULE +#error "-sMAIN_MODULE is not supported with -sWASM_WORKERS!" +#endif +#if PROXY_TO_WORKER +#error "-sPROXY_TO_WORKER is not supported with -sWASM_WORKERS!" +#endif + +#endif // ~WASM_WORKERS + + +mergeInto(LibraryManager.library, { + wasm_workers: {}, + wasm_workers_id: 1, + + // 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 + // startup has finished, after which these postponed function calls can be dispatched. + _wasm_worker_delayedMessageQueue: [], + + _wasm_worker_appendToQueue: function(e) { + __wasm_worker_delayedMessageQueue.push(e); + }, + + // Executes a wasm function call received via a postMessage. + _wasm_worker_runPostMessage: function(e) { + let data = e.data, wasmCall = data['_wsc']; // '_wsc' is short for 'wasm call', trying to use an identifier name that will never conflict with user code + wasmCall && getWasmTableEntry(wasmCall)(...data['x']); + }, + + // src/postamble_minimal.js brings this symbol in to the build, and calls this function synchronously + // from main JS file at the startup of each Worker. + _wasm_worker_initializeRuntime__deps: ['_wasm_worker_delayedMessageQueue', '_wasm_worker_runPostMessage'], + _wasm_worker_initializeRuntime: function() { + let m = Module; +#if ASSERTIONS + assert(m['sb'] % 16 == 0); + assert(m['sz'] % 16 == 0); +#endif + + // Run the C side Worker initialization for stack and TLS. + _emscripten_wasm_worker_initialize(m['sb'], m['sz'] +#if !WASM_WORKERS_NO_TLS + , m['tb'] +#endif + ); + // The above function initializes the stack for this Worker, but C code cannot + // call to extern __set_stack_limits() function, or Binaryen breaks with + // "Fatal: Module::addFunction: __set_stack_limits already exists". + // So for now, invoke the function from JS side. TODO: remove this in the future. +#if STACK_OVERFLOW_CHECK >= 2 + ___set_stack_limits(_emscripten_stack_get_base(), _emscripten_stack_get_end()); +#endif + + // The Wasm Worker runtime is now up, so we can start processing + // any postMessage function calls that have been received. Drop the temp + // message handler that queued any pending incoming postMessage function calls ... + removeEventListener('message', __wasm_worker_appendToQueue); + // ... then flush whatever messages we may have already gotten in the queue, + // and clear __wasm_worker_delayedMessageQueue to undefined ... + __wasm_worker_delayedMessageQueue = __wasm_worker_delayedMessageQueue.forEach(__wasm_worker_runPostMessage); + // ... and finally register the proper postMessage handler that immediately + // dispatches incoming function calls without queueing them. + addEventListener('message', __wasm_worker_runPostMessage); + }, + +#if WASM_WORKERS == 2 + // In WASM_WORKERS == 2 build mode, we create the Wasm Worker global scope script from a string bundled in the main application JS file. This simplifies the number of deployed JS files with the app, + // but has a downside that the generated build output will no longer be csp-eval compliant. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#unsafe_eval_expressions + _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_with_tls__deps: ['wasm_workers', 'wasm_workers_id', '_wasm_worker_appendToQueue', '_wasm_worker_runPostMessage' +#if WASM_WORKERS == 2 + , '_wasmWorkerBlobUrl' +#endif + ], + _emscripten_create_wasm_worker_with_tls__postset: 'if (ENVIRONMENT_IS_WASM_WORKER) {\n' + + '_wasm_workers[0] = this;\n' + + 'addEventListener("message", __wasm_worker_appendToQueue);\n' + + '}\n', + _emscripten_create_wasm_worker_with_tls: function(stackLowestAddress, stackSize, tlsAddress) { +#if ASSERTIONS + assert(stackLowestAddress % 16 == 0); + assert(stackSize % 16 == 0); +#endif + let worker = _wasm_workers[_wasm_workers_id] = 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 + Module['$wb'] // $wb="Wasm worker Blob", abbreviated since not DCEable +#else // default runtime loads the .ww.js file on demand. + locateFile('{{{ WASM_WORKER_FILE }}}') +#endif + ); + // Craft the Module object for the Wasm Worker scope: + worker.postMessage({ + '$ww': _wasm_workers_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'], + 'mem': wasmMemory, +#else + 'wasm': wasmModule, + 'js': Module['mainScriptUrlOrBlob'] || _scriptDir, + 'wasmMemory': wasmMemory, +#endif + 'sb': stackLowestAddress, // sb = stack base + 'sz': stackSize, // sz = stack size +#if !WASM_WORKERS_NO_TLS + 'tb': tlsAddress, // tb = TLS base +#endif + }); + worker.addEventListener('message', __wasm_worker_runPostMessage); + return _wasm_workers_id++; + }, + _emscripten_create_wasm_worker_no_tls__deps: ['_emscripten_create_wasm_worker_with_tls'], + _emscripten_create_wasm_worker_no_tls: function(stackLowestAddress, stackSize) { + return __emscripten_create_wasm_worker_with_tls(stackLowestAddress, stackSize, 0, 0); + }, + + emscripten_terminate_wasm_worker: function(id) { +#if ASSERTIONS + assert(id != 0, 'emscripten_terminate_wasm_worker() cannot be called with id=0!'); +#endif + if (_wasm_workers[id]) { + _wasm_workers[id].terminate(); + delete _wasm_workers[id]; + } + }, + + emscripten_terminate_all_wasm_workers: function() { +#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(_wasm_workers).forEach((worker) => { + worker.terminate(); + }); + _wasm_workers = {}; + }, + + emscripten_current_thread_is_wasm_worker: function() { +#if WASM_WORKERS + return ENVIRONMENT_IS_WASM_WORKER; +#else + // implicit return 0; +#endif + }, + + emscripten_wasm_worker_self_id: function() { + return Module['$ww']; + }, + + emscripten_wasm_worker_post_function_v__sig: 'vii', + emscripten_wasm_worker_post_function_v: function(id, funcPtr) { + _wasm_workers[id].postMessage({'_wsc': funcPtr, 'x': [] }); // "WaSm Call" + }, + + emscripten_wasm_worker_post_function_1__sig: 'viid', + emscripten_wasm_worker_post_function_1: function(id, funcPtr, arg0) { + _wasm_workers[id].postMessage({'_wsc': funcPtr, 'x': [arg0] }); // "WaSm Call" + }, + + emscripten_wasm_worker_post_function_vi: 'emscripten_wasm_worker_post_function_1', + emscripten_wasm_worker_post_function_vd: 'emscripten_wasm_worker_post_function_1', + + emscripten_wasm_worker_post_function_2__sig: 'viidd', + emscripten_wasm_worker_post_function_2: function(id, funcPtr, arg0, arg1) { + _wasm_workers[id].postMessage({'_wsc': funcPtr, 'x': [arg0, arg1] }); // "WaSm Call" + }, + emscripten_wasm_worker_post_function_vii: 'emscripten_wasm_worker_post_function_2', + emscripten_wasm_worker_post_function_vdd: 'emscripten_wasm_worker_post_function_2', + + emscripten_wasm_worker_post_function_3__sig: 'viiddd', + emscripten_wasm_worker_post_function_3: function(id, funcPtr, arg0, arg1, arg2) { + _wasm_workers[id].postMessage({'_wsc': funcPtr, 'x': [arg0, arg1, arg2] }); // "WaSm Call" + }, + emscripten_wasm_worker_post_function_viii: 'emscripten_wasm_worker_post_function_3', + emscripten_wasm_worker_post_function_vddd: 'emscripten_wasm_worker_post_function_3', + + emscripten_wasm_worker_post_function_sig__deps: ['$readAsmConstArgs'], + emscripten_wasm_worker_post_function_sig: function(id, funcPtr, sigPtr, varargs) { +#if ASSERTIONS + assert(id >= 0); + assert(funcPtr); + assert(sigPtr); + 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 + _wasm_workers[id].postMessage({'_wsc': funcPtr, 'x': readAsmConstArgs(sigPtr, varargs) }); + }, + + _emscripten_atomic_wait_states: "['ok', 'not-equal', 'timed-out']", + +// Chrome 87 (and hence Edge 87) shipped Atomics.waitAsync (https://www.chromestatus.com/feature/6243382101803008) +// However its implementation is faulty: https://bugs.chromium.org/p/chromium/issues/detail?id=1167541 +// Firefox Nightly 86.0a1 (2021-01-15) does not yet have it, https://bugzilla.mozilla.org/show_bug.cgi?id=1467846 +// And at the time of writing, no other browser has it either. +#if MIN_EDGE_VERSION != TARGET_NOT_SUPPORTED || MIN_CHROME_VERSION != TARGET_NOT_SUPPORTED || MIN_SAFARI_VERSION != TARGET_NOT_SUPPORTED || MIN_FIREFOX_VERSION != TARGET_NOT_SUPPORTED || ENVIRONMENT_MAY_BE_NODE + // Partially polyfill Atomics.waitAsync() if not available in the browser. + // Also always polyfill in Chrome-based browsers, where Atomics.waitAsync is broken, + // see https://bugs.chromium.org/p/chromium/issues/detail?id=1167541 + // https://github.com/tc39/proposal-atomics-wait-async/blob/master/PROPOSAL.md + // This polyfill performs polling with setTimeout() to observe a change in the target memory location. + emscripten_atomic_wait_async__postset: "if (!Atomics['waitAsync'] || navigator.userAgent.indexOf('Chrome') != -1) { \n"+ +"let __Atomics_waitAsyncAddresses = [/*[i32a, index, value, maxWaitMilliseconds, promiseResolve]*/];\n"+ +"function __Atomics_pollWaitAsyncAddresses() {\n"+ +" let now = performance.now();\n"+ +" let l = __Atomics_waitAsyncAddresses.length;\n"+ +" for(let i = 0; i < l; ++i) {\n"+ +" let a = __Atomics_waitAsyncAddresses[i];\n"+ +" let expired = (now > a[3]);\n"+ +" let awoken = (Atomics.load(a[0], a[1]) != a[2]);\n"+ +" if (expired || awoken) {\n"+ +" __Atomics_waitAsyncAddresses[i--] = __Atomics_waitAsyncAddresses[--l];\n"+ +" __Atomics_waitAsyncAddresses.length = l;\n"+ +" a[4](awoken ? 'ok': 'timed-out');\n"+ +" }\n"+ +" }\n"+ +" if (l) {\n"+ +" // If we still have addresses to wait, loop the timeout handler to continue polling.\n"+ +" setTimeout(__Atomics_pollWaitAsyncAddresses, 10);\n"+ +" }\n"+ +"}\n"+ +#if ASSERTIONS +" if (!ENVIRONMENT_IS_WASM_WORKER) console.error('Current environment does not support Atomics.waitAsync(): polyfilling it, but this is going to be suboptimal.');\n"+ +#endif +"Atomics['waitAsync'] = function(i32a, index, value, maxWaitMilliseconds) {\n"+ +" let val = Atomics.load(i32a, index);\n"+ +" if (val != value) return { async: false, value: 'not-equal' };\n"+ +" if (maxWaitMilliseconds <= 0) return { async: false, value: 'timed-out' };\n"+ +" maxWaitMilliseconds = performance.now() + (maxWaitMilliseconds || Infinity);\n"+ +" let promiseResolve;\n"+ +" let promise = new Promise((resolve) => { promiseResolve = resolve; });\n"+ +" if (!__Atomics_waitAsyncAddresses[0]) setTimeout(__Atomics_pollWaitAsyncAddresses, 10);\n"+ +" __Atomics_waitAsyncAddresses.push([i32a, index, value, maxWaitMilliseconds, promiseResolve]);\n"+ +" return { async: true, value: promise };\n"+ +"};\n"+ +"}", + + // These dependencies are artificial, issued so that we still get the waitAsync polyfill emitted + // if code only calls emscripten_lock/semaphore_async_acquire() + // but not emscripten_atomic_wait_async() directly. + emscripten_lock_async_acquire__deps: ['emscripten_atomic_wait_async'], + emscripten_semaphore_async_acquire__deps: ['emscripten_atomic_wait_async'], + +#endif + + _emscripten_atomic_live_wait_asyncs: '{}', + _emscripten_atomic_live_wait_asyncs_counter: '0', + + emscripten_atomic_wait_async__deps: ['_emscripten_atomic_wait_states', '_emscripten_atomic_live_wait_asyncs', '_emscripten_atomic_live_wait_asyncs_counter'], + emscripten_atomic_wait_async: function(addr, val, asyncWaitFinished, userData, maxWaitMilliseconds) { + let wait = Atomics['waitAsync'](HEAP32, addr >> 2, val, maxWaitMilliseconds); + if (!wait.async) return __emscripten_atomic_wait_states.indexOf(wait.value); + // Increment waitAsync generation counter, account for wraparound in case application does huge amounts of waitAsyncs per second (not sure if possible?) + // Valid counterrange: 0...2^31-1 + let counter = __emscripten_atomic_live_wait_asyncs_counter; + __emscripten_atomic_live_wait_asyncs_counter = Math.max(0, (__emscripten_atomic_live_wait_asyncs_counter+1)|0); + __emscripten_atomic_live_wait_asyncs[counter] = addr >> 2; + wait.value.then((value) => { + if (__emscripten_atomic_live_wait_asyncs[counter]) { + delete __emscripten_atomic_live_wait_asyncs[counter]; + {{{ makeDynCall('viiii', 'asyncWaitFinished') }}}(addr, val, __emscripten_atomic_wait_states.indexOf(value), userData); + } + }); + return -counter; + }, + + emscripten_atomic_cancel_wait_async__deps: ['_emscripten_atomic_live_wait_asyncs'], + emscripten_atomic_cancel_wait_async: function(waitToken) { +#if ASSERTIONS + if (waitToken == 1 /* ATOMICS_WAIT_NOT_EQUAL */) warnOnce('Attempted to call emscripten_atomic_cancel_wait_async() with a value ATOMICS_WAIT_NOT_EQUAL (1) that is not a valid wait token! Check success in return value from call to emscripten_atomic_wait_async()'); + else if (waitToken == 2 /* ATOMICS_WAIT_TIMED_OUT */) warnOnce('Attempted to call emscripten_atomic_cancel_wait_async() with a value ATOMICS_WAIT_TIMED_OUT (2) that is not a valid wait token! Check success in return value from call to emscripten_atomic_wait_async()'); + else if (waitToken > 0) warnOnce('Attempted to call emscripten_atomic_cancel_wait_async() with an invalid wait token value ' + waitToken); +#endif + if (__emscripten_atomic_live_wait_asyncs[waitToken]) { + // Notify the waitAsync waiters on the memory location, so that JavaScript garbage collection can occur + // See https://github.com/WebAssembly/threads/issues/176 + // This has the unfortunate effect of causing spurious wakeup of all other waiters at the address (which + // causes a small performance loss) + Atomics.notify(HEAP32, __emscripten_atomic_live_wait_asyncs[waitToken]); + delete __emscripten_atomic_live_wait_asyncs[waitToken]; + return 0 /* EMSCRIPTEN_RESULT_SUCCESS */; + } + // This waitToken does not exist. + return -5 /* EMSCRIPTEN_RESULT_INVALID_PARAM */; + }, + + emscripten_atomic_cancel_all_wait_asyncs__deps: ['_emscripten_atomic_live_wait_asyncs'], + emscripten_atomic_cancel_all_wait_asyncs: function() { + let waitAsyncs = Object.values(__emscripten_atomic_live_wait_asyncs); + waitAsyncs.forEach((address) => { + Atomics.notify(HEAP32, address); + }); + __emscripten_atomic_live_wait_asyncs = {}; + return waitAsyncs.length; + }, + + emscripten_atomic_cancel_all_wait_asyncs_at_address__deps: ['_emscripten_atomic_live_wait_asyncs'], + emscripten_atomic_cancel_all_wait_asyncs_at_address: function(address) { + address >>= 2; + let numCancelled = 0; + Object.keys(__emscripten_atomic_live_wait_asyncs).forEach((waitToken) => { + if (__emscripten_atomic_live_wait_asyncs[waitToken] == address) { + Atomics.notify(HEAP32, address); + delete __emscripten_atomic_live_wait_asyncs[waitToken]; + ++numCancelled; + } + }); + return numCancelled; + }, + + emscripten_navigator_hardware_concurrency: function() { +#if ENVIRONMENT_MAY_BE_NODE + if (ENVIRONMENT_IS_NODE) return require('os').cpus().length; +#endif + return navigator['hardwareConcurrency']; + }, + + emscripten_atomics_is_lock_free: function(width) { + return Atomics.isLockFree(width); + }, + + emscripten_lock_async_acquire: function(lock, asyncWaitFinished, userData, maxWaitMilliseconds) { + let dispatch = (val, ret) => { + setTimeout(() => { + {{{ makeDynCall('viiii', 'asyncWaitFinished') }}}(lock, val, /*waitResult=*/ret, userData); + }, 0); + }; + let tryAcquireLock = () => { + do { + let val = Atomics.compareExchange(HEAPU32, lock >> 2, 0/*zero represents lock being free*/, 1/*one represents lock being acquired*/); + if (!val) return dispatch(0, 0/*'ok'*/); + var wait = Atomics['waitAsync'](HEAPU32, lock >> 2, val, maxWaitMilliseconds); + } while(wait.value === 'not-equal'); +#if ASSERTIONS + assert(wait.async || wait.value === 'timed-out'); +#endif + if (wait.async) wait.value.then(tryAcquireLock); + else dispatch(val, 2/*'timed-out'*/); + }; + tryAcquireLock(); + }, + + emscripten_semaphore_async_acquire: function(sem, num, asyncWaitFinished, userData, maxWaitMilliseconds) { + let dispatch = (idx, ret) => { + setTimeout(() => { + {{{ makeDynCall('viiii', 'asyncWaitFinished') }}}(sem, /*val=*/idx, /*waitResult=*/ret, userData); + }, 0); + }; + let tryAcquireSemaphore = () => { + let val = num; + do { + let ret = Atomics.compareExchange(HEAPU32, sem >> 2, + val, /* We expect this many semaphore resoures to be available*/ + val - num /* Acquire 'num' of them */); + if (ret == val) return dispatch(ret/*index of resource acquired*/, 0/*'ok'*/); + val = ret; + let wait = Atomics['waitAsync'](HEAPU32, sem >> 2, ret, maxWaitMilliseconds); + } while(wait.value === 'not-equal'); +#if ASSERTIONS + assert(wait.async || wait.value === 'timed-out'); +#endif + if (wait.async) wait.value.then(tryAcquireSemaphore); + else dispatch(-1/*idx*/, 2/*'timed-out'*/); + }; + tryAcquireSemaphore(); + } +}); diff --git a/src/parseTools.js b/src/parseTools.js index c6b8ed57b0d4..fe58a34b3043 100644 --- a/src/parseTools.js +++ b/src/parseTools.js @@ -961,7 +961,11 @@ function modifyFunction(text, func) { } function runOnMainThread(text) { - if (USE_PTHREADS) { + if (WASM_WORKERS && USE_PTHREADS) { + return 'if (!ENVIRONMENT_IS_WASM_WORKER && !ENVIRONMENT_IS_PTHREAD) { ' + text + ' }'; + } else if (WASM_WORKERS) { + return 'if (!ENVIRONMENT_IS_WASM_WORKER) { ' + text + ' }'; + } else if (USE_PTHREADS) { return 'if (!ENVIRONMENT_IS_PTHREAD) { ' + text + ' }'; } else { return text; diff --git a/src/postamble.js b/src/postamble.js index 594d2bec55fd..dd1f20c4825e 100644 --- a/src/postamble.js +++ b/src/postamble.js @@ -263,6 +263,15 @@ function run(args) { } #endif +#if WASM_WORKERS + if (ENVIRONMENT_IS_WASM_WORKER) { +#if MODULARIZE + readyPromiseResolve(Module); +#endif // MODULARIZE + return initRuntime(); + } +#endif + #if USE_PTHREADS if (ENVIRONMENT_IS_PTHREAD) { #if MODULARIZE diff --git a/src/postamble_minimal.js b/src/postamble_minimal.js index f977ccf26e6a..1f2ad681f5bd 100644 --- a/src/postamble_minimal.js +++ b/src/postamble_minimal.js @@ -72,6 +72,10 @@ function initRuntime(asm) { } #endif +#if WASM_WORKERS + if (ENVIRONMENT_IS_WASM_WORKER) return __wasm_worker_initializeRuntime(); +#endif + #if STACK_OVERFLOW_CHECK _emscripten_stack_init(); writeStackCookie(); diff --git a/src/preamble.js b/src/preamble.js index 30b432e6a249..737c3c2e230b 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -368,6 +368,10 @@ function initRuntime() { #endif runtimeInitialized = true; +#if WASM_WORKERS + if (ENVIRONMENT_IS_WASM_WORKER) return __wasm_worker_initializeRuntime(); +#endif + #if USE_PTHREADS if (ENVIRONMENT_IS_PTHREAD) return; #endif @@ -1080,9 +1084,15 @@ function createWasm() { exportAsmFunctions(exports); #endif -#if USE_PTHREADS// || WASM_WORKERS +#if USE_PTHREADS || WASM_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; +#endif + +#if WASM_WORKERS + if (!ENVIRONMENT_IS_WASM_WORKER) { +#endif +#if USE_PTHREADS // Instantiation is synchronous in pthreads and we assert on run dependencies. if (!ENVIRONMENT_IS_PTHREAD) { #if PTHREAD_POOL_SIZE @@ -1103,7 +1113,11 @@ function createWasm() { } #else // singlethreaded build: removeRunDependency('wasm-instantiate'); +#endif // ~USE_PTHREADS +#if WASM_WORKERS + } #endif + } // we can't run yet (except in a pthread, where we have a custom sync instantiator) {{{ runOnMainThread("addRunDependency('wasm-instantiate');") }}} diff --git a/src/preamble_minimal.js b/src/preamble_minimal.js index 967cd79e6065..3a9a0b7ba385 100644 --- a/src/preamble_minimal.js +++ b/src/preamble_minimal.js @@ -82,7 +82,11 @@ function updateGlobalBufferAndViews(b) { #if USE_PTHREADS if (!ENVIRONMENT_IS_PTHREAD) { #endif - wasmMemory = new WebAssembly.Memory({ + wasmMemory = +#if WASM_WORKERS + Module['mem'] || +#endif + new WebAssembly.Memory({ 'initial': {{{ INITIAL_MEMORY >>> 16 }}} #if SHARED_MEMORY || !ALLOW_MEMORY_GROWTH || MAXIMUM_MEMORY != FOUR_GB , 'maximum': {{{ (ALLOW_MEMORY_GROWTH && MAXIMUM_MEMORY != FOUR_GB ? MAXIMUM_MEMORY : INITIAL_MEMORY) >>> 16 }}} diff --git a/src/settings.js b/src/settings.js index 0431c0455a0f..6b8852c1a34c 100644 --- a/src/settings.js +++ b/src/settings.js @@ -1474,6 +1474,18 @@ var SHARED_MEMORY = 0; // [compile+link] - affects user code at compile and system libraries at link. var USE_PTHREADS = 0; +// If true, enables support for Wasm Workers. Wasm Workers enable applications +// to create threads using a lightweight web-specific API that builds on top +// of Wasm SharedArrayBuffer + Atomics API. +// [compile+link] - affects user code at compile and system libraries at link. +var WASM_WORKERS = 0; + +// Wasm Worker option: set to 1 to disable TLS for small code size gain when +// not using TLS. This setting can be used to verify that there is no leftover +// state stored in a Worker when manually implementing thread pooling in Workers. +// [link] +var WASM_WORKERS_NO_TLS = 0; + // 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. diff --git a/src/settings_internal.js b/src/settings_internal.js index bd841688a1d0..92bf51b76916 100644 --- a/src/settings_internal.js +++ b/src/settings_internal.js @@ -129,6 +129,9 @@ var WASM_BINARY_FILE = ''; // name of the file containing the pthread *.worker.js, if relevant var PTHREAD_WORKER_FILE = ''; +// name of the file containing the Wasm Worker *.ww.js, if relevant +var WASM_WORKER_FILE = ''; + // Base URL the source mapfile, if relevant var SOURCE_MAP_BASE = ''; diff --git a/src/shell.js b/src/shell.js index 8fa63d0baae9..66da99ff5677 100644 --- a/src/shell.js +++ b/src/shell.js @@ -121,6 +121,10 @@ if (Module['ENVIRONMENT']) { var ENVIRONMENT_IS_PTHREAD = Module['ENVIRONMENT_IS_PTHREAD'] || false; #endif +#if WASM_WORKERS +var ENVIRONMENT_IS_WASM_WORKER = Module['$ww']; +#endif + #if SHARED_MEMORY && !MODULARIZE // In MODULARIZE mode _scriptDir needs to be captured already at the very top of the page immediately when the page is parsed, so it is generated there // before the page load. In non-MODULARIZE modes generate it here. diff --git a/src/shell_minimal.js b/src/shell_minimal.js index d317aa61eb1a..d9e810400383 100644 --- a/src/shell_minimal.js +++ b/src/shell_minimal.js @@ -46,6 +46,10 @@ var ENVIRONMENT_IS_WEB = !ENVIRONMENT_IS_NODE; #endif #endif // ASSERTIONS || USE_PTHREADS +#if WASM_WORKERS +var ENVIRONMENT_IS_WASM_WORKER = Module['$ww']; +#endif + #if ASSERTIONS && ENVIRONMENT_MAY_BE_NODE && ENVIRONMENT_MAY_BE_SHELL if (ENVIRONMENT_IS_NODE && ENVIRONMENT_IS_SHELL) { throw 'unclear environment'; @@ -111,18 +115,10 @@ function ready() { readyPromiseResolve(Module); #endif // MODULARIZE #if INVOKE_RUN && HAS_MAIN -#if USE_PTHREADS - if (!ENVIRONMENT_IS_PTHREAD) { -#endif - run(); -#if USE_PTHREADS - } -#endif -#else -#if ASSERTIONS + {{{ runOnMainThread("run();") }}} +#elif ASSERTIONS console.log('ready() called, and INVOKE_RUN=0. The runtime is now ready for you to call run() to invoke application _main(). You can also override ready() in a --pre-js file to get this signal as a callback') #endif -#endif } #if POLYFILL @@ -147,7 +143,12 @@ var _scriptDir = (typeof document != 'undefined' && document.currentScript) ? do // MINIMAL_RUNTIME does not support --proxy-to-worker option, so Worker and Pthread environments // coincide. +#if WASM_WORKERS +var ENVIRONMENT_IS_WORKER = typeof importScripts == 'function', + ENVIRONMENT_IS_PTHREAD = ENVIRONMENT_IS_WORKER && !ENVIRONMENT_IS_WASM_WORKER; +#else var ENVIRONMENT_IS_WORKER = ENVIRONMENT_IS_PTHREAD = typeof importScripts == 'function'; +#endif var currentScriptUrl = typeof _scriptDir != 'undefined' ? _scriptDir : ((typeof document != 'undefined' && document.currentScript) ? document.currentScript.src : undefined); #endif // USE_PTHREADS diff --git a/src/wasm_worker.js b/src/wasm_worker.js new file mode 100644 index 000000000000..3ab43fcbd4b8 --- /dev/null +++ b/src/wasm_worker.js @@ -0,0 +1,24 @@ +// N.B. The contents of this file are duplicated in src/library_wasm_worker.js +// in variable "_wasmWorkerBlobUrl" (where the contents are pre-minified) If +// doing any changes to this file, be sure to update the contents there too. +onmessage = function(d) { + // The first message sent to the Worker is always the bootstrap message. + // Drop this message listener, it served its purpose of bootstrapping + // the Wasm Module load, and is no longer needed. Let user code register + // any desired message handlers from now on. + onmessage = null; + d = d.data; +#if !MODULARIZE + self.{{{ EXPORT_NAME }}} = d; +#endif +#if !MINIMAL_RUNTIME + d['instantiateWasm'] = (info, receiveInstance) => { var instance = new WebAssembly.Instance(d['wasm'], info); receiveInstance(instance, d['wasm']); return instance.exports; } +#endif + importScripts(d.js); +#if MODULARIZE + {{{ EXPORT_NAME }}}(d); +#endif + // Drop now unneeded references to from the Module object in this Worker, + // these are not needed anymore. + d.wasm = d.mem = d.js = 0; +} diff --git a/system/include/emscripten/wasm_worker.h b/system/include/emscripten/wasm_worker.h new file mode 100644 index 000000000000..5fc52afb5e33 --- /dev/null +++ b/system/include/emscripten/wasm_worker.h @@ -0,0 +1,338 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define emscripten_wasm_worker_t int +#define EMSCRIPTEN_WASM_WORKER_ID_PARENT 0 + +/* Creates a new Worker() that is attached to executing this WebAssembly.Instance and WebAssembly.Memory. + + emscripten_malloc_wasm_worker: Creates a new Worker, dynamically allocating stack and TLS for it. Unfortunately + due to the asynchronous no-notifications nature of how Worker API specification teardown + behaves, the dynamically allocated memory can never be freed, so use this function + only in scenarios where the page does not need to deinitialize/tear itself down. + + emscripten_create_wasm_worker_no_tls: Creates a Wasm Worker on the given placed stack address area, but no TLS. + Use this function on codebase that explicitly do not have any TLS state, + e.g. when the Worker is being reset to reinit execution at runtime, or to micro- + optimize code size when TLS is not needed. This function will assert() fail if + the compiled code does have any TLS uses in it. This function does not use any + dynamic memory allocation. + + emscripten_create_wasm_worker_with_tls: Creates a Wasm Worker on given placed stack address and TLS area. + Use the Wasm built-in functions __builtin_wasm_tls_align() and + __builtin_wasm_tls_size() to obtain the needed memory size for the TLS area + (though note that __builtin_wasm_tls_align() can return zero when the TLS size + is zero, so be careful to avoid a divide by zero if/when rounding to this alignment) + Use this function to manually manage the memory that a Worker should use. + This function does not use any dynamic memory allocation. + + Returns an ID that represents the given Worker. If not building with Wasm workers enabled (-s WASM_WORKERS=0), + these functions will return 0 to denote failure. + Note that the Worker will be loaded up asynchronously, and initially will not be executing any code. Use + emscripten_wasm_worker_post_function_*() set of functions to start executing code on the Worker. */ +emscripten_wasm_worker_t emscripten_malloc_wasm_worker(uint32_t stackSize); +emscripten_wasm_worker_t emscripten_create_wasm_worker_no_tls(void *stackLowestAddress, uint32_t stackSize); +emscripten_wasm_worker_t emscripten_create_wasm_worker_with_tls(void *stackLowestAddress, uint32_t stackSize, void *tlsAddress, uint32_t tlsSize); + +// Terminates the given Wasm Worker some time after it has finished executing its current, or possibly some subsequent +// posted functions. Note that this function is not C++ RAII safe, but you must manually coordinate to release any +// resources from the given Worker that it may have allocated from the heap or may have stored on its TLS slots. +// There are no TLS destructors that would execute. +// Exists, but is a no-op if not building with Wasm Workers enabled (-s WASM_WORKERS=0) +void emscripten_terminate_wasm_worker(emscripten_wasm_worker_t id); + +// Note the comment on emscripten_terminate_wasm_worker(id) about thread destruction. +// Exists, but is a no-op if not building with Wasm Workers enabled (-s WASM_WORKERS=0) +void emscripten_terminate_all_wasm_workers(void); + +// Returns EM_TRUE if the current thread is executing a Wasm Worker, EM_FALSE otherwise. +// Note that calling this function can be relatively slow as it incurs a Wasm->JS transition, +// so avoid calling it in hot paths. +EM_BOOL emscripten_current_thread_is_wasm_worker(void); + +// Returns a unique ID that identifies the calling Wasm Worker. Similar to pthread_self(). +// The main browser thread will return 0 as the ID. First Wasm Worker will return 1, and so on. +uint32_t emscripten_wasm_worker_self_id(void); + +/* emscripten_wasm_worker_post_function_*: Post a pointer to a C/C++ function to be executed on the + target Wasm Worker (via sending a postMessage() to the target thread). Notes: + - If running inside a Wasm Worker, specify worker ID 0 to pass a message to the parent thread. + - When specifying non-zero ID, the target worker must have been created by the calling thread. That is, + a Wasm Worker can only send a message to its parent or its children, but not to its siblings. + - The target function pointer will be executed on the target Worker only after it yields back to its + event loop. If the target Wasm Worker executes an infinite loop that never yields, then the function + pointer will never be called. + - Passing messages between threads with this family of functions is relatively slow and has a really high + latency cost compared to direct coordination using atomics and synchronization primitives like mutexes + and synchronization primitives. Additionally these functions will generate garbage on the JS heap. + Therefore avoid using these functions where performance is critical. */ +void emscripten_wasm_worker_post_function_v(emscripten_wasm_worker_t id, void (*funcPtr)(void)); +void emscripten_wasm_worker_post_function_vi(emscripten_wasm_worker_t id, void (*funcPtr)(int), int arg0); +void emscripten_wasm_worker_post_function_vii(emscripten_wasm_worker_t id, void (*funcPtr)(int, int), int arg0, int arg1); +void emscripten_wasm_worker_post_function_viii(emscripten_wasm_worker_t id, void (*funcPtr)(int, int, int), int arg0, int arg1, int arg2); +void emscripten_wasm_worker_post_function_vd(emscripten_wasm_worker_t id, void (*funcPtr)(double), double arg0); +void emscripten_wasm_worker_post_function_vdd(emscripten_wasm_worker_t id, void (*funcPtr)(double, double), double arg0, double arg1); +void emscripten_wasm_worker_post_function_vddd(emscripten_wasm_worker_t id, void (*funcPtr)(double, double, double), double arg0, double arg1, double arg2); +void emscripten_wasm_worker_post_function_sig(emscripten_wasm_worker_t id, void *funcPtr, const char *sig, ...); + +#define ATOMICS_WAIT_RESULT_T int + +// Numbering dictated by https://github.com/WebAssembly/threads/blob/master/proposals/threads/Overview.md#wait +#define ATOMICS_WAIT_OK 0 +#define ATOMICS_WAIT_NOT_EQUAL 1 +#define ATOMICS_WAIT_TIMED_OUT 2 + +#define ATOMICS_WAIT_DURATION_INFINITE -1ll + +// Issues the wasm 'memory.atomic.wait32' instruction: +// If the given memory address contains value 'expectedValue', puts the calling thread to sleep to wait for that address to be notified. +// Returns one of the ATOMICS_WAIT_* return codes. +// NOTE: This function takes in the wait value in int64_t nanosecond units. Pass in maxWaitNanoseconds = -1 (or ATOMICS_WAIT_DURATION_INFINITE) to wait infinitely long. +static __attribute__((always_inline)) ATOMICS_WAIT_RESULT_T emscripten_wasm_wait_i32(int32_t *address, int expectedValue, int64_t maxWaitNanoseconds) +{ + return __builtin_wasm_memory_atomic_wait32(address, expectedValue, maxWaitNanoseconds); +} + +// Issues the wasm 'memory.atomic.wait64' instruction: +// If the given memory address contains value 'expectedValue', puts the calling thread to sleep to wait for that address to be notified. +// Returns one of the ATOMICS_WAIT_* return codes. +// NOTE: This function takes in the wait value in int64_t nanosecond units. Pass in maxWaitNanoseconds = -1 (or ATOMICS_WAIT_DURATION_INFINITE) to wait infinitely long. +static __attribute__((always_inline)) ATOMICS_WAIT_RESULT_T emscripten_wasm_wait_i64(int64_t *address, int64_t expectedValue, int64_t maxWaitNanoseconds) +{ + return __builtin_wasm_memory_atomic_wait64(address, expectedValue, maxWaitNanoseconds); +} + +#define EMSCRIPTEN_NOTIFY_ALL_WAITERS (-1LL) + +// Issues the wasm 'memory.atomic.notify' instruction: +// Notifies the given number of threads waiting on a location. +// Pass count == EMSCRIPTEN_NOTIFY_ALL_WAITERS to notify all waiters on the given location. +// Returns the number of threads that were woken up. +// Note: this function is used to notify both waiters waiting on an i32 and i64 addresses. +static __attribute__((always_inline)) int64_t emscripten_wasm_notify(int32_t *address, int64_t count) +{ + return __builtin_wasm_memory_atomic_notify(address, count); +} + +#define EMSCRIPTEN_WAIT_ASYNC_INFINITY __builtin_inf() + +// Represents a pending 'Atomics.waitAsync' wait operation. +#define ATOMICS_WAIT_TOKEN_T int + +#define EMSCRIPTEN_IS_VALID_WAIT_TOKEN(token) ((token) <= 0) + +// Issues the JavaScript 'Atomics.waitAsync' instruction: +// performs an asynchronous wait operation on the main thread. If the given 'address' contains 'value', issues a +// deferred wait that will invoke the specified callback function 'asyncWaitFinished' once that +// address has been notified by another thread. +// NOTE: Unlike functions emscripten_wasm_wait_i32() and emscripten_wasm_wait_i64() which take in the +// wait timeout parameter as int64 nanosecond units, this function takes in the wait timeout parameter +// as double millisecond units. See https://github.com/WebAssembly/threads/issues/175 for more information. +// Pass in maxWaitMilliseconds == EMSCRIPTEN_WAIT_ASYNC_INFINITY (==__builtin_inf()) to wait infinitely long. +// Returns one of: +// - ATOMICS_WAIT_NOT_EQUAL if the waitAsync operation could not be registered since the memory value did not +// contain the value 'value'. +// - ATOMICS_WAIT_TIMED_OUT if the waitAsync operation timeout parameter was <= 0. +// - Any other value: denotes a 'wait token' that can be passed to function emscripten_atomic_cancel_wait_async() +// to unregister an asynchronous wait. You can use the macro EMSCRIPTEN_IS_VALID_WAIT_TOKEN(retval) to check +// if this function returned a valid wait token. +ATOMICS_WAIT_TOKEN_T emscripten_atomic_wait_async(int32_t *address, + uint32_t value, + void (*asyncWaitFinished)(int32_t *address, uint32_t value, ATOMICS_WAIT_RESULT_T waitResult, void *userData), + void *userData, + double maxWaitMilliseconds); + +// Unregisters a pending Atomics.waitAsync operation that was established via a call to emscripten_atomic_wait_async() +// in the calling thread. Pass in the wait token handle that was received as the return value from the wait function. +// Returns EMSCRIPTEN_RESULT_SUCCESS if the cancellation was successful, or EMSCRIPTEN_RESULT_INVALID_PARAM if the +// asynchronous wait has already resolved prior and the callback has already been called. +// NOTE: Because of needing to work around issue https://github.com/WebAssembly/threads/issues/176, calling this +// function has an effect of introducing spurious wakeups to any other threads waiting on the +// same address that the async wait denoted by the token does. This means that in order to safely use this function, +// the mechanisms used in any wait code on that address must be written to be spurious wakeup safe. (this is the +// case for all the synchronization primitives declared in this header, but if you are rolling out your own, you +// need to be aware of this). If https://github.com/tc39/proposal-cancellation/issues/29 is resolved, then the +// spurious wakeups can be avoided. +EMSCRIPTEN_RESULT emscripten_atomic_cancel_wait_async(ATOMICS_WAIT_TOKEN_T waitToken); + +// Cancels all pending async waits in the calling thread. Because of https://github.com/WebAssembly/threads/issues/176, +// if you are using asynchronous waits in your application, and need to be able to let GC reclaim Wasm heap memory +// when deinitializing an application, you *must* call this function to help the GC unpin all necessary memory. +// Otherwise, you can wrap the Wasm content in an iframe and unload the iframe to let GC occur. (navigating away +// from the page or closing that tab will also naturally reclaim the memory) +int emscripten_atomic_cancel_all_wait_asyncs(void); + +// Cancels all pending async waits in the calling thread to the given memory address. +// Returns the number of async waits canceled. +int emscripten_atomic_cancel_all_wait_asyncs_at_address(int32_t *address); + +// Sleeps the calling wasm worker for the given nanoseconds. Calling this function on the main thread +// either results in a TypeError exception (Firefox), or a silent return without waiting (Chrome), +// see https://github.com/WebAssembly/threads/issues/174 +void emscripten_wasm_worker_sleep(int64_t nanoseconds); + +// Returns the value of navigator.hardwareConcurrency, i.e. the number of logical threads available for +// the user agent. NOTE: If the execution environment does not support navigator.hardwareConcurrency, +// this function will return zero to signal no support. (If the value 1 is returned, then it means that +// navigator.hardwareConcurrency is supported, but there is only one logical thread of concurrency available) +int emscripten_navigator_hardware_concurrency(void); + +// Returns the value of the expression "Atomics.isLockFree(byteWidth)": true if the given memory access +// width can be accessed atomically, and false otherwise. Generally will return true +// on 1, 2 and 4 byte accesses. On 8 byte accesses, behavior differs across browsers, see +// - https://bugzilla.mozilla.org/show_bug.cgi?id=1246139 +// - https://bugs.chromium.org/p/chromium/issues/detail?id=1167449 +int emscripten_atomics_is_lock_free(int byteWidth); + +#define emscripten_lock_t volatile uint32_t + +// Use with syntax "emscripten_lock_t l = EMSCRIPTEN_LOCK_T_STATIC_INITIALIZER;" +#define EMSCRIPTEN_LOCK_T_STATIC_INITIALIZER 0 + +void emscripten_lock_init(emscripten_lock_t *lock); + +// Attempts to acquire the specified lock. If the lock is free, then this function acquires +// the lock and immediately returns EM_TRUE. If the lock is not free at the time of the call, +// the calling thread is set to synchronously sleep for at most maxWaitNanoseconds long, until +// another thread releases the lock. If the lock is acquired within that period, the function +// returns EM_TRUE. If the lock is not acquired within the specified period, then the wait +// times out and EM_FALSE is returned. +// NOTE: This function can be only called in a Worker, and not on the main browser thread, +// because the main browser thread cannot synchronously sleep to wait for locks. +EM_BOOL emscripten_lock_wait_acquire(emscripten_lock_t *lock, int64_t maxWaitNanoseconds); + +// Similar to emscripten_lock_wait_acquire(), but instead of waiting for at most a specified +// timeout value, the thread will wait indefinitely long until the lock can be acquired. +// NOTE: The only way to abort this wait is to call emscripten_terminate_wasm_worker() on the +// Worker. +// NOTE: This function can be only called in a Worker, and not on the main browser thread, +// because the main browser thread cannot synchronously sleep to wait for locks. +void emscripten_lock_waitinf_acquire(emscripten_lock_t *lock); + +// Similar to emscripten_lock_wait_acquire(), but instead of placing the calling thread to +// sleep until the lock can be acquired, this function will burn CPU cycles attempting to +// acquire the lock, until the given timeout is met. +// This function can be called in both main thread and in Workers. +// NOTE: The wait period used for this function is specified in milliseconds instead of +// nanoseconds, see https://github.com/WebAssembly/threads/issues/175 for details. +// NOTE: If this function is called on the main thread, be sure to use a reasonable max wait +// value, or otherwise a "slow script dialog" notification can pop up, and can cause the +// browser to stop executing the page. +EM_BOOL emscripten_lock_busyspin_wait_acquire(emscripten_lock_t *lock, double maxWaitMilliseconds); + +// Similar to emscripten_lock_wait_acquire(), but instead of placing the calling thread to +// sleep until the lock can be acquired, this function will burn CPU cycles indefinitely until +// the given lock can be acquired. +// This function can be called in both main thread and in Workers. +// NOTE: The only way to abort this wait is to call emscripten_terminate_wasm_worker() on the +// Worker. If called on the main thread, and the lock cannot be acquired within a reasonable +// time period, this function will *HANG* the browser page content process, and show up +// a "slow script dialog", and/or cause the browser to stop the page. If you call this +// function on the main browser thread, be extra careful to analyze that the given lock will +// be extremely fast to acquire without contention from other threads. +void emscripten_lock_busyspin_waitinf_acquire(emscripten_lock_t *lock); + +// Registers an *asynchronous* lock acquire operation. The calling thread will asynchronously +// try to obtain the given lock after the calling thread yields back to the event loop. If the +// attempt is successful within maxWaitMilliseconds period, then the given callback asyncWaitFinished +// is called with waitResult == ATOMICS_WAIT_OK. If the lock is not acquired within the timeout +// period, then the callback asyncWaitFinished is called with waitResult == ATOMICS_WAIT_TIMED_OUT. +// NOTE: Unlike function emscripten_lock_wait_acquire() which takes in the +// wait timeout parameter as int64 nanosecond units, this function takes in the wait timeout parameter +// as double millisecond units. See https://github.com/WebAssembly/threads/issues/175 for more information. +// NOTE: This function can be called in both main thread and in Workers. If you use this API in Worker, +// you cannot utilise an infinite loop programming model. +void emscripten_lock_async_acquire(emscripten_lock_t *lock, + void (*asyncWaitFinished)(volatile void *address, uint32_t value, ATOMICS_WAIT_RESULT_T waitResult, void *userData), + void *userData, + double maxWaitMilliseconds); + +// Attempts to acquire a lock, returning EM_TRUE if successful. If the lock is already held, +// this function will not sleep to wait until the lock is released, but immediately returns +// EM_FALSE. +// This function can be called on both main thread and in Workers. +EM_BOOL emscripten_lock_try_acquire(emscripten_lock_t *lock); + +// Unlocks the specified lock for another thread to access. Note that locks are extermely +// lightweight, there is no "lock owner" tracking: this function does not actually check +// whether the calling thread owns the specified lock, but any thread can call this function +// to release a lock on behalf of whichever thread owns it. +// This function can be called on both main thread and in Workers. +void emscripten_lock_release(emscripten_lock_t *lock); + +#define emscripten_semaphore_t volatile uint32_t + +// Use with syntax emscripten_semaphore_t s = EMSCRIPTEN_SEMAPHORE_T_STATIC_INITIALIZER(num); +#define EMSCRIPTEN_SEMAPHORE_T_STATIC_INITIALIZER(num) ((int)(num)) + +void emscripten_semaphore_init(emscripten_semaphore_t *sem, int num); + +// main thread, try acquire num instances, but do not sleep to wait if not available. +// Returns idx that was acquired or -1 if acquire failed. +int emscripten_semaphore_try_acquire(emscripten_semaphore_t *sem, int num); + +// main thread, poll to try acquire num instances. Returns idx that was acquired. If you use this API in Worker, you cannot run an infinite loop. +void emscripten_semaphore_async_acquire(emscripten_semaphore_t *sem, + int num, + void (*asyncWaitFinished)(volatile void *address, uint32_t idx, ATOMICS_WAIT_RESULT_T result, void *userData), + void *userData, + double maxWaitMilliseconds); + +// worker, sleep to acquire num instances. Returns idx that was acquired, or -1 if timed out unable to acquire. +int emscripten_semaphore_wait_acquire(emscripten_semaphore_t *sem, int num, int64_t maxWaitNanoseconds); + +// worker, sleep infinitely long to acquire num instances. Returns idx that was acquired. +int emscripten_semaphore_waitinf_acquire(emscripten_semaphore_t *sem, int num); + +// Releases the given number of resources back to the semaphore. Note +// that the ownership of resources is completely conceptual - there is +// no actual checking that the calling thread had previously acquired +// that many resouces, so programs need to keep check of their +// semaphore usage consistency themselves. +// Returns how many resources were available in the semaphore before the +// new resources were released back to the semaphore. (i.e. the index where +// the resource was put back to) +// [main thread or worker] +uint32_t emscripten_semaphore_release(emscripten_semaphore_t *sem, int num); + +// Condition variable is an object that can be waited on, and another thread can signal, while coordinating an access to a related mutex. +#define emscripten_condvar_t volatile uint32_t + +// Use with syntax emscripten_condvar_t cv = EMSCRIPTEN_CONDVAR_T_STATIC_INITIALIZER; +#define EMSCRIPTEN_CONDVAR_T_STATIC_INITIALIZER ((int)(0)) + +// Creates a new condition variable to the given memory location. +void emscripten_condvar_init(emscripten_condvar_t *condvar); + +// Atomically performs the following: +// 1. releases the given lock. The lock should (but does not strictly need to) be held by the calling thread prior to this call. +// 2. sleep the calling thread to wait for the specified condition variable to be signaled. +// 3. once the sleep has finished (another thread has signaled the condition variable), the calling thread wakes up +// and reacquires the lock prior to returning from this function. +void emscripten_condvar_waitinf(emscripten_condvar_t *condvar, emscripten_lock_t *lock); + +// Same as the above, except that an attempt to wait for the condition variable to become true is only performed for a maximum duration. +// On success (no timeout), this function will return EM_TRUE. If the wait times out, this function will return EM_FALSE. In this case, +// the calling thread will not try to reacquire the lock. +EM_BOOL emscripten_condvar_wait(emscripten_condvar_t *condvar, emscripten_lock_t *lock, int64_t maxWaitNanoseconds); + +// Asynchronously wait for the given condition variable to signal. +ATOMICS_WAIT_TOKEN_T emscripten_condvar_wait_async(emscripten_condvar_t *condvar, + emscripten_lock_t *lock, + void (*asyncWaitFinished)(int32_t *address, uint32_t value, ATOMICS_WAIT_RESULT_T waitResult, void *userData), + void *userData, + double maxWaitMilliseconds); + +// Signals the given number of waiters on the specified condition variable. +// Pass numWaitersToSignal == EMSCRIPTEN_NOTIFY_ALL_WAITERS to wake all waiters ("broadcast" operation). +void emscripten_condvar_signal(emscripten_condvar_t *condvar, int64_t numWaitersToSignal); + +#ifdef __cplusplus +} +#endif diff --git a/system/lib/compiler-rt/stack_limits.S b/system/lib/compiler-rt/stack_limits.S index 628345fc3df7..929c05e4dd44 100644 --- a/system/lib/compiler-rt/stack_limits.S +++ b/system/lib/compiler-rt/stack_limits.S @@ -77,6 +77,35 @@ emscripten_stack_get_free: PTR.sub end_function +#ifdef __EMSCRIPTEN_WASM_WORKERS__ +# TODO: Relocate the following to its own file wasm_worker.S, but need to figure out how to reference +# __stack_base and __stack_end globals from a separate file as externs in order for that to work. +.globl emscripten_wasm_worker_initialize +emscripten_wasm_worker_initialize: + .functype emscripten_wasm_worker_initialize (PTR, i32, PTR) -> () + + local.get 0 + global.set __stack_end + + local.get 0 + local.get 1 +#ifdef __wasm64__ + i64.extend_i32_u +#endif + PTR.add + local.tee 0 + global.set __stack_base + + local.get 0 + global.set __stack_pointer + + local.get 2 + .functype __wasm_init_tls (PTR) -> () + call __wasm_init_tls + + end_function +#endif + # Add emscripten_stack_init to static ctors .section .init_array.1,"",@ .p2align ALIGN diff --git a/system/lib/wasm_worker/library_wasm_worker.c b/system/lib/wasm_worker/library_wasm_worker.c new file mode 100644 index 000000000000..4803dbc77a5e --- /dev/null +++ b/system/lib/wasm_worker/library_wasm_worker.c @@ -0,0 +1,240 @@ +#include +#include +#include +#include +#include + +#ifndef __EMSCRIPTEN_WASM_WORKERS__ +#error __EMSCRIPTEN_WASM_WORKERS__ should be defined when building this file! +#endif + +// Options: +// #define WASM_WORKER_NO_TLS 0/1 : set to 1 to disable TLS compilation support for a small code size gain +// #define STACK_OVERFLOW_CHECK 0/1/2 : set to the current stack overflow check mode + +// Internal implementation function in JavaScript side that emscripten_create_wasm_worker() calls to +// to perform the wasm worker creation. +emscripten_wasm_worker_t _emscripten_create_wasm_worker_with_tls(void *stackLowestAddress, uint32_t stackSize, void *tlsAddress); +emscripten_wasm_worker_t _emscripten_create_wasm_worker_no_tls(void *stackLowestAddress, uint32_t stackSize); + +void __wasm_init_tls(void *memory); + +#if !WASM_WORKER_NO_TLS +__attribute__((constructor(48))) +static void emscripten_wasm_worker_main_thread_initialize() { + uintptr_t* sbrk_ptr = emscripten_get_sbrk_ptr(); + __wasm_init_tls((void*)*sbrk_ptr); + *sbrk_ptr += __builtin_wasm_tls_size(); +} +#endif + +emscripten_wasm_worker_t emscripten_create_wasm_worker_with_tls(void *stackLowestAddress, uint32_t stackSize, void *tlsAddress, uint32_t tlsSize) +{ +#if WASM_WORKER_NO_TLS + return emscripten_create_wasm_worker_no_tls(stackLowestAddress, stackSize); +#else + assert((uintptr_t)stackLowestAddress % 16 == 0); + assert(stackSize % 16 == 0); + assert(__builtin_wasm_tls_align() != 0 || __builtin_wasm_tls_size() == 0); // Internal consistency check: Clang can report __builtin_wasm_tls_align to be zero if there is no TLS used - double check it never reports zero if it is used. + assert(__builtin_wasm_tls_align() == 0 || (uintptr_t)tlsAddress % __builtin_wasm_tls_align() == 0 && "TLS memory address not aligned in a call to emscripten_create_wasm_worker_with_tls()! Please allocate memory with alignment from __builtin_wasm_tls_align() when creating a Wasm Worker!"); + assert(tlsSize != 0 || __builtin_wasm_tls_size() == 0 && "Program code contains TLS: please use function emscripten_create_wasm_worker_with_tls() to create a Wasm Worker!"); + assert(tlsSize == __builtin_wasm_tls_size() && "TLS size mismatch! Please reserve exactly __builtin_wasm_tls_size() TLS memory in a call to emscripten_create_wasm_worker_with_tls()"); + assert(tlsAddress != 0 || tlsSize == 0); + return _emscripten_create_wasm_worker_with_tls((void*)stackLowestAddress, stackSize, tlsAddress); +#endif +} + +emscripten_wasm_worker_t emscripten_create_wasm_worker_no_tls(void *stackLowestAddress, uint32_t stackSize) +{ +#if !WASM_WORKER_NO_TLS + return emscripten_create_wasm_worker_with_tls(stackLowestAddress, stackSize, 0, 0); +#else + assert((uintptr_t)stackLowestAddress % 16 == 0); + assert(stackSize % 16 == 0); + assert(__builtin_wasm_tls_size() == 0 && "Cannot disable TLS with -sWASM_WORKERS_NO_TLS=1 when compiling code that does require TLS! Rebuild with -sWASM_WORKERS_NO_TLS=1 removed, or remove uses of TLS from the codebase."); + return _emscripten_create_wasm_worker_no_tls((void*)stackLowestAddress, stackSize); +#endif +} + +emscripten_wasm_worker_t emscripten_malloc_wasm_worker(uint32_t stackSize) +{ +#if WASM_WORKER_NO_TLS + return emscripten_create_wasm_worker_no_tls(memalign(16, stackSize), stackSize); +#else + uint32_t tlsSize = __builtin_wasm_tls_size(); + return emscripten_create_wasm_worker_with_tls(memalign(16, stackSize), stackSize, tlsSize ? memalign(__builtin_wasm_tls_align(), tlsSize) : 0, tlsSize); +#endif +} + +void emscripten_wasm_worker_sleep(int64_t nsecs) +{ + int32_t addr = 0; + emscripten_wasm_wait_i32(&addr, 0, nsecs); +} + +void emscripten_lock_init(emscripten_lock_t *lock) +{ + emscripten_atomic_store_u32((void*)lock, EMSCRIPTEN_LOCK_T_STATIC_INITIALIZER); +} + +EM_BOOL emscripten_lock_wait_acquire(emscripten_lock_t *lock, int64_t maxWaitNanoseconds) +{ + emscripten_lock_t val = emscripten_atomic_cas_u32((void*)lock, 0, 1); + if (!val) return EM_TRUE; + int64_t waitEnd = (int64_t)(emscripten_performance_now() * 1e6) + maxWaitNanoseconds; + while(maxWaitNanoseconds > 0) + { + emscripten_wasm_wait_i32((int32_t*)lock, val, maxWaitNanoseconds); + val = emscripten_atomic_cas_u32((void*)lock, 0, 1); + if (!val) return EM_TRUE; + maxWaitNanoseconds = waitEnd - (int64_t)(emscripten_performance_now() * 1e6); + } + return EM_FALSE; +} + +void emscripten_lock_waitinf_acquire(emscripten_lock_t *lock) +{ + emscripten_lock_t val; + do + { + val = emscripten_atomic_cas_u32((void*)lock, 0, 1); + if (val) + emscripten_wasm_wait_i32((int32_t*)lock, val, ATOMICS_WAIT_DURATION_INFINITE); + } while(val); +} + +EM_BOOL emscripten_lock_busyspin_wait_acquire(emscripten_lock_t *lock, double maxWaitMilliseconds) +{ + emscripten_lock_t val = emscripten_atomic_cas_u32((void*)lock, 0, 1); + if (!val) return EM_TRUE; + + double t = emscripten_performance_now(); + double waitEnd = t + maxWaitMilliseconds; + while(t < waitEnd) + { + val = emscripten_atomic_cas_u32((void*)lock, 0, 1); + if (!val) return EM_TRUE; + t = emscripten_performance_now(); + } + return EM_FALSE; +} + +void emscripten_lock_busyspin_waitinf_acquire(emscripten_lock_t *lock) +{ + emscripten_lock_t val; + do + { + val = emscripten_atomic_cas_u32((void*)lock, 0, 1); + } while(val); +} + +EM_BOOL emscripten_lock_try_acquire(emscripten_lock_t *lock) +{ + emscripten_lock_t val = emscripten_atomic_cas_u32((void*)lock, 0, 1); + return !val; +} + +void emscripten_lock_release(emscripten_lock_t *lock) +{ + emscripten_atomic_store_u32((void*)lock, 0); + emscripten_wasm_notify((int32_t*)lock, 1); +} + +void emscripten_semaphore_init(emscripten_semaphore_t *sem, int num) +{ + emscripten_atomic_store_u32((void*)sem, num); +} + +int emscripten_semaphore_try_acquire(emscripten_semaphore_t *sem, int num) +{ + uint32_t val = num; + for(;;) + { + uint32_t ret = emscripten_atomic_cas_u32((void*)sem, val, val - num); + if (ret == val) return val - num; + if (ret < num) return -1; + val = ret; + } +} + +int emscripten_semaphore_wait_acquire(emscripten_semaphore_t *sem, int num, int64_t maxWaitNanoseconds) +{ + int val = emscripten_atomic_load_u32((void*)sem); + for(;;) + { + while(val < num) + { + // TODO: Shave off maxWaitNanoseconds + ATOMICS_WAIT_RESULT_T waitResult = emscripten_wasm_wait_i32((int32_t*)sem, val, maxWaitNanoseconds); + if (waitResult == ATOMICS_WAIT_TIMED_OUT) return -1; + val = emscripten_atomic_load_u32((void*)sem); + } + int ret = (int)emscripten_atomic_cas_u32((void*)sem, val, val - num); + if (ret == val) return val - num; + val = ret; + } +} + +int emscripten_semaphore_waitinf_acquire(emscripten_semaphore_t *sem, int num) +{ + int val = emscripten_atomic_load_u32((void*)sem); + for(;;) + { + while(val < num) + { + emscripten_wasm_wait_i32((int32_t*)sem, val, ATOMICS_WAIT_DURATION_INFINITE); + val = emscripten_atomic_load_u32((void*)sem); + } + int ret = (int)emscripten_atomic_cas_u32((void*)sem, val, val - num); + if (ret == val) return val - num; + val = ret; + } +} + +uint32_t emscripten_semaphore_release(emscripten_semaphore_t *sem, int num) +{ + uint32_t ret = emscripten_atomic_add_u32((void*)sem, num); + emscripten_wasm_notify((int*)sem, num); + return ret; +} + +void emscripten_condvar_init(emscripten_condvar_t *condvar) +{ + *condvar = EMSCRIPTEN_CONDVAR_T_STATIC_INITIALIZER; +} + +void emscripten_condvar_waitinf(emscripten_condvar_t *condvar, emscripten_lock_t *lock) +{ + int val = emscripten_atomic_load_u32((void*)condvar); + emscripten_lock_release(lock); + emscripten_wasm_wait_i32((int32_t*)condvar, val, ATOMICS_WAIT_DURATION_INFINITE); + emscripten_lock_waitinf_acquire(lock); +} + +int emscripten_condvar_wait(emscripten_condvar_t *condvar, emscripten_lock_t *lock, int64_t maxWaitNanoseconds) +{ + int val = emscripten_atomic_load_u32((void*)condvar); + emscripten_lock_release(lock); + int waitValue = emscripten_wasm_wait_i32((int32_t*)condvar, val, maxWaitNanoseconds); + if (waitValue == ATOMICS_WAIT_TIMED_OUT) + return EM_FALSE; + + return emscripten_lock_wait_acquire(lock, maxWaitNanoseconds); +} + +ATOMICS_WAIT_TOKEN_T emscripten_condvar_wait_async(emscripten_condvar_t *condvar, + emscripten_lock_t *lock, + void (*asyncWaitFinished)(int32_t *address, uint32_t value, ATOMICS_WAIT_RESULT_T waitResult, void *userData), + void *userData, + double maxWaitMilliseconds) +{ + int val = emscripten_atomic_load_u32((void*)condvar); + emscripten_lock_release(lock); + return emscripten_atomic_wait_async((int32_t *)condvar, val, asyncWaitFinished, userData, maxWaitMilliseconds); +} + +void emscripten_condvar_signal(emscripten_condvar_t *condvar, int64_t numWaitersToSignal) +{ + emscripten_atomic_add_u32((void*)condvar, 1); + emscripten_wasm_notify((int*)condvar, numWaitersToSignal); +} diff --git a/system/lib/wasm_worker/library_wasm_worker_stub.c b/system/lib/wasm_worker/library_wasm_worker_stub.c new file mode 100644 index 000000000000..131ba6392535 --- /dev/null +++ b/system/lib/wasm_worker/library_wasm_worker_stub.c @@ -0,0 +1,92 @@ +#include +#include +#include +#include +#include + +#ifdef __EMSCRIPTEN_WASM_WORKERS__ +#error __EMSCRIPTEN_WASM_WORKERS__ should not be defined when building this file! +#endif + +emscripten_wasm_worker_t emscripten_create_wasm_worker_with_tls(void *stackLowestAddress, uint32_t stackSize, void *tlsAddress, uint32_t tlsSize) { + return 0; +} + +emscripten_wasm_worker_t emscripten_create_wasm_worker_no_tls(void *stackLowestAddress, uint32_t stackSize) { + return 0; +} + +emscripten_wasm_worker_t emscripten_malloc_wasm_worker(uint32_t stackSize) { + return 0; +} + +void emscripten_wasm_worker_sleep(int64_t nsecs) { +} + +void emscripten_lock_init(emscripten_lock_t *lock) { +} + +EM_BOOL emscripten_lock_wait_acquire(emscripten_lock_t *lock, int64_t maxWaitNanoseconds) { + return EM_TRUE; +} + +void emscripten_lock_waitinf_acquire(emscripten_lock_t *lock) { +} + +EM_BOOL emscripten_lock_busyspin_wait_acquire(emscripten_lock_t *lock, double maxWaitMilliseconds) { + return EM_TRUE; +} + +void emscripten_lock_busyspin_waitinf_acquire(emscripten_lock_t *lock) { +} + +EM_BOOL emscripten_lock_try_acquire(emscripten_lock_t *lock) { + return EM_TRUE; +} + +void emscripten_lock_release(emscripten_lock_t *lock) { +} + +void emscripten_semaphore_init(emscripten_semaphore_t *sem, int num) { +} + +int emscripten_semaphore_try_acquire(emscripten_semaphore_t *sem, int num) { + *sem -= num; + return *sem; +} + +int emscripten_semaphore_wait_acquire(emscripten_semaphore_t *sem, int num, int64_t maxWaitNanoseconds) { + *sem -= num; + return *sem; +} + +int emscripten_semaphore_waitinf_acquire(emscripten_semaphore_t *sem, int num) { + *sem -= num; + return *sem; +} + +uint32_t emscripten_semaphore_release(emscripten_semaphore_t *sem, int num) { + *sem += num; + return *sem - num; +} + +void emscripten_condvar_init(emscripten_condvar_t *condvar) { +} + +void emscripten_condvar_waitinf(emscripten_condvar_t *condvar, emscripten_lock_t *lock) { +} + +int emscripten_condvar_wait(emscripten_condvar_t *condvar, emscripten_lock_t *lock, int64_t maxWaitNanoseconds) { + return EM_TRUE; +} + +ATOMICS_WAIT_TOKEN_T emscripten_condvar_wait_async(emscripten_condvar_t *condvar, + emscripten_lock_t *lock, + void (*asyncWaitFinished)(int32_t *address, uint32_t value, ATOMICS_WAIT_RESULT_T waitResult, void *userData), + void *userData, + double maxWaitMilliseconds) { + return 0; +} + +void emscripten_condvar_signal(emscripten_condvar_t *condvar, int64_t numWaitersToSignal) { +} diff --git a/tests/code_size/hello_wasm_worker_wasm.js b/tests/code_size/hello_wasm_worker_wasm.js new file mode 100644 index 000000000000..a412326192bc --- /dev/null +++ b/tests/code_size/hello_wasm_worker_wasm.js @@ -0,0 +1,75 @@ +var b = Module; + +var c = b.$ww; + +new function(a) { + new TextDecoder(a); +}("utf8"); + +var e, f; + +e = b.mem || new WebAssembly.Memory({ + initial: 256, + maximum: 256, + shared: !0 +}); + +var g = e.buffer; + +var h = {}, k = 1; + +function l(a) { + m.push(a); +} + +function n(a) { + a = a.data; + let d = a._wsc; + d && f.get(d)(...a.x); +} + +var m = []; + +c && (h[0] = this, addEventListener("message", l)); + +var p, q; + +WebAssembly.instantiate(b.wasm, { + a: { + b: function(a, d, t) { + let r = h[k] = new Worker(b.$wb); + r.postMessage({ + $ww: k, + wasm: b.wasm, + js: b.js, + mem: e, + sb: a, + sz: d, + tb: t + }); + r.addEventListener("message", n); + return k++; + }, + c: function() { + return !1; + }, + d: function(a, d) { + h[a].postMessage({ + _wsc: d, + x: [] + }); + }, + e: function() { + console.log("Hello from wasm worker!"); + }, + a: e + } +}).then((function(a) { + a = a.instance.exports; + p = a.g; + q = a.i; + f = a.h; + c ? (a = b, q(a.sb, a.sz, a.tb), removeEventListener("message", l), m = m.forEach(n), + addEventListener("message", n)) : a.f(); + c || p(); +})); \ No newline at end of file diff --git a/tests/code_size/hello_wasm_worker_wasm.json b/tests/code_size/hello_wasm_worker_wasm.json new file mode 100644 index 000000000000..76112d47ad62 --- /dev/null +++ b/tests/code_size/hello_wasm_worker_wasm.json @@ -0,0 +1,10 @@ +{ + "a.html": 737, + "a.html.gz": 433, + "a.js": 813, + "a.js.gz": 501, + "a.wasm": 1791, + "a.wasm.gz": 991, + "total": 3341, + "total_gz": 1925 +} diff --git a/tests/report_result.h b/tests/report_result.h index d84bf68b235e..7fc2f19cad07 100644 --- a/tests/report_result.h +++ b/tests/report_result.h @@ -23,7 +23,7 @@ void _MaybeReportResult(int result, int sync); } #endif -#if defined __EMSCRIPTEN__ && defined __EMSCRIPTEN_PTHREADS__ +#if defined __EMSCRIPTEN__ && defined __EMSCRIPTEN_PTHREADS__ && !defined(__EMSCRIPTEN_WASM_WORKERS__) #include #include #define REPORT_RESULT(result) emscripten_async_run_in_main_runtime_thread(EM_FUNC_SIG_VII, _ReportResult, (result), 0) diff --git a/tests/test_browser.py b/tests/test_browser.py index 0fed2a854f78..014a924bb93f 100644 --- a/tests/test_browser.py +++ b/tests/test_browser.py @@ -22,7 +22,7 @@ from common import BrowserCore, RunnerCore, path_from_root, has_browser, EMTEST_BROWSER, Reporting from common import create_file, parameterized, ensure_dir, disabled, test_file, WEBIDL_BINDER -from common import read_file, require_v8 +from common import read_file, require_v8, also_with_minimal_runtime from tools import shared from tools import ports from tools.shared import EMCC, WINDOWS, FILE_PACKAGER, PIPE @@ -5083,6 +5083,203 @@ def test_wasm2js_fallback_on_wasm_compilation_failure(self): def test_system(self): self.btest_exit(test_file('system.c')) + # Tests the hello_wasm_worker.c documentation example code. + @also_with_minimal_runtime + def test_wasm_worker_hello(self): + self.btest(test_file('wasm_worker/hello_wasm_worker.c'), expected='0', args=['-sWASM_WORKERS']) + + # Tests the WASM_WORKERS=2 build mode, which embeds the Wasm Worker bootstrap JS script file to the main JS file. + @also_with_minimal_runtime + def test_wasm_worker_embedded(self): + self.btest(test_file('wasm_worker/hello_wasm_worker.c'), expected='0', args=['-sWASM_WORKERS=2']) + + # Tests Wasm Worker thread stack setup + @also_with_minimal_runtime + def test_wasm_worker_thread_stack(self): + self.btest(test_file('wasm_worker/thread_stack.c'), expected='0', args=['-sWASM_WORKERS']) + + # Tests Wasm Worker thread stack setup without TLS support active + @also_with_minimal_runtime + def test_wasm_worker_thread_stack_no_tls(self): + self.btest(test_file('wasm_worker/thread_stack.c'), expected='0', args=['-sWASM_WORKERS', '-sWASM_WORKERS_NO_TLS']) + + # Tests emscripten_malloc_wasm_worker() and emscripten_current_thread_is_wasm_worker() functions + @also_with_minimal_runtime + def test_wasm_worker_malloc(self): + self.btest(test_file('wasm_worker/malloc_wasm_worker.c'), expected='0', args=['-sWASM_WORKERS']) + + # Tests Wasm Worker+pthreads simultaneously + @also_with_minimal_runtime + def test_wasm_worker_and_pthreads(self): + self.btest(test_file('wasm_worker/wasm_worker_and_pthread.c'), expected='0', args=['-sWASM_WORKERS', '-pthread']) + + # Tests emscripten_wasm_worker_self_id() function + @also_with_minimal_runtime + def test_wasm_worker_self_id(self): + self.btest(test_file('wasm_worker/wasm_worker_self_id.c'), expected='0', args=['-sWASM_WORKERS']) + + # Tests direct Wasm Assembly .S file based TLS variables in Wasm Workers + @also_with_minimal_runtime + def test_wasm_worker_tls_wasm_assembly(self): + self.btest(test_file('wasm_worker/wasm_worker_tls_wasm_assembly.c'), + expected='42', args=['-sWASM_WORKERS', test_file('wasm_worker/wasm_worker_tls_wasm_assembly.S')]) + + # Tests C++11 keyword thread_local for TLS in Wasm Workers + @also_with_minimal_runtime + def test_wasm_worker_cpp11_thread_local(self): + if not WINDOWS: + self.skipTest('''wasm-ld: /b/s/w/ir/cache/builder/emscripten-releases/llvm-project/llvm/include/llvm/ADT/Optional.h:199: const T &llvm::optional_detail::OptionalStorage::getValue() const & [T = unsigned int]: Assertion `hasVal' failed. +PLEASE submit a bug report to https://github.com/llvm/llvm-project/issues/ and include the crash backtrace. +Stack dump: + #8 0x000000000068173f lld::wasm::GlobalSymbol::getGlobalIndex() const (/root/emsdk/upstream/bin/wasm-ld+0x68173f) + #9 0x0000000000699894 lld::wasm::GlobalSection::generateRelocationCode(llvm::raw_ostream&, bool) const (/root/emsdk/upstream/bin/wasm-ld+0x699894) +#10 0x0000000000688888 lld::wasm::(anonymous namespace)::Writer::run() Writer.cpp:0:0 +#11 0x0000000000682524 lld::wasm::writeResult() (/root/emsdk/upstream/bin/wasm-ld+0x682524) +#12 0x0000000000662f89 lld::wasm::(anonymous namespace)::LinkerDriver::linkerMain(llvm::ArrayRef) Driver.cpp:0:0 +#13 0x000000000065dd5e lld::wasm::link(llvm::ArrayRef, llvm::raw_ostream&, llvm::raw_ostream&, bool, bool) (/root/emsdk/upstream/bin/wasm-ld+0x65dd5e) +#14 0x000000000036f799 lldMain(int, char const**, llvm::raw_ostream&, llvm::raw_ostream&, bool) lld.cpp:0:0''') + self.btest(test_file('wasm_worker/cpp11_thread_local.cpp'), expected='42', args=['-sWASM_WORKERS']) + + # Tests C11 keyword _Thread_local for TLS in Wasm Workers + @also_with_minimal_runtime + def test_wasm_worker_c11__Thread_local(self): + if not WINDOWS: + self.skipTest('''wasm-ld: /b/s/w/ir/cache/builder/emscripten-releases/llvm-project/llvm/include/llvm/ADT/Optional.h:199: const T &llvm::optional_detail::OptionalStorage::getValue() const & [T = unsigned int]: Assertion `hasVal' failed. +PLEASE submit a bug report to https://github.com/llvm/llvm-project/issues/ and include the crash backtrace. +Stack dump: + #8 0x000000000068173f lld::wasm::GlobalSymbol::getGlobalIndex() const (/root/emsdk/upstream/bin/wasm-ld+0x68173f) + #9 0x0000000000699894 lld::wasm::GlobalSection::generateRelocationCode(llvm::raw_ostream&, bool) const (/root/emsdk/upstream/bin/wasm-ld+0x699894) +#10 0x0000000000688888 lld::wasm::(anonymous namespace)::Writer::run() Writer.cpp:0:0 +#11 0x0000000000682524 lld::wasm::writeResult() (/root/emsdk/upstream/bin/wasm-ld+0x682524) +#12 0x0000000000662f89 lld::wasm::(anonymous namespace)::LinkerDriver::linkerMain(llvm::ArrayRef) Driver.cpp:0:0 +#13 0x000000000065dd5e lld::wasm::link(llvm::ArrayRef, llvm::raw_ostream&, llvm::raw_ostream&, bool, bool) (/root/emsdk/upstream/bin/wasm-ld+0x65dd5e) +#14 0x000000000036f799 lldMain(int, char const**, llvm::raw_ostream&, llvm::raw_ostream&, bool) lld.cpp:0:0''') + + self.btest(test_file('wasm_worker/c11__Thread_local.c'), expected='42', args=['-sWASM_WORKERS', '-std=gnu11']) # Cannot test C11 - because of EM_ASM must test Gnu11. + + # Tests GCC specific extension keyword __thread for TLS in Wasm Workers + @also_with_minimal_runtime + def test_wasm_worker_gcc___thread(self): + if not WINDOWS: + self.skipTest('''wasm-ld: /b/s/w/ir/cache/builder/emscripten-releases/llvm-project/llvm/include/llvm/ADT/Optional.h:199: const T &llvm::optional_detail::OptionalStorage::getValue() const & [T = unsigned int]: Assertion `hasVal' failed. +PLEASE submit a bug report to https://github.com/llvm/llvm-project/issues/ and include the crash backtrace. +Stack dump: + #8 0x000000000068173f lld::wasm::GlobalSymbol::getGlobalIndex() const (/root/emsdk/upstream/bin/wasm-ld+0x68173f) + #9 0x0000000000699894 lld::wasm::GlobalSection::generateRelocationCode(llvm::raw_ostream&, bool) const (/root/emsdk/upstream/bin/wasm-ld+0x699894) +#10 0x0000000000688888 lld::wasm::(anonymous namespace)::Writer::run() Writer.cpp:0:0 +#11 0x0000000000682524 lld::wasm::writeResult() (/root/emsdk/upstream/bin/wasm-ld+0x682524) +#12 0x0000000000662f89 lld::wasm::(anonymous namespace)::LinkerDriver::linkerMain(llvm::ArrayRef) Driver.cpp:0:0 +#13 0x000000000065dd5e lld::wasm::link(llvm::ArrayRef, llvm::raw_ostream&, llvm::raw_ostream&, bool, bool) (/root/emsdk/upstream/bin/wasm-ld+0x65dd5e) +#14 0x000000000036f799 lldMain(int, char const**, llvm::raw_ostream&, llvm::raw_ostream&, bool) lld.cpp:0:0''') + self.btest(test_file('wasm_worker/gcc___Thread.c'), expected='42', args=['-sWASM_WORKERS', '-std=gnu11']) + + # Tests emscripten_wasm_worker_sleep() + @also_with_minimal_runtime + def test_wasm_worker_sleep(self): + self.btest(test_file('wasm_worker/wasm_worker_sleep.c'), expected='1', args=['-sWASM_WORKERS']) + + # Tests emscripten_terminate_wasm_worker() + @also_with_minimal_runtime + def test_wasm_worker_terminate(self): + self.btest(test_file('wasm_worker/terminate_wasm_worker.c'), expected='0', args=['-sWASM_WORKERS']) + + # Tests emscripten_terminate_all_wasm_workers() + @also_with_minimal_runtime + def test_wasm_worker_terminate_all(self): + self.btest(test_file('wasm_worker/terminate_all_wasm_workers.c'), expected='0', args=['-sWASM_WORKERS']) + + # Tests emscripten_wasm_worker_post_function_*() API + @also_with_minimal_runtime + def test_wasm_worker_post_function(self): + self.btest(test_file('wasm_worker/post_function.c'), expected='8', args=['-sWASM_WORKERS']) + + # Tests emscripten_wasm_worker_post_function_*() API and EMSCRIPTEN_WASM_WORKER_ID_PARENT + # to send a message back from Worker to its parent thread. + @also_with_minimal_runtime + def test_wasm_worker_post_function_to_main_thread(self): + self.btest(test_file('wasm_worker/post_function_to_main_thread.c'), expected='10', args=['-sWASM_WORKERS']) + + # Tests emscripten_navigator_hardware_concurrency() and emscripten_atomics_is_lock_free() + @also_with_minimal_runtime + def test_wasm_worker_hardware_concurrency_is_lock_free(self): + self.btest(test_file('wasm_worker/hardware_concurrency_is_lock_free.c'), expected='0', args=['-sWASM_WORKERS']) + + # Tests emscripten_wasm_wait_i32() and emscripten_wasm_notify() functions. + @also_with_minimal_runtime + def test_wasm_worker_wait32_notify(self): + self.btest(test_file('wasm_worker/wait32_notify.c'), expected='2', args=['-sWASM_WORKERS']) + + # Tests emscripten_wasm_wait_i64() and emscripten_wasm_notify() functions. + @also_with_minimal_runtime + def test_wasm_worker_wait64_notify(self): + self.btest(test_file('wasm_worker/wait64_notify.c'), expected='2', args=['-sWASM_WORKERS']) + + # Tests emscripten_atomic_wait_async() function. + @also_with_minimal_runtime + def test_wasm_worker_wait_async(self): + self.btest(test_file('wasm_worker/wait_async.c'), expected='0', args=['-sWASM_WORKERS']) + + # Tests emscripten_atomic_cancel_wait_async() function. + @also_with_minimal_runtime + def test_wasm_worker_cancel_wait_async(self): + self.btest(test_file('wasm_worker/cancel_wait_async.c'), expected='1', args=['-sWASM_WORKERS']) + + # Tests emscripten_atomic_cancel_all_wait_asyncs() function. + @also_with_minimal_runtime + def test_wasm_worker_cancel_all_wait_asyncs(self): + self.btest(test_file('wasm_worker/cancel_all_wait_asyncs.c'), expected='1', args=['-sWASM_WORKERS']) + + # Tests emscripten_atomic_cancel_all_wait_asyncs_at_address() function. + @also_with_minimal_runtime + def test_wasm_worker_cancel_all_wait_asyncs_at_address(self): + self.btest(test_file('wasm_worker/cancel_all_wait_asyncs_at_address.c'), expected='1', args=['-sWASM_WORKERS']) + + # Tests emscripten_lock_init(), emscripten_lock_waitinf_acquire() and emscripten_lock_release() + @also_with_minimal_runtime + def test_wasm_worker_lock_waitinf(self): + self.btest(test_file('wasm_worker/lock_waitinf_acquire.c'), expected='4000', args=['-sWASM_WORKERS']) + + # Tests emscripten_lock_wait_acquire() and emscripten_lock_try_acquire() in Worker. + @also_with_minimal_runtime + def test_wasm_worker_lock_wait(self): + self.btest(test_file('wasm_worker/lock_wait_acquire.c'), expected='0', args=['-sWASM_WORKERS']) + + # Tests emscripten_lock_wait_acquire() between two Wasm Workers. + @also_with_minimal_runtime + def test_wasm_worker_lock_wait2(self): + self.btest(test_file('wasm_worker/lock_wait_acquire2.c'), expected='0', args=['-sWASM_WORKERS']) + + # Tests emscripten_lock_async_acquire() function. + @also_with_minimal_runtime + def test_wasm_worker_lock_async_acquire(self): + self.btest(test_file('wasm_worker/lock_async_acquire.c'), expected='0', args=['-sWASM_WORKERS']) + + # Tests emscripten_lock_busyspin_wait_acquire() in Worker and main thread. + @also_with_minimal_runtime + def test_wasm_worker_lock_busyspin_wait(self): + self.btest(test_file('wasm_worker/lock_busyspin_wait_acquire.c'), expected='0', args=['-sWASM_WORKERS']) + + # Tests emscripten_lock_busyspin_waitinf_acquire() in Worker and main thread. + @also_with_minimal_runtime + def test_wasm_worker_lock_busyspin_waitinf(self): + self.btest(test_file('wasm_worker/lock_busyspin_waitinf_acquire.c'), expected='1', args=['-sWASM_WORKERS']) + + # Tests that proxied JS functions cannot be called from Wasm Workers + @also_with_minimal_runtime + def test_wasm_worker_no_proxied_js_functions(self): + self.btest(test_file('wasm_worker/no_proxied_js_functions.c'), expected='0', + args=['--js-library', test_file('wasm_worker/no_proxied_js_functions.js'), '-sWASM_WORKERS', '-sASSERTIONS']) + + # Tests emscripten_semaphore_init(), emscripten_semaphore_waitinf_acquire() and emscripten_semaphore_release() + @also_with_minimal_runtime + def test_wasm_worker_semaphore_waitinf_acquire(self): + self.btest(test_file('wasm_worker/semaphore_waitinf_acquire.c'), expected='0', args=['-sWASM_WORKERS']) + + # Tests emscripten_semaphore_try_acquire() on the main thread + @also_with_minimal_runtime + def test_wasm_worker_semaphore_try_acquire(self): + self.btest(test_file('wasm_worker/semaphore_try_acquire.c'), expected='0', args=['-sWASM_WORKERS']) + @no_firefox('no 4GB support yet') @require_v8 def test_zzz_zzz_4gb(self): diff --git a/tests/test_other.py b/tests/test_other.py index ada6c6cf6e77..9b7922bc4891 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -9137,6 +9137,7 @@ def test(args, closure, opt): 'hello_webgl2_wasm': ('hello_webgl2', False), 'hello_webgl2_wasm2js': ('hello_webgl2', True), 'math': ('math', False), + 'hello_wasm_worker': ('hello_wasm_worker', False, True), }) def test_minimal_runtime_code_size(self, test_name, js, compare_js_output=False): smallest_code_size_args = ['-sMINIMAL_RUNTIME=2', @@ -9177,13 +9178,15 @@ def test_minimal_runtime_code_size(self, test_name, js, compare_js_output=False) '-sUSES_DYNAMIC_ALLOC', '-lwebgl.js', '-sMODULARIZE'] hello_webgl2_sources = hello_webgl_sources + ['-sMAX_WEBGL_VERSION=2'] + hello_wasm_worker_sources = [test_file('wasm_worker/wasm_worker_code_size.c'), '-sWASM_WORKERS', '-sENVIRONMENT=web,worker'] sources = { 'hello_world': hello_world_sources, 'random_printf': random_printf_sources, 'hello_webgl': hello_webgl_sources, 'math': math_sources, - 'hello_webgl2': hello_webgl2_sources}[test_name] + 'hello_webgl2': hello_webgl2_sources, + 'hello_wasm_worker': hello_wasm_worker_sources}[test_name] def print_percent(actual, expected): if actual == expected: @@ -11787,5 +11790,15 @@ def test_shared_memory(self): self.do_runf(test_file('wasm_worker/shared_memory.c'), '0', emcc_args=[]) self.node_args += ['--experimental-wasm-threads', '--experimental-wasm-bulk-memory'] self.do_runf(test_file('wasm_worker/shared_memory.c'), '1', emcc_args=['-sSHARED_MEMORY']) -# self.do_runf(test_file('wasm_worker/shared_memory.c'), '1', emcc_args=['-sWASM_WORKERS']) + self.do_runf(test_file('wasm_worker/shared_memory.c'), '1', emcc_args=['-sWASM_WORKERS']) self.do_runf(test_file('wasm_worker/shared_memory.c'), '1', emcc_args=['-pthread']) + + # Tests C preprocessor flags with -sSHARED_MEMORY + @also_with_minimal_runtime + def test_shared_memory_preprocessor_flags(self): + self.run_process([EMCC, '-c', test_file('wasm_worker/shared_memory_preprocessor_flags.c'), '-sSHARED_MEMORY']) + + # Tests C preprocessor flags with -sWASM_WORKERS + @also_with_minimal_runtime + def test_wasm_worker_preprocessor_flags(self): + self.run_process([EMCC, '-c', test_file('wasm_worker/wasm_worker_preprocessor_flags.c'), '-sWASM_WORKERS']) diff --git a/tests/wasm_worker/c11__Thread_local.c b/tests/wasm_worker/c11__Thread_local.c new file mode 100644 index 000000000000..05719d8d76e9 --- /dev/null +++ b/tests/wasm_worker/c11__Thread_local.c @@ -0,0 +1,37 @@ +#include +#include +#include +#include + +_Thread_local int tls = 1; + +void main_thread_func() +{ + assert(!emscripten_current_thread_is_wasm_worker()); + EM_ASM(console.log($0), tls); +#ifdef REPORT_RESULT + REPORT_RESULT(tls); +#endif +} + +void worker_main() +{ + assert(emscripten_current_thread_is_wasm_worker()); + assert(tls != 42); + assert(tls != 0); + assert(tls == 1); + tls = 123456; // Try to write garbage data to the memory location. + emscripten_wasm_worker_post_function_v(0, main_thread_func); +} + +char stack[1024]; +char tlsBase[1024]; + +int main() +{ + EM_ASM(console.log($0), tls); + assert(!emscripten_current_thread_is_wasm_worker()); + tls = 42; + emscripten_wasm_worker_t worker = emscripten_create_wasm_worker_with_tls(stack, sizeof(stack), tlsBase, __builtin_wasm_tls_size()); + emscripten_wasm_worker_post_function_v(worker, worker_main); +} diff --git a/tests/wasm_worker/cancel_all_wait_asyncs.c b/tests/wasm_worker/cancel_all_wait_asyncs.c new file mode 100644 index 000000000000..4823cc165126 --- /dev/null +++ b/tests/wasm_worker/cancel_all_wait_asyncs.c @@ -0,0 +1,80 @@ +#include +#include +#include +#include + +// Test emscripten_atomic_cancel_all_wait_asyncs() function. + +EM_JS(void, console_log, (char* str), { + console.log(UTF8ToString(str)); +}); + +volatile int32_t addr = 1; + +EM_BOOL testSucceeded = 1; + +void asyncWaitFinishedShouldNotBeCalled(int32_t *ptr, uint32_t val, ATOMICS_WAIT_RESULT_T waitResult, void *userData) +{ + console_log("asyncWaitFinishedShouldNotBeCalled"); + testSucceeded = 0; + assert(0); // We should not reach here +} + +void asyncWaitFinishedShouldBeCalled(int32_t *ptr, uint32_t val, ATOMICS_WAIT_RESULT_T waitResult, void *userData) +{ + console_log("asyncWaitFinishedShouldBeCalled"); +#ifdef REPORT_RESULT + REPORT_RESULT(testSucceeded); +#endif +} + +int main() +{ + console_log("Async waiting on address should give a wait token"); + ATOMICS_WAIT_TOKEN_T ret = emscripten_atomic_wait_async((int32_t*)&addr, 1, asyncWaitFinishedShouldNotBeCalled, (void*)42, EMSCRIPTEN_WAIT_ASYNC_INFINITY); + assert(EMSCRIPTEN_IS_VALID_WAIT_TOKEN(ret)); + + console_log("Async waiting on address should give a wait token"); + ATOMICS_WAIT_TOKEN_T ret2 = emscripten_atomic_wait_async((int32_t*)&addr, 1, asyncWaitFinishedShouldNotBeCalled, (void*)42, EMSCRIPTEN_WAIT_ASYNC_INFINITY); + assert(EMSCRIPTEN_IS_VALID_WAIT_TOKEN(ret2)); + + console_log("Canceling all async waits should return the number of waits cancelled"); + int numCancelled = emscripten_atomic_cancel_all_wait_asyncs(); + assert(numCancelled == 2); + + console_log("Canceling an async wait that has already been cancelled should give an invalid param"); + EMSCRIPTEN_RESULT r = emscripten_atomic_cancel_wait_async(ret); + assert(r == EMSCRIPTEN_RESULT_INVALID_PARAM); + + console_log("Canceling an async wait that has already been cancelled should give an invalid param"); + r = emscripten_atomic_cancel_wait_async(ret2); + assert(r == EMSCRIPTEN_RESULT_INVALID_PARAM); + + console_log("Notifying an async wait should not trigger the callback function"); + int64_t numWoken = emscripten_wasm_notify((int32_t*)&addr, EMSCRIPTEN_NOTIFY_ALL_WAITERS); + + console_log("Notifying an async wait should return 0 threads woken"); + assert(numWoken == 0); + + addr = 2; + console_log("Notifying an async wait even after changed value should not trigger the callback function"); + numWoken = emscripten_wasm_notify((int32_t*)&addr, EMSCRIPTEN_NOTIFY_ALL_WAITERS); + + console_log("Notifying an async wait should return 0 threads woken"); + assert(numWoken == 0); + + console_log("Async waiting on address should give a wait token"); + ret = emscripten_atomic_wait_async((int32_t*)&addr, 2, asyncWaitFinishedShouldBeCalled, (void*)42, EMSCRIPTEN_WAIT_ASYNC_INFINITY); + assert(EMSCRIPTEN_IS_VALID_WAIT_TOKEN(ret)); + +#if 0 + console_log("Notifying an async wait without value changing should still trigger the callback"); + numWoken = emscripten_wasm_notify((int32_t*)&addr, EMSCRIPTEN_NOTIFY_ALL_WAITERS); + assert(numWoken == 1); +#else + // TODO: Switch to the above test instead after the Atomics.waitAsync() polyfill is dropped. + addr = 3; + console_log("Notifying an async wait after value changing should trigger the callback"); + numWoken = emscripten_wasm_notify((int32_t*)&addr, EMSCRIPTEN_NOTIFY_ALL_WAITERS); +#endif +} diff --git a/tests/wasm_worker/cancel_all_wait_asyncs_at_address.c b/tests/wasm_worker/cancel_all_wait_asyncs_at_address.c new file mode 100644 index 000000000000..c9192e796a34 --- /dev/null +++ b/tests/wasm_worker/cancel_all_wait_asyncs_at_address.c @@ -0,0 +1,84 @@ +#include +#include +#include +#include + +// Test emscripten_atomic_cancel_all_wait_asyncs() function. + +EM_JS(void, console_log, (char* str), { + console.log(UTF8ToString(str)); +}); + +volatile int32_t addr = 1; + +EM_BOOL testSucceeded = 1; + +void asyncWaitFinishedShouldNotBeCalled(int32_t *ptr, uint32_t val, ATOMICS_WAIT_RESULT_T waitResult, void *userData) +{ + console_log("asyncWaitFinishedShouldNotBeCalled"); + testSucceeded = 0; + assert(0); // We should not reach here +} + +void asyncWaitFinishedShouldBeCalled(int32_t *ptr, uint32_t val, ATOMICS_WAIT_RESULT_T waitResult, void *userData) +{ + console_log("asyncWaitFinishedShouldBeCalled"); +#ifdef REPORT_RESULT + REPORT_RESULT(testSucceeded); +#endif +} + +int main() +{ + console_log("Async waiting on address should give a wait token"); + ATOMICS_WAIT_TOKEN_T ret = emscripten_atomic_wait_async((int32_t*)&addr, 1, asyncWaitFinishedShouldNotBeCalled, (void*)42, EMSCRIPTEN_WAIT_ASYNC_INFINITY); + assert(EMSCRIPTEN_IS_VALID_WAIT_TOKEN(ret)); + + console_log("Async waiting on address should give a wait token"); + ATOMICS_WAIT_TOKEN_T ret2 = emscripten_atomic_wait_async((int32_t*)&addr, 1, asyncWaitFinishedShouldNotBeCalled, (void*)42, EMSCRIPTEN_WAIT_ASYNC_INFINITY); + assert(EMSCRIPTEN_IS_VALID_WAIT_TOKEN(ret2)); + + console_log("Canceling all async waits at wait address should return the number of waits cancelled"); + int numCancelled = emscripten_atomic_cancel_all_wait_asyncs_at_address((int32_t*)&addr); + assert(numCancelled == 2); + + console_log("Canceling all async waits at some other address should return 0"); + numCancelled = emscripten_atomic_cancel_all_wait_asyncs_at_address((int32_t*)0xbad); + assert(numCancelled == 0); + + console_log("Canceling an async wait that has already been cancelled should give an invalid param"); + EMSCRIPTEN_RESULT r = emscripten_atomic_cancel_wait_async(ret); + assert(r == EMSCRIPTEN_RESULT_INVALID_PARAM); + + console_log("Canceling an async wait that has already been cancelled should give an invalid param"); + r = emscripten_atomic_cancel_wait_async(ret2); + assert(r == EMSCRIPTEN_RESULT_INVALID_PARAM); + + console_log("Notifying an async wait should not trigger the callback function"); + int64_t numWoken = emscripten_wasm_notify((int32_t*)&addr, EMSCRIPTEN_NOTIFY_ALL_WAITERS); + + console_log("Notifying an async wait should return 0 threads woken"); + assert(numWoken == 0); + + addr = 2; + console_log("Notifying an async wait even after changed value should not trigger the callback function"); + numWoken = emscripten_wasm_notify((int32_t*)&addr, EMSCRIPTEN_NOTIFY_ALL_WAITERS); + + console_log("Notifying an async wait should return 0 threads woken"); + assert(numWoken == 0); + + console_log("Async waiting on address should give a wait token"); + ret = emscripten_atomic_wait_async((int32_t*)&addr, 2, asyncWaitFinishedShouldBeCalled, (void*)42, EMSCRIPTEN_WAIT_ASYNC_INFINITY); + assert(EMSCRIPTEN_IS_VALID_WAIT_TOKEN(ret)); + +#if 0 + console_log("Notifying an async wait without value changing should still trigger the callback"); + numWoken = emscripten_wasm_notify((int32_t*)&addr, EMSCRIPTEN_NOTIFY_ALL_WAITERS); + assert(numWoken == 1); +#else + // TODO: Switch to the above test instead after the Atomics.waitAsync() polyfill is dropped. + addr = 3; + console_log("Notifying an async wait after value changing should trigger the callback"); + numWoken = emscripten_wasm_notify((int32_t*)&addr, EMSCRIPTEN_NOTIFY_ALL_WAITERS); +#endif +} diff --git a/tests/wasm_worker/cancel_wait_async.c b/tests/wasm_worker/cancel_wait_async.c new file mode 100644 index 000000000000..c2822630791b --- /dev/null +++ b/tests/wasm_worker/cancel_wait_async.c @@ -0,0 +1,72 @@ +#include +#include +#include +#include + +// Test emscripten_cancel_wait_async() function. + +EM_JS(void, console_log, (char* str), { + console.log(UTF8ToString(str)); +}); + +volatile int32_t addr = 1; + +EM_BOOL testSucceeded = 1; + +void asyncWaitFinishedShouldNotBeCalled(int32_t *ptr, uint32_t val, ATOMICS_WAIT_RESULT_T waitResult, void *userData) +{ + console_log("asyncWaitFinishedShouldNotBeCalled"); + testSucceeded = 0; + assert(0); // We should not reach here +} + +void asyncWaitFinishedShouldBeCalled(int32_t *ptr, uint32_t val, ATOMICS_WAIT_RESULT_T waitResult, void *userData) +{ + console_log("asyncWaitFinishedShouldBeCalled"); +#ifdef REPORT_RESULT + REPORT_RESULT(testSucceeded); +#endif +} + +int main() +{ + console_log("Async waiting on address should give a wait token"); + ATOMICS_WAIT_TOKEN_T ret = emscripten_atomic_wait_async((int32_t*)&addr, 1, asyncWaitFinishedShouldNotBeCalled, (void*)42, EMSCRIPTEN_WAIT_ASYNC_INFINITY); + assert(EMSCRIPTEN_IS_VALID_WAIT_TOKEN(ret)); + + console_log("Canceling an async wait should succeed"); + EMSCRIPTEN_RESULT r = emscripten_atomic_cancel_wait_async(ret); + assert(r == EMSCRIPTEN_RESULT_SUCCESS); + + console_log("Canceling an async wait a second time should give invalid param"); + r = emscripten_atomic_cancel_wait_async(ret); + assert(r == EMSCRIPTEN_RESULT_INVALID_PARAM); + + console_log("Notifying an async wait should not trigger the callback function"); + int64_t numWoken = emscripten_wasm_notify((int32_t*)&addr, EMSCRIPTEN_NOTIFY_ALL_WAITERS); + + console_log("Notifying an async wait should return 0 threads woken"); + assert(numWoken == 0); + + addr = 2; + console_log("Notifying an async wait even after changed value should not trigger the callback function"); + numWoken = emscripten_wasm_notify((int32_t*)&addr, EMSCRIPTEN_NOTIFY_ALL_WAITERS); + + console_log("Notifying an async wait should return 0 threads woken"); + assert(numWoken == 0); + + console_log("Async waiting on address should give a wait token"); + ret = emscripten_atomic_wait_async((int32_t*)&addr, 2, asyncWaitFinishedShouldBeCalled, (void*)42, EMSCRIPTEN_WAIT_ASYNC_INFINITY); + assert(EMSCRIPTEN_IS_VALID_WAIT_TOKEN(ret)); + +#if 0 + console_log("Notifying an async wait without value changing should still trigger the callback"); + numWoken = emscripten_wasm_notify((int32_t*)&addr, EMSCRIPTEN_NOTIFY_ALL_WAITERS); + assert(numWoken == 1); +#else + // TODO: Switch to the above test instead after the Atomics.waitAsync() polyfill is dropped. + addr = 3; + console_log("Notifying an async wait after value changing should trigger the callback"); + numWoken = emscripten_wasm_notify((int32_t*)&addr, EMSCRIPTEN_NOTIFY_ALL_WAITERS); +#endif +} diff --git a/tests/wasm_worker/cpp11_thread_local.cpp b/tests/wasm_worker/cpp11_thread_local.cpp new file mode 100644 index 000000000000..3dcc01bdd1c3 --- /dev/null +++ b/tests/wasm_worker/cpp11_thread_local.cpp @@ -0,0 +1,36 @@ +#include +#include +#include + +thread_local int tls = 1; + +void main_thread_func() +{ + assert(!emscripten_current_thread_is_wasm_worker()); + EM_ASM(console.log($0), tls); +#ifdef REPORT_RESULT + REPORT_RESULT(tls); +#endif +} + +void worker_main() +{ + assert(emscripten_current_thread_is_wasm_worker()); + assert(tls != 42); + assert(tls != 0); + assert(tls == 1); + tls = 123456; // Try to write garbage data to the memory location. + emscripten_wasm_worker_post_function_v(0, main_thread_func); +} + +char stack[1024]; +char tlsBase[1024]; + +int main() +{ + EM_ASM(console.log($0), tls); + assert(!emscripten_current_thread_is_wasm_worker()); + tls = 42; + emscripten_wasm_worker_t worker = emscripten_create_wasm_worker_with_tls(stack, sizeof(stack), tlsBase, __builtin_wasm_tls_size()); + emscripten_wasm_worker_post_function_v(worker, worker_main); +} diff --git a/tests/wasm_worker/gcc___Thread.c b/tests/wasm_worker/gcc___Thread.c new file mode 100644 index 000000000000..e9808b63e76a --- /dev/null +++ b/tests/wasm_worker/gcc___Thread.c @@ -0,0 +1,37 @@ +#include +#include +#include +#include + +__thread int tls = 1; + +void main_thread_func() +{ + assert(!emscripten_current_thread_is_wasm_worker()); + EM_ASM(console.log($0), tls); +#ifdef REPORT_RESULT + REPORT_RESULT(tls); +#endif +} + +void worker_main() +{ + assert(emscripten_current_thread_is_wasm_worker()); + assert(tls != 42); + assert(tls != 0); + assert(tls == 1); + tls = 123456; // Try to write garbage data to the memory location. + emscripten_wasm_worker_post_function_v(0, main_thread_func); +} + +char stack[1024]; +char tlsBase[1024]; + +int main() +{ + EM_ASM(console.log($0), tls); + assert(!emscripten_current_thread_is_wasm_worker()); + tls = 42; + emscripten_wasm_worker_t worker = emscripten_create_wasm_worker_with_tls(stack, sizeof(stack), tlsBase, __builtin_wasm_tls_size()); + emscripten_wasm_worker_post_function_v(worker, worker_main); +} diff --git a/tests/wasm_worker/hardware_concurrency_is_lock_free.c b/tests/wasm_worker/hardware_concurrency_is_lock_free.c new file mode 100644 index 000000000000..39e26ac1650d --- /dev/null +++ b/tests/wasm_worker/hardware_concurrency_is_lock_free.c @@ -0,0 +1,35 @@ +#include +#include +#include + +// Test emscripten_navigator_hardware_concurrency() and emscripten_atomics_is_lock_free() functions + +void test() +{ + // Assume that test suite does have navigator.hardwareConcurrency. + assert(emscripten_navigator_hardware_concurrency() >= 2); + assert(emscripten_atomics_is_lock_free(1)); + assert(emscripten_atomics_is_lock_free(2)); + assert(emscripten_atomics_is_lock_free(4)); + // Chrome is buggy, see + // https://bugs.chromium.org/p/chromium/issues/detail?id=1167449 + //assert(emscripten_atomics_is_lock_free(8)); + assert(!emscripten_atomics_is_lock_free(31)); +} + +void worker_main() +{ + test(); +#ifdef REPORT_RESULT + REPORT_RESULT(0); +#endif +} + +char stack[1024]; + +int main() +{ + test(); + emscripten_wasm_worker_t worker = emscripten_create_wasm_worker_no_tls(stack, sizeof(stack)); + emscripten_wasm_worker_post_function_v(worker, worker_main); +} diff --git a/tests/wasm_worker/hello_wasm_worker.c b/tests/wasm_worker/hello_wasm_worker.c new file mode 100644 index 000000000000..eeeaba390a95 --- /dev/null +++ b/tests/wasm_worker/hello_wasm_worker.c @@ -0,0 +1,18 @@ +#include +#include + +// This is the code example in site/source/docs/api_reference/wasm_workers.rst + +void run_in_worker() +{ + printf("Hello from wasm worker!\n"); +#ifdef REPORT_RESULT + REPORT_RESULT(0); +#endif +} + +int main() +{ + emscripten_wasm_worker_t worker = emscripten_malloc_wasm_worker(/*stack size: */1024); + emscripten_wasm_worker_post_function_v(worker, run_in_worker); +} diff --git a/tests/wasm_worker/lock_async_acquire.c b/tests/wasm_worker/lock_async_acquire.c new file mode 100644 index 000000000000..1a8e876545bc --- /dev/null +++ b/tests/wasm_worker/lock_async_acquire.c @@ -0,0 +1,108 @@ +#include +#include +#include +#include +#include + +// Tests emscripten_lock_async_acquire() function. + +EM_JS(void, console_log, (char* str), { + console.log(UTF8ToString(str)); +}); + +emscripten_lock_t lock = EMSCRIPTEN_LOCK_T_STATIC_INITIALIZER; + +// Two shared variables, always a delta distance of one from each other. +volatile int sharedState0 = 0; +volatile int sharedState1 = 1; + +EM_BOOL testFinished = EM_FALSE; + +int numTimesMainThreadAcquiredLock = 0; +int numTimesWasmWorkerAcquiredLock = 0; + +void work() +{ +// console_log("work"); + volatile int x = sharedState0; + volatile int y = sharedState1; + assert(x == y+1 || y == x+1); + + if (emscripten_current_thread_is_wasm_worker()) + ++numTimesWasmWorkerAcquiredLock; + else + ++numTimesMainThreadAcquiredLock; + + if (x < y) + { + x = y + 1; + if (emscripten_current_thread_is_wasm_worker()) + emscripten_wasm_worker_sleep(/*nsecs=*/(rand()%100000)); + sharedState0 = x; + } + else + { + y = x + 1; + if (emscripten_current_thread_is_wasm_worker()) + emscripten_wasm_worker_sleep(/*nsecs=*/(rand()%100000)); + sharedState1 = y; + + if (y > 100 && numTimesMainThreadAcquiredLock && numTimesWasmWorkerAcquiredLock) + { + if (!testFinished) + { + console_log("test finished"); +#ifdef REPORT_RESULT + REPORT_RESULT(0); +#endif + } + testFinished = EM_TRUE; + } + } +} + +void schedule_work(void *userData); + +void lock_async_acquired(volatile void *addr, uint32_t val, ATOMICS_WAIT_RESULT_T waitResult, void *userData) +{ +// console_log("async lock acquired"); + assert(addr == &lock); + assert(val == 0 || val == 1); + assert(waitResult == ATOMICS_WAIT_OK); + assert(userData == (void*)42); + work(); + emscripten_lock_release(&lock); + + if (!testFinished) + emscripten_set_timeout(schedule_work, 10, 0); +} + +void schedule_work(void *userData) +{ + if (emscripten_current_thread_is_wasm_worker() && emscripten_random() > 0.5) + { + emscripten_lock_waitinf_acquire(&lock); +// console_log("sync lock acquired"); + work(); + emscripten_lock_release(&lock); + if (!testFinished) + emscripten_set_timeout(schedule_work, 0, 0); + } + else + { + emscripten_lock_async_acquire(&lock, lock_async_acquired, (void*)42, EMSCRIPTEN_WAIT_ASYNC_INFINITY); + } +} + +int main() +{ +#define NUM_THREADS 10 + for(int i = 0; i < NUM_THREADS; ++i) + { + void *stack = memalign(16, 1024); + emscripten_wasm_worker_t worker = emscripten_create_wasm_worker_no_tls(stack, 1024); + emscripten_wasm_worker_post_function_vi(worker, (void (*)(int))schedule_work, 0); + } + + schedule_work(0); +} diff --git a/tests/wasm_worker/lock_busyspin_wait_acquire.c b/tests/wasm_worker/lock_busyspin_wait_acquire.c new file mode 100644 index 000000000000..6a250a50f67e --- /dev/null +++ b/tests/wasm_worker/lock_busyspin_wait_acquire.c @@ -0,0 +1,50 @@ +#include +#include +#include +#include +#include + +// Tests emscripten_lock_busyspin_wait_acquire(). + +emscripten_lock_t lock = EMSCRIPTEN_LOCK_T_STATIC_INITIALIZER; + +void test() +{ + EM_BOOL success = emscripten_lock_busyspin_wait_acquire(&lock, 0); // Expect no contention on free lock. + assert(success == EM_TRUE); + + double t0 = emscripten_performance_now(); + success = emscripten_lock_busyspin_wait_acquire(&lock, 0); // We already have the lock, and emscripten_lock is not recursive, so this should fail. + double t1 = emscripten_performance_now(); + assert(!success); + assert(t1 - t0 < 25); // Shouldn't have taken too much time to try the lock. + + success = emscripten_lock_try_acquire(&lock); + assert(!success); // We already have the lock. + + t0 = emscripten_performance_now(); + success = emscripten_lock_busyspin_wait_acquire(&lock, 1000.0); // We already have the lock, and emscripten_lock is not recursive, so this should fail. + t1 = emscripten_performance_now(); + assert(!success); + assert(t1 - t0 >= 900); // We should have waited for the requested duration for the lock.. apply some slack since timing can have some noise. + + emscripten_lock_release(&lock); +} + +void worker_main() +{ + test(); +#ifdef REPORT_RESULT + REPORT_RESULT(0); +#endif +} + +char stack[1024]; + +int main() +{ + test(); + + emscripten_wasm_worker_t worker = emscripten_create_wasm_worker_no_tls(stack, sizeof(stack)); + emscripten_wasm_worker_post_function_v(worker, worker_main); +} diff --git a/tests/wasm_worker/lock_busyspin_waitinf_acquire.c b/tests/wasm_worker/lock_busyspin_waitinf_acquire.c new file mode 100644 index 000000000000..a8459ab467f8 --- /dev/null +++ b/tests/wasm_worker/lock_busyspin_waitinf_acquire.c @@ -0,0 +1,42 @@ +#include +#include +#include +#include +#include + +// Tests emscripten_lock_busyspin_waitinf_acquire(). + +emscripten_lock_t lock = EMSCRIPTEN_LOCK_T_STATIC_INITIALIZER; + +volatile int sharedState = 0; + +void worker_main() +{ + emscripten_lock_busyspin_waitinf_acquire(&lock); + emscripten_atomic_add_u32((void*)&sharedState, 1); +#ifdef REPORT_RESULT + REPORT_RESULT(sharedState); +#endif +} + +char stack[1024]; + +void releaseLock(void *userData) +{ + emscripten_atomic_sub_u32((void*)&sharedState, 1); + emscripten_lock_release(&lock); +} + +int main() +{ + // Acquire the lock at startup. + emscripten_lock_busyspin_waitinf_acquire(&lock); + emscripten_atomic_add_u32((void*)&sharedState, 1); + + // Spawn a Worker to try to take the lock. It will succeed only after releaseLock() + // gets called. + emscripten_wasm_worker_t worker = emscripten_create_wasm_worker_no_tls(stack, sizeof(stack)); + emscripten_wasm_worker_post_function_v(worker, worker_main); + + emscripten_set_timeout(releaseLock, 1000, 0); +} diff --git a/tests/wasm_worker/lock_wait_acquire.c b/tests/wasm_worker/lock_wait_acquire.c new file mode 100644 index 000000000000..6604a1220d23 --- /dev/null +++ b/tests/wasm_worker/lock_wait_acquire.c @@ -0,0 +1,47 @@ +#include +#include +#include +#include +#include + +// Tests emscripten_lock_wait_acquire() and emscripten_lock_try_acquire() in Worker. + +emscripten_lock_t lock = EMSCRIPTEN_LOCK_T_STATIC_INITIALIZER; + +void worker_main() +{ + EM_BOOL success = emscripten_lock_wait_acquire(&lock, 0); // Expect no contention on free lock. + assert(success == EM_TRUE); + + double t0 = emscripten_performance_now(); + success = emscripten_lock_wait_acquire(&lock, 0); // We already have the lock, and emscripten_lock is not recursive, so this should fail. + double t1 = emscripten_performance_now(); + assert(!success); + assert(t1 - t0 < 25); // Shouldn't have taken too much time to try the lock. + + success = emscripten_lock_try_acquire(&lock); + assert(!success); // We already have the lock. + + t0 = emscripten_performance_now(); + success = emscripten_lock_wait_acquire(&lock, 1000 * 1000000ull); // We already have the lock, and emscripten_lock is not recursive, so this should fail. + t1 = emscripten_performance_now(); + assert(!success); + assert(t1 - t0 >= 1000); // We should have waited for the requested duration for the lock. + + emscripten_lock_release(&lock); + + success = emscripten_lock_try_acquire(&lock); + assert(success); + +#ifdef REPORT_RESULT + REPORT_RESULT(0); +#endif +} + +char stack[1024]; + +int main() +{ + emscripten_wasm_worker_t worker = emscripten_create_wasm_worker_no_tls(stack, sizeof(stack)); + emscripten_wasm_worker_post_function_v(worker, worker_main); +} diff --git a/tests/wasm_worker/lock_wait_acquire2.c b/tests/wasm_worker/lock_wait_acquire2.c new file mode 100644 index 000000000000..10f00d3d99a8 --- /dev/null +++ b/tests/wasm_worker/lock_wait_acquire2.c @@ -0,0 +1,57 @@ +#include +#include +#include +#include +#include + +// Tests emscripten_lock_wait_acquire() between two Wasm Workers. + +EM_JS(void, console_log, (char* str), { + console.log(UTF8ToString(str)); +}); + +emscripten_lock_t lock = EMSCRIPTEN_LOCK_T_STATIC_INITIALIZER; + +void worker1_main() +{ + console_log("worker1 main try_acquiring lock"); + EM_BOOL success = emscripten_lock_try_acquire(&lock); // Expect no contention on free lock. + console_log("worker1 try_acquire lock finished"); + assert(success); + console_log("worker1 try_acquire lock success, sleeping 1000 msecs"); + emscripten_wasm_worker_sleep(1000 * 1000000ull); + + console_log("worker1 slept 1000 msecs, releasing lock"); + emscripten_lock_release(&lock); + console_log("worker1 released lock"); +} + +void worker2_main() +{ + console_log("worker2 main sleeping 500 msecs"); + emscripten_wasm_worker_sleep(500 * 1000000ull); + console_log("worker2 slept 500 msecs, try_acquiring lock"); + EM_BOOL success = emscripten_lock_try_acquire(&lock); // At this time, the other thread should have the lock. + console_log("worker2 try_acquire lock finished"); + assert(!success); + + // Wait enough time to cover over the time that the other thread held the lock. + success = emscripten_lock_wait_acquire(&lock, 2000 * 1000000ull); + console_log("worker2 wait_acquired lock"); + assert(success); + +#ifdef REPORT_RESULT + REPORT_RESULT(0); +#endif +} + +char stack1[1024]; +char stack2[1024]; + +int main() +{ + emscripten_wasm_worker_t worker1 = emscripten_create_wasm_worker_no_tls(stack1, sizeof(stack1)); + emscripten_wasm_worker_t worker2 = emscripten_create_wasm_worker_no_tls(stack2, sizeof(stack2)); + emscripten_wasm_worker_post_function_v(worker1, worker1_main); + emscripten_wasm_worker_post_function_v(worker2, worker2_main); +} diff --git a/tests/wasm_worker/lock_waitinf_acquire.c b/tests/wasm_worker/lock_waitinf_acquire.c new file mode 100644 index 000000000000..c7afb93d0768 --- /dev/null +++ b/tests/wasm_worker/lock_waitinf_acquire.c @@ -0,0 +1,75 @@ +#include +#include +#include +#include +#include +#include + +// Tests emscripten_lock_init(), emscripten_lock_waitinf_acquire() and emscripten_lock_release() + +emscripten_lock_t lock = (emscripten_lock_t)12345315; // initialize with garbage + +// Two shared variables, always a delta distance of one from each other. +volatile int sharedState0 = 0; +volatile int sharedState1 = 1; + +volatile int numWorkersAlive = 0; + +void test_ended() +{ + EM_ASM(console.log(`Worker ${$0} last thread to finish. Reporting test end with sharedState0=${$1}, sharedState1=${$2}`), emscripten_wasm_worker_self_id(), sharedState0, sharedState1); + assert(sharedState0 == sharedState1 + 1 || sharedState1 == sharedState0 + 1); +#ifdef REPORT_RESULT + REPORT_RESULT(sharedState0); +#endif +} + +void worker_main() +{ + EM_ASM(console.log(`Worker ${$0} running...`), emscripten_wasm_worker_self_id()); + // Create contention on the lock from each thread, and stress the shared state + // in a racy way that would show a breakage if the lock is not watertight. + for(int i = 0; i < 1000; ++i) + { + emscripten_lock_waitinf_acquire(&lock); + volatile int x = sharedState0; + volatile int y = sharedState1; + assert(x == y+1 || y == x+1); + if (x < y) + { + x = y + 1; + emscripten_wasm_worker_sleep(/*nsecs=*/((uint64_t)(emscripten_math_random()*1000))); + sharedState0 = x; + } + else + { + y = x + 1; + emscripten_wasm_worker_sleep(/*nsecs=*/((uint64_t)(emscripten_math_random()*1000))); + sharedState1 = y; + } + emscripten_lock_release(&lock); + } + + EM_ASM(console.log(`Worker ${$0} finished.`), emscripten_wasm_worker_self_id()); + + // Are we the last thread to finish? If so, test has ended. + uint32_t v = emscripten_atomic_sub_u32((void*)&numWorkersAlive, 1); + if (v == 1) + { + test_ended(); + } +} + +int main() +{ + emscripten_lock_init(&lock); + +#define NUM_THREADS 4 + numWorkersAlive = NUM_THREADS; + for(int i = 0; i < NUM_THREADS; ++i) + { + void *stack = memalign(16, 1024); + emscripten_wasm_worker_t worker = emscripten_create_wasm_worker_no_tls(stack, 1024); + emscripten_wasm_worker_post_function_v(worker, worker_main); + } +} diff --git a/tests/wasm_worker/malloc_wasm_worker.c b/tests/wasm_worker/malloc_wasm_worker.c new file mode 100644 index 000000000000..10f5a8fda033 --- /dev/null +++ b/tests/wasm_worker/malloc_wasm_worker.c @@ -0,0 +1,25 @@ +#include +#include +#include + +// Test emscripten_malloc_wasm_worker() and emscripten_current_thread_is_wasm_worker() functions + +EM_JS(void, console_log, (char* str), { + console.log(UTF8ToString(str)); +}); + +void worker_main() +{ + console_log("Hello from wasm worker!"); + assert(emscripten_current_thread_is_wasm_worker()); +#ifdef REPORT_RESULT + REPORT_RESULT(0); +#endif +} + +int main() +{ + assert(!emscripten_current_thread_is_wasm_worker()); + emscripten_wasm_worker_t worker = emscripten_malloc_wasm_worker(/*stack size: */1024); + emscripten_wasm_worker_post_function_v(worker, worker_main); +} diff --git a/tests/wasm_worker/no_proxied_js_functions.c b/tests/wasm_worker/no_proxied_js_functions.c new file mode 100644 index 000000000000..afd2b50f40dc --- /dev/null +++ b/tests/wasm_worker/no_proxied_js_functions.c @@ -0,0 +1,52 @@ +#include +#include +#include + +// Test that proxied JS functions cannot be called in Wasm Workers in ASSERTIONS builds. + +void proxied_js_function(void); + +int should_throw(void(*func)()) +{ + int threw = EM_ASM_INT({ + // Patch over assert() so that it does not abort execution on assert failure, but instead + // throws a catchable exception. + assert = function(condition, text) { + if (!condition) { + throw 'Assertion failed' + (text ? ": " + text : ""); + } + }; + + try { + dynCall('v', $0); + } catch(e) { + console.error('Threw an exception like expected: ' + e); + return 1; + } + console.error('Function was expected to throw, but did not!'); + return 0; + }, (int)func); + return threw; +} + +void test() +{ + proxied_js_function(); +} + +void worker_main() +{ + assert(should_throw(test)); +#ifdef REPORT_RESULT + REPORT_RESULT(0); +#endif +} + +char stack[1024]; + +int main() +{ + proxied_js_function(); + emscripten_wasm_worker_t worker = emscripten_create_wasm_worker_no_tls(stack, sizeof(stack)); + emscripten_wasm_worker_post_function_v(worker, worker_main); +} diff --git a/tests/wasm_worker/no_proxied_js_functions.js b/tests/wasm_worker/no_proxied_js_functions.js new file mode 100644 index 000000000000..d2b827d0f62a --- /dev/null +++ b/tests/wasm_worker/no_proxied_js_functions.js @@ -0,0 +1,5 @@ +mergeInto(LibraryManager.library, { + proxied_js_function__proxy: 'sync', + proxied_js_function: function() { + } +}); diff --git a/tests/wasm_worker/post_function.c b/tests/wasm_worker/post_function.c new file mode 100644 index 000000000000..23899cb63118 --- /dev/null +++ b/tests/wasm_worker/post_function.c @@ -0,0 +1,107 @@ +#include +#include +#include + +// Test emscripten_wasm_worker_post_function_*() API + +EM_JS(void, console_log, (char* str), { + console.log(UTF8ToString(str)); +}); + +volatile int success = 0; + +void v() +{ + console_log("v"); + ++success; +} + +void vi(int i) +{ + console_log("vi"); + assert(i == 1); + ++success; +} + +void vii(int i, int j) +{ + console_log("vii"); + assert(i == 2); + assert(j == 3); + ++success; +} + +void viii(int i, int j, int k) +{ + console_log("viii"); + assert(i == 4); + assert(j == 5); + assert(k == 6); + ++success; +} + +void vd(double i) +{ + console_log("vd"); + assert(i == 1.5); + ++success; +} + +void vdd(double i, double j) +{ + console_log("vdd"); + assert(i == 2.5); + assert(j == 3.5); + ++success; +} + +void vddd(double i, double j, double k) +{ + console_log("vddd"); + assert(i == 4.5); + assert(j == 5.5); + assert(k == 6.5); + ++success; +} + +void viiiiiidddddd(int a, int b, int c, int d, int e, int f, double g, double h, double i, double j, double k, double l) +{ + console_log("viiiiiidddddd"); + assert(a == 10); + assert(b == 11); + assert(c == 12); + assert(d == 13); + assert(e == 14); + assert(f == 15); + assert(g == 16.5); + assert(h == 17.5); + assert(i == 18.5); + assert(j == 19.5); + assert(k == 20.5); + assert(l == 21.5); + ++success; +} + +void test_finished() +{ +#ifdef REPORT_RESULT + REPORT_RESULT(success); +#endif +} + +char stack[1024]; + +int main() +{ + assert(!emscripten_current_thread_is_wasm_worker()); + emscripten_wasm_worker_t worker = emscripten_create_wasm_worker_no_tls(stack, sizeof(stack)); + emscripten_wasm_worker_post_function_v(worker, v); + emscripten_wasm_worker_post_function_vi(worker, vi, 1); + emscripten_wasm_worker_post_function_vii(worker, vii, 2, 3); + emscripten_wasm_worker_post_function_viii(worker, viii, 4, 5, 6); + emscripten_wasm_worker_post_function_vd(worker, vd, 1.5); + emscripten_wasm_worker_post_function_vdd(worker, vdd, 2.5, 3.5); + emscripten_wasm_worker_post_function_vddd(worker, vddd, 4.5, 5.5, 6.5); + emscripten_wasm_worker_post_function_sig(worker, viiiiiidddddd, "iiiiiidddddd", 10, 11, 12, 13, 14, 15, 16.5, 17.5, 18.5, 19.5, 20.5, 21.5); + emscripten_wasm_worker_post_function_v(worker, test_finished); +} diff --git a/tests/wasm_worker/post_function_to_main_thread.c b/tests/wasm_worker/post_function_to_main_thread.c new file mode 100644 index 000000000000..043a89cddb93 --- /dev/null +++ b/tests/wasm_worker/post_function_to_main_thread.c @@ -0,0 +1,36 @@ +#include +#include +#include + +// Test emscripten_wasm_worker_post_function_*() API and EMSCRIPTEN_WASM_WORKER_ID_PARENT +// to send a message back from Worker to its parent thread. + +EM_JS(void, console_log, (char* str), { + console.log(UTF8ToString(str)); +}); + +void test_success(int i, double d) +{ + console_log("test_success"); + assert(!emscripten_current_thread_is_wasm_worker()); + assert(i == 10); + assert(d == 0.5); +#ifdef REPORT_RESULT + REPORT_RESULT(i); +#endif +} + +void worker_main() +{ + console_log("worker_main"); + assert(emscripten_current_thread_is_wasm_worker()); + emscripten_wasm_worker_post_function_sig(EMSCRIPTEN_WASM_WORKER_ID_PARENT, test_success, "id", 10, 0.5); +} + +char stack[1024]; + +int main() +{ + emscripten_wasm_worker_t worker = emscripten_create_wasm_worker_no_tls(stack, sizeof(stack)); + emscripten_wasm_worker_post_function_v(worker, worker_main); +} diff --git a/tests/wasm_worker/semaphore_try_acquire.c b/tests/wasm_worker/semaphore_try_acquire.c new file mode 100644 index 000000000000..76f840e491c3 --- /dev/null +++ b/tests/wasm_worker/semaphore_try_acquire.c @@ -0,0 +1,41 @@ +#include +#include +#include +#include +#include + +// Tests emscripten_semaphore_try_acquire() on the main thread + +EM_JS(void, console_log, (char* str), { + console.log(UTF8ToString(str)); +}); + +emscripten_semaphore_t unavailable = EMSCRIPTEN_SEMAPHORE_T_STATIC_INITIALIZER(0); +emscripten_semaphore_t available = EMSCRIPTEN_SEMAPHORE_T_STATIC_INITIALIZER(1); + +int main() +{ + console_log("try_acquiring unavailable semaphore should fail"); + int idx = emscripten_semaphore_try_acquire(&unavailable, 1); + assert(idx == -1); + + console_log("try_acquiring too many resources from an available semaphore should fail"); + idx = emscripten_semaphore_try_acquire(&available, 2); + assert(idx == -1); + + console_log("try_acquiring a resource from an available semaphore should succeed"); + idx = emscripten_semaphore_try_acquire(&available, 1); + assert(idx == 0); + + console_log("releasing semaphore resources on main thread should succeed"); + idx = emscripten_semaphore_release(&available, 10); + assert(idx == 0); + + console_log("try_acquiring multiple resources from an available semaphore should succeed"); + idx = emscripten_semaphore_try_acquire(&available, 9); + assert(idx == 1); + +#ifdef REPORT_RESULT + REPORT_RESULT(0); +#endif +} diff --git a/tests/wasm_worker/semaphore_waitinf_acquire.c b/tests/wasm_worker/semaphore_waitinf_acquire.c new file mode 100644 index 000000000000..893fd4af8ab3 --- /dev/null +++ b/tests/wasm_worker/semaphore_waitinf_acquire.c @@ -0,0 +1,91 @@ +#include +#include +#include +#include +#include + +// Tests emscripten_semaphore_init(), emscripten_semaphore_waitinf_acquire() and emscripten_semaphore_release() + +EM_JS(void, console_log, (char* str), { + console.log(UTF8ToString(str)); +}); + +emscripten_semaphore_t threadsWaiting = (emscripten_semaphore_t)12345315; // initialize with garbage +emscripten_semaphore_t threadsRunning = EMSCRIPTEN_SEMAPHORE_T_STATIC_INITIALIZER(0); // initialize with static initializer +emscripten_semaphore_t threadsCompleted = EMSCRIPTEN_SEMAPHORE_T_STATIC_INITIALIZER(0); + +int threadCounter = 0; + +void worker_main() +{ + console_log("worker_main"); + + // Increment semaphore to mark that this thread is waiting for a signal from control thread to start. + emscripten_semaphore_release(&threadsWaiting, 1); + + // Acquire thread run semaphore once main thread has given this thread a go signal. + console_log("worker_main: waiting for thread run signal"); + emscripten_semaphore_waitinf_acquire(&threadsRunning, 1); + + // Do heavy computation: + console_log("worker_main: incrementing work"); + emscripten_atomic_add_u32((void*)&threadCounter, 1); + + // Increment semaphore to signal that this thread has finished. + console_log("worker_main: thread completed"); + emscripten_semaphore_release(&threadsCompleted, 1); +} + +void control_thread() +{ + // Wait until we have three threads available to start running. + console_log("control_thread: waiting for three threads to complete loading"); + emscripten_semaphore_waitinf_acquire(&threadsWaiting, 3); + + // Set the three waiting threads to run simultaneously. + assert(threadCounter == 0); + console_log("control_thread: release three threads to run"); + emscripten_semaphore_release(&threadsRunning, 3); + + // Wait until we have 3 threads completed their run. + console_log("control_thread: waiting for three threads to complete"); + emscripten_semaphore_waitinf_acquire(&threadsCompleted, 3); + assert(threadCounter == 3); + + // Wait until we have next 3 threads available to start running. + console_log("control_thread: waiting for next three threads to be ready"); + emscripten_semaphore_waitinf_acquire(&threadsWaiting, 3); + + // Set the three waiting threads to run simultaneously. + assert(threadCounter == 3); + console_log("control_thread: setting next three threads go"); + emscripten_semaphore_release(&threadsRunning, 3); + + // Wait until we have the final 3 threads completed their run. + console_log("control_thread: waiting for the last three threads to finish"); + emscripten_semaphore_waitinf_acquire(&threadsCompleted, 3); + console_log("control_thread: threads finished"); + assert(threadCounter == 6); + + console_log("control_thread: test finished"); +#ifdef REPORT_RESULT + REPORT_RESULT(0); +#endif +} + +int main() +{ + emscripten_semaphore_init(&threadsWaiting, 0); + + void *stack = memalign(16, 1024); + emscripten_wasm_worker_t worker = emscripten_create_wasm_worker_no_tls(stack, 1024); + emscripten_wasm_worker_post_function_v(worker, control_thread); + +#define NUM_THREADS 6 + for(int i = 0; i < NUM_THREADS; ++i) + { + void *stack = memalign(16, 1024); + emscripten_wasm_worker_t worker = emscripten_create_wasm_worker_no_tls(stack, 1024); + emscripten_wasm_worker_post_function_v(worker, worker_main); + } +} diff --git a/tests/wasm_worker/shared_memory_preprocessor_flags.c b/tests/wasm_worker/shared_memory_preprocessor_flags.c new file mode 100644 index 000000000000..5a268f484de1 --- /dev/null +++ b/tests/wasm_worker/shared_memory_preprocessor_flags.c @@ -0,0 +1,13 @@ +// This file should be compiled with -sSHARED_MEMORY=1 + +#ifndef __EMSCRIPTEN_SHARED_MEMORY__ +#error __EMSCRIPTEN_SHARED_MEMORY__ should be defined when building with -sSHARED_MEMORY=1! +#endif + +#ifdef __EMSCRIPTEN_WASM_WORKERS__ +#error __EMSCRIPTEN_WASM_WORKERS__ should not defined when building with -sSHARED_MEMORY=1! +#endif + +#ifdef __EMSCRIPTEN_PTHREADS__ +#error __EMSCRIPTEN_PTHREADS__ should not be defined when building with -sSHARED_MEMORY=1! +#endif diff --git a/tests/wasm_worker/terminate_all_wasm_workers.c b/tests/wasm_worker/terminate_all_wasm_workers.c new file mode 100644 index 000000000000..018d0e38ddd4 --- /dev/null +++ b/tests/wasm_worker/terminate_all_wasm_workers.c @@ -0,0 +1,88 @@ +#include +#include +#include + +// Tests that calling emscripten_terminate_all_wasm_workers() properly terminates +// each child Wasm Worker of the calling thread. + +EM_JS(void, console_error, (char* str), { + console.error(UTF8ToString(str)); +}); + +static volatile int worker_started = 0; + +void this_function_should_not_be_called(void *userData) +{ + worker_started = -1; + console_error("this_function_should_not_be_called"); +#ifdef REPORT_RESULT + REPORT_RESULT(1/*fail*/); +#endif +} + +void test_passed(void *userData) +{ + if (worker_started == 2) + { + console_error("test_passed"); +#ifdef REPORT_RESULT + REPORT_RESULT(0/*ok*/); +#endif + } +} + +void worker_main() +{ + ++worker_started; + console_error("Hello from wasm worker!"); + // Schedule a function to be called, that should never happen, since the Worker + // dies before that. + emscripten_set_timeout(this_function_should_not_be_called, 2000, 0); +} + +char stack1[1024]; +char stack2[1024]; + +int should_throw(void(*func)(emscripten_wasm_worker_t worker), emscripten_wasm_worker_t worker) +{ + int threw = EM_ASM_INT({ + try { + dynCall('vi', $0, $1); + } catch(e) { + console.error('Threw an exception like expected: ' + e); + return 1; + } + console.error('Function was expected to throw, but did not!'); + return 0; + }, (int)func, worker); + return threw; +} + +emscripten_wasm_worker_t worker[2]; + +void post_bad_function(emscripten_wasm_worker_t worker) +{ + // Try to post a function to the worker, this should throw + emscripten_wasm_worker_post_function_vi(worker, (void(*)(int))this_function_should_not_be_called, 0); +} + +void terminate_worker(void *userData) +{ + emscripten_terminate_all_wasm_workers(); + assert(should_throw(post_bad_function, worker[0])); + assert(should_throw(post_bad_function, worker[1])); +} + +int main() +{ + worker[0] = emscripten_create_wasm_worker_no_tls(stack1, sizeof(stack1)); + worker[1] = emscripten_create_wasm_worker_no_tls(stack2, sizeof(stack2)); + emscripten_wasm_worker_post_function_v(worker[0], worker_main); + emscripten_wasm_worker_post_function_v(worker[1], worker_main); + + // Terminate both workers after a small delay + emscripten_set_timeout(terminate_worker, 1000, 0); + + // Wait a while, if the bad function does not trigger, then the test succeeds. + emscripten_set_timeout(test_passed, 3000, 0); +} diff --git a/tests/wasm_worker/terminate_wasm_worker.c b/tests/wasm_worker/terminate_wasm_worker.c new file mode 100644 index 000000000000..45f86b3683ea --- /dev/null +++ b/tests/wasm_worker/terminate_wasm_worker.c @@ -0,0 +1,84 @@ +#include +#include +#include + +// Tests that calling emscripten_terminate_wasm_worker() properly terminates +// a Wasm Worker. + +EM_JS(void, console_error, (char* str), { + console.error(UTF8ToString(str)); +}); + +static volatile int worker_started = 0; + +void this_function_should_not_be_called(void *userData) +{ + worker_started = -1; + console_error("this_function_should_not_be_called"); +#ifdef REPORT_RESULT + REPORT_RESULT(1/*fail*/); +#endif +} + +void test_passed(void *userData) +{ + if (worker_started == 1) + { + console_error("test_passed"); +#ifdef REPORT_RESULT + REPORT_RESULT(0/*ok*/); +#endif + } +} + +void worker_main() +{ + worker_started = 1; + console_error("Hello from wasm worker!"); + // Schedule a function to be called, that should never happen, since the Worker + // dies before that. + emscripten_set_timeout(this_function_should_not_be_called, 2000, 0); +} + +char stack[1024]; + +int should_throw(void(*func)()) +{ + int threw = EM_ASM_INT({ + try { + dynCall('v', $0); + } catch(e) { + console.error('Threw an exception like expected: ' + e); + return 1; + } + console.error('Function was expected to throw, but did not!'); + return 0; + }, (int)func); + return threw; +} + +emscripten_wasm_worker_t worker = 0; + +void post_bad_function() +{ + // Try to post a function to the worker, this should throw + emscripten_wasm_worker_post_function_vi(worker, (void(*)(int))this_function_should_not_be_called, 0); +} + +void terminate_worker(void *userData) +{ + emscripten_terminate_wasm_worker(worker); + assert(should_throw(post_bad_function)); +} + +int main() +{ + worker = emscripten_create_wasm_worker_no_tls(stack, sizeof(stack)); + emscripten_wasm_worker_post_function_v(worker, worker_main); + + // Terminate the worker after a small delay + emscripten_set_timeout(terminate_worker, 1000, 0); + + // Wait a while, if the bad function does not trigger, then the test succeeds. + emscripten_set_timeout(test_passed, 3000, 0); +} diff --git a/tests/wasm_worker/thread_stack.c b/tests/wasm_worker/thread_stack.c new file mode 100644 index 000000000000..1b6fdac51d52 --- /dev/null +++ b/tests/wasm_worker/thread_stack.c @@ -0,0 +1,43 @@ +#include +#include +#include +#include +#include +#include + +#define THREAD_STACK_SIZE 1024 +#define NUM_THREADS 2 +void *thread_stack[NUM_THREADS]; + +volatile int threadsOk = 0; + +void test_stack(int i) +{ + EM_ASM(console.log(`In thread ${$0}, stack low addr=0x${$1.toString(16)}, emscripten_stack_get_base()=0x${$2.toString(16)}, emscripten_stack_get_end()=0x${$3.toString(16)}, THREAD_STACK_SIZE=0x${$4.toString(16)}`), + i, thread_stack[i], emscripten_stack_get_base(), emscripten_stack_get_end(), THREAD_STACK_SIZE); + assert(emscripten_stack_get_base() == (uintptr_t)thread_stack[i] + THREAD_STACK_SIZE); + assert(emscripten_stack_get_end() == (uintptr_t)thread_stack[i]); + + int ok = __sync_fetch_and_add(&threadsOk, 1); + EM_ASM(console.log($0), ok); + if (ok == 1) + { + EM_ASM(console.log(`Test finished!`)); +#ifdef REPORT_RESULT + REPORT_RESULT(0); +#endif + } +} + +int main() +{ + EM_ASM(console.log(`Main thread stack base=0x${$0.toString(16)}, end=0x${$1.toString(16)}`), emscripten_stack_get_base(), emscripten_stack_get_end()); + + for(int i = 0; i < NUM_THREADS; ++i) + { + thread_stack[i] = memalign(16, THREAD_STACK_SIZE); + emscripten_wasm_worker_t worker = emscripten_create_wasm_worker_no_tls(thread_stack[i], THREAD_STACK_SIZE); + EM_ASM(console.log(`Created thread ${$0} with stack ptr=0x${$1.toString(16)}, size=0x${$2.toString(16)}`), i, thread_stack[i], THREAD_STACK_SIZE); + emscripten_wasm_worker_post_function_vi(worker, test_stack, i); + } +} diff --git a/tests/wasm_worker/wait32_notify.c b/tests/wasm_worker/wait32_notify.c new file mode 100644 index 000000000000..25d84b2da24c --- /dev/null +++ b/tests/wasm_worker/wait32_notify.c @@ -0,0 +1,76 @@ +#include +#include +#include +#include + +// Test emscripten_wasm_wait_i32() and emscripten_wasm_notify() functions. + +EM_JS(void, console_log, (char* str), { + console.log(UTF8ToString(str)); +}); + +volatile int32_t addr = 0; + +void worker_main() +{ + console_log("worker_main"); + emscripten_atomic_store_u32((void*)&addr, 1); + + console_log("Waiting on address with unexpected value should return 'not-equal'"); + ATOMICS_WAIT_RESULT_T ret = emscripten_wasm_wait_i32((int32_t*)&addr, 2, /*timeout=*/-1); + assert(ret == ATOMICS_WAIT_NOT_EQUAL); + + console_log("Waiting on address with unexpected value should return 'not-equal' also if timeout==0"); + ret = emscripten_wasm_wait_i32((int32_t*)&addr, 2, /*timeout=*/0); + assert(ret == ATOMICS_WAIT_NOT_EQUAL); + + console_log("Waiting for 0 nanoseconds should return 'timed-out'"); + ret = emscripten_wasm_wait_i32((int32_t*)&addr, 1, /*timeout=*/0); + assert(ret == ATOMICS_WAIT_TIMED_OUT); + + console_log("Waiting for >0 nanoseconds should return 'timed-out'"); + ret = emscripten_wasm_wait_i32((int32_t*)&addr, 1, /*timeout=*/1000); + assert(ret == ATOMICS_WAIT_TIMED_OUT); + + console_log("Waiting for infinitely long should return 'ok'"); + ret = emscripten_wasm_wait_i32((int32_t*)&addr, 1, /*timeout=*/-1); + assert(ret == ATOMICS_WAIT_OK); + + console_log("Test finished"); + +#ifdef REPORT_RESULT + REPORT_RESULT(addr); +#endif +} + +char stack[1024]; + +EM_BOOL main_loop(double time, void *userData) +{ + if (addr == 1) + { + // Burn one second to make sure worker finishes its test. + console_log("main: seen worker running"); + double t0 = emscripten_performance_now(); + while(emscripten_performance_now() < t0 + 1000); + + // Wake the waiter + console_log("main: waking worker"); + addr = 2; + emscripten_wasm_notify((int32_t*)&addr, 1); + + return EM_FALSE; + } + return EM_TRUE; +} + +int main() +{ + console_log("main: creating worker"); + emscripten_wasm_worker_t worker = emscripten_create_wasm_worker_no_tls(stack, sizeof(stack)); + console_log("main: posting function"); + emscripten_wasm_worker_post_function_v(worker, worker_main); + + console_log("main: entering timeout loop to wait for wasm worker to run"); + emscripten_set_timeout_loop(main_loop, 1000, 0); +} diff --git a/tests/wasm_worker/wait64_notify.c b/tests/wasm_worker/wait64_notify.c new file mode 100644 index 000000000000..0f9a17a68334 --- /dev/null +++ b/tests/wasm_worker/wait64_notify.c @@ -0,0 +1,76 @@ +#include +#include +#include +#include + +// Test emscripten_wasm_wait_i64() and emscripten_wasm_notify() functions. + +EM_JS(void, console_log, (char* str), { + console.log(UTF8ToString(str)); +}); + +volatile int64_t addr = 0; + +void worker_main() +{ + console_log("worker_main"); + emscripten_atomic_store_u64((void*)&addr, 0x100000000ull); + + console_log("Waiting on address with unexpected value should return 'not-equal'"); + ATOMICS_WAIT_RESULT_T ret = emscripten_wasm_wait_i64((int64_t*)&addr, 0x200000000ull, /*timeout=*/-1); + assert(ret == ATOMICS_WAIT_NOT_EQUAL); + + console_log("Waiting on address with unexpected value should return 'not-equal' also if timeout==0"); + ret = emscripten_wasm_wait_i64((int64_t*)&addr, 0x200000000ull, /*timeout=*/0); + assert(ret == ATOMICS_WAIT_NOT_EQUAL); + + console_log("Waiting for 0 nanoseconds should return 'timed-out'"); + ret = emscripten_wasm_wait_i64((int64_t*)&addr, 0x100000000ull, /*timeout=*/0); + assert(ret == ATOMICS_WAIT_TIMED_OUT); + + console_log("Waiting for >0 nanoseconds should return 'timed-out'"); + ret = emscripten_wasm_wait_i64((int64_t*)&addr, 0x100000000ull, /*timeout=*/1000); + assert(ret == ATOMICS_WAIT_TIMED_OUT); + + console_log("Waiting for infinitely long should return 'ok'"); + ret = emscripten_wasm_wait_i64((int64_t*)&addr, 0x100000000ull, /*timeout=*/-1); + assert(ret == ATOMICS_WAIT_OK); + + console_log("Test finished"); + +#ifdef REPORT_RESULT + REPORT_RESULT(addr >> 32); +#endif +} + +char stack[1024]; + +EM_BOOL main_loop(double time, void *userData) +{ + if (addr == 0x100000000ull) + { + // Burn one second to make sure worker finishes its test. + console_log("main: seen worker running"); + double t0 = emscripten_performance_now(); + while(emscripten_performance_now() < t0 + 1000); + + // Wake the waiter + console_log("main: waking worker"); + addr = 0x200000000ull; + emscripten_wasm_notify((int32_t*)&addr, 1); + + return EM_FALSE; + } + return EM_TRUE; +} + +int main() +{ + console_log("main: creating worker"); + emscripten_wasm_worker_t worker = emscripten_create_wasm_worker_no_tls(stack, sizeof(stack)); + console_log("main: posting function"); + emscripten_wasm_worker_post_function_v(worker, worker_main); + + console_log("main: entering timeout loop to wait for wasm worker to run"); + emscripten_set_timeout_loop(main_loop, 1000, 0); +} diff --git a/tests/wasm_worker/wait_async.c b/tests/wasm_worker/wait_async.c new file mode 100644 index 000000000000..02632e35d2a8 --- /dev/null +++ b/tests/wasm_worker/wait_async.c @@ -0,0 +1,87 @@ +#include +#include +#include +#include + +// Test emscripten_wasm_wait_i64() and emscripten_wasm_notify() functions. + +EM_JS(void, console_log, (char* str), { + console.log(UTF8ToString(str)); +}); + +volatile int32_t addr = 0; + +void worker_main() +{ + console_log("worker_main"); + emscripten_wasm_worker_sleep(1000 * 1000000ull); // Sleep one second. + console_log("worker: addr = 1234"); + emscripten_atomic_store_u32((void*)&addr, 1234); + console_log("worker: notify async waiting main thread"); + emscripten_wasm_notify((int32_t*)&addr, 1); +} + +char stack[1024]; + +int numCalled = 0; + +void asyncWaitShouldTimeout(int32_t *ptr, uint32_t val, ATOMICS_WAIT_RESULT_T waitResult, void *userData) +{ + console_log("main: asyncWaitShouldTimeout"); + ++numCalled; + assert(numCalled == 1); + assert(ptr == &addr); + assert(val == 1); + assert(userData == (void*)42); + assert(waitResult == ATOMICS_WAIT_TIMED_OUT); +} + +void asyncWaitFinishedShouldNotBeCalled(int32_t *ptr, uint32_t val, ATOMICS_WAIT_RESULT_T waitResult, void *userData) +{ + assert(0); // We should not reach here +} + +void asyncWaitFinishedShouldBeOk(int32_t *ptr, uint32_t val, ATOMICS_WAIT_RESULT_T waitResult, void *userData) +{ + console_log("main: asyncWaitFinished"); + assert(ptr == &addr); + assert(val == 1); + assert(userData == (void*)42); + ++numCalled; + assert(numCalled == 2); + assert(waitResult == ATOMICS_WAIT_OK); + console_log("test finished"); +#ifdef REPORT_RESULT + REPORT_RESULT(0); +#endif +} + +int main() +{ + console_log("main: creating worker"); + emscripten_wasm_worker_t worker = emscripten_create_wasm_worker_no_tls(stack, sizeof(stack)); + console_log("main: posting function"); + emscripten_wasm_worker_post_function_v(worker, worker_main); + + addr = 1; + + console_log("Async waiting on address with unexpected value should return 'not-equal'"); + ATOMICS_WAIT_TOKEN_T ret = emscripten_atomic_wait_async((int32_t*)&addr, 2, asyncWaitFinishedShouldNotBeCalled, (void*)42, EMSCRIPTEN_WAIT_ASYNC_INFINITY); + assert(ret == ATOMICS_WAIT_NOT_EQUAL); + + console_log("Waiting on address with unexpected value should return 'not-equal' also if timeout==0"); + ret = emscripten_atomic_wait_async((int32_t*)&addr, 2, asyncWaitFinishedShouldNotBeCalled, (void*)42, 0); + assert(ret == ATOMICS_WAIT_NOT_EQUAL); + + console_log("Waiting for 0 milliseconds should return 'timed-out'"); + ret = emscripten_atomic_wait_async((int32_t*)&addr, 1, asyncWaitFinishedShouldNotBeCalled, (void*)42, 0); + assert(ret == ATOMICS_WAIT_TIMED_OUT); + + console_log("Waiting for >0 milliseconds should return 'ok' (but successfully time out in first call to asyncWaitFinished)"); + ret = emscripten_atomic_wait_async((int32_t*)&addr, 1, asyncWaitShouldTimeout, (void*)42, 10); + assert(EMSCRIPTEN_IS_VALID_WAIT_TOKEN(ret)); + + console_log("Waiting for infinitely long should return 'ok' (and return 'ok' in second call to asyncWaitFinished)"); + ret = emscripten_atomic_wait_async((int32_t*)&addr, 1, asyncWaitFinishedShouldBeOk, (void*)42, EMSCRIPTEN_WAIT_ASYNC_INFINITY); + assert(EMSCRIPTEN_IS_VALID_WAIT_TOKEN(ret)); +} diff --git a/tests/wasm_worker/wasm_worker_and_pthread.c b/tests/wasm_worker/wasm_worker_and_pthread.c new file mode 100644 index 000000000000..b6dedb0f9e3d --- /dev/null +++ b/tests/wasm_worker/wasm_worker_and_pthread.c @@ -0,0 +1,50 @@ +#include +#include +#include +#include +#include + +volatile int pthread_ran = 0; + +EM_JS(int, am_i_pthread, (), { + return ENVIRONMENT_IS_PTHREAD; +}); + +EM_JS(int, am_i_wasm_worker, (), { + return ENVIRONMENT_IS_WASM_WORKER; +}); + +void *thread_main(void *arg) +{ + EM_ASM(out('hello from pthread!')); + assert(am_i_pthread()); + assert(!am_i_wasm_worker()); + assert(!emscripten_current_thread_is_wasm_worker()); + assert(emscripten_wasm_worker_self_id() == 0); + pthread_ran = 1; + return 0; +} + +void worker_main() +{ + EM_ASM(out('hello from wasm worker!')); + assert(!am_i_pthread()); + assert(am_i_wasm_worker()); + assert(emscripten_current_thread_is_wasm_worker()); + assert(emscripten_wasm_worker_self_id() != 0); + + while(!emscripten_atomic_cas_u32((void*)&pthread_ran, 0, 1)) + emscripten_wasm_worker_sleep(10); +#ifdef REPORT_RESULT + REPORT_RESULT(0); +#endif +} + +int main() +{ + pthread_t thread; + pthread_create(&thread, NULL, thread_main, NULL); + + emscripten_wasm_worker_t worker = emscripten_malloc_wasm_worker(/*stack size: */1024); + emscripten_wasm_worker_post_function_v(worker, worker_main); +} diff --git a/tests/wasm_worker/wasm_worker_code_size.c b/tests/wasm_worker/wasm_worker_code_size.c new file mode 100644 index 000000000000..9930298614c6 --- /dev/null +++ b/tests/wasm_worker/wasm_worker_code_size.c @@ -0,0 +1,18 @@ +#include +#include + +// This file contains an absolute minimal sized Wasm Worker example, to keep a check on generated code size. + +EM_JS(void, hello, (void), { + console.log("Hello from wasm worker!"); +}); + +void run_in_worker() +{ + hello(); +} + +int main() +{ + emscripten_wasm_worker_post_function_v(emscripten_malloc_wasm_worker(1024), run_in_worker); +} diff --git a/tests/wasm_worker/wasm_worker_preprocessor_flags.c b/tests/wasm_worker/wasm_worker_preprocessor_flags.c new file mode 100644 index 000000000000..1e9d157f20e0 --- /dev/null +++ b/tests/wasm_worker/wasm_worker_preprocessor_flags.c @@ -0,0 +1,13 @@ +// This file should be compiled with -sWASM_WORKERS=1 + +#ifndef __EMSCRIPTEN_SHARED_MEMORY__ +#error __EMSCRIPTEN_SHARED_MEMORY__ should be defined when building with -sWASM_WORKERS=1! +#endif + +#ifndef __EMSCRIPTEN_WASM_WORKERS__ +#error __EMSCRIPTEN_WASM_WORKERS__ should be defined when building with -sWASM_WORKERS=1! +#endif + +#ifdef __EMSCRIPTEN_PTHREADS__ +#error __EMSCRIPTEN_PTHREADS__ should not be defined when building with -sWASM_WORKERS=1! +#endif diff --git a/tests/wasm_worker/wasm_worker_self_id.c b/tests/wasm_worker/wasm_worker_self_id.c new file mode 100644 index 000000000000..e6491f5e5c8e --- /dev/null +++ b/tests/wasm_worker/wasm_worker_self_id.c @@ -0,0 +1,52 @@ +#include +#include +#include + +// Test the function emscripten_wasm_worker_self_id() + +EM_JS(void, console_log, (char* str), { + console.log(UTF8ToString(str)); +}); + +emscripten_wasm_worker_t worker1 = 0; +emscripten_wasm_worker_t worker2 = 0; + +int successes = 0; + +void test_success() +{ +#ifdef REPORT_RESULT + if (__atomic_add_fetch(&successes, 1, __ATOMIC_SEQ_CST) == 2) + { + REPORT_RESULT(0); + } +#endif +} + +void worker1_main() +{ + assert(emscripten_wasm_worker_self_id() != 0); + assert(emscripten_wasm_worker_self_id() == worker1); + if (emscripten_wasm_worker_self_id() == worker1) + test_success(); +} + +void worker2_main() +{ + assert(emscripten_wasm_worker_self_id() != 0); + assert(emscripten_wasm_worker_self_id() == worker2); + if (emscripten_wasm_worker_self_id() == worker2) + test_success(); +} + +char stack1[1024]; +char stack2[1024]; + +int main() +{ + assert(emscripten_wasm_worker_self_id() == 0); + worker1 = emscripten_create_wasm_worker_no_tls(stack1, sizeof(stack1)); + worker2 = emscripten_create_wasm_worker_no_tls(stack2, sizeof(stack2)); + emscripten_wasm_worker_post_function_v(worker1, worker1_main); + emscripten_wasm_worker_post_function_v(worker2, worker2_main); +} diff --git a/tests/wasm_worker/wasm_worker_sleep.c b/tests/wasm_worker/wasm_worker_sleep.c new file mode 100644 index 000000000000..d25ec8683889 --- /dev/null +++ b/tests/wasm_worker/wasm_worker_sleep.c @@ -0,0 +1,24 @@ +#include +#include + +EM_JS(void, console_log, (char* str), { + console.log(UTF8ToString(str)); +}); + +void worker_main() +{ + double t0 = emscripten_performance_now(); + emscripten_wasm_worker_sleep(/*nsecs=*/1500*1000000); + double t1 = emscripten_performance_now(); +#ifdef REPORT_RESULT + REPORT_RESULT(t1-t0 >= 1500); +#endif +} + +char stack[1024]; + +int main() +{ + emscripten_wasm_worker_t worker = emscripten_create_wasm_worker_no_tls(stack, sizeof(stack)); + emscripten_wasm_worker_post_function_v(worker, worker_main); +} diff --git a/tests/wasm_worker/wasm_worker_tls_wasm_assembly.S b/tests/wasm_worker/wasm_worker_tls_wasm_assembly.S new file mode 100644 index 000000000000..cffb0901264e --- /dev/null +++ b/tests/wasm_worker/wasm_worker_tls_wasm_assembly.S @@ -0,0 +1,15 @@ +.globaltype tlsVariable, i32 +tlsVariable: + +.globl get_tls_variable +get_tls_variable: + .functype get_tls_variable () -> (i32) + global.get tlsVariable + end_function + +.globl set_tls_variable +set_tls_variable: + .functype set_tls_variable (i32) -> () + local.get 0 + global.set tlsVariable + end_function diff --git a/tests/wasm_worker/wasm_worker_tls_wasm_assembly.c b/tests/wasm_worker/wasm_worker_tls_wasm_assembly.c new file mode 100644 index 000000000000..81038c0e1199 --- /dev/null +++ b/tests/wasm_worker/wasm_worker_tls_wasm_assembly.c @@ -0,0 +1,40 @@ +#include +#include +#include + +// Verify that the global heap is not overrun (reinitialized) on worker creation. +int globalData = 1; + +int get_tls_variable(void); +void set_tls_variable(int var); + +void main_thread_func() +{ + assert(!emscripten_current_thread_is_wasm_worker()); + assert(globalData == 3); +#ifdef REPORT_RESULT + REPORT_RESULT(get_tls_variable()); +#endif +} + +void worker_main() +{ + assert(emscripten_current_thread_is_wasm_worker()); + assert(get_tls_variable() == 0); + assert(globalData == 2); + globalData = 3; + set_tls_variable(123456); // Try to write garbage data to the memory location. + emscripten_wasm_worker_post_function_v(0, main_thread_func); +} + +char stack[1024]; + +int main() +{ + assert(!emscripten_current_thread_is_wasm_worker()); + assert(globalData == 1); + globalData = 2; + set_tls_variable(42); + emscripten_wasm_worker_t worker = emscripten_create_wasm_worker_no_tls(stack, sizeof(stack)); + emscripten_wasm_worker_post_function_v(worker, worker_main); +} diff --git a/tools/minimal_runtime_shell.py b/tools/minimal_runtime_shell.py index 419109f90273..9db73878b725 100644 --- a/tools/minimal_runtime_shell.py +++ b/tools/minimal_runtime_shell.py @@ -62,6 +62,19 @@ def generate_minimal_runtime_load_statement(target_basename): if download_wasm: files_to_load += [download_wasm] + # Download wasm_worker file + if settings.WASM_WORKERS: + if settings.MODULARIZE: + if settings.WASM_WORKERS == 1: # '$wb': Wasm Worker Blob + modularize_imports += ['$wb: URL.createObjectURL(new Blob([r[%d]], { type: \'application/javascript\' }))' % len(files_to_load)] + modularize_imports += ['js: js'] + else: + if settings.WASM_WORKERS == 1: + then_statements += ['%s.$wb = URL.createObjectURL(new Blob([r[%d]], { type: \'application/javascript\' }));' % (settings.EXPORT_NAME, len(files_to_load))] + + if download_wasm and settings.WASM_WORKERS == 1: + files_to_load += ["binary('%s')" % (target_basename + '.ww.js')] + if settings.MODULARIZE and settings.USE_PTHREADS: modularize_imports += ["worker: '{{{ PTHREAD_WORKER_FILE }}}'"] @@ -75,7 +88,12 @@ def generate_minimal_runtime_load_statement(target_basename): # Execute compiled output when building with MODULARIZE if settings.MODULARIZE: - then_statements += ['''// Detour the JS code to a separate variable to avoid instantiating with 'r' array as "this" directly to avoid strict ECMAScript/Firefox GC problems that cause a leak, see https://bugzilla.mozilla.org/show_bug.cgi?id=1540101 + + if settings.WASM_WORKERS: + then_statements += ['''// Detour the JS code to a separate variable to avoid instantiating with 'r' array as "this" directly to avoid strict ECMAScript/Firefox GC problems that cause a leak, see https://bugzilla.mozilla.org/show_bug.cgi?id=1540101 + var js = URL.createObjectURL(new Blob([r[0]], { type: \'application/javascript\' }));\n script(js).then(function(c) { c({ %s }); });''' % ',\n '.join(modularize_imports)] + else: + then_statements += ['''// Detour the JS code to a separate variable to avoid instantiating with 'r' array as "this" directly to avoid strict ECMAScript/Firefox GC problems that cause a leak, see https://bugzilla.mozilla.org/show_bug.cgi?id=1540101 var js = r[0];\n js({ %s });''' % ',\n '.join(modularize_imports)] binary_xhr = ''' function binary(url) { // Downloads a binary file and outputs it in the specified callback @@ -129,20 +147,26 @@ def generate_minimal_runtime_load_statement(target_basename): else: return script_xhr + files_to_load[0] + ";" - if not settings.MODULARIZE: + if not settings.MODULARIZE or settings.WASM_WORKERS: # If downloading multiple files like .wasm or .mem, those need to be loaded in # before we can add the main runtime script to the DOM, so convert the main .js # script load from direct script() load to a binary() load so we can still # immediately start the download, but can control when we add the script to the # DOM. - if settings.USE_PTHREADS: + if settings.USE_PTHREADS or settings.WASM_WORKERS: script_load = "script(url)" else: script_load = "script(url).then(() => { URL.revokeObjectURL(url) });" + if settings.WASM_WORKERS: + save_js = '%s.js = ' % settings.EXPORT_NAME + else: + save_js = '' + files_to_load[0] = "binary('%s')" % (target_basename + '.js') - then_statements += ["var url = URL.createObjectURL(new Blob([r[0]], { type: 'application/javascript' }));", - script_load] + if not settings.MODULARIZE: + then_statements += ["var url = %sURL.createObjectURL(new Blob([r[0]], { type: 'application/javascript' }));" % save_js, + script_load] # Add in binary() XHR loader if used: if any("binary(" in s for s in files_to_load + then_statements): diff --git a/tools/settings.py b/tools/settings.py index 10ef731c357a..36b9c8f86427 100644 --- a/tools/settings.py +++ b/tools/settings.py @@ -66,6 +66,7 @@ 'SUPPORT_LONGJMP', 'DEFAULT_TO_CXX', 'WASM_OBJECT_FILES', + 'WASM_WORKERS', # Internal settings used during compilation 'EXCEPTION_CATCHING_ALLOWED', diff --git a/tools/system_libs.py b/tools/system_libs.py index 9d35a7e4cd93..ab50a52eada0 100644 --- a/tools/system_libs.py +++ b/tools/system_libs.py @@ -468,27 +468,38 @@ def get_usable_variations(cls): class MTLibrary(Library): def __init__(self, **kwargs): self.is_mt = kwargs.pop('is_mt') + self.is_ww = kwargs.pop('is_ww') and not self.is_mt super().__init__(**kwargs) def get_cflags(self): cflags = super().get_cflags() if self.is_mt: - cflags += ['-sUSE_PTHREADS'] + cflags += ['-sUSE_PTHREADS', '-sWASM_WORKERS'] + if self.is_ww: + cflags += ['-sWASM_WORKERS'] return cflags def get_base_name(self): name = super().get_base_name() if self.is_mt: name += '-mt' + if self.is_ww: + name += '-ww' return name @classmethod def vary_on(cls): - return super().vary_on() + ['is_mt'] + return super().vary_on() + ['is_mt', 'is_ww'] @classmethod def get_default_variation(cls, **kwargs): - return super().get_default_variation(is_mt=settings.USE_PTHREADS, **kwargs) + return super().get_default_variation(is_mt=settings.USE_PTHREADS, is_ww=settings.WASM_WORKERS and not settings.USE_PTHREADS, **kwargs) + + @classmethod + def variations(cls): + combos = super(MTLibrary, cls).variations() + # To save on # of variations, pthreads and Wasm workers when used together, just use pthreads variation. + return [combo for combo in combos if not combo['is_mt'] or not combo['is_ww']] class DebugLibrary(Library): @@ -1016,6 +1027,54 @@ def can_use(self): return super(libprintf_long_double, self).can_use() and settings.PRINTF_LONG_DOUBLE +class libwasm_workers(MTLibrary): + def __init__(self, **kwargs): + self.tls = kwargs.pop('tls') + self.stack_check = kwargs.pop('stack_check') + self.debug = kwargs.pop('debug') + super().__init__(**kwargs) + + name = 'libwasm_workers' + + def get_cflags(self): + cflags = ['-pthread', + '-D_DEBUG' if self.debug else '-Oz', + '-DSTACK_OVERFLOW_CHECK=' + ('2' if self.stack_check else '0'), + '-DWASM_WORKER_NO_TLS=' + ('0' if self.tls else '1')] + if not self.debug: + cflags += ['-DNDEBUG'] + if self.is_ww or self.is_mt: + cflags += ['-sWASM_WORKERS'] + if settings.MAIN_MODULE: + cflags += ['-fPIC'] + return cflags + + def get_base_name(self): + name = 'libwasm_workers' + if not self.is_ww and not self.is_mt: + name += '_stub' + if not self.tls: + name += '-notls' + if self.debug: + name += '-debug' + if self.stack_check: + name += '-stackcheck' + return name + + @classmethod + def vary_on(cls): + return super().vary_on() + ['tls', 'debug', 'stack_check'] + + @classmethod + def get_default_variation(cls, **kwargs): + return super().get_default_variation(tls=not settings.WASM_WORKERS_NO_TLS, debug=settings.ASSERTIONS >= 1, stack_check=settings.STACK_OVERFLOW_CHECK == 2, **kwargs) + + def get_files(self): + return files_in_path( + path='system/lib/wasm_worker', + filenames=['library_wasm_worker.c' if self.is_ww or self.is_mt else 'library_wasm_worker_stub.c']) + + class libsockets(MuslInternalLibrary, MTLibrary): name = 'libsockets' @@ -1097,7 +1156,7 @@ class libcxxabi(NoExceptLibrary, MTLibrary): def get_cflags(self): cflags = super().get_cflags() cflags.append('-DNDEBUG') - if not self.is_mt: + if not self.is_mt and not self.is_ww: cflags.append('-D_LIBCXXABI_HAS_NO_THREADS') if self.eh_mode == Exceptions.NONE: cflags.append('-D_LIBCXXABI_NO_EXCEPTIONS') @@ -1190,7 +1249,7 @@ def can_use(self): def get_cflags(self): cflags = super().get_cflags() cflags.append('-DNDEBUG') - if not self.is_mt: + if not self.is_mt and not self.is_ww: cflags.append('-D_LIBUNWIND_HAS_NO_THREADS') if self.eh_mode == Exceptions.NONE: cflags.append('-D_LIBUNWIND_HAS_NO_EXCEPTIONS') @@ -1800,6 +1859,9 @@ def add_library(libname): if settings.USE_WEBGPU: add_library('libwebgpu_cpp') + if settings.WASM_WORKERS: + add_library('libwasm_workers') + return libs_to_link