From 33b5c4a0f65adf1740ece58381b300e15f0757ed Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Mon, 27 Mar 2023 22:49:07 +0200 Subject: [PATCH 01/37] [wasm] Bump emscripten to 3.1.34 --- src/mono/wasm/emscripten-version.txt | 2 +- src/mono/wasm/wasm.proj | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/mono/wasm/emscripten-version.txt b/src/mono/wasm/emscripten-version.txt index f4e47c2e5e20a..45dd392530866 100644 --- a/src/mono/wasm/emscripten-version.txt +++ b/src/mono/wasm/emscripten-version.txt @@ -1 +1 @@ -3.1.30 \ No newline at end of file +3.1.34 \ No newline at end of file diff --git a/src/mono/wasm/wasm.proj b/src/mono/wasm/wasm.proj index 9755e405afed4..8342ee12786eb 100644 --- a/src/mono/wasm/wasm.proj +++ b/src/mono/wasm/wasm.proj @@ -293,7 +293,6 @@ <_EmccLinkFlags Include="-s EXPORTED_RUNTIME_METHODS=$(_EmccExportedRuntimeMethods)" /> <_EmccLinkFlags Include="-s EXPORTED_FUNCTIONS=$(_EmccExportedFunctions)" /> <_EmccLinkFlags Include="--source-map-base http://example.com" /> - <_EmccLinkFlags Include="-s STRICT_JS=1" /> <_EmccLinkFlags Include="-s WASM_BIGINT=1" /> <_EmccLinkFlags Include="-s EXPORT_NAME="'createDotnetRuntime'"" /> <_EmccLinkFlags Include="-s MODULARIZE=1"/> From dff44d4ff438a811f9c60532dd7fb7d83b6a62d0 Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Mon, 27 Mar 2023 22:53:30 +0200 Subject: [PATCH 02/37] Update emsdk deps --- eng/Version.Details.xml | 4 ++-- eng/Versions.props | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 8570eb65ca9f5..28a624fa653bb 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -85,9 +85,9 @@ f32e148d67dbf348685c3076a37e8bc68ab3a30f - + https://github.com/dotnet/emsdk - a464820353b7956538b07c9b53103d793b5e15b6 + e4089ed2abe29bdc25bab2c261940175d0846824 diff --git a/eng/Versions.props b/eng/Versions.props index a73f7cb713843..ff97380e6cae2 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -241,7 +241,7 @@ Note: when the name is updated, make sure to update dependency name in eng/pipelines/common/xplat-setup.yml like - DarcDependenciesChanged.Microsoft_NET_Workload_Emscripten_Current_Manifest-8_0_100_Transport --> - 8.0.0-preview.4.23170.1 + 8.0.0-preview.4.23177.1 $(MicrosoftNETWorkloadEmscriptenCurrentManifest80100TransportVersion) 1.1.87-gba258badda From ce1a265a4ce87a4d0a575ecb6e64618c815638f5 Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Mon, 27 Mar 2023 22:56:13 +0200 Subject: [PATCH 03/37] Update icu deps --- eng/Version.Details.xml | 4 ++-- eng/Versions.props | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 28a624fa653bb..42bd8bb7e5666 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,8 +1,8 @@ - + https://github.com/dotnet/icu - b3cb54fec4eb845b37038c934d6c9dc17cdfb181 + 389d19d09d3cf16ec0143dba065fcd704ab8e48c https://github.com/dotnet/msquic diff --git a/eng/Versions.props b/eng/Versions.props index ff97380e6cae2..9b268b6d877a5 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -222,7 +222,7 @@ 0.11.4-alpha.23163.1 - 8.0.0-preview.3.23163.3 + 8.0.0-preview.4.23177.3 2.1.7 8.0.0-alpha.1.23166.1 From ed96a04bd686d6fcbf02318062a26bcce7e21c0f Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Mon, 27 Mar 2023 23:00:14 +0200 Subject: [PATCH 04/37] Use new images --- eng/pipelines/common/templates/pipeline-with-resources.yml | 4 ++-- eng/pipelines/libraries/helix-queues-setup.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/pipelines/common/templates/pipeline-with-resources.yml b/eng/pipelines/common/templates/pipeline-with-resources.yml index e849da704781c..e5c710bd7b821 100644 --- a/eng/pipelines/common/templates/pipeline-with-resources.yml +++ b/eng/pipelines/common/templates/pipeline-with-resources.yml @@ -60,10 +60,10 @@ resources: ROOTFS_DIR: /crossrootfs/ppc64le - container: browser_wasm - image: mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-18.04-webassembly-net8 + image: mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-18.04-webassembly-net8-20230327150025-4404b5c - container: wasi_wasm - image: mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-20.04-webassembly-net8 + image: mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-20.04-webassembly-net8-20230327150037-4404b5c - container: freebsd_x64 image: mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-18.04-cross-freebsd-12 diff --git a/eng/pipelines/libraries/helix-queues-setup.yml b/eng/pipelines/libraries/helix-queues-setup.yml index 41e1eb4207fbe..6c6837a74938a 100644 --- a/eng/pipelines/libraries/helix-queues-setup.yml +++ b/eng/pipelines/libraries/helix-queues-setup.yml @@ -201,6 +201,6 @@ jobs: # Browser WebAssembly windows - ${{ if in(parameters.platform, 'browser_wasm_win', 'wasi_wasm_win') }}: - - (Windows.Amd64.Server2022.Open)windows.amd64.server2022.open@mcr.microsoft.com/dotnet-buildtools/prereqs:windowsservercore-ltsc2022-helix-webassembly-net8 + - (Windows.Amd64.Server2022.Open)windows.amd64.server2022.open@mcr.microsoft.com/dotnet-buildtools/prereqs:windowsservercore-ltsc2022-helix-webassembly-net8-20230327150108-4404b5c ${{ insert }}: ${{ parameters.jobParameters }} From 8e6a28ccdb6fa820c481cf63c561ed3854208ef4 Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Mon, 27 Mar 2023 23:26:46 +0200 Subject: [PATCH 05/37] Use emscripten_main_runtime_thread_id --- src/mono/mono/utils/mono-threads-wasm.c | 2 +- src/mono/wasm/runtime/pthreads/shared/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mono/mono/utils/mono-threads-wasm.c b/src/mono/mono/utils/mono-threads-wasm.c index 74cc69825f29f..96e5446388a43 100644 --- a/src/mono/mono/utils/mono-threads-wasm.c +++ b/src/mono/mono/utils/mono-threads-wasm.c @@ -403,7 +403,7 @@ mono_threads_wasm_browser_thread_tid (void) #ifdef DISABLE_THREADS return (MonoNativeThreadId)1; #else - return (MonoNativeThreadId)emscripten_main_browser_thread_id (); + return (MonoNativeThreadId)emscripten_main_runtime_thread_id (); #endif } diff --git a/src/mono/wasm/runtime/pthreads/shared/index.ts b/src/mono/wasm/runtime/pthreads/shared/index.ts index c71a27f5f3967..774dbc76ec8af 100644 --- a/src/mono/wasm/runtime/pthreads/shared/index.ts +++ b/src/mono/wasm/runtime/pthreads/shared/index.ts @@ -20,7 +20,7 @@ export const MainThread: PThreadInfo = { let browser_thread_id_lazy: pthread_ptr | undefined; export function getBrowserThreadID(): pthread_ptr { if (browser_thread_id_lazy === undefined) { - browser_thread_id_lazy = (Module)["_emscripten_main_browser_thread_id"]() as pthread_ptr; + browser_thread_id_lazy = (Module)["_emscripten_main_runtime_thread_id"]() as pthread_ptr; } return browser_thread_id_lazy; } From 81672f4243de7e3b0dba7f744ceb3945f1328352 Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Tue, 28 Mar 2023 13:54:27 +0200 Subject: [PATCH 06/37] Ignore ExitStatus exceptions This should fix these errors: [wasm test] [23:10:04] dbug: Reached wasm exit [wasm test] [23:10:04] info: node:internal/process/promises:246 [wasm test] [23:10:04] info: triggerUncaughtException(err, true /* fromPromise */); [wasm test] [23:10:04] info: ^ [wasm test] [23:10:04] info: [wasm test] [23:10:04] info: [UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "#".] { [wasm test] [23:10:04] info: code: 'ERR_UNHANDLED_REJECTION' [wasm test] [23:10:04] info: } [wasm test] [23:10:04] info: [wasm test] [23:10:04] info: Node.js v17.3.1 [wasm test] [23:10:04] info: Process node.exe exited with 1 --- src/mono/wasm/test-main.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/mono/wasm/test-main.js b/src/mono/wasm/test-main.js index 60c5473492962..9de965c3f5f62 100644 --- a/src/mono/wasm/test-main.js +++ b/src/mono/wasm/test-main.js @@ -23,6 +23,16 @@ if (is_node && process.versions.node.split(".")[0] < 14) { throw new Error(`NodeJS at '${process.execPath}' has too low version '${process.versions.node}'`); } +if (is_node) { + // the emscripten 3.1.34 stopped handling these when MODULARIZE is enabled + process.on('uncaughtException', function(ex) { + // ignore ExitStatus exceptions + if (ex !== 'unwind' && !(ex instanceof ExitStatus) && !(ex.context instanceof ExitStatus)) { + throw ex; + } + }); +} + if (!is_node && !is_browser && typeof globalThis.crypto === 'undefined') { // **NOTE** this is a simple insecure polyfill for testing purposes only // /dev/random doesn't work on js shells, so define our own From c2cbb94edd4b4f67542603edc3c6d67180bb51f2 Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Tue, 28 Mar 2023 20:44:06 +0200 Subject: [PATCH 07/37] Handle UnhandledPromiseRejection for ExitStatus --- src/mono/wasm/Wasm.Build.Tests/data/test-main-7.0.js | 10 ++++++++++ src/mono/wasm/test-main.js | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/mono/wasm/Wasm.Build.Tests/data/test-main-7.0.js b/src/mono/wasm/Wasm.Build.Tests/data/test-main-7.0.js index bac3f856160c5..3b32add2f39e5 100644 --- a/src/mono/wasm/Wasm.Build.Tests/data/test-main-7.0.js +++ b/src/mono/wasm/Wasm.Build.Tests/data/test-main-7.0.js @@ -23,6 +23,16 @@ if (is_node && process.versions.node.split(".")[0] < 14) { throw new Error(`NodeJS at '${process.execPath}' has too low version '${process.versions.node}'`); } +if (is_node) { + // the emscripten 3.1.34 stopped handling these when MODULARIZE is enabled + process.on('uncaughtException', function(ex) { + // ignore UnhandledPromiseRejection exceptions with exit status + if (ex !== 'unwind' && (ex.name !== "UnhandledPromiseRejection" || !ex.message.includes('"#"'))) { + throw ex; + } + }); +} + if (typeof globalThis.crypto === 'undefined') { // **NOTE** this is a simple insecure polyfill for testing purposes only // /dev/random doesn't work on js shells, so define our own diff --git a/src/mono/wasm/test-main.js b/src/mono/wasm/test-main.js index 9de965c3f5f62..3d835c0bb6140 100644 --- a/src/mono/wasm/test-main.js +++ b/src/mono/wasm/test-main.js @@ -26,8 +26,8 @@ if (is_node && process.versions.node.split(".")[0] < 14) { if (is_node) { // the emscripten 3.1.34 stopped handling these when MODULARIZE is enabled process.on('uncaughtException', function(ex) { - // ignore ExitStatus exceptions - if (ex !== 'unwind' && !(ex instanceof ExitStatus) && !(ex.context instanceof ExitStatus)) { + // ignore UnhandledPromiseRejection exceptions with exit status + if (ex !== 'unwind' && (ex.name !== "UnhandledPromiseRejection" || !ex.message.includes('"#"'))) { throw ex; } }); From 4c0cb9e3ac0e3a1c1a8fc1998a955a6595f08c2b Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 15 Mar 2023 14:50:09 -0400 Subject: [PATCH 08/37] [wasm][threads] flip YieldFromDispatchLoop; specialize PortableThreadPool.WorkerThread --- .../PortableThreadPool.WorkerThread.cs | 2 ++ .../System/Threading/ThreadPool.Portable.cs | 2 ++ .../System.Private.CoreLib.csproj | 2 ++ ...adPool.WorkerThread.Browser.Threads.Mono.cs | 18 ++++++++++++++++++ .../ThreadPool.Browser.Threads.Mono.cs | 13 +++++++++++++ 5 files changed, 37 insertions(+) create mode 100644 src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs create mode 100644 src/mono/System.Private.CoreLib/src/System/Threading/ThreadPool.Browser.Threads.Mono.cs diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.cs index 96578b9de6b8d..99290e98f889a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.cs @@ -7,6 +7,7 @@ namespace System.Threading { internal sealed partial class PortableThreadPool { +#if !(TARGET_BROWSER && FEATURE_WASM_THREADS) /// /// The worker thread infastructure for the CLR thread pool. /// @@ -312,5 +313,6 @@ private static void CreateWorkerThread() workerThread.UnsafeStart(); } } +#endif // !(TARGET_BROWSER && FEATURE_WASM_THREADS) } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Portable.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Portable.cs index 39e1d6453263e..a9c4e038129a4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Portable.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Portable.cs @@ -19,7 +19,9 @@ public static partial class ThreadPool { // Indicates whether the thread pool should yield the thread from the dispatch loop to the runtime periodically so that // the runtime may use the thread for processing other work +#if !(TARGET_BROWSER && FEATURE_WASM_THREADS) internal static bool YieldFromDispatchLoop => false; +#endif #if NATIVEAOT private const bool IsWorkerTrackingEnabledInConfig = false; diff --git a/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj b/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj index 0e3a627d8f961..f5cd65c4a54a3 100644 --- a/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj +++ b/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj @@ -280,6 +280,8 @@ + + diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs new file mode 100644 index 0000000000000..50e4c61ed6f4f --- /dev/null +++ b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Tracing; + +namespace System.Threading +{ + internal sealed partial class PortableThreadPool + { + /// + /// The worker thread infastructure for the CLR thread pool. + /// + private static partial class WorkerThread + { + + } + } +} diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/ThreadPool.Browser.Threads.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/ThreadPool.Browser.Threads.Mono.cs new file mode 100644 index 0000000000000..7933e49db422b --- /dev/null +++ b/src/mono/System.Private.CoreLib/src/System/Threading/ThreadPool.Browser.Threads.Mono.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Threading +{ + public static partial class ThreadPool + { + // Indicates that the threadpool should yield the thread from the dispatch loop to the + // runtime periodically. We use this to return back to the JS event loop so that the JS + // event queue can be drained + internal static bool YieldFromDispatchLoop => true; + } +} From 3d88608ca8d5e493d8c189662f1f20ea56986cf0 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Mon, 20 Mar 2023 12:57:41 -0400 Subject: [PATCH 09/37] [mono] Implement a LifoJSSemaphore This is a LIFO semaphore with an asynchronous wait that triggers callbacks on the JS event loop in case of Release or timeout. --- src/mono/mono/utils/lifo-semaphore.c | 238 +++++++++++++++++++++++++++ src/mono/mono/utils/lifo-semaphore.h | 93 +++++++++++ 2 files changed, 331 insertions(+) diff --git a/src/mono/mono/utils/lifo-semaphore.c b/src/mono/mono/utils/lifo-semaphore.c index 624e2bb3b74e6..146589ba47608 100644 --- a/src/mono/mono/utils/lifo-semaphore.c +++ b/src/mono/mono/utils/lifo-semaphore.c @@ -1,5 +1,10 @@ #include +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) +#include +#include +#endif + LifoSemaphore * mono_lifo_semaphore_init (void) { @@ -87,3 +92,236 @@ mono_lifo_semaphore_release (LifoSemaphore *semaphore, uint32_t count) mono_coop_mutex_unlock (&semaphore->mutex); } + +#if defined(HOST_BROWSSER) && !defined(DISABLE_THREADS) + +LifoJSSemaphore * +mono_lifo_js_semaphore_init (void) +{ + LifoJSSemaphore *sem = g_new0 (LifoJSSemahore, 1); + if (sem == NULL) + return NULL; + + mono_coop_mutex_init (&sem->mutex); + + return sem; +} + +void +mono_lifo_js_semaphore_delete (LifoJSSemaphore *sem) +{ + /* FIXME: this is probably hard to guarantee - in-flight signaled semaphores still have wait entries */ + g_assert (sem->head == NULL); + mono_coop_mutex_destroy (&sem->mutex); + g_free (semaphore); +} + +enum { + LIFO_JS_WAITING = 0, + LIFO_JS_SIGNALED = 1, + LIFO_JS_SIGNALED_TIMEOUT_IGNORED = 2, + +}; + +static void +lifo_js_wait_entry_on_timeout (void *wait_entry_as_user_data); +static void +lifo_js_wait_entry_on_success (void *wait_entry_as_user_data); + + +static void +lifo_js_wait_entry_push (LifoJSSemaphoreWaitEntry **head, + LifoJSSemaphoreWaitEntry *entry) +{ + LifoJSSemaphoreWaitEntry *next = *head; + *head = entry; + entry->next = next; + next->previous = entry; +} + +static void +lifo_js_wait_entry_unlink (LifoJSSemaphoreWaitEntry **head, + LifoJSSemaphoreWaitEntry *entry) +{ + if (*head == entry) { + *head = entry->next; + } + if (entry->previous) { + entry->previous->next = entry->next; + } + if (entry->next) { + entry->next->previous = entry->previous; + } +} + +/* LOCKING: assumes semaphore is locked */ +static LifoJSSemaphoreWaitEntry * +lifo_js_find_waiter (LifoJSSemaphoreWaitEntry *entry) +{ + while (entry) { + if (entry->state == LIFO_JS_WAITING) + return entry; + entry = entry->next; + } + return NULL; +} + +static gboolean +lifo_js_wait_entry_no_thread (LifoJSSemaphoreWaitEntry *entry, + pthread_t cur) +{ + while (entry) { + if (entry->waiting_thread == cur) + return FALSE; + entry = entry->next; + } + return TRUE; +} + +void +mono_lifo_js_semaphore_prepare_wait (LifoJSSemaphore *sem, + int32_t timeout_ms, + LifoJSSemaphoreCallbackFn success_cb, + LifoJSSemaphoreCallbackFn timeout_cb, + uint32_t gchandle, + void *user_data) +{ + mono_coop_mutex_lock (&sem->mutex); + if (sem->pending_signals > 0) { + sem->pending_signals--; + mono_coop_mutex_unlock (&sem->mutex); + success_cb (sem, gchandle, user_data); // FIXME: queue microtask + return; + } + + pthread_t cur = pthread_self (); + + /* Don't allow the current thread to wait multiple times. + * No particular reason for it, except that it makes reasoning a bit easier. + * This can probably be relaxed if there's a need. + */ + g_assert (lifo_js_wait_entry_no_thread(sem->head, cur)); + + LifoJSSemaphoreWaitEntry wait_entry = g_new0 (LifoJSSemaphoreWaitEntry, 1); + wait_entry->success_cb = success_cb; + wait_entry->timeout_cb = timeout_cb; + wait_entry->sem = sem; + wait_entry->gchandle = gchandle; + wait_entry->user_data = user_data; + wait_entry->waiting_thread = pthread_self(); + wait_entry->state = LIFO_JS_WAITING; + wait_entry->refcount = 1; // timeout owns the wait entry + wait_entry->js_timeout_id = emscripten_set_timeout (lifo_js_wait_entry_on_timeout, (double)timeout_ms, &wait_entry); + lifo_js_wait_entry_push (&sem->head, wait_entry); + mono_coop_mutex_unlock (&sem->mutex); + return; +} + +static void +mono_lifo_js_semaphore_release (LifoJSSemaphore *sem, + uint32_t count) +{ + mono_coop_mutex_lock (&sem->mutex); + + while (count > 0) { + LifoSemaphoreWaitEntry *wait_entry = lifo_js_find_waiter (sem->head); + if (wait_entry != NULL) { + /* found one. set its status and queue some work to run on the signaled thread */ + pthread_t target = wait_entry->thread; + wait_entry->state = LIFO_JS_SIGNALED; + wait_entry->refcount++; + // we're under the mutex - if we got here the timeout hasn't fired yet + g_assert (wait_entry->refcount == 2); + --count; + /* if we're on the same thread, don't run the callback while holding the lock */ + emscripten_dispatch_to_thread_async (target, EM_FUNC_SIG_VI, lifo_js_wait_entry_on_success, NULL, wait_entry); + } else { + semaphore->pending_signals += count; + count = 0; + } + } + + mono_coop_mutex_unlock (&semaphore->mutex); +} + +static void +lifo_js_wait_entry_on_timeout (void *wait_entry_as_user_data) +{ + LifoJSSemaphoreWaitEntry *wait_entry = (LifoJSSemaphoreWaitEntry *)wait_entry_as_user_data; + g_assert (pthread_equal (wait_entry->thread, pthread_self())); + g_assert (wait_entry->sem != NULL); + LifoJSSemaphore *sem = wait_entry->sem; + gboolean call_timeout_cb = FALSE;; + LifoJSSemaphoreCallbackFn *timeout_cb = NULL; + uint32_t gchandle gchandle = 0; + void *user_data = NULL; + mono_coop_mutex_lock (&sem->mutex); + switch (wait_entry->state) { + case LIFO_JS_WAITING: + /* semaphore timed out before a Release. */ + g_assert (wait_entry->refcount == 1); + /* unlink and free the wait entry, run the user timeout_cb. */ + lifo_js_wait_entry_unlink (&sem->head, wait_entry); + timeout_cb = wait_entry->timeout_cb; + gchandle = wait_entry->gchandle; + user_data = wait_entry->user_data; + g_free (wait_entry); + call_timeout_cb = TRUE; + break; + case LIFO_JS_SIGNALED: + /* seamphore was signaled, but the timeout callback ran before the success callback arrived */ + g_assert (wait_entry->refcount == 2); + /* set state to LIFO_JS_SIGNALED_TIMEOUT_IGNORED, decrement refcount, return */ + wait_entry->state = LIFO_JS_SIGNALED_TIMEOUT_IGNORED; + wait_entry->refcount--; + break; + case LIFO_JS_SIGNALED_TIMEOUT_IGNORED: + default: + g_assert_not_reached(); + } + mono_coop_mutex_unlock (&sem->mutex); + if (call_timeout_cb) { + timeout_cb (sem, gchandle, user_data); + } +} + +static void +lifo_js_wait_entry_on_success (void *wait_entry_as_user_data) +{ + LifoJSSemaphoreWaitEntry *wait_entry = (LifoJSSemaphoreWaitEntry *)wait_entry_as_user_data; + g_assert (pthread_equal (wait_entry->thread, pthread_self())); + g_assert (wait_entry->sem != NULL); + LifoJSSemaphore *sem = wait_entry->sem; + gboolean call_success_cb = FALSE; + LifoJSSemaphoreCallbackFn *success_cb = NULL; + uint32_t gchandle = 0; + void *user_data = NULL; + mono_coop_mutex_lock (&sem->mutex); + switch (wait_entry->state) { + case LIFO_JS_SIGNALED: + g_assert (wait_entry->refcount == 2); + emscripten_clear_timeout (wait_entry->js_timeout_id); + /* emscripten safeSetTimeout calls keepalive push which is popped by the timeout + * callback. If we cancel the timeout, we have to pop the keepalive ourselves. */ + emscripten_runtime_keepalive_pop(); + wait_entry->refcount--; + /* fallthru */ + case LIFO_JS_SIGNALED_TIMEOUT_IGNORED: + g_assert (wait_entry->refcount == 1); + lifo_js_wait_entry_unlink (&sem->head, wait_entry); + success_cb = wait_entry->success_cb; + gchandle = wait_entry->gchandle; + user_data = wait_entry->user_data; + g_free (wait_entry); + call_success_cb = TRUE; + break; + case LIFO_JS_WAITING: + default: + g_assert_not_reached(); + } + mono_coop_mutex_unlock (&sem->mutex); + g_assert (call_success_cb); + success_cb (sem, gchandle, user_data); +} + +#endif /* HOST_BROWSER && !DISABLE_THREADS */ diff --git a/src/mono/mono/utils/lifo-semaphore.h b/src/mono/mono/utils/lifo-semaphore.h index 766f41aaaab6d..f636dcc5c4d3f 100644 --- a/src/mono/mono/utils/lifo-semaphore.h +++ b/src/mono/mono/utils/lifo-semaphore.h @@ -31,4 +31,97 @@ mono_lifo_semaphore_timed_wait (LifoSemaphore *semaphore, int32_t timeout_ms); void mono_lifo_semaphore_release (LifoSemaphore *semaphore, uint32_t count); +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) +/* A type of lifo semaphore that can be waited from the JS event loop. + * + * Instead of a blocking timed_wait function, it uses a pair of callbacks: a success callback and a + * timeout callback. The wait function returns immediately and the callbacks will fire on the JS + * event loop when the semaphore is released or the timeout expires. + */ +typedef struct _LifoJSSemaphore LifoJSSemaphore; +/* + * Because the callbacks are asynchronous, it's possible for the same thread to attempt to wait + * multiple times for the same semaphore. For simplicity of reasoning, we dissallow that and + * assert. In principle we could support it, but we haven't implemented that. + */ +typedef struct _LifoJSSemaphoreWaitEntry LifoJSSemaphoreWaitEntry; + +typedef void (*LifoJSSemaphoreCallbackFn)(LifoJSSemaphore *semaphore, uint32_t gch, void *user_data); + +struct _LifoJSSemaphoreWaitEntry { + LifoJSSemaphoreWaitEntry *previous; + LifoJSSemaphoreWaitEntry *next; + LifoJSSemaphoreCallbackFn success_cb; + LifoJSSemaphoreCallbackFn timeout_cb; + LifoJSSemaphore *sem; + void *user_data; + pthread_t waiting_thread; + uint32_t gchandle; // what do we want in here? + int32_t js_timeout_id; // only valid to access from the waiting thread + /* state and refcount are protected by the semaphore mutex */ + uint16_t state; /* 0 waiting, 1 signaled, 2 signaled - timeout ignored */ + uint16_t refcount; /* 1 if waiting, 2 if signaled, 1 if timeout fired while signaled and we're ignoring the timeout */ +}; + +struct _LifoJSSemaphore { + MonoCoopMutex mutex; + LifoSemaphoreWaitEntry *head; + uint32_t pending_signals; +}; + +LifoJSSemaphore * +mono_lifo_js_semaphore_init (void); + +/* what to do with waiters? + * might be kind of academic - we don't expect to destroy these + */ +LifoJSSemaphore * +mono_lifo_js_semahore_delete (void); + +/* + * the timeout_cb is triggered by a JS setTimeout callback + * + * the success_cb is triggered using Emscripten's capability to push async work from one thread to + * another. That means the main thread will need to be able to process JS events (in order to + * assist threads in pushing work from one thread to another) in order for success callbacks to + * function. Emscripten also pumps the async work queues in other circumstances (during sleeps) but + * the main thread still needs to participate. + * + * There's a potential race the implementation needs to be careful about: + * when one thread releases a semaphore and queues the success callback to run, + * while the success callback is in flight, the timeout callback can fire. + * It is important that the called back functions don't destroy the wait entry until either both + * callbacks have fired, or the success callback has a chance to cancel the timeout callback. + * + * We use a refcount to delimit the lifetime of the wait entry. When the wait is created, the + * refcount is 1 and it is notionally owned by the timeout callback. When a sempahore is released, + * the refcount goes to 2. When a continuation fires, it decreases the refcount. If the timeout + * callback fires first if it sees a refcount of 2 it can decrement and return - we know a success + * continuation is in flight and we can allow it to complete. If the refcount is 1 we need to take the semaphore's mutex and remove the wait entry. (With double check locking - the refcount could go up). + * + * When the success continuation fires,it will examine the refcount. If the refcount is 1 at the + * outset, then the cancelation already tried to fire while we were in flight. If the refcount is 2 + * at the outset, then the success contination fired before the timeout, so we can cancel the + * timeout. In either case we can remove the wait entry. + * + * Both the success and timeout code only calls the user provided callbacks after the wait entry is + * destroyed. + * + * FIXME: should we just always use the mutex to protect the wait entry status+refcount? + * + * TODO: when we call emscripten_set_timeout it implicitly calls emscripten_runtime_keepalive_push which is + * popped when the timeout runs. But emscripten_clear_timeout doesn't pop - we need to pop ourselves + */ +void +mono_lifo_js_semaphore_prepare_wait (LifoJSSemaphore *semaphore, int32_t timeout_ms, + LifoJSSemaphoreCallbackFn success_cb, + LifoJSSemaphoreCallbackFn timeout_cb, + uint32_t gchandle, + void *user_data); + +void +mono_lifo_js_semaphore_release (LifoJSSemaphore *semaphore, uint32_t count); + +#endif /* HOST_BROWSER && !DISABLE_THREADS */ + #endif // __MONO_LIFO_SEMAPHORE_H__ From 55b4649f8baaf02b7627ec7c5715af75fd1586ec Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Mon, 20 Mar 2023 16:51:19 -0400 Subject: [PATCH 10/37] Make managed LowLevelJSSemaphore --- .../System.Private.CoreLib.csproj | 1 + .../LowLevelJSSemaphore.Browser.Mono.cs | 88 +++++++++++++++++++ src/mono/mono/metadata/icall-decl.h | 7 ++ src/mono/mono/metadata/icall-def.h | 12 ++- src/mono/mono/metadata/threads.c | 29 ++++++ 5 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 src/mono/System.Private.CoreLib/src/System/Threading/LowLevelJSSemaphore.Browser.Mono.cs diff --git a/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj b/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj index f5cd65c4a54a3..ac5ad6a9d7507 100644 --- a/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj +++ b/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj @@ -282,6 +282,7 @@ + diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelJSSemaphore.Browser.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelJSSemaphore.Browser.Mono.cs new file mode 100644 index 0000000000000..c211eefc28c24 --- /dev/null +++ b/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelJSSemaphore.Browser.Mono.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.Threading; + +// +// This class provides a way for browser threads to asynchronously wait for a sempahore +// from JS, without using the threadpool. It is used to implement threadpool workers. +// +[StructLayout(LayoutOptions.Sequential)] +internal partial class LowLevelJSSemaphore : IDisposable +{ + private IntPtr lifo_semaphore; + + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern IntPtr InitInternal(); + +#pragma warning disable IDE0060 + private void Create(int maximumSignalCount) + { + lifo_semaphore = InitInternal(); + } +#pragma warning restore IDE0060 + + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern void DeleteInternal(IntPtr semaphore); + + public void Dispose() + { + DeleteInternal(lifo_semaphore); + lifo_semaphore = IntPtr.Zero; + } + + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern void ReleaseInternal(IntPtr semaphore, int count); + + internal void Release(int additionalCount) + { + ReleaseInternal(lifo_semaphore, count); + } + + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern void PrepareWaitInternal(IntPtr semaphore, + int timeout_ms, + delegate unmanaged* success_cb, + delegate unmanaged* timeout_cb, + GCHandle object, + IntPtr user_data); + + private class WaitEntry + { + public WaitEntry(LowLevelJSSemaphore semaphore, Action onSuccess, Action onTimeout, object? state) + { + OnSuccess = onSuccess; + OnTimeout = onTimeout; + Semaphore = semaphore; + State = state; + } + public object? State {get; init; } + public Action OnSuccess {get; init;} + public Action OnTimeout {get; init;} + public LowLevelJSSemaphore Semaphore {get; init;} + } + + internal void PrepareWait(int timeout_ms, Action onSuccess, Action onTimeout, object? state) + { + WaitEntry entry = new (this, onSuccess, onTimeout, state); + GCHandle gchandle = GCHandle.Alloc (entry); + PrepareWaitInternal (lifo_semaphore, timeout_ms, &SuccessCallback, &TimeoutCallback, gchandle, IntPtr.Zero); + } + + private static void SuccessCallback(IntPtr lifo_semaphore, GCHandle gchandle, IntPtr user_data) + { + WaitEntry entry = (WaitEntry)gchandle.Target!; + GCHandle.Free(gchandle); + entry.OnSuccess(entry.Semaphore, entry.State); + } + + private static void TimeoutCallback(IntPtr lifo_semaphore, GCHandle gchandle, IntPtr user_data) + { + WaitEntry entry = (WaitEntry)gchandle.Target!; + GCHandle.Free(gchandle); + entry.OnTimeout(entry.Semaphore, entry.State); + } + +} diff --git a/src/mono/mono/metadata/icall-decl.h b/src/mono/mono/metadata/icall-decl.h index 5947224866181..32fa36cebec76 100644 --- a/src/mono/mono/metadata/icall-decl.h +++ b/src/mono/mono/metadata/icall-decl.h @@ -184,6 +184,13 @@ ICALL_EXPORT void ves_icall_System_Threading_LowLevelLifoSemaphore_DeleteInt ICALL_EXPORT gint32 ves_icall_System_Threading_LowLevelLifoSemaphore_TimedWaitInternal (gpointer sem_ptr, gint32 timeout_ms); ICALL_EXPORT void ves_icall_System_Threading_LowLevelLifoSemaphore_ReleaseInternal (gpointer sem_ptr, gint32 count); +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) +ICALL_EXPORT gpointer ves_icall_System_Threading_LowLevelJSSemaphore_InitInternal (void); +ICALL_EXPORT void ves_icall_System_Threading_LowLevelJSSemaphore_DeleteInternal (gpointer sem_ptr); +ICALL_EXPORT gint32 ves_icall_System_Threading_LowLevelJSSemaphore_PrepareWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timeout_cb, gpointer gchandle, gpointer user_data); +ICALL_EXPORT void ves_icall_System_Threading_LowLevelJSSemaphore_ReleaseInternal (gpointer sem_ptr, gint32 count); +#endif + #ifdef TARGET_AMD64 ICALL_EXPORT void ves_icall_System_Runtime_Intrinsics_X86_X86Base___cpuidex (int abcd[4], int function_id, int subfunction_id); #endif diff --git a/src/mono/mono/metadata/icall-def.h b/src/mono/mono/metadata/icall-def.h index 33e6de4f92780..aa978896f8d3b 100644 --- a/src/mono/mono/metadata/icall-def.h +++ b/src/mono/mono/metadata/icall-def.h @@ -567,11 +567,19 @@ NOHANDLES(ICALL(ILOCK_21, "Increment(long&)", ves_icall_System_Threading_Interlo NOHANDLES(ICALL(ILOCK_22, "MemoryBarrierProcessWide", ves_icall_System_Threading_Interlocked_MemoryBarrierProcessWide)) NOHANDLES(ICALL(ILOCK_23, "Read(long&)", ves_icall_System_Threading_Interlocked_Read_Long)) +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) +ICALL_TYPE(JSSEM, "System.Threading.LowLevelJSSemaphore", JSSEM_1) +NOHANDLES(ICALL(JSSEM_1, "DeleteInternal", ves_icall_System_Threading_LowLevelJSSemaphore_DeleteInternal)) +NOHANDLES(ICALL(JSSEM_2, "InitInternal", ves_icall_System_Threading_LowLevelJSSemaphore_InitInternal)) +NOHANDLES(ICALL(JSSEM_3, "ReleaseInternal", ves_icall_System_Threading_LowLevelJSSemaphore_ReleaseInternal)) +NOHANDLES(ICALL(JSSEM_4, "TimedWaitInternal", ves_icall_System_Threading_LowLevelJSSemaphore_TimedWaitInternal)) +#endif + ICALL_TYPE(LIFOSEM, "System.Threading.LowLevelLifoSemaphore", LIFOSEM_1) NOHANDLES(ICALL(LIFOSEM_1, "DeleteInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_DeleteInternal)) NOHANDLES(ICALL(LIFOSEM_2, "InitInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_InitInternal)) -NOHANDLES(ICALL(LIFOSEM_3, "ReleaseInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_ReleaseInternal)) -NOHANDLES(ICALL(LIFOSEM_4, "TimedWaitInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_TimedWaitInternal)) +NOHANDLES(ICALL(LIFOSEM_3, "PrepareWaitInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_PrepareWaitInternal)) +NOHANDLES(ICALL(LIFOSEM_4, "ReleaseInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_ReleaseInternal)) ICALL_TYPE(MONIT, "System.Threading.Monitor", MONIT_0) HANDLES(MONIT_0, "Enter", ves_icall_System_Threading_Monitor_Monitor_Enter, void, 1, (MonoObject)) diff --git a/src/mono/mono/metadata/threads.c b/src/mono/mono/metadata/threads.c index a6bb38f55fea8..469303e06ad47 100644 --- a/src/mono/mono/metadata/threads.c +++ b/src/mono/mono/metadata/threads.c @@ -4963,3 +4963,32 @@ ves_icall_System_Threading_LowLevelLifoSemaphore_ReleaseInternal (gpointer sem_p LifoSemaphore *sem = (LifoSemaphore *)sem_ptr; mono_lifo_semaphore_release (sem, count); } + +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) +gpointer +ves_icall_System_Threading_LowLevelJSSemaphore_InitInternal (void) +{ + return (gpointer)mono_lifo_js_semaphore_init (); +} + +void +ves_icall_System_Threading_LowLevelJSSemaphore_DeleteInternal (gpointer sem_ptr) +{ + LifoJSSemaphore *sem = (LifoSemaphore *)sem_ptr; + mono_lifo_js_semaphore_delete (sem); +} + +gint32 +ves_icall_System_Threading_LowLevelJSSemaphore_TimedWaitInternal (gpointer sem_ptr, gint32 timeout_ms) +{ + LifoJSSemaphore *sem = (LifoJSSemaphore *)sem_ptr; + return mono_lifo_js_semaphore_timed_wait (sem, timeout_ms); +} + +void +ves_icall_System_Threading_LowLevelJSSemaphore_ReleaseInternal (gpointer sem_ptr, gint32 count) +{ + LifoJSSemaphore *sem = (LifoJSSemaphore *)sem_ptr; + mono_lifo_js_semaphore_release (sem, count); +} +#endif /* HOST_BROWSER && !DISABLE_THREADS */ From 8c74deaab2b74110875fe9f44040234c56eec4b3 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Mon, 20 Mar 2023 16:51:33 -0400 Subject: [PATCH 11/37] copy-paste PortableThreadPool.WorkerThread for threaded WASM no changes yet. just copying verbatim to a separate file --- ...dPool.WorkerThread.Browser.Threads.Mono.cs | 290 ++++++++++++++++++ 1 file changed, 290 insertions(+) diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs index 50e4c61ed6f4f..964679fc8a379 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs @@ -12,7 +12,297 @@ internal sealed partial class PortableThreadPool /// private static partial class WorkerThread { + private const int SemaphoreSpinCountDefaultBaseline = 70; + private const int SemaphoreSpinCountDefault = SemaphoreSpinCountDefaultBaseline; + // This value represents an assumption of how much uncommitted stack space a worker thread may use in the future. + // Used in calculations to estimate when to throttle the rate of thread injection to reduce the possibility of + // preexisting threads from running out of memory when using new stack space in low-memory situations. + public const int EstimatedAdditionalStackUsagePerThreadBytes = 64 << 10; // 64 KB + + /// + /// Semaphore for controlling how many threads are currently working. + /// + private static readonly LowLevelLifoSemaphore s_semaphore = + new LowLevelLifoSemaphore( + 0, + MaxPossibleThreadCount, + AppContextConfigHelper.GetInt32Config( + "System.Threading.ThreadPool.UnfairSemaphoreSpinLimit", + SemaphoreSpinCountDefault, + false), + onWait: () => + { + if (NativeRuntimeEventSource.Log.IsEnabled()) + { + NativeRuntimeEventSource.Log.ThreadPoolWorkerThreadWait( + (uint)ThreadPoolInstance._separated.counts.VolatileRead().NumExistingThreads); + } + }); + + private static readonly ThreadStart s_workerThreadStart = WorkerThreadStart; + + private static void WorkerThreadStart() + { + Thread.CurrentThread.SetThreadPoolWorkerThreadName(); + + PortableThreadPool threadPoolInstance = ThreadPoolInstance; + + if (NativeRuntimeEventSource.Log.IsEnabled()) + { + NativeRuntimeEventSource.Log.ThreadPoolWorkerThreadStart( + (uint)threadPoolInstance._separated.counts.VolatileRead().NumExistingThreads); + } + + LowLevelLock threadAdjustmentLock = threadPoolInstance._threadAdjustmentLock; + LowLevelLifoSemaphore semaphore = s_semaphore; + + while (true) + { + bool spinWait = true; + while (semaphore.Wait(ThreadPoolThreadTimeoutMs, spinWait)) + { + bool alreadyRemovedWorkingWorker = false; + while (TakeActiveRequest(threadPoolInstance)) + { + threadPoolInstance._separated.lastDequeueTime = Environment.TickCount; + if (!ThreadPoolWorkQueue.Dispatch()) + { + // ShouldStopProcessingWorkNow() caused the thread to stop processing work, and it would have + // already removed this working worker in the counts. This typically happens when hill climbing + // decreases the worker thread count goal. + alreadyRemovedWorkingWorker = true; + break; + } + + if (threadPoolInstance._separated.numRequestedWorkers <= 0) + { + break; + } + + // In highly bursty cases with short bursts of work, especially in the portable thread pool + // implementation, worker threads are being released and entering Dispatch very quickly, not finding + // much work in Dispatch, and soon afterwards going back to Dispatch, causing extra thrashing on + // data and some interlocked operations, and similarly when the thread pool runs out of work. Since + // there is a pending request for work, introduce a slight delay before serving the next request. + // The spin-wait is mainly for when the sleep is not effective due to there being no other threads + // to schedule. + Thread.UninterruptibleSleep0(); + if (!Environment.IsSingleProcessor) + { + Thread.SpinWait(1); + } + } + + // Don't spin-wait on the semaphore next time if the thread was actively stopped from processing work, + // as it's unlikely that the worker thread count goal would be increased again so soon afterwards that + // the semaphore would be released within the spin-wait window + spinWait = !alreadyRemovedWorkingWorker; + + if (!alreadyRemovedWorkingWorker) + { + // If we woke up but couldn't find a request, or ran out of work items to process, we need to update + // the number of working workers to reflect that we are done working for now + RemoveWorkingWorker(threadPoolInstance); + } + } + + // The thread cannot exit if it has IO pending, otherwise the IO may be canceled + if (IsIOPending) + { + continue; + } + + threadAdjustmentLock.Acquire(); + try + { + // At this point, the thread's wait timed out. We are shutting down this thread. + // We are going to decrement the number of existing threads to no longer include this one + // and then change the max number of threads in the thread pool to reflect that we don't need as many + // as we had. Finally, we are going to tell hill climbing that we changed the max number of threads. + ThreadCounts counts = threadPoolInstance._separated.counts; + while (true) + { + // Since this thread is currently registered as an existing thread, if more work comes in meanwhile, + // this thread would be expected to satisfy the new work. Ensure that NumExistingThreads is not + // decreased below NumProcessingWork, as that would be indicative of such a case. + if (counts.NumExistingThreads <= counts.NumProcessingWork) + { + // In this case, enough work came in that this thread should not time out and should go back to work. + break; + } + + ThreadCounts newCounts = counts; + short newNumExistingThreads = --newCounts.NumExistingThreads; + short newNumThreadsGoal = + Math.Max( + threadPoolInstance.MinThreadsGoal, + Math.Min(newNumExistingThreads, counts.NumThreadsGoal)); + newCounts.NumThreadsGoal = newNumThreadsGoal; + + ThreadCounts oldCounts = + threadPoolInstance._separated.counts.InterlockedCompareExchange(newCounts, counts); + if (oldCounts == counts) + { + HillClimbing.ThreadPoolHillClimber.ForceChange( + newNumThreadsGoal, + HillClimbing.StateOrTransition.ThreadTimedOut); + if (NativeRuntimeEventSource.Log.IsEnabled()) + { + NativeRuntimeEventSource.Log.ThreadPoolWorkerThreadStop((uint)newNumExistingThreads); + } + return; + } + + counts = oldCounts; + } + } + finally + { + threadAdjustmentLock.Release(); + } + } + } + + /// + /// Reduce the number of working workers by one, but maybe add back a worker (possibily this thread) if a thread request comes in while we are marking this thread as not working. + /// + private static void RemoveWorkingWorker(PortableThreadPool threadPoolInstance) + { + // A compare-exchange loop is used instead of Interlocked.Decrement or Interlocked.Add to defensively prevent + // NumProcessingWork from underflowing. See the setter for NumProcessingWork. + ThreadCounts counts = threadPoolInstance._separated.counts; + while (true) + { + ThreadCounts newCounts = counts; + newCounts.NumProcessingWork--; + + ThreadCounts countsBeforeUpdate = + threadPoolInstance._separated.counts.InterlockedCompareExchange(newCounts, counts); + if (countsBeforeUpdate == counts) + { + break; + } + + counts = countsBeforeUpdate; + } + + // It's possible that we decided we had thread requests just before a request came in, + // but reduced the worker count *after* the request came in. In this case, we might + // miss the notification of a thread request. So we wake up a thread (maybe this one!) + // if there is work to do. + if (threadPoolInstance._separated.numRequestedWorkers > 0) + { + MaybeAddWorkingWorker(threadPoolInstance); + } + } + + internal static void MaybeAddWorkingWorker(PortableThreadPool threadPoolInstance) + { + ThreadCounts counts = threadPoolInstance._separated.counts; + short numExistingThreads, numProcessingWork, newNumExistingThreads, newNumProcessingWork; + while (true) + { + numProcessingWork = counts.NumProcessingWork; + if (numProcessingWork >= counts.NumThreadsGoal) + { + return; + } + + newNumProcessingWork = (short)(numProcessingWork + 1); + numExistingThreads = counts.NumExistingThreads; + newNumExistingThreads = Math.Max(numExistingThreads, newNumProcessingWork); + + ThreadCounts newCounts = counts; + newCounts.NumProcessingWork = newNumProcessingWork; + newCounts.NumExistingThreads = newNumExistingThreads; + + ThreadCounts oldCounts = threadPoolInstance._separated.counts.InterlockedCompareExchange(newCounts, counts); + + if (oldCounts == counts) + { + break; + } + + counts = oldCounts; + } + + int toCreate = newNumExistingThreads - numExistingThreads; + int toRelease = newNumProcessingWork - numProcessingWork; + + if (toRelease > 0) + { + s_semaphore.Release(toRelease); + } + + while (toCreate > 0) + { + CreateWorkerThread(); + toCreate--; + } + } + + /// + /// Returns if the current thread should stop processing work on the thread pool. + /// A thread should stop processing work on the thread pool when work remains only when + /// there are more worker threads in the thread pool than we currently want. + /// + /// Whether or not this thread should stop processing work even if there is still work in the queue. + internal static bool ShouldStopProcessingWorkNow(PortableThreadPool threadPoolInstance) + { + ThreadCounts counts = threadPoolInstance._separated.counts; + while (true) + { + // When there are more threads processing work than the thread count goal, it may have been decided + // to decrease the number of threads. Stop processing if the counts can be updated. We may have more + // threads existing than the thread count goal and that is ok, the cold ones will eventually time out if + // the thread count goal is not increased again. This logic is a bit different from the original CoreCLR + // code from which this implementation was ported, which turns a processing thread into a retired thread + // and checks for pending requests like RemoveWorkingWorker. In this implementation there are + // no retired threads, so only the count of threads processing work is considered. + if (counts.NumProcessingWork <= counts.NumThreadsGoal) + { + return false; + } + + ThreadCounts newCounts = counts; + newCounts.NumProcessingWork--; + + ThreadCounts oldCounts = threadPoolInstance._separated.counts.InterlockedCompareExchange(newCounts, counts); + + if (oldCounts == counts) + { + return true; + } + counts = oldCounts; + } + } + + private static bool TakeActiveRequest(PortableThreadPool threadPoolInstance) + { + int count = threadPoolInstance._separated.numRequestedWorkers; + while (count > 0) + { + int prevCount = Interlocked.CompareExchange(ref threadPoolInstance._separated.numRequestedWorkers, count - 1, count); + if (prevCount == count) + { + return true; + } + count = prevCount; + } + return false; + } + + private static void CreateWorkerThread() + { + // Thread pool threads must start in the default execution context without transferring the context, so + // using UnsafeStart() instead of Start() + Thread workerThread = new Thread(s_workerThreadStart); + workerThread.IsThreadPoolThread = true; + workerThread.IsBackground = true; + // thread name will be set in thread proc + workerThread.UnsafeStart(); + } } } } From b323f0eda2b593cfea56768522fc866719e45e01 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 21 Mar 2023 12:09:50 -0400 Subject: [PATCH 12/37] fixup native code for lifo semaphore --- src/mono/mono/metadata/icall-decl.h | 2 +- src/mono/mono/metadata/icall-def.h | 4 ++-- src/mono/mono/metadata/threads.c | 8 ++++---- src/mono/mono/utils/lifo-semaphore.c | 30 +++++++++++++++------------- src/mono/mono/utils/lifo-semaphore.h | 8 ++++---- 5 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/mono/mono/metadata/icall-decl.h b/src/mono/mono/metadata/icall-decl.h index 32fa36cebec76..7c28171130f80 100644 --- a/src/mono/mono/metadata/icall-decl.h +++ b/src/mono/mono/metadata/icall-decl.h @@ -187,7 +187,7 @@ ICALL_EXPORT void ves_icall_System_Threading_LowLevelLifoSemaphore_ReleaseIn #if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) ICALL_EXPORT gpointer ves_icall_System_Threading_LowLevelJSSemaphore_InitInternal (void); ICALL_EXPORT void ves_icall_System_Threading_LowLevelJSSemaphore_DeleteInternal (gpointer sem_ptr); -ICALL_EXPORT gint32 ves_icall_System_Threading_LowLevelJSSemaphore_PrepareWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timeout_cb, gpointer gchandle, gpointer user_data); +ICALL_EXPORT void ves_icall_System_Threading_LowLevelJSSemaphore_PrepareWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timeout_cb, gpointer gchandle, gpointer user_data); ICALL_EXPORT void ves_icall_System_Threading_LowLevelJSSemaphore_ReleaseInternal (gpointer sem_ptr, gint32 count); #endif diff --git a/src/mono/mono/metadata/icall-def.h b/src/mono/mono/metadata/icall-def.h index aa978896f8d3b..6e3d63955eb6e 100644 --- a/src/mono/mono/metadata/icall-def.h +++ b/src/mono/mono/metadata/icall-def.h @@ -572,13 +572,13 @@ ICALL_TYPE(JSSEM, "System.Threading.LowLevelJSSemaphore", JSSEM_1) NOHANDLES(ICALL(JSSEM_1, "DeleteInternal", ves_icall_System_Threading_LowLevelJSSemaphore_DeleteInternal)) NOHANDLES(ICALL(JSSEM_2, "InitInternal", ves_icall_System_Threading_LowLevelJSSemaphore_InitInternal)) NOHANDLES(ICALL(JSSEM_3, "ReleaseInternal", ves_icall_System_Threading_LowLevelJSSemaphore_ReleaseInternal)) -NOHANDLES(ICALL(JSSEM_4, "TimedWaitInternal", ves_icall_System_Threading_LowLevelJSSemaphore_TimedWaitInternal)) +NOHANDLES(ICALL(JSSEM_4, "PrepareWaitInternal", ves_icall_System_Threading_LowLevelJSSemaphore_PrepareWaitInternal)) #endif ICALL_TYPE(LIFOSEM, "System.Threading.LowLevelLifoSemaphore", LIFOSEM_1) NOHANDLES(ICALL(LIFOSEM_1, "DeleteInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_DeleteInternal)) NOHANDLES(ICALL(LIFOSEM_2, "InitInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_InitInternal)) -NOHANDLES(ICALL(LIFOSEM_3, "PrepareWaitInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_PrepareWaitInternal)) +NOHANDLES(ICALL(LIFOSEM_3, "TimedWaitInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_TimedWaitInternal)) NOHANDLES(ICALL(LIFOSEM_4, "ReleaseInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_ReleaseInternal)) ICALL_TYPE(MONIT, "System.Threading.Monitor", MONIT_0) diff --git a/src/mono/mono/metadata/threads.c b/src/mono/mono/metadata/threads.c index 469303e06ad47..59cdf8d932ce8 100644 --- a/src/mono/mono/metadata/threads.c +++ b/src/mono/mono/metadata/threads.c @@ -4974,15 +4974,15 @@ ves_icall_System_Threading_LowLevelJSSemaphore_InitInternal (void) void ves_icall_System_Threading_LowLevelJSSemaphore_DeleteInternal (gpointer sem_ptr) { - LifoJSSemaphore *sem = (LifoSemaphore *)sem_ptr; + LifoJSSemaphore *sem = (LifoJSSemaphore *)sem_ptr; mono_lifo_js_semaphore_delete (sem); } -gint32 -ves_icall_System_Threading_LowLevelJSSemaphore_TimedWaitInternal (gpointer sem_ptr, gint32 timeout_ms) +void +ves_icall_System_Threading_LowLevelJSSemaphore_PrepareWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timedout_cb, gpointer gchandle, gpointer user_data) { LifoJSSemaphore *sem = (LifoJSSemaphore *)sem_ptr; - return mono_lifo_js_semaphore_timed_wait (sem, timeout_ms); + mono_lifo_js_semaphore_prepare_wait (sem, timeout_ms, (LifoJSSemaphoreCallbackFn)success_cb, (LifoJSSemaphoreCallbackFn)timedout_cb, (uint32_t)(MonoGCHandle)gchandle, user_data); } void diff --git a/src/mono/mono/utils/lifo-semaphore.c b/src/mono/mono/utils/lifo-semaphore.c index 146589ba47608..c05b7d6f9fc62 100644 --- a/src/mono/mono/utils/lifo-semaphore.c +++ b/src/mono/mono/utils/lifo-semaphore.c @@ -1,3 +1,5 @@ +#include +#include #include #if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) @@ -93,12 +95,12 @@ mono_lifo_semaphore_release (LifoSemaphore *semaphore, uint32_t count) mono_coop_mutex_unlock (&semaphore->mutex); } -#if defined(HOST_BROWSSER) && !defined(DISABLE_THREADS) +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) LifoJSSemaphore * mono_lifo_js_semaphore_init (void) { - LifoJSSemaphore *sem = g_new0 (LifoJSSemahore, 1); + LifoJSSemaphore *sem = g_new0 (LifoJSSemaphore, 1); if (sem == NULL) return NULL; @@ -113,7 +115,7 @@ mono_lifo_js_semaphore_delete (LifoJSSemaphore *sem) /* FIXME: this is probably hard to guarantee - in-flight signaled semaphores still have wait entries */ g_assert (sem->head == NULL); mono_coop_mutex_destroy (&sem->mutex); - g_free (semaphore); + g_free (sem); } enum { @@ -171,7 +173,7 @@ lifo_js_wait_entry_no_thread (LifoJSSemaphoreWaitEntry *entry, pthread_t cur) { while (entry) { - if (entry->waiting_thread == cur) + if (pthread_equal (entry->thread, cur)) return FALSE; entry = entry->next; } @@ -202,13 +204,13 @@ mono_lifo_js_semaphore_prepare_wait (LifoJSSemaphore *sem, */ g_assert (lifo_js_wait_entry_no_thread(sem->head, cur)); - LifoJSSemaphoreWaitEntry wait_entry = g_new0 (LifoJSSemaphoreWaitEntry, 1); + LifoJSSemaphoreWaitEntry *wait_entry = g_new0 (LifoJSSemaphoreWaitEntry, 1); wait_entry->success_cb = success_cb; wait_entry->timeout_cb = timeout_cb; wait_entry->sem = sem; wait_entry->gchandle = gchandle; wait_entry->user_data = user_data; - wait_entry->waiting_thread = pthread_self(); + wait_entry->thread = pthread_self(); wait_entry->state = LIFO_JS_WAITING; wait_entry->refcount = 1; // timeout owns the wait entry wait_entry->js_timeout_id = emscripten_set_timeout (lifo_js_wait_entry_on_timeout, (double)timeout_ms, &wait_entry); @@ -217,14 +219,14 @@ mono_lifo_js_semaphore_prepare_wait (LifoJSSemaphore *sem, return; } -static void +void mono_lifo_js_semaphore_release (LifoJSSemaphore *sem, uint32_t count) { mono_coop_mutex_lock (&sem->mutex); while (count > 0) { - LifoSemaphoreWaitEntry *wait_entry = lifo_js_find_waiter (sem->head); + LifoJSSemaphoreWaitEntry *wait_entry = lifo_js_find_waiter (sem->head); if (wait_entry != NULL) { /* found one. set its status and queue some work to run on the signaled thread */ pthread_t target = wait_entry->thread; @@ -236,12 +238,12 @@ mono_lifo_js_semaphore_release (LifoJSSemaphore *sem, /* if we're on the same thread, don't run the callback while holding the lock */ emscripten_dispatch_to_thread_async (target, EM_FUNC_SIG_VI, lifo_js_wait_entry_on_success, NULL, wait_entry); } else { - semaphore->pending_signals += count; + sem->pending_signals += count; count = 0; } } - mono_coop_mutex_unlock (&semaphore->mutex); + mono_coop_mutex_unlock (&sem->mutex); } static void @@ -251,9 +253,9 @@ lifo_js_wait_entry_on_timeout (void *wait_entry_as_user_data) g_assert (pthread_equal (wait_entry->thread, pthread_self())); g_assert (wait_entry->sem != NULL); LifoJSSemaphore *sem = wait_entry->sem; - gboolean call_timeout_cb = FALSE;; - LifoJSSemaphoreCallbackFn *timeout_cb = NULL; - uint32_t gchandle gchandle = 0; + gboolean call_timeout_cb = FALSE; + LifoJSSemaphoreCallbackFn timeout_cb = NULL; + uint32_t gchandle = 0; void *user_data = NULL; mono_coop_mutex_lock (&sem->mutex); switch (wait_entry->state) { @@ -293,7 +295,7 @@ lifo_js_wait_entry_on_success (void *wait_entry_as_user_data) g_assert (wait_entry->sem != NULL); LifoJSSemaphore *sem = wait_entry->sem; gboolean call_success_cb = FALSE; - LifoJSSemaphoreCallbackFn *success_cb = NULL; + LifoJSSemaphoreCallbackFn success_cb = NULL; uint32_t gchandle = 0; void *user_data = NULL; mono_coop_mutex_lock (&sem->mutex); diff --git a/src/mono/mono/utils/lifo-semaphore.h b/src/mono/mono/utils/lifo-semaphore.h index f636dcc5c4d3f..fe1fcae71666e 100644 --- a/src/mono/mono/utils/lifo-semaphore.h +++ b/src/mono/mono/utils/lifo-semaphore.h @@ -55,7 +55,7 @@ struct _LifoJSSemaphoreWaitEntry { LifoJSSemaphoreCallbackFn timeout_cb; LifoJSSemaphore *sem; void *user_data; - pthread_t waiting_thread; + pthread_t thread; uint32_t gchandle; // what do we want in here? int32_t js_timeout_id; // only valid to access from the waiting thread /* state and refcount are protected by the semaphore mutex */ @@ -65,7 +65,7 @@ struct _LifoJSSemaphoreWaitEntry { struct _LifoJSSemaphore { MonoCoopMutex mutex; - LifoSemaphoreWaitEntry *head; + LifoJSSemaphoreWaitEntry *head; uint32_t pending_signals; }; @@ -75,8 +75,8 @@ mono_lifo_js_semaphore_init (void); /* what to do with waiters? * might be kind of academic - we don't expect to destroy these */ -LifoJSSemaphore * -mono_lifo_js_semahore_delete (void); +void +mono_lifo_js_semaphore_delete (LifoJSSemaphore *semaphore); /* * the timeout_cb is triggered by a JS setTimeout callback From 51b1fbf9a9de976fb3677859c07aca368dff4230 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 21 Mar 2023 12:10:28 -0400 Subject: [PATCH 13/37] fixup managed code for LowLevelJSSemaphore --- .../LowLevelJSSemaphore.Browser.Mono.cs | 204 +++++++++++++++--- 1 file changed, 175 insertions(+), 29 deletions(-) diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelJSSemaphore.Browser.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelJSSemaphore.Browser.Mono.cs index c211eefc28c24..3c112fa8dc2d9 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelJSSemaphore.Browser.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelJSSemaphore.Browser.Mono.cs @@ -1,6 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace System.Threading; @@ -9,10 +13,33 @@ namespace System.Threading; // This class provides a way for browser threads to asynchronously wait for a sempahore // from JS, without using the threadpool. It is used to implement threadpool workers. // -[StructLayout(LayoutOptions.Sequential)] -internal partial class LowLevelJSSemaphore : IDisposable +internal sealed partial class LowLevelJSSemaphore : IDisposable { + // TODO: implement some of the managed stuff from LowLevelLifoSemaphore private IntPtr lifo_semaphore; + private CacheLineSeparatedCounts _separated; + + private readonly int _maximumSignalCount; + private readonly int _spinCount; + private readonly Action _onWait; + + // private const int SpinSleep0Threshold = 10; + + internal LowLevelJSSemaphore(int initialSignalCount, int maximumSignalCount, int spinCount, Action onWait) + { + Debug.Assert(initialSignalCount >= 0); + Debug.Assert(initialSignalCount <= maximumSignalCount); + Debug.Assert(maximumSignalCount > 0); + Debug.Assert(spinCount >= 0); + + _separated = default; + _separated._counts.SignalCount = (uint)initialSignalCount; + _maximumSignalCount = maximumSignalCount; + _spinCount = spinCount; + _onWait = onWait; + + Create(maximumSignalCount); + } [MethodImpl(MethodImplOptions.InternalCall)] private static extern IntPtr InitInternal(); @@ -38,51 +65,170 @@ public void Dispose() internal void Release(int additionalCount) { - ReleaseInternal(lifo_semaphore, count); + ReleaseInternal(lifo_semaphore, additionalCount); } - + [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void PrepareWaitInternal(IntPtr semaphore, - int timeout_ms, - delegate unmanaged* success_cb, - delegate unmanaged* timeout_cb, - GCHandle object, - IntPtr user_data); - - private class WaitEntry - { - public WaitEntry(LowLevelJSSemaphore semaphore, Action onSuccess, Action onTimeout, object? state) - { - OnSuccess = onSuccess; - OnTimeout = onTimeout; - Semaphore = semaphore; - State = state; - } - public object? State {get; init; } - public Action OnSuccess {get; init;} - public Action OnTimeout {get; init;} - public LowLevelJSSemaphore Semaphore {get; init;} - } + private static extern unsafe void PrepareWaitInternal(IntPtr semaphore, + int timeoutMs, + /*delegate* unmanaged successCallback*/ void* successCallback, + /*delegate* unmanaged timeoutCallback*/ void* timeoutCallback, + GCHandle handle, + IntPtr userData); + + private sealed record WaitEntry (LowLevelJSSemaphore Semaphore, Action OnSuccess, Action OnTimeout, object? State); - internal void PrepareWait(int timeout_ms, Action onSuccess, Action onTimeout, object? state) + internal void PrepareWait(int timeout_ms, Action onSuccess, Action onTimeout, object? state) { WaitEntry entry = new (this, onSuccess, onTimeout, state); GCHandle gchandle = GCHandle.Alloc (entry); - PrepareWaitInternal (lifo_semaphore, timeout_ms, &SuccessCallback, &TimeoutCallback, gchandle, IntPtr.Zero); + unsafe { + delegate* unmanaged successCallback = &SuccessCallback; + delegate* unmanaged timeoutCallback = &TimeoutCallback; + PrepareWaitInternal (lifo_semaphore, timeout_ms, successCallback, timeoutCallback, gchandle, IntPtr.Zero); + } } + [UnmanagedCallersOnly] private static void SuccessCallback(IntPtr lifo_semaphore, GCHandle gchandle, IntPtr user_data) { WaitEntry entry = (WaitEntry)gchandle.Target!; - GCHandle.Free(gchandle); + gchandle.Free(); entry.OnSuccess(entry.Semaphore, entry.State); } + [UnmanagedCallersOnly] private static void TimeoutCallback(IntPtr lifo_semaphore, GCHandle gchandle, IntPtr user_data) { WaitEntry entry = (WaitEntry)gchandle.Target!; - GCHandle.Free(gchandle); + gchandle.Free(); entry.OnTimeout(entry.Semaphore, entry.State); } +#region Counts + private struct Counts : IEquatable + { + private const byte SignalCountShift = 0; + private const byte WaiterCountShift = 32; + private const byte SpinnerCountShift = 48; + private const byte CountOfWaitersSignaledToWakeShift = 56; + + private ulong _data; + + private Counts(ulong data) => _data = data; + + private uint GetUInt32Value(byte shift) => (uint)(_data >> shift); + private void SetUInt32Value(uint value, byte shift) => + _data = (_data & ~((ulong)uint.MaxValue << shift)) | ((ulong)value << shift); + private ushort GetUInt16Value(byte shift) => (ushort)(_data >> shift); + private void SetUInt16Value(ushort value, byte shift) => + _data = (_data & ~((ulong)ushort.MaxValue << shift)) | ((ulong)value << shift); + private byte GetByteValue(byte shift) => (byte)(_data >> shift); + private void SetByteValue(byte value, byte shift) => + _data = (_data & ~((ulong)byte.MaxValue << shift)) | ((ulong)value << shift); + + public uint SignalCount + { + get => GetUInt32Value(SignalCountShift); + set => SetUInt32Value(value, SignalCountShift); + } + + public void AddSignalCount(uint value) + { + Debug.Assert(value <= uint.MaxValue - SignalCount); + _data += (ulong)value << SignalCountShift; + } + + public void IncrementSignalCount() => AddSignalCount(1); + + public void DecrementSignalCount() + { + Debug.Assert(SignalCount != 0); + _data -= (ulong)1 << SignalCountShift; + } + + public ushort WaiterCount + { + get => GetUInt16Value(WaiterCountShift); + set => SetUInt16Value(value, WaiterCountShift); + } + + public void IncrementWaiterCount() + { + Debug.Assert(WaiterCount < ushort.MaxValue); + _data += (ulong)1 << WaiterCountShift; + } + + public void DecrementWaiterCount() + { + Debug.Assert(WaiterCount != 0); + _data -= (ulong)1 << WaiterCountShift; + } + + public void InterlockedDecrementWaiterCount() + { + var countsAfterUpdate = new Counts(Interlocked.Add(ref _data, unchecked((ulong)-1) << WaiterCountShift)); + Debug.Assert(countsAfterUpdate.WaiterCount != ushort.MaxValue); // underflow check + } + + public byte SpinnerCount + { + get => GetByteValue(SpinnerCountShift); + set => SetByteValue(value, SpinnerCountShift); + } + + public void IncrementSpinnerCount() + { + Debug.Assert(SpinnerCount < byte.MaxValue); + _data += (ulong)1 << SpinnerCountShift; + } + + public void DecrementSpinnerCount() + { + Debug.Assert(SpinnerCount != 0); + _data -= (ulong)1 << SpinnerCountShift; + } + + public byte CountOfWaitersSignaledToWake + { + get => GetByteValue(CountOfWaitersSignaledToWakeShift); + set => SetByteValue(value, CountOfWaitersSignaledToWakeShift); + } + + public void AddUpToMaxCountOfWaitersSignaledToWake(uint value) + { + uint availableCount = (uint)(byte.MaxValue - CountOfWaitersSignaledToWake); + if (value > availableCount) + { + value = availableCount; + } + _data += (ulong)value << CountOfWaitersSignaledToWakeShift; + } + + public void DecrementCountOfWaitersSignaledToWake() + { + Debug.Assert(CountOfWaitersSignaledToWake != 0); + _data -= (ulong)1 << CountOfWaitersSignaledToWakeShift; + } + + public Counts InterlockedCompareExchange(Counts newCounts, Counts oldCounts) => + new Counts(Interlocked.CompareExchange(ref _data, newCounts._data, oldCounts._data)); + + public static bool operator ==(Counts lhs, Counts rhs) => lhs.Equals(rhs); + public static bool operator !=(Counts lhs, Counts rhs) => !lhs.Equals(rhs); + + public override bool Equals([NotNullWhen(true)] object? obj) => obj is Counts other && Equals(other); + public bool Equals(Counts other) => _data == other._data; + public override int GetHashCode() => (int)_data + (int)(_data >> 32); + } + + [StructLayout(LayoutKind.Sequential)] + private struct CacheLineSeparatedCounts + { + private readonly Internal.PaddingFor32 _pad1; + public Counts _counts; + private readonly Internal.PaddingFor32 _pad2; + } +#endregion + } From 3727ff196343fcdd27ce2bf012b4bbd0393e29bd Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 21 Mar 2023 12:10:47 -0400 Subject: [PATCH 14/37] Implement PortableThreadPool loop using semaphore callbacks --- ...dPool.WorkerThread.Browser.Threads.Mono.cs | 227 +++++++++++------- 1 file changed, 136 insertions(+), 91 deletions(-) diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs index 964679fc8a379..46bfd0c6c1c4f 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs @@ -23,8 +23,8 @@ private static partial class WorkerThread /// /// Semaphore for controlling how many threads are currently working. /// - private static readonly LowLevelLifoSemaphore s_semaphore = - new LowLevelLifoSemaphore( + private static readonly LowLevelJSSemaphore s_semaphore = + new LowLevelJSSemaphore( 0, MaxPossibleThreadCount, AppContextConfigHelper.GetInt32Config( @@ -42,6 +42,15 @@ private static partial class WorkerThread private static readonly ThreadStart s_workerThreadStart = WorkerThreadStart; + private sealed record SemaphoreWaitState(PortableThreadPool ThreadPoolInstance, LowLevelLock ThreadAdjustmentLock) + { + public bool SpinWait = true; + + public void ResetIteration() { + SpinWait = true; + } + } + private static void WorkerThreadStart() { Thread.CurrentThread.SetThreadPoolWorkerThreadName(); @@ -55,113 +64,149 @@ private static void WorkerThreadStart() } LowLevelLock threadAdjustmentLock = threadPoolInstance._threadAdjustmentLock; - LowLevelLifoSemaphore semaphore = s_semaphore; + SemaphoreWaitState state = new(threadPoolInstance, threadAdjustmentLock) { SpinWait = true }; + WaitForWorkLoop(s_semaphore, state); + } - while (true) - { - bool spinWait = true; - while (semaphore.Wait(ThreadPoolThreadTimeoutMs, spinWait)) - { - bool alreadyRemovedWorkingWorker = false; - while (TakeActiveRequest(threadPoolInstance)) - { - threadPoolInstance._separated.lastDequeueTime = Environment.TickCount; - if (!ThreadPoolWorkQueue.Dispatch()) - { - // ShouldStopProcessingWorkNow() caused the thread to stop processing work, and it would have - // already removed this working worker in the counts. This typically happens when hill climbing - // decreases the worker thread count goal. - alreadyRemovedWorkingWorker = true; - break; - } + private static readonly Action s_WorkLoopSemaphoreSuccess = new(WorkLoopSemaphoreSuccess); + private static readonly Action s_WorkLoopSemaphoreTimedOut = new(WorkLoopSemaphoreTimedOut); - if (threadPoolInstance._separated.numRequestedWorkers <= 0) - { - break; - } + private static void WaitForWorkLoop(LowLevelJSSemaphore semaphore, SemaphoreWaitState state) + { + semaphore.PrepareWait(ThreadPoolThreadTimeoutMs, s_WorkLoopSemaphoreSuccess, s_WorkLoopSemaphoreTimedOut, state); + } - // In highly bursty cases with short bursts of work, especially in the portable thread pool - // implementation, worker threads are being released and entering Dispatch very quickly, not finding - // much work in Dispatch, and soon afterwards going back to Dispatch, causing extra thrashing on - // data and some interlocked operations, and similarly when the thread pool runs out of work. Since - // there is a pending request for work, introduce a slight delay before serving the next request. - // The spin-wait is mainly for when the sleep is not effective due to there being no other threads - // to schedule. - Thread.UninterruptibleSleep0(); - if (!Environment.IsSingleProcessor) - { - Thread.SpinWait(1); - } - } + private static void WorkLoopSemaphoreSuccess(LowLevelJSSemaphore semaphore, object? stateObject) + { + SemaphoreWaitState state = (SemaphoreWaitState)stateObject!; + WorkerDoWork(state.ThreadPoolInstance, ref state.SpinWait); + // Go around the loop one more time, keeping existing mutated state + WaitForWorkLoop(semaphore, state); + } - // Don't spin-wait on the semaphore next time if the thread was actively stopped from processing work, - // as it's unlikely that the worker thread count goal would be increased again so soon afterwards that - // the semaphore would be released within the spin-wait window - spinWait = !alreadyRemovedWorkingWorker; + private static void WorkLoopSemaphoreTimedOut(LowLevelJSSemaphore semaphore, object? stateObject) + { + SemaphoreWaitState state = (SemaphoreWaitState)stateObject!; + if (WorkerTimedOutMaybeStop(state.ThreadPoolInstance, state.ThreadAdjustmentLock)) { + // we're done, kill the thread. + + // emscriptenKeepalivePop(); + // emscriptenThreadExit(); + return; + } else { + // more work showed up while we were shutting down, go around one more time + state.ResetIteration(); + WaitForWorkLoop(semaphore, state); + } + } - if (!alreadyRemovedWorkingWorker) - { - // If we woke up but couldn't find a request, or ran out of work items to process, we need to update - // the number of working workers to reflect that we are done working for now - RemoveWorkingWorker(threadPoolInstance); - } + private static void WorkerDoWork(PortableThreadPool threadPoolInstance, ref bool spinWait) + { + bool alreadyRemovedWorkingWorker = false; + while (TakeActiveRequest(threadPoolInstance)) + { + threadPoolInstance._separated.lastDequeueTime = Environment.TickCount; + if (!ThreadPoolWorkQueue.Dispatch()) + { + // ShouldStopProcessingWorkNow() caused the thread to stop processing work, and it would have + // already removed this working worker in the counts. This typically happens when hill climbing + // decreases the worker thread count goal. + alreadyRemovedWorkingWorker = true; + break; } - // The thread cannot exit if it has IO pending, otherwise the IO may be canceled - if (IsIOPending) + if (threadPoolInstance._separated.numRequestedWorkers <= 0) { - continue; + break; } - threadAdjustmentLock.Acquire(); - try + // In highly bursty cases with short bursts of work, especially in the portable thread pool + // implementation, worker threads are being released and entering Dispatch very quickly, not finding + // much work in Dispatch, and soon afterwards going back to Dispatch, causing extra thrashing on + // data and some interlocked operations, and similarly when the thread pool runs out of work. Since + // there is a pending request for work, introduce a slight delay before serving the next request. + // The spin-wait is mainly for when the sleep is not effective due to there being no other threads + // to schedule. + Thread.UninterruptibleSleep0(); + if (!Environment.IsSingleProcessor) + { + Thread.SpinWait(1); + } + } + + // Don't spin-wait on the semaphore next time if the thread was actively stopped from processing work, + // as it's unlikely that the worker thread count goal would be increased again so soon afterwards that + // the semaphore would be released within the spin-wait window + spinWait = !alreadyRemovedWorkingWorker; + + if (!alreadyRemovedWorkingWorker) + { + // If we woke up but couldn't find a request, or ran out of work items to process, we need to update + // the number of working workers to reflect that we are done working for now + RemoveWorkingWorker(threadPoolInstance); + } + } + + // returns true if we shouldn't re-queue for another spin + // returns false if the worker is shutting down + private static bool WorkerTimedOutMaybeStop (PortableThreadPool threadPoolInstance, LowLevelLock threadAdjustmentLock) + { + // The thread cannot exit if it has IO pending, otherwise the IO may be canceled + if (IsIOPending) + { + return false; + } + + threadAdjustmentLock.Acquire(); + try + { + // At this point, the thread's wait timed out. We are shutting down this thread. + // We are going to decrement the number of existing threads to no longer include this one + // and then change the max number of threads in the thread pool to reflect that we don't need as many + // as we had. Finally, we are going to tell hill climbing that we changed the max number of threads. + ThreadCounts counts = threadPoolInstance._separated.counts; + while (true) { - // At this point, the thread's wait timed out. We are shutting down this thread. - // We are going to decrement the number of existing threads to no longer include this one - // and then change the max number of threads in the thread pool to reflect that we don't need as many - // as we had. Finally, we are going to tell hill climbing that we changed the max number of threads. - ThreadCounts counts = threadPoolInstance._separated.counts; - while (true) + // Since this thread is currently registered as an existing thread, if more work comes in meanwhile, + // this thread would be expected to satisfy the new work. Ensure that NumExistingThreads is not + // decreased below NumProcessingWork, as that would be indicative of such a case. + if (counts.NumExistingThreads <= counts.NumProcessingWork) { - // Since this thread is currently registered as an existing thread, if more work comes in meanwhile, - // this thread would be expected to satisfy the new work. Ensure that NumExistingThreads is not - // decreased below NumProcessingWork, as that would be indicative of such a case. - if (counts.NumExistingThreads <= counts.NumProcessingWork) - { - // In this case, enough work came in that this thread should not time out and should go back to work. - break; - } + // In this case, enough work came in that this thread should not time out and should go back to work. + break; + } - ThreadCounts newCounts = counts; - short newNumExistingThreads = --newCounts.NumExistingThreads; - short newNumThreadsGoal = - Math.Max( - threadPoolInstance.MinThreadsGoal, - Math.Min(newNumExistingThreads, counts.NumThreadsGoal)); - newCounts.NumThreadsGoal = newNumThreadsGoal; - - ThreadCounts oldCounts = - threadPoolInstance._separated.counts.InterlockedCompareExchange(newCounts, counts); - if (oldCounts == counts) + ThreadCounts newCounts = counts; + short newNumExistingThreads = --newCounts.NumExistingThreads; + short newNumThreadsGoal = + Math.Max( + threadPoolInstance.MinThreadsGoal, + Math.Min(newNumExistingThreads, counts.NumThreadsGoal)); + newCounts.NumThreadsGoal = newNumThreadsGoal; + + ThreadCounts oldCounts = + threadPoolInstance._separated.counts.InterlockedCompareExchange(newCounts, counts); + if (oldCounts == counts) + { + HillClimbing.ThreadPoolHillClimber.ForceChange( + newNumThreadsGoal, + HillClimbing.StateOrTransition.ThreadTimedOut); + if (NativeRuntimeEventSource.Log.IsEnabled()) { - HillClimbing.ThreadPoolHillClimber.ForceChange( - newNumThreadsGoal, - HillClimbing.StateOrTransition.ThreadTimedOut); - if (NativeRuntimeEventSource.Log.IsEnabled()) - { - NativeRuntimeEventSource.Log.ThreadPoolWorkerThreadStop((uint)newNumExistingThreads); - } - return; + NativeRuntimeEventSource.Log.ThreadPoolWorkerThreadStop((uint)newNumExistingThreads); } - - counts = oldCounts; + return true; } - } - finally - { - threadAdjustmentLock.Release(); + + counts = oldCounts; } } + finally + { + threadAdjustmentLock.Release(); + } + // if we get here new work came in and we're going to keep running + return false; } /// From 763edab3835f730863c0fa01466e6a6df59acfa8 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 22 Mar 2023 12:02:11 -0400 Subject: [PATCH 15/37] manage emscripten event loop from PortableThreadPool.WorkerThread make sure to keep the thread alive after setting up the semaphore wait. Cleanup the thread when exiting --- ...dPool.WorkerThread.Browser.Threads.Mono.cs | 22 ++++++++++++++++-- src/mono/mono/metadata/icall-decl.h | 4 ++++ src/mono/mono/metadata/icall-def.h | 12 ++++++++-- src/mono/mono/metadata/threads.c | 23 +++++++++++++++++++ 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs index 46bfd0c6c1c4f..641a25c237707 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Tracing; +using System.Runtime.CompilerServices; namespace System.Threading { @@ -65,9 +67,25 @@ private static void WorkerThreadStart() LowLevelLock threadAdjustmentLock = threadPoolInstance._threadAdjustmentLock; SemaphoreWaitState state = new(threadPoolInstance, threadAdjustmentLock) { SpinWait = true }; + // set up the callbacks for semaphore waits, tell + // emscripten to keep the thread alive, and return to + // the JS event loop. WaitForWorkLoop(s_semaphore, state); + EmscriptenKeepalivePush(); + EmscriptenUnwindToJsEventLoop(); } + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern void EmscriptenKeepalivePush(); + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern void EmscriptenKeepalivePop(); + [DoesNotReturn] + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern void EmscriptenUnwindToJsEventLoop(); + [DoesNotReturn] + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern void MonoThreadExit(); // FIXME: this is not Emscripten, it's just mono_thread_exit(); + private static readonly Action s_WorkLoopSemaphoreSuccess = new(WorkLoopSemaphoreSuccess); private static readonly Action s_WorkLoopSemaphoreTimedOut = new(WorkLoopSemaphoreTimedOut); @@ -90,8 +108,8 @@ private static void WorkLoopSemaphoreTimedOut(LowLevelJSSemaphore semaphore, obj if (WorkerTimedOutMaybeStop(state.ThreadPoolInstance, state.ThreadAdjustmentLock)) { // we're done, kill the thread. - // emscriptenKeepalivePop(); - // emscriptenThreadExit(); + EmscriptenKeepalivePop(); + MonoThreadExit(); return; } else { // more work showed up while we were shutting down, go around one more time diff --git a/src/mono/mono/metadata/icall-decl.h b/src/mono/mono/metadata/icall-decl.h index 7c28171130f80..b25451313ded9 100644 --- a/src/mono/mono/metadata/icall-decl.h +++ b/src/mono/mono/metadata/icall-decl.h @@ -189,6 +189,10 @@ ICALL_EXPORT gpointer ves_icall_System_Threading_LowLevelJSSemaphore_InitInterna ICALL_EXPORT void ves_icall_System_Threading_LowLevelJSSemaphore_DeleteInternal (gpointer sem_ptr); ICALL_EXPORT void ves_icall_System_Threading_LowLevelJSSemaphore_PrepareWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timeout_cb, gpointer gchandle, gpointer user_data); ICALL_EXPORT void ves_icall_System_Threading_LowLevelJSSemaphore_ReleaseInternal (gpointer sem_ptr, gint32 count); + +ICALL_EXPORT void ves_icall_System_Threading_PortableThreadPool_WorkerThread_EmscriptenKeepalivePop (void); +ICALL_EXPORT void ves_icall_System_Threading_PortableThreadPool_WorkerThread_EmscriptenKeepalivePush (void); +ICALL_EXPORT void ves_icall_System_Threading_PortableThreadPool_WorkerThread_EmscriptenUnwindToJsEventLoop (void); #endif #ifdef TARGET_AMD64 diff --git a/src/mono/mono/metadata/icall-def.h b/src/mono/mono/metadata/icall-def.h index 6e3d63955eb6e..00c9ce7c33836 100644 --- a/src/mono/mono/metadata/icall-def.h +++ b/src/mono/mono/metadata/icall-def.h @@ -571,8 +571,8 @@ NOHANDLES(ICALL(ILOCK_23, "Read(long&)", ves_icall_System_Threading_Interlocked_ ICALL_TYPE(JSSEM, "System.Threading.LowLevelJSSemaphore", JSSEM_1) NOHANDLES(ICALL(JSSEM_1, "DeleteInternal", ves_icall_System_Threading_LowLevelJSSemaphore_DeleteInternal)) NOHANDLES(ICALL(JSSEM_2, "InitInternal", ves_icall_System_Threading_LowLevelJSSemaphore_InitInternal)) -NOHANDLES(ICALL(JSSEM_3, "ReleaseInternal", ves_icall_System_Threading_LowLevelJSSemaphore_ReleaseInternal)) -NOHANDLES(ICALL(JSSEM_4, "PrepareWaitInternal", ves_icall_System_Threading_LowLevelJSSemaphore_PrepareWaitInternal)) +NOHANDLES(ICALL(JSSEM_3, "PrepareWaitInternal", ves_icall_System_Threading_LowLevelJSSemaphore_PrepareWaitInternal)) +NOHANDLES(ICALL(JSSEM_4, "ReleaseInternal", ves_icall_System_Threading_LowLevelJSSemaphore_ReleaseInternal)) #endif ICALL_TYPE(LIFOSEM, "System.Threading.LowLevelLifoSemaphore", LIFOSEM_1) @@ -590,6 +590,14 @@ HANDLES(MONIT_7, "Monitor_wait", ves_icall_System_Threading_Monitor_Monitor_wait NOHANDLES(ICALL(MONIT_8, "get_LockContentionCount", ves_icall_System_Threading_Monitor_Monitor_LockContentionCount)) HANDLES(MONIT_9, "try_enter_with_atomic_var", ves_icall_System_Threading_Monitor_Monitor_try_enter_with_atomic_var, void, 4, (MonoObject, guint32, MonoBoolean, MonoBoolean_ref)) +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) +ICALL_TYPE(TPOOL_WORKER, "System.Threading.PortableThreadPool/WorkerThread", TPOOL_WORKER_1) +NOHANDLES(ICALL(TPOOL_WORKER_1, "EmscriptenKeepalivePop", ves_icall_System_Threading_PortableThreadPool_WorkerThread_EmscriptenKeepalivePop)) +NOHANDLES(ICALL(TPOOL_WORKER_2, "EmscriptenKeepalivePush", ves_icall_System_Threading_PortableThreadPool_WorkerThread_EmscriptenKeepalivePush)) +NOHANDLES(ICALL(TPOOL_WORKER_3, "EmscriptenUnwindToJsEventLoop", ves_icall_System_Threading_PortableThreadPool_WorkerThread_EmscriptenUnwindToJsEventLoop)) +NOHANDLES(ICALL(TPOOL_WORKER_4, "MonoThreadExit", mono_thread_exit)) +#endif + ICALL_TYPE(THREAD, "System.Threading.Thread", THREAD_1) HANDLES(THREAD_1, "ClrState", ves_icall_System_Threading_Thread_ClrState, void, 2, (MonoInternalThread, guint32)) HANDLES(ITHREAD_2, "FreeInternal", ves_icall_System_Threading_InternalThread_Thread_free_internal, void, 1, (MonoInternalThread)) diff --git a/src/mono/mono/metadata/threads.c b/src/mono/mono/metadata/threads.c index 59cdf8d932ce8..6f3df54ee46d1 100644 --- a/src/mono/mono/metadata/threads.c +++ b/src/mono/mono/metadata/threads.c @@ -91,6 +91,10 @@ mono_native_thread_join_handle (HANDLE thread_handle, gboolean close_handle); #include #endif +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) +#include +#endif + #include "icall-decl.h" /*#define THREAD_DEBUG(a) do { a; } while (0)*/ @@ -4991,4 +4995,23 @@ ves_icall_System_Threading_LowLevelJSSemaphore_ReleaseInternal (gpointer sem_ptr LifoJSSemaphore *sem = (LifoJSSemaphore *)sem_ptr; mono_lifo_js_semaphore_release (sem, count); } + +void +ves_icall_System_Threading_PortableThreadPool_WorkerThread_EmscriptenKeepalivePop (void) +{ + emscripten_runtime_keepalive_pop(); +} + +void +ves_icall_System_Threading_PortableThreadPool_WorkerThread_EmscriptenKeepalivePush (void) +{ + emscripten_runtime_keepalive_push(); +} + +void +ves_icall_System_Threading_PortableThreadPool_WorkerThread_EmscriptenUnwindToJsEventLoop (void) +{ + emscripten_unwind_to_js_event_loop(); +} + #endif /* HOST_BROWSER && !DISABLE_THREADS */ From b45d001d82e4775441640ed1d9252473338ebe62 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 22 Mar 2023 16:33:29 -0400 Subject: [PATCH 16/37] FIXME: thread equality assertion in timeout callback --- src/mono/mono/utils/lifo-semaphore.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/mono/utils/lifo-semaphore.c b/src/mono/mono/utils/lifo-semaphore.c index c05b7d6f9fc62..1cbf9cd248044 100644 --- a/src/mono/mono/utils/lifo-semaphore.c +++ b/src/mono/mono/utils/lifo-semaphore.c @@ -250,7 +250,7 @@ static void lifo_js_wait_entry_on_timeout (void *wait_entry_as_user_data) { LifoJSSemaphoreWaitEntry *wait_entry = (LifoJSSemaphoreWaitEntry *)wait_entry_as_user_data; - g_assert (pthread_equal (wait_entry->thread, pthread_self())); + g_assert (pthread_equal (wait_entry->thread, pthread_self())); // FIXME: failing here sometimes - thread already exited? g_assert (wait_entry->sem != NULL); LifoJSSemaphore *sem = wait_entry->sem; gboolean call_timeout_cb = FALSE; From 1f7620c74f3a586c20154b6d9a93c9488672f514 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 22 Mar 2023 16:34:01 -0400 Subject: [PATCH 17/37] XXX REVERT ME - minimal async timeout test --- .../wasm/browser-threads-minimal/Program.cs | 45 ++++++++++++++++++- .../wasm/browser-threads-minimal/main.js | 29 +++++++----- 2 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/mono/sample/wasm/browser-threads-minimal/Program.cs b/src/mono/sample/wasm/browser-threads-minimal/Program.cs index 0b9784836bbd7..a008351959b7f 100644 --- a/src/mono/sample/wasm/browser-threads-minimal/Program.cs +++ b/src/mono/sample/wasm/browser-threads-minimal/Program.cs @@ -18,6 +18,30 @@ public static int Main(string[] args) return 0; } + [JSImport("globalThis.setTimeout")] + static partial void GlobalThisSetTimeout([JSMarshalAs] Action cb, int timeoutMs); + + [JSExport] + public static async Task Hello() + { + var t = Task.Run(TimeOutThenComplete); + await t; + Console.WriteLine ($"XYZ: Main Thread caught task tid:{Thread.CurrentThread.ManagedThreadId}"); + } + + private static async Task TimeOutThenComplete() + { + var tcs = new TaskCompletionSource(); + Console.WriteLine ($"XYZ: Task running tid:{Thread.CurrentThread.ManagedThreadId}"); + GlobalThisSetTimeout(() => { + tcs.SetResult(); + Console.WriteLine ($"XYZ: Timeout fired tid:{Thread.CurrentThread.ManagedThreadId}"); + }, 250); + Console.WriteLine ($"XYZ: Task sleeping tid:{Thread.CurrentThread.ManagedThreadId}"); + await tcs.Task; + Console.WriteLine ($"XYZ: Task resumed tid:{Thread.CurrentThread.ManagedThreadId}"); + } + [JSExport] public static async Task RunBackgroundThreadCompute() { @@ -41,10 +65,27 @@ public static async Task RunBackgroundLongRunningTaskCompute() return await t; } + [JSExport] + public static async Task RunBackgroundTaskRunCompute() + { + var t1 = Task.Run (() => { + var n = CountingCollatzTest(); + return n; + }); + var t2 = Task.Run (() => { + var n = CountingCollatzTest(); + return n; + }); + var rs = await Task.WhenAll (new [] { t1, t2 }); + if (rs[0] != rs[1]) + throw new Exception ($"Results from two tasks {rs[0]}, {rs[1]}, differ"); + return rs[0]; + } + public static int CountingCollatzTest() { const int limit = 5000; - const int maxInput = 500_000; + const int maxInput = 200_000; int bigly = 0; int hugely = 0; int maxSteps = 0; @@ -60,7 +101,7 @@ public static int CountingCollatzTest() Console.WriteLine ($"Bigly: {bigly}, Hugely: {hugely}, maxSteps: {maxSteps}"); - if (bigly == 241677 && hugely == 0 && maxSteps == 448) + if (bigly == 86187 && hugely == 0 && maxSteps == 382) return 524; else return 0; diff --git a/src/mono/sample/wasm/browser-threads-minimal/main.js b/src/mono/sample/wasm/browser-threads-minimal/main.js index f607d96c2846a..12cf791a005ff 100644 --- a/src/mono/sample/wasm/browser-threads-minimal/main.js +++ b/src/mono/sample/wasm/browser-threads-minimal/main.js @@ -15,18 +15,23 @@ try { const exports = await getAssemblyExports(assemblyName); - const r1 = await exports.Sample.Test.RunBackgroundThreadCompute(); - if (r1 !== 524) { - const msg = `Unexpected result ${r1} from RunBackgroundThreadCompute()`; - document.getElementById("out").innerHTML = msg; - throw new Error(msg); - } - const r2 = await exports.Sample.Test.RunBackgroundLongRunningTaskCompute(); - if (r2 !== 524) { - const msg = `Unexpected result ${r2} from RunBackgorundLongRunningTaskCompute()`; - document.getElementById("out").innerHTML = msg; - throw new Error(msg); - } + console.log ("XYZ: running hello"); + await exports.Sample.Test.Hello(); + console.log ("XYZ: hello done"); + + console.log ("XYZ: running hello"); + await exports.Sample.Test.Hello(); + console.log ("XYZ: hello done"); + + //console.log ("HHH: running TaskRunCompute"); + //const r1 = await exports.Sample.Test.RunBackgroundTaskRunCompute(); + //if (r1 !== 524) { + // const msg = `Unexpected result ${r1} from RunBackgorundTaskRunCompute()`; + // document.getElementById("out").innerHTML = msg; + // throw new Error(msg); + //} + //console.log ("HHH: TaskRunCompute done"); + let exit_code = await runMain(assemblyName, []); exit(exit_code); From 06a3cc1977af2cdb939e1676547416e9fd84b153 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 23 Mar 2023 12:47:15 -0400 Subject: [PATCH 18/37] BUGFIX: &wait_entry ===> wait_entry --- src/mono/mono/utils/lifo-semaphore.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/mono/utils/lifo-semaphore.c b/src/mono/mono/utils/lifo-semaphore.c index 1cbf9cd248044..03e8dbcaba8c6 100644 --- a/src/mono/mono/utils/lifo-semaphore.c +++ b/src/mono/mono/utils/lifo-semaphore.c @@ -213,7 +213,7 @@ mono_lifo_js_semaphore_prepare_wait (LifoJSSemaphore *sem, wait_entry->thread = pthread_self(); wait_entry->state = LIFO_JS_WAITING; wait_entry->refcount = 1; // timeout owns the wait entry - wait_entry->js_timeout_id = emscripten_set_timeout (lifo_js_wait_entry_on_timeout, (double)timeout_ms, &wait_entry); + wait_entry->js_timeout_id = emscripten_set_timeout (lifo_js_wait_entry_on_timeout, (double)timeout_ms, wait_entry); lifo_js_wait_entry_push (&sem->head, wait_entry); mono_coop_mutex_unlock (&sem->mutex); return; From 85743682d9a39ae5c396558929974d6628f1a74b Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 23 Mar 2023 13:39:54 -0400 Subject: [PATCH 19/37] nit: log thread id as hex in .ts Match the C logging for easier grepping --- src/mono/wasm/runtime/pthreads/worker/index.ts | 4 ++-- src/mono/wasm/runtime/startup.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mono/wasm/runtime/pthreads/worker/index.ts b/src/mono/wasm/runtime/pthreads/worker/index.ts index da4c780804c55..71a6f3bcfbe7e 100644 --- a/src/mono/wasm/runtime/pthreads/worker/index.ts +++ b/src/mono/wasm/runtime/pthreads/worker/index.ts @@ -114,7 +114,7 @@ function onMonoConfigReceived(config: MonoConfigInternal): void { export function mono_wasm_pthread_on_pthread_attached(pthread_id: pthread_ptr): void { const self = pthread_self; mono_assert(self !== null && self.pthread_id == pthread_id, "expected pthread_self to be set already when attaching"); - console.debug("MONO_WASM: attaching pthread to runtime", pthread_id); + console.debug("MONO_WASM: attaching pthread to runtime 0x" + pthread_id.toString(16)); preRunWorker(); currentWorkerThreadEvents.dispatchEvent(makeWorkerThreadEvent(dotnetPthreadAttached, self)); } @@ -127,7 +127,7 @@ export function afterThreadInitTLS(): void { if (ENVIRONMENT_IS_PTHREAD) { const pthread_ptr = (Module)["_pthread_self"](); mono_assert(!is_nullish(pthread_ptr), "pthread_self() returned null"); - console.debug("MONO_WASM: after thread init, pthread ptr", pthread_ptr); + console.debug("MONO_WASM: after thread init, pthread ptr 0x" + pthread_ptr.toString(16)); const self = setupChannelToMainThread(pthread_ptr); currentWorkerThreadEvents.dispatchEvent(makeWorkerThreadEvent(dotnetPthreadCreated, self)); } diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index d1a76cf7f5a78..8d807991363b8 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -748,7 +748,7 @@ export async function mono_wasm_pthread_worker_init(module: DotnetModule, export pthreads_worker.setupPreloadChannelToMainThread(); // This is a good place for subsystems to attach listeners for pthreads_worker.currentWorkerThreadEvents pthreads_worker.currentWorkerThreadEvents.addEventListener(pthreads_worker.dotnetPthreadCreated, (ev) => { - console.debug("MONO_WASM: pthread created", ev.pthread_self.pthread_id); + console.debug("MONO_WASM: pthread created 0x" + ev.pthread_self.pthread_id.toString(16)); }); // this is the only event which is called on worker From c86cb2624b1e99f9e565d0843f2895e37639403f Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 23 Mar 2023 14:43:43 -0400 Subject: [PATCH 20/37] XXX minimal sample - fetch on a background thread works --- .../wasm/browser-threads-minimal/Program.cs | 29 +++++++++++++++++++ ...Wasm.Browser.Threads.Minimal.Sample.csproj | 2 ++ .../wasm/browser-threads-minimal/blurst.txt | 1 + .../browser-threads-minimal/fetchhelper.js | 4 +++ .../wasm/browser-threads-minimal/main.js | 15 ++++++---- 5 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 src/mono/sample/wasm/browser-threads-minimal/blurst.txt create mode 100644 src/mono/sample/wasm/browser-threads-minimal/fetchhelper.js diff --git a/src/mono/sample/wasm/browser-threads-minimal/Program.cs b/src/mono/sample/wasm/browser-threads-minimal/Program.cs index a008351959b7f..b188a745e6986 100644 --- a/src/mono/sample/wasm/browser-threads-minimal/Program.cs +++ b/src/mono/sample/wasm/browser-threads-minimal/Program.cs @@ -21,6 +21,9 @@ public static int Main(string[] args) [JSImport("globalThis.setTimeout")] static partial void GlobalThisSetTimeout([JSMarshalAs] Action cb, int timeoutMs); + [JSImport("globalThis.fetch")] + private static partial Task GlobalThisFetch(string url); + [JSExport] public static async Task Hello() { @@ -29,6 +32,32 @@ public static async Task Hello() Console.WriteLine ($"XYZ: Main Thread caught task tid:{Thread.CurrentThread.ManagedThreadId}"); } + const string fetchhelper = "./fetchelper.js"; + + [JSImport("responseText", fetchhelper)] + private static partial Task FetchHelperResponseText(JSObject response); + + [JSExport] + public static async Task FetchBackground(string url) + { + var t = Task.Run(async () => + { + await JSHost.ImportAsync(fetchhelper, "./fetchhelper.js"); + var r = await GlobalThisFetch(url); + var ok = (bool)r.GetPropertyAsBoolean("ok"); + + Console.WriteLine($"XYZ: FetchBackground fetch returned to thread:{Thread.CurrentThread.ManagedThreadId}, ok: {ok}"); + if (ok) + { + var text = await FetchHelperResponseText(r); + Console.WriteLine($"XYZ: FetchBackground fetch returned to thread:{Thread.CurrentThread.ManagedThreadId}, text: {text}"); + } + return ok; + }); + await t; + Console.WriteLine($"XYZ: FetchBackground thread:{Thread.CurrentThread.ManagedThreadId} background thread returned"); + } + private static async Task TimeOutThenComplete() { var tcs = new TaskCompletionSource(); diff --git a/src/mono/sample/wasm/browser-threads-minimal/Wasm.Browser.Threads.Minimal.Sample.csproj b/src/mono/sample/wasm/browser-threads-minimal/Wasm.Browser.Threads.Minimal.Sample.csproj index f9c81f4b40e71..defce7521ac7f 100644 --- a/src/mono/sample/wasm/browser-threads-minimal/Wasm.Browser.Threads.Minimal.Sample.csproj +++ b/src/mono/sample/wasm/browser-threads-minimal/Wasm.Browser.Threads.Minimal.Sample.csproj @@ -6,6 +6,8 @@ + + diff --git a/src/mono/sample/wasm/browser-threads-minimal/blurst.txt b/src/mono/sample/wasm/browser-threads-minimal/blurst.txt new file mode 100644 index 0000000000000..6679d914da1c7 --- /dev/null +++ b/src/mono/sample/wasm/browser-threads-minimal/blurst.txt @@ -0,0 +1 @@ +It was the best of times, it was the blurst of times. diff --git a/src/mono/sample/wasm/browser-threads-minimal/fetchhelper.js b/src/mono/sample/wasm/browser-threads-minimal/fetchhelper.js new file mode 100644 index 0000000000000..0ed160165e926 --- /dev/null +++ b/src/mono/sample/wasm/browser-threads-minimal/fetchhelper.js @@ -0,0 +1,4 @@ + +export function responseText(response) /* Promise */ { + return response.text(); +} diff --git a/src/mono/sample/wasm/browser-threads-minimal/main.js b/src/mono/sample/wasm/browser-threads-minimal/main.js index 12cf791a005ff..373ef3f836c64 100644 --- a/src/mono/sample/wasm/browser-threads-minimal/main.js +++ b/src/mono/sample/wasm/browser-threads-minimal/main.js @@ -15,14 +15,17 @@ try { const exports = await getAssemblyExports(assemblyName); - console.log ("XYZ: running hello"); - await exports.Sample.Test.Hello(); - console.log ("XYZ: hello done"); + //console.log ("XYZ: running hello"); + //await exports.Sample.Test.Hello(); + //console.log ("XYZ: hello done"); - console.log ("XYZ: running hello"); - await exports.Sample.Test.Hello(); - console.log ("XYZ: hello done"); + console.log ("XYZ: running FetchBackground"); + await exports.Sample.Test.FetchBackground("./blurst.txt"); + console.log ("XYZ: FetchBackground done"); + console.log ("XYZ: running FetchBackground(missing)"); + await exports.Sample.Test.FetchBackground("./missing.txt"); + console.log ("XYZ: FetchBackground(missing) done"); //console.log ("HHH: running TaskRunCompute"); //const r1 = await exports.Sample.Test.RunBackgroundTaskRunCompute(); //if (r1 !== 524) { From c3cad7473cb4f62279dbc6a43fd36e6773c1f2d6 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 24 Mar 2023 11:13:54 -0400 Subject: [PATCH 21/37] fix non-wasm non-threads builds --- src/mono/mono/metadata/icall-def.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mono/mono/metadata/icall-def.h b/src/mono/mono/metadata/icall-def.h index 00c9ce7c33836..9ad56254fc2d6 100644 --- a/src/mono/mono/metadata/icall-def.h +++ b/src/mono/mono/metadata/icall-def.h @@ -578,8 +578,8 @@ NOHANDLES(ICALL(JSSEM_4, "ReleaseInternal", ves_icall_System_Threading_LowLevelJ ICALL_TYPE(LIFOSEM, "System.Threading.LowLevelLifoSemaphore", LIFOSEM_1) NOHANDLES(ICALL(LIFOSEM_1, "DeleteInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_DeleteInternal)) NOHANDLES(ICALL(LIFOSEM_2, "InitInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_InitInternal)) -NOHANDLES(ICALL(LIFOSEM_3, "TimedWaitInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_TimedWaitInternal)) -NOHANDLES(ICALL(LIFOSEM_4, "ReleaseInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_ReleaseInternal)) +NOHANDLES(ICALL(LIFOSEM_3, "ReleaseInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_ReleaseInternal)) +NOHANDLES(ICALL(LIFOSEM_4, "TimedWaitInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_TimedWaitInternal)) ICALL_TYPE(MONIT, "System.Threading.Monitor", MONIT_0) HANDLES(MONIT_0, "Enter", ves_icall_System_Threading_Monitor_Monitor_Enter, void, 1, (MonoObject)) From e57b262757a5db37943ad1f63bb94eb87ca32af4 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 24 Mar 2023 12:10:10 -0400 Subject: [PATCH 22/37] Add WebWorkerEventLoop internal class to managed event loop keepalive Don't explicitly call UnwindToJs as it doesn't know about managed code. Also avoid mono_thread_exit as that also won't necessarily clean up after the interpreter --- .../System.Private.CoreLib.csproj | 1 + ...dPool.WorkerThread.Browser.Threads.Mono.cs | 21 ++----- ...WebWorkerEventLoop.Browser.Threads.Mono.cs | 59 +++++++++++++++++++ src/mono/mono/metadata/icall-decl.h | 5 +- src/mono/mono/metadata/icall-def.h | 14 ++--- src/mono/mono/metadata/threads.c | 12 +--- 6 files changed, 77 insertions(+), 35 deletions(-) create mode 100644 src/mono/System.Private.CoreLib/src/System/Threading/WebWorkerEventLoop.Browser.Threads.Mono.cs diff --git a/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj b/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj index ac5ad6a9d7507..d2a6df7f6f9b7 100644 --- a/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj +++ b/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj @@ -283,6 +283,7 @@ + diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs index 641a25c237707..12d762fb7994e 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs @@ -71,21 +71,11 @@ private static void WorkerThreadStart() // emscripten to keep the thread alive, and return to // the JS event loop. WaitForWorkLoop(s_semaphore, state); - EmscriptenKeepalivePush(); - EmscriptenUnwindToJsEventLoop(); + WebWorkerEventLoop.KeepalivePush(); + // return from thread start with keepalive - the thread will stay alive in the JS event loop + // WebWorkerEventLoop.UnwindToJs(); // FIXME: this is a bad idea - it doesn't run C# finally clauses and maybe leaks Mono interpreter frames } - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void EmscriptenKeepalivePush(); - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void EmscriptenKeepalivePop(); - [DoesNotReturn] - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void EmscriptenUnwindToJsEventLoop(); - [DoesNotReturn] - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void MonoThreadExit(); // FIXME: this is not Emscripten, it's just mono_thread_exit(); - private static readonly Action s_WorkLoopSemaphoreSuccess = new(WorkLoopSemaphoreSuccess); private static readonly Action s_WorkLoopSemaphoreTimedOut = new(WorkLoopSemaphoreTimedOut); @@ -108,8 +98,9 @@ private static void WorkLoopSemaphoreTimedOut(LowLevelJSSemaphore semaphore, obj if (WorkerTimedOutMaybeStop(state.ThreadPoolInstance, state.ThreadAdjustmentLock)) { // we're done, kill the thread. - EmscriptenKeepalivePop(); - MonoThreadExit(); + // we're wrapped in an emscripten eventloop handler which will consult the keepalive count, destroy the thread and run the TLS dtor which will unregister the thread from Mono + WebWorkerEventLoop.KeepalivePop(); + //WebWorkerEventLoop.ThreadExit(); // FIXME: will this clean up the interpreter stack return; } else { // more work showed up while we were shutting down, go around one more time diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/WebWorkerEventLoop.Browser.Threads.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/WebWorkerEventLoop.Browser.Threads.Mono.cs new file mode 100644 index 0000000000000..d2dadd9699fda --- /dev/null +++ b/src/mono/System.Private.CoreLib/src/System/Threading/WebWorkerEventLoop.Browser.Threads.Mono.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Tracing; +using System.Runtime.CompilerServices; + +namespace System.Threading; + +/// +/// Keep a pthread alive in its WebWorker after its pthread start function returns. +/// +internal static class WebWorkerEventLoop +{ + // FIXME: these keepalive calls could be qcalls with a SuppressGCTransitionAttribute + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern void KeepalivePushInternal(); + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern void KeepalivePopInternal(); +#if false + [DoesNotReturn] + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern void UnwindToJsInternal(); + [DoesNotReturn] + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern void ThreadExitInternal(); +#endif + + /// + /// Increment the keepalive count. A thread with a positive keepalive can return from its + /// thread start function or a JS event loop invocation and continue running in the JS event + /// loop. + /// + internal static void KeepalivePush() => KeepalivePushInternal(); + + /// + /// Decrement the keepalive count. A thread with a zero keepalive count will terminate when it + /// returns from its start function or from an async invocation from the JS event loop. + /// + internal static void KeepalivePop() => KeepalivePopInternal(); + + // FIXME: these are dangerous they will not unwind managad frames (so finally clauses wont' run) and maybe leak in the interpreter memory +#if false + /// + /// Abort the current execution and unwind to the JS event loop + /// + /// + // FIXME: we should probably setup some managed exception to + // unwind the managed stack before calling the emscripten + // unwind_to_js to unwind the native stack. + [DoesNotReturn] + internal static void UnwindToJs() => UnwindToJsInternal(); + + /// + /// Terminate the current thread, even if the thread was kept alive with KeepalivePush + /// + internal static void ThreadExit() => ThreadExitInternal(); +#endif +} diff --git a/src/mono/mono/metadata/icall-decl.h b/src/mono/mono/metadata/icall-decl.h index b25451313ded9..3b7ca63ad77c4 100644 --- a/src/mono/mono/metadata/icall-decl.h +++ b/src/mono/mono/metadata/icall-decl.h @@ -190,9 +190,8 @@ ICALL_EXPORT void ves_icall_System_Threading_LowLevelJSSemaphore_DeleteInter ICALL_EXPORT void ves_icall_System_Threading_LowLevelJSSemaphore_PrepareWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timeout_cb, gpointer gchandle, gpointer user_data); ICALL_EXPORT void ves_icall_System_Threading_LowLevelJSSemaphore_ReleaseInternal (gpointer sem_ptr, gint32 count); -ICALL_EXPORT void ves_icall_System_Threading_PortableThreadPool_WorkerThread_EmscriptenKeepalivePop (void); -ICALL_EXPORT void ves_icall_System_Threading_PortableThreadPool_WorkerThread_EmscriptenKeepalivePush (void); -ICALL_EXPORT void ves_icall_System_Threading_PortableThreadPool_WorkerThread_EmscriptenUnwindToJsEventLoop (void); +ICALL_EXPORT void ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePushInternal (void); +ICALL_EXPORT void ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal (void); #endif #ifdef TARGET_AMD64 diff --git a/src/mono/mono/metadata/icall-def.h b/src/mono/mono/metadata/icall-def.h index 9ad56254fc2d6..95488e32b73c7 100644 --- a/src/mono/mono/metadata/icall-def.h +++ b/src/mono/mono/metadata/icall-def.h @@ -590,14 +590,6 @@ HANDLES(MONIT_7, "Monitor_wait", ves_icall_System_Threading_Monitor_Monitor_wait NOHANDLES(ICALL(MONIT_8, "get_LockContentionCount", ves_icall_System_Threading_Monitor_Monitor_LockContentionCount)) HANDLES(MONIT_9, "try_enter_with_atomic_var", ves_icall_System_Threading_Monitor_Monitor_try_enter_with_atomic_var, void, 4, (MonoObject, guint32, MonoBoolean, MonoBoolean_ref)) -#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) -ICALL_TYPE(TPOOL_WORKER, "System.Threading.PortableThreadPool/WorkerThread", TPOOL_WORKER_1) -NOHANDLES(ICALL(TPOOL_WORKER_1, "EmscriptenKeepalivePop", ves_icall_System_Threading_PortableThreadPool_WorkerThread_EmscriptenKeepalivePop)) -NOHANDLES(ICALL(TPOOL_WORKER_2, "EmscriptenKeepalivePush", ves_icall_System_Threading_PortableThreadPool_WorkerThread_EmscriptenKeepalivePush)) -NOHANDLES(ICALL(TPOOL_WORKER_3, "EmscriptenUnwindToJsEventLoop", ves_icall_System_Threading_PortableThreadPool_WorkerThread_EmscriptenUnwindToJsEventLoop)) -NOHANDLES(ICALL(TPOOL_WORKER_4, "MonoThreadExit", mono_thread_exit)) -#endif - ICALL_TYPE(THREAD, "System.Threading.Thread", THREAD_1) HANDLES(THREAD_1, "ClrState", ves_icall_System_Threading_Thread_ClrState, void, 2, (MonoInternalThread, guint32)) HANDLES(ITHREAD_2, "FreeInternal", ves_icall_System_Threading_InternalThread_Thread_free_internal, void, 1, (MonoInternalThread)) @@ -613,6 +605,12 @@ HANDLES(THREAD_10, "SetState", ves_icall_System_Threading_Thread_SetState, void, HANDLES(THREAD_13, "StartInternal", ves_icall_System_Threading_Thread_StartInternal, void, 2, (MonoThreadObject, gint32)) NOHANDLES(ICALL(THREAD_14, "YieldInternal", ves_icall_System_Threading_Thread_YieldInternal)) +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) +ICALL_TYPE(WEBWORKERLOOP, "System.Threading.WebWorkerEventLoop", WEBWORKERLOOP_1) +NOHANDLES(ICALL(WEBWORKERLOOP_1, "KeepalivePopInternal", ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal)) +NOHANDLES(ICALL(WEBWORKERLOOP_2, "KeepalivePushInternal", ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePushInternal)) +#endif + ICALL_TYPE(TYPE, "System.Type", TYPE_1) HANDLES(TYPE_1, "internal_from_handle", ves_icall_System_Type_internal_from_handle, MonoReflectionType, 1, (MonoType_ref)) diff --git a/src/mono/mono/metadata/threads.c b/src/mono/mono/metadata/threads.c index 6f3df54ee46d1..902b49462a19a 100644 --- a/src/mono/mono/metadata/threads.c +++ b/src/mono/mono/metadata/threads.c @@ -4997,21 +4997,15 @@ ves_icall_System_Threading_LowLevelJSSemaphore_ReleaseInternal (gpointer sem_ptr } void -ves_icall_System_Threading_PortableThreadPool_WorkerThread_EmscriptenKeepalivePop (void) -{ - emscripten_runtime_keepalive_pop(); -} - -void -ves_icall_System_Threading_PortableThreadPool_WorkerThread_EmscriptenKeepalivePush (void) +ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePushInternal (void) { emscripten_runtime_keepalive_push(); } void -ves_icall_System_Threading_PortableThreadPool_WorkerThread_EmscriptenUnwindToJsEventLoop (void) +ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal (void) { - emscripten_unwind_to_js_event_loop(); + emscripten_runtime_keepalive_pop(); } #endif /* HOST_BROWSER && !DISABLE_THREADS */ From 07eabcd6eaba86c24a94925daec1d996f796105f Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 24 Mar 2023 14:34:10 -0400 Subject: [PATCH 23/37] Start threadpool threads with keepalive checks Add a flag to mono's thread start wrappers to keep track of threads that may not want cleanup to run after the Start function returns. Use the flag when starting threadpool threads. --- ...dPool.WorkerThread.Browser.Threads.Mono.cs | 24 +++++---- ...WebWorkerEventLoop.Browser.Threads.Mono.cs | 50 ++++++++++++++++--- src/mono/mono/metadata/threads-types.h | 3 ++ src/mono/mono/metadata/threads.c | 39 ++++++++++++++- 4 files changed, 98 insertions(+), 18 deletions(-) diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs index 12d762fb7994e..aadacd9ab455f 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Tracing; using System.Runtime.CompilerServices; @@ -44,7 +45,7 @@ private static partial class WorkerThread private static readonly ThreadStart s_workerThreadStart = WorkerThreadStart; - private sealed record SemaphoreWaitState(PortableThreadPool ThreadPoolInstance, LowLevelLock ThreadAdjustmentLock) + private sealed record SemaphoreWaitState(PortableThreadPool ThreadPoolInstance, LowLevelLock ThreadAdjustmentLock, WebWorkerEventLoop.KeepaliveToken KeepaliveToken) { public bool SpinWait = true; @@ -66,14 +67,13 @@ private static void WorkerThreadStart() } LowLevelLock threadAdjustmentLock = threadPoolInstance._threadAdjustmentLock; - SemaphoreWaitState state = new(threadPoolInstance, threadAdjustmentLock) { SpinWait = true }; + var keepaliveToken = WebWorkerEventLoop.KeepalivePush(); + SemaphoreWaitState state = new(threadPoolInstance, threadAdjustmentLock, keepaliveToken) { SpinWait = true }; // set up the callbacks for semaphore waits, tell // emscripten to keep the thread alive, and return to // the JS event loop. WaitForWorkLoop(s_semaphore, state); - WebWorkerEventLoop.KeepalivePush(); // return from thread start with keepalive - the thread will stay alive in the JS event loop - // WebWorkerEventLoop.UnwindToJs(); // FIXME: this is a bad idea - it doesn't run C# finally clauses and maybe leaks Mono interpreter frames } private static readonly Action s_WorkLoopSemaphoreSuccess = new(WorkLoopSemaphoreSuccess); @@ -82,6 +82,8 @@ private static void WorkerThreadStart() private static void WaitForWorkLoop(LowLevelJSSemaphore semaphore, SemaphoreWaitState state) { semaphore.PrepareWait(ThreadPoolThreadTimeoutMs, s_WorkLoopSemaphoreSuccess, s_WorkLoopSemaphoreTimedOut, state); + // thread should still be kept alive + Debug.Assert(state.KeepaliveToken.Valid); } private static void WorkLoopSemaphoreSuccess(LowLevelJSSemaphore semaphore, object? stateObject) @@ -98,9 +100,10 @@ private static void WorkLoopSemaphoreTimedOut(LowLevelJSSemaphore semaphore, obj if (WorkerTimedOutMaybeStop(state.ThreadPoolInstance, state.ThreadAdjustmentLock)) { // we're done, kill the thread. - // we're wrapped in an emscripten eventloop handler which will consult the keepalive count, destroy the thread and run the TLS dtor which will unregister the thread from Mono - WebWorkerEventLoop.KeepalivePop(); - //WebWorkerEventLoop.ThreadExit(); // FIXME: will this clean up the interpreter stack + // we're wrapped in an emscripten eventloop handler which will consult the + // keepalive count, destroy the thread and run the TLS dtor which will + // unregister the thread from Mono + state.KeepaliveToken.Pop(); return; } else { // more work showed up while we were shutting down, go around one more time @@ -350,12 +353,15 @@ private static bool TakeActiveRequest(PortableThreadPool threadPoolInstance) private static void CreateWorkerThread() { // Thread pool threads must start in the default execution context without transferring the context, so - // using UnsafeStart() instead of Start() + // using captureContext: false. Thread workerThread = new Thread(s_workerThreadStart); workerThread.IsThreadPoolThread = true; workerThread.IsBackground = true; // thread name will be set in thread proc - workerThread.UnsafeStart(); + + // This thread will return to the JS event loop - tell the runtime not to cleanup + // after the start function returns, if the Emscripten keepalive is non-zero. + WebWorkerEventLoop.StartExitable(workerThread, captureContext: false); } } } diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/WebWorkerEventLoop.Browser.Threads.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/WebWorkerEventLoop.Browser.Threads.Mono.cs index d2dadd9699fda..0984d6937822b 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/WebWorkerEventLoop.Browser.Threads.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/WebWorkerEventLoop.Browser.Threads.Mono.cs @@ -26,18 +26,38 @@ internal static class WebWorkerEventLoop private static extern void ThreadExitInternal(); #endif + internal sealed class KeepaliveToken + { + public bool Valid {get; private set; } + + private KeepaliveToken() { Valid = true; } + + /// + /// Decrement the Emscripten keepalive count. A thread with + /// a zero keepalive count will terminate when it returns + /// from its start function or from an async invocation from + /// the JS event loop. + /// + internal void Pop() { + if (!Valid) + throw new InvalidOperationException(); + Valid = false; + KeepalivePopInternal(); + } + + internal static KeepaliveToken Create() + { + KeepalivePushInternal(); + return new KeepaliveToken(); + } + } + /// - /// Increment the keepalive count. A thread with a positive keepalive can return from its + /// Increment the Emscripten keepalive count. A thread with a positive keepalive can return from its /// thread start function or a JS event loop invocation and continue running in the JS event /// loop. /// - internal static void KeepalivePush() => KeepalivePushInternal(); - - /// - /// Decrement the keepalive count. A thread with a zero keepalive count will terminate when it - /// returns from its start function or from an async invocation from the JS event loop. - /// - internal static void KeepalivePop() => KeepalivePopInternal(); + internal static KeepaliveToken KeepalivePush() => KeepaliveToken.Create(); // FIXME: these are dangerous they will not unwind managad frames (so finally clauses wont' run) and maybe leak in the interpreter memory #if false @@ -56,4 +76,18 @@ internal static class WebWorkerEventLoop /// internal static void ThreadExit() => ThreadExitInternal(); #endif + + + internal static void StartExitable(Thread thread, bool captureContext) + { + // don't support captureContext == true, for now, since it's + // not needed by PortableThreadPool.WorkerThread + if (captureContext) + throw new InvalidOperationException(); + // hack: threadpool threads are exitable, and nothing else is. + // see create_thread() in mono/metadata/threads.c + if (!thread.IsThreadPoolThread) + throw new InvalidOperationException(); + thread.UnsafeStart(); + } } diff --git a/src/mono/mono/metadata/threads-types.h b/src/mono/mono/metadata/threads-types.h index fe57d74a02e39..b9652aa33eb32 100644 --- a/src/mono/mono/metadata/threads-types.h +++ b/src/mono/mono/metadata/threads-types.h @@ -78,6 +78,9 @@ typedef enum { MONO_THREAD_CREATE_FLAGS_DEBUGGER = 0x02, MONO_THREAD_CREATE_FLAGS_FORCE_CREATE = 0x04, MONO_THREAD_CREATE_FLAGS_SMALL_STACK = 0x08, +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) + MONO_THREAD_CREATE_FLAGS_RETURNS_TO_JS_EVENT_LOOP = 0x10, +#endif } MonoThreadCreateFlags; MONO_COMPONENT_API MonoInternalThread* diff --git a/src/mono/mono/metadata/threads.c b/src/mono/mono/metadata/threads.c index 902b49462a19a..9f440c62a6fb6 100644 --- a/src/mono/mono/metadata/threads.c +++ b/src/mono/mono/metadata/threads.c @@ -1087,6 +1087,9 @@ typedef struct { MonoThreadStart start_func; gpointer start_func_arg; gboolean force_attach; +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) + gboolean returns_to_js_event_loop; +#endif gboolean failed; MonoCoopSem registered; } StartInfo; @@ -1171,6 +1174,10 @@ start_wrapper_internal (StartInfo *start_info, gsize *stack_ptr) /* Let the thread that called Start() know we're ready */ mono_coop_sem_post (&start_info->registered); +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) + gboolean returns_to_js_event_loop = start_info->returns_to_js_event_loop; +#endif + if (mono_atomic_dec_i32 (&start_info->ref) == 0) { mono_coop_sem_destroy (&start_info->registered); g_free (start_info); @@ -1238,6 +1245,14 @@ start_wrapper_internal (StartInfo *start_info, gsize *stack_ptr) THREAD_DEBUG (g_message ("%s: (%" G_GSIZE_FORMAT ") Start wrapper terminating", __func__, mono_native_thread_id_get ())); +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) + if (returns_to_js_event_loop) { + /* if the thread wants to stay alive, don't clean up after it */ + if (emscripten_runtime_keepalive_check()) + return 0; + } +#endif + /* Do any cleanup needed for apartment state. This * cannot be done in mono_thread_detach_internal since * mono_thread_detach_internal could be called for a @@ -1264,9 +1279,20 @@ start_wrapper (gpointer data) info = mono_thread_info_attach (); info->runtime_thread = TRUE; +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) + gboolean returns_to_js_event_loop = start_info->returns_to_js_event_loop; +#endif /* Run the actual main function of the thread */ res = start_wrapper_internal (start_info, (gsize*)info->stack_end); +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) + if (returns_to_js_event_loop) { + /* if the thread wants to stay alive, don't clean up after it */ + if (emscripten_runtime_keepalive_check()) + return 0; + } +#endif + mono_thread_info_exit (res); g_assert_not_reached (); @@ -1353,6 +1379,9 @@ create_thread (MonoThread *thread, MonoInternalThread *internal, MonoThreadStart start_info->start_func_arg = start_func_arg; start_info->force_attach = flags & MONO_THREAD_CREATE_FLAGS_FORCE_CREATE; start_info->failed = FALSE; +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) + start_info->returns_to_js_event_loop = (flags & MONO_THREAD_CREATE_FLAGS_RETURNS_TO_JS_EVENT_LOOP) != 0; +#endif mono_coop_sem_init (&start_info->registered, 0); if (flags != MONO_THREAD_CREATE_FLAGS_SMALL_STACK) @@ -4911,7 +4940,15 @@ ves_icall_System_Threading_Thread_StartInternal (MonoThreadObjectHandle thread_h return; } - res = create_thread (internal, internal, NULL, NULL, stack_size, MONO_THREAD_CREATE_FLAGS_NONE, error); + MonoThreadCreateFlags create_flags = MONO_THREAD_CREATE_FLAGS_NONE; +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) + // HACK: threadpool threads can return to the JS event loop + // WISH: support this for other threads, too + if (internal->threadpool_thread) + create_flags |= MONO_THREAD_CREATE_FLAGS_RETURNS_TO_JS_EVENT_LOOP; +#endif + + res = create_thread (internal, internal, NULL, NULL, stack_size, create_flags, error); if (!res) { UNLOCK_THREAD (internal); return; From 474607e0caec193fb972a9b82afde5598093d2a5 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 24 Mar 2023 16:32:40 -0400 Subject: [PATCH 24/37] HACK: kind of work around the emscripten_runtime_keepalive_push/pop no-op Keep a thread local counter in mono. that we "know" will be right for us and manually call unwind_to_js and thread_exit. This is super-fragile since we don't know what emscripten internals might be trying to manipulate the keepalive count and also we are exiting the thread with active managed frames, so we might be skipping finally clauses and possibly leaking interpreter memory. This is mainly meant to keep work going on this branch and not something we necessarily want to commit --- src/mono/mono/metadata/threads.c | 41 +++++++++++++++++++++++++ src/mono/mono/utils/mono-threads-wasm.h | 14 +++++++++ 2 files changed, 55 insertions(+) diff --git a/src/mono/mono/metadata/threads.c b/src/mono/mono/metadata/threads.c index 9f440c62a6fb6..44de8e2cf4920 100644 --- a/src/mono/mono/metadata/threads.c +++ b/src/mono/mono/metadata/threads.c @@ -92,6 +92,7 @@ mono_native_thread_join_handle (HANDLE thread_handle, gboolean close_handle); #endif #if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) +#include #include #endif @@ -1117,6 +1118,12 @@ fire_attach_profiler_events (MonoNativeThreadId tid) "Handle Stack")); } + +#ifdef MONO_EMSCRIPTEN_KEEPALIVE_WORKAROUND_HACK +/* See ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal */ +__thread uint mono_emscripten_keepalive_hack_count; +#endif + static guint32 WINAPI start_wrapper_internal (StartInfo *start_info, gsize *stack_ptr) { @@ -1248,8 +1255,14 @@ start_wrapper_internal (StartInfo *start_info, gsize *stack_ptr) #if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) if (returns_to_js_event_loop) { /* if the thread wants to stay alive, don't clean up after it */ +#ifdef MONO_EMSCRIPTEN_KEEPALIVE_WORKAROUND_HACK + /* we "know" that threadpool threads set their keepalive count correctly and will return here */ + g_assert (mono_emscripten_keepalive_hack_count > 0); + return 0; +#else if (emscripten_runtime_keepalive_check()) return 0; +#endif } #endif @@ -1288,8 +1301,15 @@ start_wrapper (gpointer data) #if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) if (returns_to_js_event_loop) { /* if the thread wants to stay alive, don't clean up after it */ +#ifdef MONO_EMSCRIPTEN_KEEPALIVE_WORKAROUND_HACK + /* we "know" the keepalive count is positive at this point for threadpool threads. Keep it alive */ + g_assert (mono_emscripten_keepalive_hack_count > 0); + emscripten_unwind_to_js_event_loop (); + g_assert_not_reached(); +#else if (emscripten_runtime_keepalive_check()) return 0; +#endif } #endif @@ -5036,6 +5056,9 @@ ves_icall_System_Threading_LowLevelJSSemaphore_ReleaseInternal (gpointer sem_ptr void ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePushInternal (void) { +#ifdef MONO_EMSCRIPTEN_KEEPALIVE_WORKAROUND_HACK + mono_emscripten_keepalive_hack_count++; +#endif emscripten_runtime_keepalive_push(); } @@ -5043,6 +5066,24 @@ void ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal (void) { emscripten_runtime_keepalive_pop(); +#ifdef MONO_EMSCRIPTEN_KEEPALIVE_WORKAROUND_HACK + /* This is a BAD IDEA: + * + * 1. We don't know if there were non-mono callers of emscripten_runtime_keepalive_push. We + * could be dropping a thread that was meant to stay alive. + * + * 2. mono_thread_exit while we have managed frames on the stack means we might leak + * resource since finally clauses didn't run. Also the mono interpreter doesn't really get + * a chance to clean up. + * + * + */ + mono_emscripten_keepalive_hack_count--; + if (!mono_emscripten_keepalive_hack_count) { + g_warning ("thread %p mono keepalive count is zero, detaching\n", (void*)(intptr_t)pthread_self()); + mono_thread_exit(); + } +#endif } #endif /* HOST_BROWSER && !DISABLE_THREADS */ diff --git a/src/mono/mono/utils/mono-threads-wasm.h b/src/mono/mono/utils/mono-threads-wasm.h index c06d8501e1ec3..b76606045ecb8 100644 --- a/src/mono/mono/utils/mono-threads-wasm.h +++ b/src/mono/mono/utils/mono-threads-wasm.h @@ -11,6 +11,20 @@ #include #include +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) +#include +/* for Emscripten < 3.1.33, + * emscripten_runtime_keepalive_push()/emscripten_runtime_keepalive_pop()/emscripten_keepalive_check() + * are no-ops when -sNO_EXIT_RUNTIME=1 (the default). Do our own bookkeeping when we can. Note + * that this is a HACK that is very sensitive to code that actually cares about this bookkeeping. + * + * Specifically we need https://github.com/emscripten-core/emscripten/commit/0c2f5896b839e25fee9763a9ac9c619f359988f4 + */ +#if (__EMSCRIPTEN_major__ < 3) || (__EMSCRIPTEN_major__ == 3 && __EMSCRIPTEN_minor__ < 1) || (__EMSCRIPTEN_major__ == 3 && __EMSCRIPTEN_minor__ == 1 && __EMSCRIPTEN_tiny__ < 33) +#define MONO_EMSCRIPTEN_KEEPALIVE_WORKAROUND_HACK 1 +#endif +#endif /*HOST_BROWSER && !DISABLE_THREADS*/ + #ifdef HOST_WASM /* From 9f451674e4aa1b6a40b68b142fc749e2ea390940 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Mon, 27 Mar 2023 16:04:58 -0400 Subject: [PATCH 25/37] support JS Semaphore with --print-icall-table cross compiler fixes smoketest on Release builds --- src/mono/mono/metadata/icall-decl.h | 3 ++- src/mono/mono/metadata/icall-def.h | 6 +++-- src/mono/mono/metadata/threads.c | 42 +++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/mono/mono/metadata/icall-decl.h b/src/mono/mono/metadata/icall-decl.h index 3b7ca63ad77c4..8681014df37d8 100644 --- a/src/mono/mono/metadata/icall-decl.h +++ b/src/mono/mono/metadata/icall-decl.h @@ -184,7 +184,8 @@ ICALL_EXPORT void ves_icall_System_Threading_LowLevelLifoSemaphore_DeleteInt ICALL_EXPORT gint32 ves_icall_System_Threading_LowLevelLifoSemaphore_TimedWaitInternal (gpointer sem_ptr, gint32 timeout_ms); ICALL_EXPORT void ves_icall_System_Threading_LowLevelLifoSemaphore_ReleaseInternal (gpointer sem_ptr, gint32 count); -#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) +/* include these declarations if we're in the threaded wasm runtime, or if we're building a wasm-targeting cross compiler and we need to support --print-icall-table */ +#if (defined(HOST_BROWSER) && !defined(DISABLE_THREADS)) || (defined(TARGET_WASM) && defined(ENABLE_ICALL_SYMBOL_MAP)) ICALL_EXPORT gpointer ves_icall_System_Threading_LowLevelJSSemaphore_InitInternal (void); ICALL_EXPORT void ves_icall_System_Threading_LowLevelJSSemaphore_DeleteInternal (gpointer sem_ptr); ICALL_EXPORT void ves_icall_System_Threading_LowLevelJSSemaphore_PrepareWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timeout_cb, gpointer gchandle, gpointer user_data); diff --git a/src/mono/mono/metadata/icall-def.h b/src/mono/mono/metadata/icall-def.h index 95488e32b73c7..40fd86479f693 100644 --- a/src/mono/mono/metadata/icall-def.h +++ b/src/mono/mono/metadata/icall-def.h @@ -567,7 +567,8 @@ NOHANDLES(ICALL(ILOCK_21, "Increment(long&)", ves_icall_System_Threading_Interlo NOHANDLES(ICALL(ILOCK_22, "MemoryBarrierProcessWide", ves_icall_System_Threading_Interlocked_MemoryBarrierProcessWide)) NOHANDLES(ICALL(ILOCK_23, "Read(long&)", ves_icall_System_Threading_Interlocked_Read_Long)) -#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) +/* include these icalls if we're in the threaded wasm runtime, or if we're building a wasm-targeting cross compiler and we need to support --print-icall-table */ +#if (defined(HOST_BROWSER) && !defined(DISABLE_THREADS)) || (defined(TARGET_WASM) && defined(ENABLE_ICALL_SYMBOL_MAP)) ICALL_TYPE(JSSEM, "System.Threading.LowLevelJSSemaphore", JSSEM_1) NOHANDLES(ICALL(JSSEM_1, "DeleteInternal", ves_icall_System_Threading_LowLevelJSSemaphore_DeleteInternal)) NOHANDLES(ICALL(JSSEM_2, "InitInternal", ves_icall_System_Threading_LowLevelJSSemaphore_InitInternal)) @@ -605,7 +606,8 @@ HANDLES(THREAD_10, "SetState", ves_icall_System_Threading_Thread_SetState, void, HANDLES(THREAD_13, "StartInternal", ves_icall_System_Threading_Thread_StartInternal, void, 2, (MonoThreadObject, gint32)) NOHANDLES(ICALL(THREAD_14, "YieldInternal", ves_icall_System_Threading_Thread_YieldInternal)) -#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) +/* include these icalls if we're in the threaded wasm runtime, or if we're building a wasm-targeting cross compiler and we need to support --print-icall-table */ +#if (defined(HOST_BROWSER) && !defined(DISABLE_THREADS)) || (defined(TARGET_WASM) && defined(ENABLE_ICALL_SYMBOL_MAP)) ICALL_TYPE(WEBWORKERLOOP, "System.Threading.WebWorkerEventLoop", WEBWORKERLOOP_1) NOHANDLES(ICALL(WEBWORKERLOOP_1, "KeepalivePopInternal", ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal)) NOHANDLES(ICALL(WEBWORKERLOOP_2, "KeepalivePushInternal", ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePushInternal)) diff --git a/src/mono/mono/metadata/threads.c b/src/mono/mono/metadata/threads.c index 44de8e2cf4920..bfeb7cc788194 100644 --- a/src/mono/mono/metadata/threads.c +++ b/src/mono/mono/metadata/threads.c @@ -5087,3 +5087,45 @@ ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal (void) } #endif /* HOST_BROWSER && !DISABLE_THREADS */ + +/* for the AOT cross compiler with --print-icall-table these don't need to be callable, they just + * need to be defined */ +#if defined(TARGET_WASM) && defined(ENABLE_ICALL_SYMBOL_MAP) +gpointer +ves_icall_System_Threading_LowLevelJSSemaphore_InitInternal (void) +{ + g_assert_not_reached(); +} + +void +ves_icall_System_Threading_LowLevelJSSemaphore_DeleteInternal (gpointer sem_ptr) +{ + g_assert_not_reached(); +} + +void +ves_icall_System_Threading_LowLevelJSSemaphore_PrepareWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timedout_cb, gpointer gchandle, gpointer user_data) +{ + g_assert_not_reached(); +} + +void +ves_icall_System_Threading_LowLevelJSSemaphore_ReleaseInternal (gpointer sem_ptr, gint32 count) +{ + g_assert_not_reached(); + +} + +void +ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePushInternal (void) +{ + g_assert_not_reached(); +} + +void +ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal (void) +{ + g_assert_not_reached(); +} + +#endif /* defined(TARGET_WASM) && defined(ENABLE_ICALL_SYMBOL_MAP) */ From 2e1f31f3131041177c8cfbdb40cf8cb4a0d77afa Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Mon, 27 Mar 2023 16:10:30 -0400 Subject: [PATCH 26/37] make minimal FetchBackground sample more like a unit test --- .../wasm/browser-threads-minimal/Program.cs | 8 +++++--- .../sample/wasm/browser-threads-minimal/main.js | 15 +++++++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/mono/sample/wasm/browser-threads-minimal/Program.cs b/src/mono/sample/wasm/browser-threads-minimal/Program.cs index b188a745e6986..b9ef3854ef163 100644 --- a/src/mono/sample/wasm/browser-threads-minimal/Program.cs +++ b/src/mono/sample/wasm/browser-threads-minimal/Program.cs @@ -38,7 +38,7 @@ public static async Task Hello() private static partial Task FetchHelperResponseText(JSObject response); [JSExport] - public static async Task FetchBackground(string url) + public static async Task FetchBackground(string url) { var t = Task.Run(async () => { @@ -51,11 +51,13 @@ public static async Task FetchBackground(string url) { var text = await FetchHelperResponseText(r); Console.WriteLine($"XYZ: FetchBackground fetch returned to thread:{Thread.CurrentThread.ManagedThreadId}, text: {text}"); + return text; } - return ok; + return "not-ok"; }); - await t; + var r = await t; Console.WriteLine($"XYZ: FetchBackground thread:{Thread.CurrentThread.ManagedThreadId} background thread returned"); + return r; } private static async Task TimeOutThenComplete() diff --git a/src/mono/sample/wasm/browser-threads-minimal/main.js b/src/mono/sample/wasm/browser-threads-minimal/main.js index 373ef3f836c64..4c6a28e916052 100644 --- a/src/mono/sample/wasm/browser-threads-minimal/main.js +++ b/src/mono/sample/wasm/browser-threads-minimal/main.js @@ -20,12 +20,23 @@ try { //console.log ("XYZ: hello done"); console.log ("XYZ: running FetchBackground"); - await exports.Sample.Test.FetchBackground("./blurst.txt"); + let s = await exports.Sample.Test.FetchBackground("./blurst.txt"); console.log ("XYZ: FetchBackground done"); + if (s !== "It was the best of times, it was the blurst of times.\n") { + const msg = `Unexpected FetchBackground result ${s}`; + document.getElementById("out").innerHTML = msg; + throw new Error (msg); + } console.log ("XYZ: running FetchBackground(missing)"); - await exports.Sample.Test.FetchBackground("./missing.txt"); + s = await exports.Sample.Test.FetchBackground("./missing.txt"); console.log ("XYZ: FetchBackground(missing) done"); + if (s !== "not-ok") { + const msg = `Unexpected FetchBackground(missing) result ${s}`; + document.getElementById("out").innerHTML = msg; + throw new Error (msg); + } + //console.log ("HHH: running TaskRunCompute"); //const r1 = await exports.Sample.Test.RunBackgroundTaskRunCompute(); //if (r1 !== 524) { From dacc0cb728b5df20bb977a4a8cb5e8c5cd1e9ba1 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 29 Mar 2023 12:51:00 -0400 Subject: [PATCH 27/37] Share PortableThreadPool.WorkerThread common code Share the code between non-browser implementations and browser+threads. The differences are just in how the work loop is started and implemented --- .../System.Private.CoreLib.Shared.projitems | 1 + ...tableThreadPool.WorkerThread.NonBrowser.cs | 81 ++++++ .../PortableThreadPool.WorkerThread.cs | 225 +++++++--------- ...dPool.WorkerThread.Browser.Threads.Mono.cs | 246 ------------------ 4 files changed, 172 insertions(+), 381 deletions(-) create mode 100644 src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.NonBrowser.cs diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index f969f5c39921a..e65522aab7633 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -2523,6 +2523,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.NonBrowser.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.NonBrowser.cs new file mode 100644 index 0000000000000..c60b5177d8784 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.NonBrowser.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Tracing; + +namespace System.Threading +{ + internal sealed partial class PortableThreadPool + { + /// + /// The worker thread infastructure for the CLR thread pool. + /// + private static partial class WorkerThread + { + + /// + /// Semaphore for controlling how many threads are currently working. + /// + private static readonly LowLevelLifoSemaphore s_semaphore = + new LowLevelLifoSemaphore( + 0, + MaxPossibleThreadCount, + AppContextConfigHelper.GetInt32Config( + "System.Threading.ThreadPool.UnfairSemaphoreSpinLimit", + SemaphoreSpinCountDefault, + false), + onWait: () => + { + if (NativeRuntimeEventSource.Log.IsEnabled()) + { + NativeRuntimeEventSource.Log.ThreadPoolWorkerThreadWait( + (uint)ThreadPoolInstance._separated.counts.VolatileRead().NumExistingThreads); + } + }); + + private static readonly ThreadStart s_workerThreadStart = WorkerThreadStart; + + private static void WorkerThreadStart() + { + Thread.CurrentThread.SetThreadPoolWorkerThreadName(); + + PortableThreadPool threadPoolInstance = ThreadPoolInstance; + + if (NativeRuntimeEventSource.Log.IsEnabled()) + { + NativeRuntimeEventSource.Log.ThreadPoolWorkerThreadStart( + (uint)threadPoolInstance._separated.counts.VolatileRead().NumExistingThreads); + } + + LowLevelLock threadAdjustmentLock = threadPoolInstance._threadAdjustmentLock; + LowLevelLifoSemaphore semaphore = s_semaphore; + + while (true) + { + bool spinWait = true; + while (semaphore.Wait(ThreadPoolThreadTimeoutMs, spinWait)) + { + WorkerDoWork(threadPoolInstance, ref spinWait); + } + + if (WorkerTimedOutMaybeStop(threadPoolInstance, threadAdjustmentLock)) + { + break; + } + } + } + + + private static void CreateWorkerThread() + { + // Thread pool threads must start in the default execution context without transferring the context, so + // using UnsafeStart() instead of Start() + Thread workerThread = new Thread(s_workerThreadStart); + workerThread.IsThreadPoolThread = true; + workerThread.IsBackground = true; + // thread name will be set in thread proc + workerThread.UnsafeStart(); + } + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.cs index 99290e98f889a..4836a03385d4e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.cs @@ -2,12 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.Tracing; +using System.Runtime.CompilerServices; namespace System.Threading { internal sealed partial class PortableThreadPool { -#if !(TARGET_BROWSER && FEATURE_WASM_THREADS) /// /// The worker thread infastructure for the CLR thread pool. /// @@ -29,148 +29,115 @@ private static partial class WorkerThread // preexisting threads from running out of memory when using new stack space in low-memory situations. public const int EstimatedAdditionalStackUsagePerThreadBytes = 64 << 10; // 64 KB - /// - /// Semaphore for controlling how many threads are currently working. - /// - private static readonly LowLevelLifoSemaphore s_semaphore = - new LowLevelLifoSemaphore( - 0, - MaxPossibleThreadCount, - AppContextConfigHelper.GetInt32Config( - "System.Threading.ThreadPool.UnfairSemaphoreSpinLimit", - SemaphoreSpinCountDefault, - false), - onWait: () => + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WorkerDoWork(PortableThreadPool threadPoolInstance, ref bool spinWait) + { + bool alreadyRemovedWorkingWorker = false; + while (TakeActiveRequest(threadPoolInstance)) + { + threadPoolInstance._separated.lastDequeueTime = Environment.TickCount; + if (!ThreadPoolWorkQueue.Dispatch()) { - if (NativeRuntimeEventSource.Log.IsEnabled()) - { - NativeRuntimeEventSource.Log.ThreadPoolWorkerThreadWait( - (uint)ThreadPoolInstance._separated.counts.VolatileRead().NumExistingThreads); - } - }); + // ShouldStopProcessingWorkNow() caused the thread to stop processing work, and it would have + // already removed this working worker in the counts. This typically happens when hill climbing + // decreases the worker thread count goal. + alreadyRemovedWorkingWorker = true; + break; + } - private static readonly ThreadStart s_workerThreadStart = WorkerThreadStart; + if (threadPoolInstance._separated.numRequestedWorkers <= 0) + { + break; + } - private static void WorkerThreadStart() - { - Thread.CurrentThread.SetThreadPoolWorkerThreadName(); + // In highly bursty cases with short bursts of work, especially in the portable thread pool + // implementation, worker threads are being released and entering Dispatch very quickly, not finding + // much work in Dispatch, and soon afterwards going back to Dispatch, causing extra thrashing on + // data and some interlocked operations, and similarly when the thread pool runs out of work. Since + // there is a pending request for work, introduce a slight delay before serving the next request. + // The spin-wait is mainly for when the sleep is not effective due to there being no other threads + // to schedule. + Thread.UninterruptibleSleep0(); + if (!Environment.IsSingleProcessor) + { + Thread.SpinWait(1); + } + } - PortableThreadPool threadPoolInstance = ThreadPoolInstance; + // Don't spin-wait on the semaphore next time if the thread was actively stopped from processing work, + // as it's unlikely that the worker thread count goal would be increased again so soon afterwards that + // the semaphore would be released within the spin-wait window + spinWait = !alreadyRemovedWorkingWorker; - if (NativeRuntimeEventSource.Log.IsEnabled()) + if (!alreadyRemovedWorkingWorker) { - NativeRuntimeEventSource.Log.ThreadPoolWorkerThreadStart( - (uint)threadPoolInstance._separated.counts.VolatileRead().NumExistingThreads); + // If we woke up but couldn't find a request, or ran out of work items to process, we need to update + // the number of working workers to reflect that we are done working for now + RemoveWorkingWorker(threadPoolInstance); } + } - LowLevelLock threadAdjustmentLock = threadPoolInstance._threadAdjustmentLock; - LowLevelLifoSemaphore semaphore = s_semaphore; + // returns true if the worker is shutting down + // returns false if we should do another iteration + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool WorkerTimedOutMaybeStop (PortableThreadPool threadPoolInstance, LowLevelLock threadAdjustmentLock) + { + // The thread cannot exit if it has IO pending, otherwise the IO may be canceled + if (IsIOPending) + { + return false; + } - while (true) + threadAdjustmentLock.Acquire(); + try { - bool spinWait = true; - while (semaphore.Wait(ThreadPoolThreadTimeoutMs, spinWait)) + // At this point, the thread's wait timed out. We are shutting down this thread. + // We are going to decrement the number of existing threads to no longer include this one + // and then change the max number of threads in the thread pool to reflect that we don't need as many + // as we had. Finally, we are going to tell hill climbing that we changed the max number of threads. + ThreadCounts counts = threadPoolInstance._separated.counts; + while (true) { - bool alreadyRemovedWorkingWorker = false; - while (TakeActiveRequest(threadPoolInstance)) + // Since this thread is currently registered as an existing thread, if more work comes in meanwhile, + // this thread would be expected to satisfy the new work. Ensure that NumExistingThreads is not + // decreased below NumProcessingWork, as that would be indicative of such a case. + if (counts.NumExistingThreads <= counts.NumProcessingWork) { - threadPoolInstance._separated.lastDequeueTime = Environment.TickCount; - if (!ThreadPoolWorkQueue.Dispatch()) - { - // ShouldStopProcessingWorkNow() caused the thread to stop processing work, and it would have - // already removed this working worker in the counts. This typically happens when hill climbing - // decreases the worker thread count goal. - alreadyRemovedWorkingWorker = true; - break; - } - - if (threadPoolInstance._separated.numRequestedWorkers <= 0) - { - break; - } - - // In highly bursty cases with short bursts of work, especially in the portable thread pool - // implementation, worker threads are being released and entering Dispatch very quickly, not finding - // much work in Dispatch, and soon afterwards going back to Dispatch, causing extra thrashing on - // data and some interlocked operations, and similarly when the thread pool runs out of work. Since - // there is a pending request for work, introduce a slight delay before serving the next request. - // The spin-wait is mainly for when the sleep is not effective due to there being no other threads - // to schedule. - Thread.UninterruptibleSleep0(); - if (!Environment.IsSingleProcessor) - { - Thread.SpinWait(1); - } + // In this case, enough work came in that this thread should not time out and should go back to work. + break; } - // Don't spin-wait on the semaphore next time if the thread was actively stopped from processing work, - // as it's unlikely that the worker thread count goal would be increased again so soon afterwards that - // the semaphore would be released within the spin-wait window - spinWait = !alreadyRemovedWorkingWorker; - - if (!alreadyRemovedWorkingWorker) + ThreadCounts newCounts = counts; + short newNumExistingThreads = --newCounts.NumExistingThreads; + short newNumThreadsGoal = + Math.Max( + threadPoolInstance.MinThreadsGoal, + Math.Min(newNumExistingThreads, counts.NumThreadsGoal)); + newCounts.NumThreadsGoal = newNumThreadsGoal; + + ThreadCounts oldCounts = + threadPoolInstance._separated.counts.InterlockedCompareExchange(newCounts, counts); + if (oldCounts == counts) { - // If we woke up but couldn't find a request, or ran out of work items to process, we need to update - // the number of working workers to reflect that we are done working for now - RemoveWorkingWorker(threadPoolInstance); - } - } - - // The thread cannot exit if it has IO pending, otherwise the IO may be canceled - if (IsIOPending) - { - continue; - } - - threadAdjustmentLock.Acquire(); - try - { - // At this point, the thread's wait timed out. We are shutting down this thread. - // We are going to decrement the number of existing threads to no longer include this one - // and then change the max number of threads in the thread pool to reflect that we don't need as many - // as we had. Finally, we are going to tell hill climbing that we changed the max number of threads. - ThreadCounts counts = threadPoolInstance._separated.counts; - while (true) - { - // Since this thread is currently registered as an existing thread, if more work comes in meanwhile, - // this thread would be expected to satisfy the new work. Ensure that NumExistingThreads is not - // decreased below NumProcessingWork, as that would be indicative of such a case. - if (counts.NumExistingThreads <= counts.NumProcessingWork) + HillClimbing.ThreadPoolHillClimber.ForceChange( + newNumThreadsGoal, + HillClimbing.StateOrTransition.ThreadTimedOut); + if (NativeRuntimeEventSource.Log.IsEnabled()) { - // In this case, enough work came in that this thread should not time out and should go back to work. - break; + NativeRuntimeEventSource.Log.ThreadPoolWorkerThreadStop((uint)newNumExistingThreads); } - - ThreadCounts newCounts = counts; - short newNumExistingThreads = --newCounts.NumExistingThreads; - short newNumThreadsGoal = - Math.Max( - threadPoolInstance.MinThreadsGoal, - Math.Min(newNumExistingThreads, counts.NumThreadsGoal)); - newCounts.NumThreadsGoal = newNumThreadsGoal; - - ThreadCounts oldCounts = - threadPoolInstance._separated.counts.InterlockedCompareExchange(newCounts, counts); - if (oldCounts == counts) - { - HillClimbing.ThreadPoolHillClimber.ForceChange( - newNumThreadsGoal, - HillClimbing.StateOrTransition.ThreadTimedOut); - if (NativeRuntimeEventSource.Log.IsEnabled()) - { - NativeRuntimeEventSource.Log.ThreadPoolWorkerThreadStop((uint)newNumExistingThreads); - } - return; - } - - counts = oldCounts; + return true; } - } - finally - { - threadAdjustmentLock.Release(); + + counts = oldCounts; } } + finally + { + threadAdjustmentLock.Release(); + } + // if we get here new work came in and we're going to keep running + return false; } /// @@ -301,18 +268,6 @@ private static bool TakeActiveRequest(PortableThreadPool threadPoolInstance) } return false; } - - private static void CreateWorkerThread() - { - // Thread pool threads must start in the default execution context without transferring the context, so - // using UnsafeStart() instead of Start() - Thread workerThread = new Thread(s_workerThreadStart); - workerThread.IsThreadPoolThread = true; - workerThread.IsBackground = true; - // thread name will be set in thread proc - workerThread.UnsafeStart(); - } } -#endif // !(TARGET_BROWSER && FEATURE_WASM_THREADS) } } diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs index aadacd9ab455f..d8d963cd45ddb 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs @@ -15,14 +15,6 @@ internal sealed partial class PortableThreadPool /// private static partial class WorkerThread { - private const int SemaphoreSpinCountDefaultBaseline = 70; - private const int SemaphoreSpinCountDefault = SemaphoreSpinCountDefaultBaseline; - - // This value represents an assumption of how much uncommitted stack space a worker thread may use in the future. - // Used in calculations to estimate when to throttle the rate of thread injection to reduce the possibility of - // preexisting threads from running out of memory when using new stack space in low-memory situations. - public const int EstimatedAdditionalStackUsagePerThreadBytes = 64 << 10; // 64 KB - /// /// Semaphore for controlling how many threads are currently working. /// @@ -112,244 +104,6 @@ private static void WorkLoopSemaphoreTimedOut(LowLevelJSSemaphore semaphore, obj } } - private static void WorkerDoWork(PortableThreadPool threadPoolInstance, ref bool spinWait) - { - bool alreadyRemovedWorkingWorker = false; - while (TakeActiveRequest(threadPoolInstance)) - { - threadPoolInstance._separated.lastDequeueTime = Environment.TickCount; - if (!ThreadPoolWorkQueue.Dispatch()) - { - // ShouldStopProcessingWorkNow() caused the thread to stop processing work, and it would have - // already removed this working worker in the counts. This typically happens when hill climbing - // decreases the worker thread count goal. - alreadyRemovedWorkingWorker = true; - break; - } - - if (threadPoolInstance._separated.numRequestedWorkers <= 0) - { - break; - } - - // In highly bursty cases with short bursts of work, especially in the portable thread pool - // implementation, worker threads are being released and entering Dispatch very quickly, not finding - // much work in Dispatch, and soon afterwards going back to Dispatch, causing extra thrashing on - // data and some interlocked operations, and similarly when the thread pool runs out of work. Since - // there is a pending request for work, introduce a slight delay before serving the next request. - // The spin-wait is mainly for when the sleep is not effective due to there being no other threads - // to schedule. - Thread.UninterruptibleSleep0(); - if (!Environment.IsSingleProcessor) - { - Thread.SpinWait(1); - } - } - - // Don't spin-wait on the semaphore next time if the thread was actively stopped from processing work, - // as it's unlikely that the worker thread count goal would be increased again so soon afterwards that - // the semaphore would be released within the spin-wait window - spinWait = !alreadyRemovedWorkingWorker; - - if (!alreadyRemovedWorkingWorker) - { - // If we woke up but couldn't find a request, or ran out of work items to process, we need to update - // the number of working workers to reflect that we are done working for now - RemoveWorkingWorker(threadPoolInstance); - } - } - - // returns true if we shouldn't re-queue for another spin - // returns false if the worker is shutting down - private static bool WorkerTimedOutMaybeStop (PortableThreadPool threadPoolInstance, LowLevelLock threadAdjustmentLock) - { - // The thread cannot exit if it has IO pending, otherwise the IO may be canceled - if (IsIOPending) - { - return false; - } - - threadAdjustmentLock.Acquire(); - try - { - // At this point, the thread's wait timed out. We are shutting down this thread. - // We are going to decrement the number of existing threads to no longer include this one - // and then change the max number of threads in the thread pool to reflect that we don't need as many - // as we had. Finally, we are going to tell hill climbing that we changed the max number of threads. - ThreadCounts counts = threadPoolInstance._separated.counts; - while (true) - { - // Since this thread is currently registered as an existing thread, if more work comes in meanwhile, - // this thread would be expected to satisfy the new work. Ensure that NumExistingThreads is not - // decreased below NumProcessingWork, as that would be indicative of such a case. - if (counts.NumExistingThreads <= counts.NumProcessingWork) - { - // In this case, enough work came in that this thread should not time out and should go back to work. - break; - } - - ThreadCounts newCounts = counts; - short newNumExistingThreads = --newCounts.NumExistingThreads; - short newNumThreadsGoal = - Math.Max( - threadPoolInstance.MinThreadsGoal, - Math.Min(newNumExistingThreads, counts.NumThreadsGoal)); - newCounts.NumThreadsGoal = newNumThreadsGoal; - - ThreadCounts oldCounts = - threadPoolInstance._separated.counts.InterlockedCompareExchange(newCounts, counts); - if (oldCounts == counts) - { - HillClimbing.ThreadPoolHillClimber.ForceChange( - newNumThreadsGoal, - HillClimbing.StateOrTransition.ThreadTimedOut); - if (NativeRuntimeEventSource.Log.IsEnabled()) - { - NativeRuntimeEventSource.Log.ThreadPoolWorkerThreadStop((uint)newNumExistingThreads); - } - return true; - } - - counts = oldCounts; - } - } - finally - { - threadAdjustmentLock.Release(); - } - // if we get here new work came in and we're going to keep running - return false; - } - - /// - /// Reduce the number of working workers by one, but maybe add back a worker (possibily this thread) if a thread request comes in while we are marking this thread as not working. - /// - private static void RemoveWorkingWorker(PortableThreadPool threadPoolInstance) - { - // A compare-exchange loop is used instead of Interlocked.Decrement or Interlocked.Add to defensively prevent - // NumProcessingWork from underflowing. See the setter for NumProcessingWork. - ThreadCounts counts = threadPoolInstance._separated.counts; - while (true) - { - ThreadCounts newCounts = counts; - newCounts.NumProcessingWork--; - - ThreadCounts countsBeforeUpdate = - threadPoolInstance._separated.counts.InterlockedCompareExchange(newCounts, counts); - if (countsBeforeUpdate == counts) - { - break; - } - - counts = countsBeforeUpdate; - } - - // It's possible that we decided we had thread requests just before a request came in, - // but reduced the worker count *after* the request came in. In this case, we might - // miss the notification of a thread request. So we wake up a thread (maybe this one!) - // if there is work to do. - if (threadPoolInstance._separated.numRequestedWorkers > 0) - { - MaybeAddWorkingWorker(threadPoolInstance); - } - } - - internal static void MaybeAddWorkingWorker(PortableThreadPool threadPoolInstance) - { - ThreadCounts counts = threadPoolInstance._separated.counts; - short numExistingThreads, numProcessingWork, newNumExistingThreads, newNumProcessingWork; - while (true) - { - numProcessingWork = counts.NumProcessingWork; - if (numProcessingWork >= counts.NumThreadsGoal) - { - return; - } - - newNumProcessingWork = (short)(numProcessingWork + 1); - numExistingThreads = counts.NumExistingThreads; - newNumExistingThreads = Math.Max(numExistingThreads, newNumProcessingWork); - - ThreadCounts newCounts = counts; - newCounts.NumProcessingWork = newNumProcessingWork; - newCounts.NumExistingThreads = newNumExistingThreads; - - ThreadCounts oldCounts = threadPoolInstance._separated.counts.InterlockedCompareExchange(newCounts, counts); - - if (oldCounts == counts) - { - break; - } - - counts = oldCounts; - } - - int toCreate = newNumExistingThreads - numExistingThreads; - int toRelease = newNumProcessingWork - numProcessingWork; - - if (toRelease > 0) - { - s_semaphore.Release(toRelease); - } - - while (toCreate > 0) - { - CreateWorkerThread(); - toCreate--; - } - } - - /// - /// Returns if the current thread should stop processing work on the thread pool. - /// A thread should stop processing work on the thread pool when work remains only when - /// there are more worker threads in the thread pool than we currently want. - /// - /// Whether or not this thread should stop processing work even if there is still work in the queue. - internal static bool ShouldStopProcessingWorkNow(PortableThreadPool threadPoolInstance) - { - ThreadCounts counts = threadPoolInstance._separated.counts; - while (true) - { - // When there are more threads processing work than the thread count goal, it may have been decided - // to decrease the number of threads. Stop processing if the counts can be updated. We may have more - // threads existing than the thread count goal and that is ok, the cold ones will eventually time out if - // the thread count goal is not increased again. This logic is a bit different from the original CoreCLR - // code from which this implementation was ported, which turns a processing thread into a retired thread - // and checks for pending requests like RemoveWorkingWorker. In this implementation there are - // no retired threads, so only the count of threads processing work is considered. - if (counts.NumProcessingWork <= counts.NumThreadsGoal) - { - return false; - } - - ThreadCounts newCounts = counts; - newCounts.NumProcessingWork--; - - ThreadCounts oldCounts = threadPoolInstance._separated.counts.InterlockedCompareExchange(newCounts, counts); - - if (oldCounts == counts) - { - return true; - } - counts = oldCounts; - } - } - - private static bool TakeActiveRequest(PortableThreadPool threadPoolInstance) - { - int count = threadPoolInstance._separated.numRequestedWorkers; - while (count > 0) - { - int prevCount = Interlocked.CompareExchange(ref threadPoolInstance._separated.numRequestedWorkers, count - 1, count); - if (prevCount == count) - { - return true; - } - count = prevCount; - } - return false; - } - private static void CreateWorkerThread() { // Thread pool threads must start in the default execution context without transferring the context, so From 10ca33054db54aca65d900d77237113de25923c6 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 30 Mar 2023 14:21:27 -0400 Subject: [PATCH 28/37] make both kinds of lifo semaphore share a base struct --- src/mono/mono/utils/lifo-semaphore.c | 40 +++++++++++++++------------- src/mono/mono/utils/lifo-semaphore.h | 19 +++++++++++-- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/src/mono/mono/utils/lifo-semaphore.c b/src/mono/mono/utils/lifo-semaphore.c index 03e8dbcaba8c6..b9cf8100edb53 100644 --- a/src/mono/mono/utils/lifo-semaphore.c +++ b/src/mono/mono/utils/lifo-semaphore.c @@ -11,10 +11,11 @@ LifoSemaphore * mono_lifo_semaphore_init (void) { LifoSemaphore *semaphore = g_new0 (LifoSemaphore, 1); + semaphore->base.kind = LIFO_SEMAPHORE_NORMAL; if (semaphore == NULL) return NULL; - mono_coop_mutex_init (&semaphore->mutex); + mono_coop_mutex_init (&semaphore->base.mutex); return semaphore; } @@ -23,7 +24,7 @@ void mono_lifo_semaphore_delete (LifoSemaphore *semaphore) { g_assert (semaphore->head == NULL); - mono_coop_mutex_destroy (&semaphore->mutex); + mono_coop_mutex_destroy (&semaphore->base.mutex); g_free (semaphore); } @@ -33,12 +34,12 @@ mono_lifo_semaphore_timed_wait (LifoSemaphore *semaphore, int32_t timeout_ms) LifoSemaphoreWaitEntry wait_entry = {0}; mono_coop_cond_init (&wait_entry.condition); - mono_coop_mutex_lock (&semaphore->mutex); + mono_coop_mutex_lock (&semaphore->base.mutex); if (semaphore->pending_signals > 0) { --semaphore->pending_signals; mono_coop_cond_destroy (&wait_entry.condition); - mono_coop_mutex_unlock (&semaphore->mutex); + mono_coop_mutex_unlock (&semaphore->base.mutex); return 1; } @@ -52,7 +53,7 @@ mono_lifo_semaphore_timed_wait (LifoSemaphore *semaphore, int32_t timeout_ms) // Wait for a signal or timeout int wait_error = 0; do { - wait_error = mono_coop_cond_timedwait (&wait_entry.condition, &semaphore->mutex, timeout_ms); + wait_error = mono_coop_cond_timedwait (&wait_entry.condition, &semaphore->base.mutex, timeout_ms); } while (wait_error == 0 && !wait_entry.signaled); if (wait_error == -1) { @@ -65,7 +66,7 @@ mono_lifo_semaphore_timed_wait (LifoSemaphore *semaphore, int32_t timeout_ms) } mono_coop_cond_destroy (&wait_entry.condition); - mono_coop_mutex_unlock (&semaphore->mutex); + mono_coop_mutex_unlock (&semaphore->base.mutex); return wait_entry.signaled; } @@ -73,7 +74,7 @@ mono_lifo_semaphore_timed_wait (LifoSemaphore *semaphore, int32_t timeout_ms) void mono_lifo_semaphore_release (LifoSemaphore *semaphore, uint32_t count) { - mono_coop_mutex_lock (&semaphore->mutex); + mono_coop_mutex_lock (&semaphore->base.mutex); while (count > 0) { LifoSemaphoreWaitEntry *wait_entry = semaphore->head; @@ -92,7 +93,7 @@ mono_lifo_semaphore_release (LifoSemaphore *semaphore, uint32_t count) } } - mono_coop_mutex_unlock (&semaphore->mutex); + mono_coop_mutex_unlock (&semaphore->base.mutex); } #if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) @@ -103,8 +104,9 @@ mono_lifo_js_semaphore_init (void) LifoJSSemaphore *sem = g_new0 (LifoJSSemaphore, 1); if (sem == NULL) return NULL; + sem->base.kind = LIFO_SEMAPHORE_ASYNC_JS; - mono_coop_mutex_init (&sem->mutex); + mono_coop_mutex_init (&sem->base.mutex); return sem; } @@ -114,7 +116,7 @@ mono_lifo_js_semaphore_delete (LifoJSSemaphore *sem) { /* FIXME: this is probably hard to guarantee - in-flight signaled semaphores still have wait entries */ g_assert (sem->head == NULL); - mono_coop_mutex_destroy (&sem->mutex); + mono_coop_mutex_destroy (&sem->base.mutex); g_free (sem); } @@ -188,10 +190,10 @@ mono_lifo_js_semaphore_prepare_wait (LifoJSSemaphore *sem, uint32_t gchandle, void *user_data) { - mono_coop_mutex_lock (&sem->mutex); + mono_coop_mutex_lock (&sem->base.mutex); if (sem->pending_signals > 0) { sem->pending_signals--; - mono_coop_mutex_unlock (&sem->mutex); + mono_coop_mutex_unlock (&sem->base.mutex); success_cb (sem, gchandle, user_data); // FIXME: queue microtask return; } @@ -215,7 +217,7 @@ mono_lifo_js_semaphore_prepare_wait (LifoJSSemaphore *sem, wait_entry->refcount = 1; // timeout owns the wait entry wait_entry->js_timeout_id = emscripten_set_timeout (lifo_js_wait_entry_on_timeout, (double)timeout_ms, wait_entry); lifo_js_wait_entry_push (&sem->head, wait_entry); - mono_coop_mutex_unlock (&sem->mutex); + mono_coop_mutex_unlock (&sem->base.mutex); return; } @@ -223,7 +225,7 @@ void mono_lifo_js_semaphore_release (LifoJSSemaphore *sem, uint32_t count) { - mono_coop_mutex_lock (&sem->mutex); + mono_coop_mutex_lock (&sem->base.mutex); while (count > 0) { LifoJSSemaphoreWaitEntry *wait_entry = lifo_js_find_waiter (sem->head); @@ -243,7 +245,7 @@ mono_lifo_js_semaphore_release (LifoJSSemaphore *sem, } } - mono_coop_mutex_unlock (&sem->mutex); + mono_coop_mutex_unlock (&sem->base.mutex); } static void @@ -257,7 +259,7 @@ lifo_js_wait_entry_on_timeout (void *wait_entry_as_user_data) LifoJSSemaphoreCallbackFn timeout_cb = NULL; uint32_t gchandle = 0; void *user_data = NULL; - mono_coop_mutex_lock (&sem->mutex); + mono_coop_mutex_lock (&sem->base.mutex); switch (wait_entry->state) { case LIFO_JS_WAITING: /* semaphore timed out before a Release. */ @@ -281,7 +283,7 @@ lifo_js_wait_entry_on_timeout (void *wait_entry_as_user_data) default: g_assert_not_reached(); } - mono_coop_mutex_unlock (&sem->mutex); + mono_coop_mutex_unlock (&sem->base.mutex); if (call_timeout_cb) { timeout_cb (sem, gchandle, user_data); } @@ -298,7 +300,7 @@ lifo_js_wait_entry_on_success (void *wait_entry_as_user_data) LifoJSSemaphoreCallbackFn success_cb = NULL; uint32_t gchandle = 0; void *user_data = NULL; - mono_coop_mutex_lock (&sem->mutex); + mono_coop_mutex_lock (&sem->base.mutex); switch (wait_entry->state) { case LIFO_JS_SIGNALED: g_assert (wait_entry->refcount == 2); @@ -321,7 +323,7 @@ lifo_js_wait_entry_on_success (void *wait_entry_as_user_data) default: g_assert_not_reached(); } - mono_coop_mutex_unlock (&sem->mutex); + mono_coop_mutex_unlock (&sem->base.mutex); g_assert (call_success_cb); success_cb (sem, gchandle, user_data); } diff --git a/src/mono/mono/utils/lifo-semaphore.h b/src/mono/mono/utils/lifo-semaphore.h index fe1fcae71666e..649d2ec552c4e 100644 --- a/src/mono/mono/utils/lifo-semaphore.h +++ b/src/mono/mono/utils/lifo-semaphore.h @@ -3,6 +3,21 @@ #include +typedef struct _LifoSemaphoreBase LifoSemaphoreBase; + +struct _LifoSemaphoreBase +{ + MonoCoopMutex mutex; + uint8_t kind; +}; + +enum { + LIFO_SEMAPHORE_NORMAL = 1, +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) + LIFO_SEMAPHORE_ASYNC_JS, +#endif +}; + typedef struct _LifoSemaphore LifoSemaphore; typedef struct _LifoSemaphoreWaitEntry LifoSemaphoreWaitEntry; @@ -14,7 +29,7 @@ struct _LifoSemaphoreWaitEntry { }; struct _LifoSemaphore { - MonoCoopMutex mutex; + LifoSemaphoreBase base; LifoSemaphoreWaitEntry *head; uint32_t pending_signals; }; @@ -64,7 +79,7 @@ struct _LifoJSSemaphoreWaitEntry { }; struct _LifoJSSemaphore { - MonoCoopMutex mutex; + LifoSemaphoreBase base; LifoJSSemaphoreWaitEntry *head; uint32_t pending_signals; }; From f4a2c02d700586a20e0a14ad614988a62a805adf Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 30 Mar 2023 16:10:02 -0400 Subject: [PATCH 29/37] Unify LowLevelLifoSemaphore for normal and async waiting --- .../System/Threading/LowLevelLifoSemaphore.cs | 3 + .../LowLevelJSSemaphore.Browser.Mono.cs | 316 +++++++++--------- .../LowLevelLifoSemaphore.Unix.Mono.cs | 22 +- ...dPool.WorkerThread.Browser.Threads.Mono.cs | 16 +- src/mono/mono/metadata/icall-decl.h | 7 +- src/mono/mono/metadata/icall-def.h | 14 +- src/mono/mono/metadata/threads.c | 89 +++-- 7 files changed, 234 insertions(+), 233 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.cs index 25a229c87ef66..b720761be85ee 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.cs @@ -39,6 +39,9 @@ public LowLevelLifoSemaphore(int initialSignalCount, int maximumSignalCount, int public bool Wait(int timeoutMs, bool spinWait) { +#if TARGET_BROWSER && FEATURE_WASM_THREADS + ThrowIfInvalidSemaphoreKind(LifoSemaphoreKind.Normal); +#endif Debug.Assert(timeoutMs >= -1); int spinCount = spinWait ? _spinCount : 0; diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelJSSemaphore.Browser.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelJSSemaphore.Browser.Mono.cs index 3c112fa8dc2d9..5552d46e8a6ff 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelJSSemaphore.Browser.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelJSSemaphore.Browser.Mono.cs @@ -10,27 +10,23 @@ namespace System.Threading; // -// This class provides a way for browser threads to asynchronously wait for a sempahore +// This class provides a way for browser threads to asynchronously wait for a semaphore // from JS, without using the threadpool. It is used to implement threadpool workers. // -internal sealed partial class LowLevelJSSemaphore : IDisposable +internal sealed partial class LowLevelLifoSemaphore : IDisposable { - // TODO: implement some of the managed stuff from LowLevelLifoSemaphore - private IntPtr lifo_semaphore; - private CacheLineSeparatedCounts _separated; - - private readonly int _maximumSignalCount; - private readonly int _spinCount; - private readonly Action _onWait; - - // private const int SpinSleep0Threshold = 10; + internal static LowLevelLifoSemaphore CreateAsyncJS (int initialSignalCount, int maximumSignalCount, int spinCount, Action onWait) + { + return new LowLevelLifoSemaphore(initialSignalCount, maximumSignalCount, spinCount, onWait, asyncJS: true); + } - internal LowLevelJSSemaphore(int initialSignalCount, int maximumSignalCount, int spinCount, Action onWait) + private LowLevelLifoSemaphore(int initialSignalCount, int maximumSignalCount, int spinCount, Action onWait, bool asyncJS) { Debug.Assert(initialSignalCount >= 0); Debug.Assert(initialSignalCount <= maximumSignalCount); Debug.Assert(maximumSignalCount > 0); Debug.Assert(spinCount >= 0); + Debug.Assert(asyncJS); _separated = default; _separated._counts.SignalCount = (uint)initialSignalCount; @@ -38,54 +34,176 @@ internal LowLevelJSSemaphore(int initialSignalCount, int maximumSignalCount, int _spinCount = spinCount; _onWait = onWait; - Create(maximumSignalCount); + CreateAsyncJS(maximumSignalCount); } - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern IntPtr InitInternal(); - #pragma warning disable IDE0060 - private void Create(int maximumSignalCount) + private void CreateAsyncJS(int maximumSignalCount) { - lifo_semaphore = InitInternal(); + _kind = LifoSemaphoreKind.AsyncJS; + lifo_semaphore = InitInternal((int)_kind); } #pragma warning restore IDE0060 [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void DeleteInternal(IntPtr semaphore); + private static extern unsafe void PrepareAsyncWaitInternal(IntPtr semaphore, + int timeoutMs, + /*delegate* unmanaged successCallback*/ void* successCallback, + /*delegate* unmanaged timeoutCallback*/ void* timeoutCallback, + GCHandle handle, + IntPtr userData); + + private sealed record WaitEntry (LowLevelLifoSemaphore Semaphore, Action OnSuccess, Action OnTimeout, object? State); + + internal void PrepareAsyncWait(int timeoutMs, Action onSuccess, Action onTimeout, object? state) + { + //FIXME(ak): the async wait never spins. Shoudl we spin a little? + Debug.Assert(timeoutMs >= -1); + + // Try to acquire the semaphore or + // [[a) register as a spinner if false and timeoutMs > 0]] + // b) register as a waiter if [[there's already too many spinners or]] true and timeoutMs > 0 + // c) bail out if timeoutMs == 0 and return false + Counts counts = _separated._counts; + while (true) + { + Debug.Assert(counts.SignalCount <= _maximumSignalCount); + Counts newCounts = counts; + if (counts.SignalCount != 0) + { + newCounts.DecrementSignalCount(); + } + else if (timeoutMs != 0) + { + // Maximum number of spinners reached, register as a waiter instead + newCounts.IncrementWaiterCount(); + } + + Counts countsBeforeUpdate = _separated._counts.InterlockedCompareExchange(newCounts, counts); + if (countsBeforeUpdate == counts) + { + if (counts.SignalCount != 0) + { + onSuccess (this, state); + return; + } + if (newCounts.WaiterCount != counts.WaiterCount) + { + PrepareAsyncWaitForSignal(timeoutMs, onSuccess, onTimeout, state); + return; + } + if (timeoutMs == 0) + { + onTimeout (this, state); + return; + } + break; + } + + counts = countsBeforeUpdate; + } + + Debug.Fail("unreachable"); + +#if false + // Unregister as spinner, and acquire the semaphore or register as a waiter + counts = _separated._counts; + while (true) + { + Counts newCounts = counts; + if (counts.SignalCount != 0) + { + newCounts.DecrementSignalCount(); + } + else + { + newCounts.IncrementWaiterCount(); + } + + Counts countsBeforeUpdate = _separated._counts.InterlockedCompareExchange(newCounts, counts); + if (countsBeforeUpdate == counts) + { + return counts.SignalCount != 0 || WaitForSignal(timeoutMs); + } + + counts = countsBeforeUpdate; + } +#endif + } - public void Dispose() + private void PrepareAsyncWaitForSignal(int timeoutMs, Action onSuccess, Action onTimeout, object? state) { - DeleteInternal(lifo_semaphore); - lifo_semaphore = IntPtr.Zero; + Debug.Assert(timeoutMs > 0 || timeoutMs == -1); + + _onWait(); + + PrepareAsyncWaitCore(timeoutMs, s_InternalAsyncWaitSuccess, s_InternalAsyncWaitTimeout, new InternalWait(timeoutMs, onSuccess, onTimeout, state)); } - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void ReleaseInternal(IntPtr semaphore, int count); + private static readonly Action s_InternalAsyncWaitSuccess = InternalAsyncWaitSuccess; - internal void Release(int additionalCount) + private static readonly Action s_InternalAsyncWaitTimeout = InternalAsyncWaitTimeout; + + internal sealed record InternalWait(int TimeoutMs, Action OnSuccess, Action OnTimeout, object? State); + + private static void InternalAsyncWaitTimeout(LowLevelLifoSemaphore self, object? internalWaitObj) { - ReleaseInternal(lifo_semaphore, additionalCount); + InternalWait i = (InternalWait)internalWaitObj!; + // Unregister the waiter. The wait subsystem used above guarantees that a thread that wakes due to a timeout does + // not observe a signal to the object being waited upon. + self._separated._counts.InterlockedDecrementWaiterCount(); + i.OnTimeout(self, i.State); } - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern unsafe void PrepareWaitInternal(IntPtr semaphore, - int timeoutMs, - /*delegate* unmanaged successCallback*/ void* successCallback, - /*delegate* unmanaged timeoutCallback*/ void* timeoutCallback, - GCHandle handle, - IntPtr userData); + private static void InternalAsyncWaitSuccess(LowLevelLifoSemaphore self, object? internalWaitObj) + { + InternalWait i = (InternalWait)internalWaitObj!; + // Unregister the waiter if this thread will not be waiting anymore, and try to acquire the semaphore + Counts counts = self._separated._counts; + while (true) + { + Debug.Assert(counts.WaiterCount != 0); + Counts newCounts = counts; + if (counts.SignalCount != 0) + { + newCounts.DecrementSignalCount(); + newCounts.DecrementWaiterCount(); + } + + // This waiter has woken up and this needs to be reflected in the count of waiters signaled to wake + if (counts.CountOfWaitersSignaledToWake != 0) + { + newCounts.DecrementCountOfWaitersSignaledToWake(); + } + + Counts countsBeforeUpdate = self._separated._counts.InterlockedCompareExchange(newCounts, counts); + if (countsBeforeUpdate == counts) + { + if (counts.SignalCount != 0) + { + i.OnSuccess(self, i.State); + return; + } + break; + } - private sealed record WaitEntry (LowLevelJSSemaphore Semaphore, Action OnSuccess, Action OnTimeout, object? State); + counts = countsBeforeUpdate; + } + // if we get here, we need to keep waiting because the SignalCount above was 0 after we did + // the CompareExchange - someone took the signal before us. + // FIXME(ak): why is the timeoutMs the same as before? wouldn't we starve? why does LowLevelLifoSemaphore.WaitForSignal not decrement timeoutMs? + self.PrepareAsyncWaitCore (i.TimeoutMs, s_InternalAsyncWaitSuccess, s_InternalAsyncWaitTimeout, i); + } - internal void PrepareWait(int timeout_ms, Action onSuccess, Action onTimeout, object? state) + internal void PrepareAsyncWaitCore(int timeout_ms, Action onSuccess, Action onTimeout, object? state) { + ThrowIfInvalidSemaphoreKind (LifoSemaphoreKind.AsyncJS); WaitEntry entry = new (this, onSuccess, onTimeout, state); GCHandle gchandle = GCHandle.Alloc (entry); unsafe { delegate* unmanaged successCallback = &SuccessCallback; delegate* unmanaged timeoutCallback = &TimeoutCallback; - PrepareWaitInternal (lifo_semaphore, timeout_ms, successCallback, timeoutCallback, gchandle, IntPtr.Zero); + PrepareAsyncWaitInternal (lifo_semaphore, timeout_ms, successCallback, timeoutCallback, gchandle, IntPtr.Zero); } } @@ -105,130 +223,4 @@ private static void TimeoutCallback(IntPtr lifo_semaphore, GCHandle gchandle, In entry.OnTimeout(entry.Semaphore, entry.State); } -#region Counts - private struct Counts : IEquatable - { - private const byte SignalCountShift = 0; - private const byte WaiterCountShift = 32; - private const byte SpinnerCountShift = 48; - private const byte CountOfWaitersSignaledToWakeShift = 56; - - private ulong _data; - - private Counts(ulong data) => _data = data; - - private uint GetUInt32Value(byte shift) => (uint)(_data >> shift); - private void SetUInt32Value(uint value, byte shift) => - _data = (_data & ~((ulong)uint.MaxValue << shift)) | ((ulong)value << shift); - private ushort GetUInt16Value(byte shift) => (ushort)(_data >> shift); - private void SetUInt16Value(ushort value, byte shift) => - _data = (_data & ~((ulong)ushort.MaxValue << shift)) | ((ulong)value << shift); - private byte GetByteValue(byte shift) => (byte)(_data >> shift); - private void SetByteValue(byte value, byte shift) => - _data = (_data & ~((ulong)byte.MaxValue << shift)) | ((ulong)value << shift); - - public uint SignalCount - { - get => GetUInt32Value(SignalCountShift); - set => SetUInt32Value(value, SignalCountShift); - } - - public void AddSignalCount(uint value) - { - Debug.Assert(value <= uint.MaxValue - SignalCount); - _data += (ulong)value << SignalCountShift; - } - - public void IncrementSignalCount() => AddSignalCount(1); - - public void DecrementSignalCount() - { - Debug.Assert(SignalCount != 0); - _data -= (ulong)1 << SignalCountShift; - } - - public ushort WaiterCount - { - get => GetUInt16Value(WaiterCountShift); - set => SetUInt16Value(value, WaiterCountShift); - } - - public void IncrementWaiterCount() - { - Debug.Assert(WaiterCount < ushort.MaxValue); - _data += (ulong)1 << WaiterCountShift; - } - - public void DecrementWaiterCount() - { - Debug.Assert(WaiterCount != 0); - _data -= (ulong)1 << WaiterCountShift; - } - - public void InterlockedDecrementWaiterCount() - { - var countsAfterUpdate = new Counts(Interlocked.Add(ref _data, unchecked((ulong)-1) << WaiterCountShift)); - Debug.Assert(countsAfterUpdate.WaiterCount != ushort.MaxValue); // underflow check - } - - public byte SpinnerCount - { - get => GetByteValue(SpinnerCountShift); - set => SetByteValue(value, SpinnerCountShift); - } - - public void IncrementSpinnerCount() - { - Debug.Assert(SpinnerCount < byte.MaxValue); - _data += (ulong)1 << SpinnerCountShift; - } - - public void DecrementSpinnerCount() - { - Debug.Assert(SpinnerCount != 0); - _data -= (ulong)1 << SpinnerCountShift; - } - - public byte CountOfWaitersSignaledToWake - { - get => GetByteValue(CountOfWaitersSignaledToWakeShift); - set => SetByteValue(value, CountOfWaitersSignaledToWakeShift); - } - - public void AddUpToMaxCountOfWaitersSignaledToWake(uint value) - { - uint availableCount = (uint)(byte.MaxValue - CountOfWaitersSignaledToWake); - if (value > availableCount) - { - value = availableCount; - } - _data += (ulong)value << CountOfWaitersSignaledToWakeShift; - } - - public void DecrementCountOfWaitersSignaledToWake() - { - Debug.Assert(CountOfWaitersSignaledToWake != 0); - _data -= (ulong)1 << CountOfWaitersSignaledToWakeShift; - } - - public Counts InterlockedCompareExchange(Counts newCounts, Counts oldCounts) => - new Counts(Interlocked.CompareExchange(ref _data, newCounts._data, oldCounts._data)); - - public static bool operator ==(Counts lhs, Counts rhs) => lhs.Equals(rhs); - public static bool operator !=(Counts lhs, Counts rhs) => !lhs.Equals(rhs); - - public override bool Equals([NotNullWhen(true)] object? obj) => obj is Counts other && Equals(other); - public bool Equals(Counts other) => _data == other._data; - public override int GetHashCode() => (int)_data + (int)(_data >> 32); - } - - [StructLayout(LayoutKind.Sequential)] - private struct CacheLineSeparatedCounts - { - private readonly Internal.PaddingFor32 _pad1; - public Counts _counts; - private readonly Internal.PaddingFor32 _pad2; - } -#endregion - } diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.Unix.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.Unix.Mono.cs index 180f802ed84ca..d4ce63957e206 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.Unix.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.Unix.Mono.cs @@ -8,14 +8,24 @@ namespace System.Threading internal sealed unsafe partial class LowLevelLifoSemaphore : IDisposable { private IntPtr lifo_semaphore; +#if FEATURE_WASM_THREADS + private LifoSemaphoreKind _kind; + + // Keep in sync with lifo-semaphore.h + private enum LifoSemaphoreKind : int { + Normal = 1, + AsyncJS = 2, + } +#endif [MethodImplAttribute(MethodImplOptions.InternalCall)] - private static extern IntPtr InitInternal(); + private static extern IntPtr InitInternal(int kind); #pragma warning disable IDE0060 private void Create(int maximumSignalCount) { - lifo_semaphore = InitInternal(); + _kind = LifoSemaphoreKind.Normal; + lifo_semaphore = InitInternal((int)_kind); } #pragma warning restore IDE0060 @@ -26,6 +36,7 @@ public void Dispose() { DeleteInternal(lifo_semaphore); lifo_semaphore = IntPtr.Zero; + _kind = (LifoSemaphoreKind)0; } [MethodImplAttribute(MethodImplOptions.InternalCall)] @@ -33,9 +44,16 @@ public void Dispose() private bool WaitCore(int timeoutMs) { + ThrowIfInvalidSemaphoreKind(LifoSemaphoreKind.Normal); return TimedWaitInternal(lifo_semaphore, timeoutMs) != 0; } + private void ThrowIfInvalidSemaphoreKind(LifoSemaphoreKind expected) + { + if (_kind != expected) + throw new InvalidOperationException ($"Unexpected LowLevelLifoSemaphore kind {_kind} expected {expected}"); + } + [MethodImplAttribute(MethodImplOptions.InternalCall)] private static extern void ReleaseInternal(IntPtr semaphore, int count); diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs index d8d963cd45ddb..b12a04836e60f 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs @@ -18,8 +18,8 @@ private static partial class WorkerThread /// /// Semaphore for controlling how many threads are currently working. /// - private static readonly LowLevelJSSemaphore s_semaphore = - new LowLevelJSSemaphore( + private static readonly LowLevelLifoSemaphore s_semaphore = + LowLevelLifoSemaphore.CreateAsyncJS( 0, MaxPossibleThreadCount, AppContextConfigHelper.GetInt32Config( @@ -68,17 +68,17 @@ private static void WorkerThreadStart() // return from thread start with keepalive - the thread will stay alive in the JS event loop } - private static readonly Action s_WorkLoopSemaphoreSuccess = new(WorkLoopSemaphoreSuccess); - private static readonly Action s_WorkLoopSemaphoreTimedOut = new(WorkLoopSemaphoreTimedOut); + private static readonly Action s_WorkLoopSemaphoreSuccess = new(WorkLoopSemaphoreSuccess); + private static readonly Action s_WorkLoopSemaphoreTimedOut = new(WorkLoopSemaphoreTimedOut); - private static void WaitForWorkLoop(LowLevelJSSemaphore semaphore, SemaphoreWaitState state) + private static void WaitForWorkLoop(LowLevelLifoSemaphore semaphore, SemaphoreWaitState state) { - semaphore.PrepareWait(ThreadPoolThreadTimeoutMs, s_WorkLoopSemaphoreSuccess, s_WorkLoopSemaphoreTimedOut, state); + semaphore.PrepareAsyncWait(ThreadPoolThreadTimeoutMs, s_WorkLoopSemaphoreSuccess, s_WorkLoopSemaphoreTimedOut, state); // thread should still be kept alive Debug.Assert(state.KeepaliveToken.Valid); } - private static void WorkLoopSemaphoreSuccess(LowLevelJSSemaphore semaphore, object? stateObject) + private static void WorkLoopSemaphoreSuccess(LowLevelLifoSemaphore semaphore, object? stateObject) { SemaphoreWaitState state = (SemaphoreWaitState)stateObject!; WorkerDoWork(state.ThreadPoolInstance, ref state.SpinWait); @@ -86,7 +86,7 @@ private static void WorkLoopSemaphoreSuccess(LowLevelJSSemaphore semaphore, obje WaitForWorkLoop(semaphore, state); } - private static void WorkLoopSemaphoreTimedOut(LowLevelJSSemaphore semaphore, object? stateObject) + private static void WorkLoopSemaphoreTimedOut(LowLevelLifoSemaphore semaphore, object? stateObject) { SemaphoreWaitState state = (SemaphoreWaitState)stateObject!; if (WorkerTimedOutMaybeStop(state.ThreadPoolInstance, state.ThreadAdjustmentLock)) { diff --git a/src/mono/mono/metadata/icall-decl.h b/src/mono/mono/metadata/icall-decl.h index 8681014df37d8..0f02b00d5f5a8 100644 --- a/src/mono/mono/metadata/icall-decl.h +++ b/src/mono/mono/metadata/icall-decl.h @@ -179,17 +179,14 @@ ICALL_EXPORT void ves_icall_Mono_SafeStringMarshal_GFree (void *c_str); ICALL_EXPORT char* ves_icall_Mono_SafeStringMarshal_StringToUtf8 (MonoString *volatile* s); ICALL_EXPORT MonoType* ves_icall_Mono_RuntimeClassHandle_GetTypeFromClass (MonoClass *klass); -ICALL_EXPORT gpointer ves_icall_System_Threading_LowLevelLifoSemaphore_InitInternal (void); +ICALL_EXPORT gpointer ves_icall_System_Threading_LowLevelLifoSemaphore_InitInternal (int32_t kind); ICALL_EXPORT void ves_icall_System_Threading_LowLevelLifoSemaphore_DeleteInternal (gpointer sem_ptr); ICALL_EXPORT gint32 ves_icall_System_Threading_LowLevelLifoSemaphore_TimedWaitInternal (gpointer sem_ptr, gint32 timeout_ms); ICALL_EXPORT void ves_icall_System_Threading_LowLevelLifoSemaphore_ReleaseInternal (gpointer sem_ptr, gint32 count); /* include these declarations if we're in the threaded wasm runtime, or if we're building a wasm-targeting cross compiler and we need to support --print-icall-table */ #if (defined(HOST_BROWSER) && !defined(DISABLE_THREADS)) || (defined(TARGET_WASM) && defined(ENABLE_ICALL_SYMBOL_MAP)) -ICALL_EXPORT gpointer ves_icall_System_Threading_LowLevelJSSemaphore_InitInternal (void); -ICALL_EXPORT void ves_icall_System_Threading_LowLevelJSSemaphore_DeleteInternal (gpointer sem_ptr); -ICALL_EXPORT void ves_icall_System_Threading_LowLevelJSSemaphore_PrepareWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timeout_cb, gpointer gchandle, gpointer user_data); -ICALL_EXPORT void ves_icall_System_Threading_LowLevelJSSemaphore_ReleaseInternal (gpointer sem_ptr, gint32 count); +ICALL_EXPORT void ves_icall_System_Threading_LowLevelLifoSemaphore_PrepareAsyncWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timeout_cb, gpointer gchandle, gpointer user_data); ICALL_EXPORT void ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePushInternal (void); ICALL_EXPORT void ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal (void); diff --git a/src/mono/mono/metadata/icall-def.h b/src/mono/mono/metadata/icall-def.h index 40fd86479f693..0de7251b26dea 100644 --- a/src/mono/mono/metadata/icall-def.h +++ b/src/mono/mono/metadata/icall-def.h @@ -567,21 +567,17 @@ NOHANDLES(ICALL(ILOCK_21, "Increment(long&)", ves_icall_System_Threading_Interlo NOHANDLES(ICALL(ILOCK_22, "MemoryBarrierProcessWide", ves_icall_System_Threading_Interlocked_MemoryBarrierProcessWide)) NOHANDLES(ICALL(ILOCK_23, "Read(long&)", ves_icall_System_Threading_Interlocked_Read_Long)) -/* include these icalls if we're in the threaded wasm runtime, or if we're building a wasm-targeting cross compiler and we need to support --print-icall-table */ -#if (defined(HOST_BROWSER) && !defined(DISABLE_THREADS)) || (defined(TARGET_WASM) && defined(ENABLE_ICALL_SYMBOL_MAP)) -ICALL_TYPE(JSSEM, "System.Threading.LowLevelJSSemaphore", JSSEM_1) -NOHANDLES(ICALL(JSSEM_1, "DeleteInternal", ves_icall_System_Threading_LowLevelJSSemaphore_DeleteInternal)) -NOHANDLES(ICALL(JSSEM_2, "InitInternal", ves_icall_System_Threading_LowLevelJSSemaphore_InitInternal)) -NOHANDLES(ICALL(JSSEM_3, "PrepareWaitInternal", ves_icall_System_Threading_LowLevelJSSemaphore_PrepareWaitInternal)) -NOHANDLES(ICALL(JSSEM_4, "ReleaseInternal", ves_icall_System_Threading_LowLevelJSSemaphore_ReleaseInternal)) -#endif - ICALL_TYPE(LIFOSEM, "System.Threading.LowLevelLifoSemaphore", LIFOSEM_1) NOHANDLES(ICALL(LIFOSEM_1, "DeleteInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_DeleteInternal)) NOHANDLES(ICALL(LIFOSEM_2, "InitInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_InitInternal)) +/* include these icalls if we're in the threaded wasm runtime, or if we're building a wasm-targeting cross compiler and we need to support --print-icall-table */ +#if (defined(HOST_BROWSER) && !defined(DISABLE_THREADS)) || (defined(TARGET_WASM) && defined(ENABLE_ICALL_SYMBOL_MAP)) +NOHANDLES(ICALL(LIFOSEM_5, "PrepareAsyncWaitInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_PrepareAsyncWaitInternal)) +#endif NOHANDLES(ICALL(LIFOSEM_3, "ReleaseInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_ReleaseInternal)) NOHANDLES(ICALL(LIFOSEM_4, "TimedWaitInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_TimedWaitInternal)) + ICALL_TYPE(MONIT, "System.Threading.Monitor", MONIT_0) HANDLES(MONIT_0, "Enter", ves_icall_System_Threading_Monitor_Monitor_Enter, void, 1, (MonoObject)) HANDLES(MONIT_1, "InternalExit", mono_monitor_exit_icall, void, 1, (MonoObject)) diff --git a/src/mono/mono/metadata/threads.c b/src/mono/mono/metadata/threads.c index bfeb7cc788194..df7789caa9c93 100644 --- a/src/mono/mono/metadata/threads.c +++ b/src/mono/mono/metadata/threads.c @@ -4999,60 +4999,74 @@ ves_icall_System_Threading_Thread_GetCurrentOSThreadId (MonoError *error) } gpointer -ves_icall_System_Threading_LowLevelLifoSemaphore_InitInternal (void) +ves_icall_System_Threading_LowLevelLifoSemaphore_InitInternal (int32_t kind) { - return (gpointer)mono_lifo_semaphore_init (); + switch (kind) { + case LIFO_SEMAPHORE_NORMAL: + return (gpointer)mono_lifo_semaphore_init (); +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) + case LIFO_SEMAPHORE_ASYNC_JS: + return (gpointer)mono_lifo_js_semaphore_init (); +#endif + default: + g_error ("Invalid LowLevelLifoSemaphore kind %d\n", kind); + g_assert_not_reached(); + } } void ves_icall_System_Threading_LowLevelLifoSemaphore_DeleteInternal (gpointer sem_ptr) { - LifoSemaphore *sem = (LifoSemaphore *)sem_ptr; - mono_lifo_semaphore_delete (sem); + LifoSemaphoreBase *sem = (LifoSemaphoreBase *)sem_ptr; + switch (sem->kind) { + case LIFO_SEMAPHORE_NORMAL: + mono_lifo_semaphore_delete ((LifoSemaphore*)sem); + break; +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) + case LIFO_SEMAPHORE_ASYNC_JS: + mono_lifo_js_semaphore_delete ((LifoJSSemaphore*)sem); + break; +#endif + default: + g_assert_not_reached(); + } } gint32 ves_icall_System_Threading_LowLevelLifoSemaphore_TimedWaitInternal (gpointer sem_ptr, gint32 timeout_ms) { LifoSemaphore *sem = (LifoSemaphore *)sem_ptr; + g_assert (sem->base.kind == LIFO_SEMAPHORE_NORMAL); return mono_lifo_semaphore_timed_wait (sem, timeout_ms); } void ves_icall_System_Threading_LowLevelLifoSemaphore_ReleaseInternal (gpointer sem_ptr, gint32 count) { - LifoSemaphore *sem = (LifoSemaphore *)sem_ptr; - mono_lifo_semaphore_release (sem, count); -} - + LifoSemaphoreBase *sem = (LifoSemaphoreBase *)sem_ptr; + switch (sem->kind) { + case LIFO_SEMAPHORE_NORMAL: + mono_lifo_semaphore_release ((LifoSemaphore*)sem, count); + break; #if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) -gpointer -ves_icall_System_Threading_LowLevelJSSemaphore_InitInternal (void) -{ - return (gpointer)mono_lifo_js_semaphore_init (); -} - -void -ves_icall_System_Threading_LowLevelJSSemaphore_DeleteInternal (gpointer sem_ptr) -{ - LifoJSSemaphore *sem = (LifoJSSemaphore *)sem_ptr; - mono_lifo_js_semaphore_delete (sem); + case LIFO_SEMAPHORE_ASYNC_JS: + mono_lifo_js_semaphore_release ((LifoJSSemaphore*)sem, count); + break; +#endif + default: + g_assert_not_reached(); + } } +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) void -ves_icall_System_Threading_LowLevelJSSemaphore_PrepareWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timedout_cb, gpointer gchandle, gpointer user_data) +ves_icall_System_Threading_LowLevelLifoSemaphore_PrepareAsyncWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timedout_cb, gpointer gchandle, gpointer user_data) { LifoJSSemaphore *sem = (LifoJSSemaphore *)sem_ptr; + g_assert (sem->base.kind == LIFO_SEMAPHORE_ASYNC_JS); mono_lifo_js_semaphore_prepare_wait (sem, timeout_ms, (LifoJSSemaphoreCallbackFn)success_cb, (LifoJSSemaphoreCallbackFn)timedout_cb, (uint32_t)(MonoGCHandle)gchandle, user_data); } -void -ves_icall_System_Threading_LowLevelJSSemaphore_ReleaseInternal (gpointer sem_ptr, gint32 count) -{ - LifoJSSemaphore *sem = (LifoJSSemaphore *)sem_ptr; - mono_lifo_js_semaphore_release (sem, count); -} - void ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePushInternal (void) { @@ -5091,31 +5105,12 @@ ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal (void) /* for the AOT cross compiler with --print-icall-table these don't need to be callable, they just * need to be defined */ #if defined(TARGET_WASM) && defined(ENABLE_ICALL_SYMBOL_MAP) -gpointer -ves_icall_System_Threading_LowLevelJSSemaphore_InitInternal (void) -{ - g_assert_not_reached(); -} - void -ves_icall_System_Threading_LowLevelJSSemaphore_DeleteInternal (gpointer sem_ptr) +ves_icall_System_Threading_LowLevelLifoSemaphore_PrepareAsyncWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timedout_cb, gpointer gchandle, gpointer user_data) { g_assert_not_reached(); } -void -ves_icall_System_Threading_LowLevelJSSemaphore_PrepareWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timedout_cb, gpointer gchandle, gpointer user_data) -{ - g_assert_not_reached(); -} - -void -ves_icall_System_Threading_LowLevelJSSemaphore_ReleaseInternal (gpointer sem_ptr, gint32 count) -{ - g_assert_not_reached(); - -} - void ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePushInternal (void) { From 42388d8678194b294a92798082d5d352383c4cf1 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 30 Mar 2023 21:48:23 -0400 Subject: [PATCH 30/37] WebWorkerEventLoop: remove dead code, update comments --- ...WebWorkerEventLoop.Browser.Threads.Mono.cs | 41 ++++++------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/WebWorkerEventLoop.Browser.Threads.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/WebWorkerEventLoop.Browser.Threads.Mono.cs index 0984d6937822b..72260e3e8fcdb 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/WebWorkerEventLoop.Browser.Threads.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/WebWorkerEventLoop.Browser.Threads.Mono.cs @@ -17,15 +17,12 @@ internal static class WebWorkerEventLoop private static extern void KeepalivePushInternal(); [MethodImpl(MethodImplOptions.InternalCall)] private static extern void KeepalivePopInternal(); -#if false - [DoesNotReturn] - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void UnwindToJsInternal(); - [DoesNotReturn] - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void ThreadExitInternal(); -#endif + /// + /// A keepalive token prevents a thread from shutting down even if it returns to the JS event + /// loop. A thread may want a keepalive token if it needs to allow JS code to run to settle JS + /// promises or execute JS timeout callbacks. + /// internal sealed class KeepaliveToken { public bool Valid {get; private set; } @@ -33,10 +30,9 @@ internal sealed class KeepaliveToken private KeepaliveToken() { Valid = true; } /// - /// Decrement the Emscripten keepalive count. A thread with - /// a zero keepalive count will terminate when it returns - /// from its start function or from an async invocation from - /// the JS event loop. + /// Decrement the Emscripten keepalive count. A thread with a zero keepalive count will + /// terminate when it returns from its start function or from an async invocation from the + /// JS event loop. /// internal void Pop() { if (!Valid) @@ -59,25 +55,12 @@ internal static KeepaliveToken Create() /// internal static KeepaliveToken KeepalivePush() => KeepaliveToken.Create(); - // FIXME: these are dangerous they will not unwind managad frames (so finally clauses wont' run) and maybe leak in the interpreter memory -#if false /// - /// Abort the current execution and unwind to the JS event loop + /// Start a thread that may be kept alive on its webworker after the start function returns, + /// if the emscripten keepalive count is positive. Once the thread returns to the JS event + /// loop it will be able to settle JS promises as well as run any queued managed async + /// callbacks. /// - /// - // FIXME: we should probably setup some managed exception to - // unwind the managed stack before calling the emscripten - // unwind_to_js to unwind the native stack. - [DoesNotReturn] - internal static void UnwindToJs() => UnwindToJsInternal(); - - /// - /// Terminate the current thread, even if the thread was kept alive with KeepalivePush - /// - internal static void ThreadExit() => ThreadExitInternal(); -#endif - - internal static void StartExitable(Thread thread, bool captureContext) { // don't support captureContext == true, for now, since it's From 853938ebb6997f3368db5ec9761e06a9b4082482 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 30 Mar 2023 21:59:51 -0400 Subject: [PATCH 31/37] remove unused arg from async wait semaphore don't need both user data and a gchandle --- .../LowLevelJSSemaphore.Browser.Mono.cs | 17 +++++++++-------- src/mono/mono/metadata/icall-decl.h | 2 +- src/mono/mono/metadata/threads.c | 6 +++--- src/mono/mono/utils/lifo-semaphore.c | 18 ++++++------------ src/mono/mono/utils/lifo-semaphore.h | 8 +++----- 5 files changed, 22 insertions(+), 29 deletions(-) diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelJSSemaphore.Browser.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelJSSemaphore.Browser.Mono.cs index 5552d46e8a6ff..581c15cb2f941 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelJSSemaphore.Browser.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelJSSemaphore.Browser.Mono.cs @@ -48,9 +48,8 @@ private void CreateAsyncJS(int maximumSignalCount) [MethodImpl(MethodImplOptions.InternalCall)] private static extern unsafe void PrepareAsyncWaitInternal(IntPtr semaphore, int timeoutMs, - /*delegate* unmanaged successCallback*/ void* successCallback, - /*delegate* unmanaged timeoutCallback*/ void* timeoutCallback, - GCHandle handle, + /*delegate* unmanaged successCallback*/ void* successCallback, + /*delegate* unmanaged timeoutCallback*/ void* timeoutCallback, IntPtr userData); private sealed record WaitEntry (LowLevelLifoSemaphore Semaphore, Action OnSuccess, Action OnTimeout, object? State); @@ -201,23 +200,25 @@ internal void PrepareAsyncWaitCore(int timeout_ms, Action successCallback = &SuccessCallback; - delegate* unmanaged timeoutCallback = &TimeoutCallback; - PrepareAsyncWaitInternal (lifo_semaphore, timeout_ms, successCallback, timeoutCallback, gchandle, IntPtr.Zero); + delegate* unmanaged successCallback = &SuccessCallback; + delegate* unmanaged timeoutCallback = &TimeoutCallback; + PrepareAsyncWaitInternal (lifo_semaphore, timeout_ms, successCallback, timeoutCallback, GCHandle.ToIntPtr(gchandle)); } } [UnmanagedCallersOnly] - private static void SuccessCallback(IntPtr lifo_semaphore, GCHandle gchandle, IntPtr user_data) + private static void SuccessCallback(IntPtr lifoSemaphore, IntPtr userData) { + GCHandle gchandle = GCHandle.FromIntPtr(userData); WaitEntry entry = (WaitEntry)gchandle.Target!; gchandle.Free(); entry.OnSuccess(entry.Semaphore, entry.State); } [UnmanagedCallersOnly] - private static void TimeoutCallback(IntPtr lifo_semaphore, GCHandle gchandle, IntPtr user_data) + private static void TimeoutCallback(IntPtr lifoSemaphore, IntPtr userData) { + GCHandle gchandle = GCHandle.FromIntPtr(userData); WaitEntry entry = (WaitEntry)gchandle.Target!; gchandle.Free(); entry.OnTimeout(entry.Semaphore, entry.State); diff --git a/src/mono/mono/metadata/icall-decl.h b/src/mono/mono/metadata/icall-decl.h index 0f02b00d5f5a8..16e9524612fec 100644 --- a/src/mono/mono/metadata/icall-decl.h +++ b/src/mono/mono/metadata/icall-decl.h @@ -186,7 +186,7 @@ ICALL_EXPORT void ves_icall_System_Threading_LowLevelLifoSemaphore_ReleaseIn /* include these declarations if we're in the threaded wasm runtime, or if we're building a wasm-targeting cross compiler and we need to support --print-icall-table */ #if (defined(HOST_BROWSER) && !defined(DISABLE_THREADS)) || (defined(TARGET_WASM) && defined(ENABLE_ICALL_SYMBOL_MAP)) -ICALL_EXPORT void ves_icall_System_Threading_LowLevelLifoSemaphore_PrepareAsyncWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timeout_cb, gpointer gchandle, gpointer user_data); +ICALL_EXPORT void ves_icall_System_Threading_LowLevelLifoSemaphore_PrepareAsyncWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timeout_cb, intptr_t user_data); ICALL_EXPORT void ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePushInternal (void); ICALL_EXPORT void ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal (void); diff --git a/src/mono/mono/metadata/threads.c b/src/mono/mono/metadata/threads.c index df7789caa9c93..3c05c5b9af15e 100644 --- a/src/mono/mono/metadata/threads.c +++ b/src/mono/mono/metadata/threads.c @@ -5060,11 +5060,11 @@ ves_icall_System_Threading_LowLevelLifoSemaphore_ReleaseInternal (gpointer sem_p #if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) void -ves_icall_System_Threading_LowLevelLifoSemaphore_PrepareAsyncWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timedout_cb, gpointer gchandle, gpointer user_data) +ves_icall_System_Threading_LowLevelLifoSemaphore_PrepareAsyncWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timedout_cb, intptr_t user_data) { LifoJSSemaphore *sem = (LifoJSSemaphore *)sem_ptr; g_assert (sem->base.kind == LIFO_SEMAPHORE_ASYNC_JS); - mono_lifo_js_semaphore_prepare_wait (sem, timeout_ms, (LifoJSSemaphoreCallbackFn)success_cb, (LifoJSSemaphoreCallbackFn)timedout_cb, (uint32_t)(MonoGCHandle)gchandle, user_data); + mono_lifo_js_semaphore_prepare_wait (sem, timeout_ms, (LifoJSSemaphoreCallbackFn)success_cb, (LifoJSSemaphoreCallbackFn)timedout_cb, user_data); } void @@ -5106,7 +5106,7 @@ ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal (void) * need to be defined */ #if defined(TARGET_WASM) && defined(ENABLE_ICALL_SYMBOL_MAP) void -ves_icall_System_Threading_LowLevelLifoSemaphore_PrepareAsyncWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timedout_cb, gpointer gchandle, gpointer user_data) +ves_icall_System_Threading_LowLevelLifoSemaphore_PrepareAsyncWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timedout_cb, intptr_t user_data) { g_assert_not_reached(); } diff --git a/src/mono/mono/utils/lifo-semaphore.c b/src/mono/mono/utils/lifo-semaphore.c index b9cf8100edb53..ec340b5b91816 100644 --- a/src/mono/mono/utils/lifo-semaphore.c +++ b/src/mono/mono/utils/lifo-semaphore.c @@ -187,14 +187,13 @@ mono_lifo_js_semaphore_prepare_wait (LifoJSSemaphore *sem, int32_t timeout_ms, LifoJSSemaphoreCallbackFn success_cb, LifoJSSemaphoreCallbackFn timeout_cb, - uint32_t gchandle, - void *user_data) + intptr_t user_data) { mono_coop_mutex_lock (&sem->base.mutex); if (sem->pending_signals > 0) { sem->pending_signals--; mono_coop_mutex_unlock (&sem->base.mutex); - success_cb (sem, gchandle, user_data); // FIXME: queue microtask + success_cb (sem, user_data); // FIXME: queue microtask return; } @@ -210,7 +209,6 @@ mono_lifo_js_semaphore_prepare_wait (LifoJSSemaphore *sem, wait_entry->success_cb = success_cb; wait_entry->timeout_cb = timeout_cb; wait_entry->sem = sem; - wait_entry->gchandle = gchandle; wait_entry->user_data = user_data; wait_entry->thread = pthread_self(); wait_entry->state = LIFO_JS_WAITING; @@ -257,8 +255,7 @@ lifo_js_wait_entry_on_timeout (void *wait_entry_as_user_data) LifoJSSemaphore *sem = wait_entry->sem; gboolean call_timeout_cb = FALSE; LifoJSSemaphoreCallbackFn timeout_cb = NULL; - uint32_t gchandle = 0; - void *user_data = NULL; + intptr_t user_data = 0; mono_coop_mutex_lock (&sem->base.mutex); switch (wait_entry->state) { case LIFO_JS_WAITING: @@ -267,7 +264,6 @@ lifo_js_wait_entry_on_timeout (void *wait_entry_as_user_data) /* unlink and free the wait entry, run the user timeout_cb. */ lifo_js_wait_entry_unlink (&sem->head, wait_entry); timeout_cb = wait_entry->timeout_cb; - gchandle = wait_entry->gchandle; user_data = wait_entry->user_data; g_free (wait_entry); call_timeout_cb = TRUE; @@ -285,7 +281,7 @@ lifo_js_wait_entry_on_timeout (void *wait_entry_as_user_data) } mono_coop_mutex_unlock (&sem->base.mutex); if (call_timeout_cb) { - timeout_cb (sem, gchandle, user_data); + timeout_cb (sem, user_data); } } @@ -298,8 +294,7 @@ lifo_js_wait_entry_on_success (void *wait_entry_as_user_data) LifoJSSemaphore *sem = wait_entry->sem; gboolean call_success_cb = FALSE; LifoJSSemaphoreCallbackFn success_cb = NULL; - uint32_t gchandle = 0; - void *user_data = NULL; + intptr_t user_data = 0; mono_coop_mutex_lock (&sem->base.mutex); switch (wait_entry->state) { case LIFO_JS_SIGNALED: @@ -314,7 +309,6 @@ lifo_js_wait_entry_on_success (void *wait_entry_as_user_data) g_assert (wait_entry->refcount == 1); lifo_js_wait_entry_unlink (&sem->head, wait_entry); success_cb = wait_entry->success_cb; - gchandle = wait_entry->gchandle; user_data = wait_entry->user_data; g_free (wait_entry); call_success_cb = TRUE; @@ -325,7 +319,7 @@ lifo_js_wait_entry_on_success (void *wait_entry_as_user_data) } mono_coop_mutex_unlock (&sem->base.mutex); g_assert (call_success_cb); - success_cb (sem, gchandle, user_data); + success_cb (sem, user_data); } #endif /* HOST_BROWSER && !DISABLE_THREADS */ diff --git a/src/mono/mono/utils/lifo-semaphore.h b/src/mono/mono/utils/lifo-semaphore.h index 649d2ec552c4e..36a73fe04e4fb 100644 --- a/src/mono/mono/utils/lifo-semaphore.h +++ b/src/mono/mono/utils/lifo-semaphore.h @@ -61,7 +61,7 @@ typedef struct _LifoJSSemaphore LifoJSSemaphore; */ typedef struct _LifoJSSemaphoreWaitEntry LifoJSSemaphoreWaitEntry; -typedef void (*LifoJSSemaphoreCallbackFn)(LifoJSSemaphore *semaphore, uint32_t gch, void *user_data); +typedef void (*LifoJSSemaphoreCallbackFn)(LifoJSSemaphore *semaphore, intptr_t user_data); struct _LifoJSSemaphoreWaitEntry { LifoJSSemaphoreWaitEntry *previous; @@ -69,9 +69,8 @@ struct _LifoJSSemaphoreWaitEntry { LifoJSSemaphoreCallbackFn success_cb; LifoJSSemaphoreCallbackFn timeout_cb; LifoJSSemaphore *sem; - void *user_data; + intptr_t user_data; pthread_t thread; - uint32_t gchandle; // what do we want in here? int32_t js_timeout_id; // only valid to access from the waiting thread /* state and refcount are protected by the semaphore mutex */ uint16_t state; /* 0 waiting, 1 signaled, 2 signaled - timeout ignored */ @@ -131,8 +130,7 @@ void mono_lifo_js_semaphore_prepare_wait (LifoJSSemaphore *semaphore, int32_t timeout_ms, LifoJSSemaphoreCallbackFn success_cb, LifoJSSemaphoreCallbackFn timeout_cb, - uint32_t gchandle, - void *user_data); + intptr_t user_data); void mono_lifo_js_semaphore_release (LifoJSSemaphore *semaphore, uint32_t count); From cb8b1688d4e17f4eaa72955d0767232ca797631d Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 30 Mar 2023 22:15:50 -0400 Subject: [PATCH 32/37] rename native semaphore to LifoSemaphoreAsyncWait prefix functions with mono_lifo_semaphore_asyncwait_ --- src/mono/mono/metadata/threads.c | 18 ++++---- src/mono/mono/utils/lifo-semaphore.c | 64 ++++++++++++++-------------- src/mono/mono/utils/lifo-semaphore.h | 41 +++++++++--------- 3 files changed, 61 insertions(+), 62 deletions(-) diff --git a/src/mono/mono/metadata/threads.c b/src/mono/mono/metadata/threads.c index 3c05c5b9af15e..ac143daf981ad 100644 --- a/src/mono/mono/metadata/threads.c +++ b/src/mono/mono/metadata/threads.c @@ -5005,8 +5005,8 @@ ves_icall_System_Threading_LowLevelLifoSemaphore_InitInternal (int32_t kind) case LIFO_SEMAPHORE_NORMAL: return (gpointer)mono_lifo_semaphore_init (); #if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) - case LIFO_SEMAPHORE_ASYNC_JS: - return (gpointer)mono_lifo_js_semaphore_init (); + case LIFO_SEMAPHORE_ASYNCWAIT: + return (gpointer)mono_lifo_semaphore_asyncwait_init (); #endif default: g_error ("Invalid LowLevelLifoSemaphore kind %d\n", kind); @@ -5023,8 +5023,8 @@ ves_icall_System_Threading_LowLevelLifoSemaphore_DeleteInternal (gpointer sem_pt mono_lifo_semaphore_delete ((LifoSemaphore*)sem); break; #if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) - case LIFO_SEMAPHORE_ASYNC_JS: - mono_lifo_js_semaphore_delete ((LifoJSSemaphore*)sem); + case LIFO_SEMAPHORE_ASYNCWAIT: + mono_lifo_semaphore_asyncwait_delete ((LifoSemaphoreAsyncWait*)sem); break; #endif default: @@ -5049,8 +5049,8 @@ ves_icall_System_Threading_LowLevelLifoSemaphore_ReleaseInternal (gpointer sem_p mono_lifo_semaphore_release ((LifoSemaphore*)sem, count); break; #if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) - case LIFO_SEMAPHORE_ASYNC_JS: - mono_lifo_js_semaphore_release ((LifoJSSemaphore*)sem, count); + case LIFO_SEMAPHORE_ASYNCWAIT: + mono_lifo_semaphore_asyncwait_release ((LifoSemaphoreAsyncWait*)sem, count); break; #endif default: @@ -5062,9 +5062,9 @@ ves_icall_System_Threading_LowLevelLifoSemaphore_ReleaseInternal (gpointer sem_p void ves_icall_System_Threading_LowLevelLifoSemaphore_PrepareAsyncWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timedout_cb, intptr_t user_data) { - LifoJSSemaphore *sem = (LifoJSSemaphore *)sem_ptr; - g_assert (sem->base.kind == LIFO_SEMAPHORE_ASYNC_JS); - mono_lifo_js_semaphore_prepare_wait (sem, timeout_ms, (LifoJSSemaphoreCallbackFn)success_cb, (LifoJSSemaphoreCallbackFn)timedout_cb, user_data); + LifoSemaphoreAsyncWait *sem = (LifoSemaphoreAsyncWait *)sem_ptr; + g_assert (sem->base.kind == LIFO_SEMAPHORE_ASYNCWAIT); + mono_lifo_semaphore_asyncwait_prepare_wait (sem, timeout_ms, (LifoSemaphoreAsyncWaitCallbackFn)success_cb, (LifoSemaphoreAsyncWaitCallbackFn)timedout_cb, user_data); } void diff --git a/src/mono/mono/utils/lifo-semaphore.c b/src/mono/mono/utils/lifo-semaphore.c index ec340b5b91816..4333782c3d90b 100644 --- a/src/mono/mono/utils/lifo-semaphore.c +++ b/src/mono/mono/utils/lifo-semaphore.c @@ -36,8 +36,8 @@ mono_lifo_semaphore_timed_wait (LifoSemaphore *semaphore, int32_t timeout_ms) mono_coop_cond_init (&wait_entry.condition); mono_coop_mutex_lock (&semaphore->base.mutex); - if (semaphore->pending_signals > 0) { - --semaphore->pending_signals; + if (semaphore->base.pending_signals > 0) { + --semaphore->base.pending_signals; mono_coop_cond_destroy (&wait_entry.condition); mono_coop_mutex_unlock (&semaphore->base.mutex); return 1; @@ -88,7 +88,7 @@ mono_lifo_semaphore_release (LifoSemaphore *semaphore, uint32_t count) mono_coop_cond_signal (&wait_entry->condition); --count; } else { - semaphore->pending_signals += count; + semaphore->base.pending_signals += count; count = 0; } } @@ -98,13 +98,13 @@ mono_lifo_semaphore_release (LifoSemaphore *semaphore, uint32_t count) #if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) -LifoJSSemaphore * -mono_lifo_js_semaphore_init (void) +LifoSemaphoreAsyncWait * +mono_lifo_semaphore_asyncwait_init (void) { - LifoJSSemaphore *sem = g_new0 (LifoJSSemaphore, 1); + LifoSemaphoreAsyncWait *sem = g_new0 (LifoSemaphoreAsyncWait, 1); if (sem == NULL) return NULL; - sem->base.kind = LIFO_SEMAPHORE_ASYNC_JS; + sem->base.kind = LIFO_SEMAPHORE_ASYNCWAIT; mono_coop_mutex_init (&sem->base.mutex); @@ -112,7 +112,7 @@ mono_lifo_js_semaphore_init (void) } void -mono_lifo_js_semaphore_delete (LifoJSSemaphore *sem) +mono_lifo_semaphore_asyncwait_delete (LifoSemaphoreAsyncWait *sem) { /* FIXME: this is probably hard to guarantee - in-flight signaled semaphores still have wait entries */ g_assert (sem->head == NULL); @@ -134,18 +134,18 @@ lifo_js_wait_entry_on_success (void *wait_entry_as_user_data); static void -lifo_js_wait_entry_push (LifoJSSemaphoreWaitEntry **head, - LifoJSSemaphoreWaitEntry *entry) +lifo_js_wait_entry_push (LifoSemaphoreAsyncWaitWaitEntry **head, + LifoSemaphoreAsyncWaitWaitEntry *entry) { - LifoJSSemaphoreWaitEntry *next = *head; + LifoSemaphoreAsyncWaitWaitEntry *next = *head; *head = entry; entry->next = next; next->previous = entry; } static void -lifo_js_wait_entry_unlink (LifoJSSemaphoreWaitEntry **head, - LifoJSSemaphoreWaitEntry *entry) +lifo_js_wait_entry_unlink (LifoSemaphoreAsyncWaitWaitEntry **head, + LifoSemaphoreAsyncWaitWaitEntry *entry) { if (*head == entry) { *head = entry->next; @@ -159,8 +159,8 @@ lifo_js_wait_entry_unlink (LifoJSSemaphoreWaitEntry **head, } /* LOCKING: assumes semaphore is locked */ -static LifoJSSemaphoreWaitEntry * -lifo_js_find_waiter (LifoJSSemaphoreWaitEntry *entry) +static LifoSemaphoreAsyncWaitWaitEntry * +lifo_js_find_waiter (LifoSemaphoreAsyncWaitWaitEntry *entry) { while (entry) { if (entry->state == LIFO_JS_WAITING) @@ -171,7 +171,7 @@ lifo_js_find_waiter (LifoJSSemaphoreWaitEntry *entry) } static gboolean -lifo_js_wait_entry_no_thread (LifoJSSemaphoreWaitEntry *entry, +lifo_js_wait_entry_no_thread (LifoSemaphoreAsyncWaitWaitEntry *entry, pthread_t cur) { while (entry) { @@ -183,15 +183,15 @@ lifo_js_wait_entry_no_thread (LifoJSSemaphoreWaitEntry *entry, } void -mono_lifo_js_semaphore_prepare_wait (LifoJSSemaphore *sem, +mono_lifo_semaphore_asyncwait_prepare_wait (LifoSemaphoreAsyncWait *sem, int32_t timeout_ms, - LifoJSSemaphoreCallbackFn success_cb, - LifoJSSemaphoreCallbackFn timeout_cb, + LifoSemaphoreAsyncWaitCallbackFn success_cb, + LifoSemaphoreAsyncWaitCallbackFn timeout_cb, intptr_t user_data) { mono_coop_mutex_lock (&sem->base.mutex); - if (sem->pending_signals > 0) { - sem->pending_signals--; + if (sem->base.pending_signals > 0) { + sem->base.pending_signals--; mono_coop_mutex_unlock (&sem->base.mutex); success_cb (sem, user_data); // FIXME: queue microtask return; @@ -205,7 +205,7 @@ mono_lifo_js_semaphore_prepare_wait (LifoJSSemaphore *sem, */ g_assert (lifo_js_wait_entry_no_thread(sem->head, cur)); - LifoJSSemaphoreWaitEntry *wait_entry = g_new0 (LifoJSSemaphoreWaitEntry, 1); + LifoSemaphoreAsyncWaitWaitEntry *wait_entry = g_new0 (LifoSemaphoreAsyncWaitWaitEntry, 1); wait_entry->success_cb = success_cb; wait_entry->timeout_cb = timeout_cb; wait_entry->sem = sem; @@ -220,13 +220,13 @@ mono_lifo_js_semaphore_prepare_wait (LifoJSSemaphore *sem, } void -mono_lifo_js_semaphore_release (LifoJSSemaphore *sem, +mono_lifo_semaphore_asyncwait_release (LifoSemaphoreAsyncWait *sem, uint32_t count) { mono_coop_mutex_lock (&sem->base.mutex); while (count > 0) { - LifoJSSemaphoreWaitEntry *wait_entry = lifo_js_find_waiter (sem->head); + LifoSemaphoreAsyncWaitWaitEntry *wait_entry = lifo_js_find_waiter (sem->head); if (wait_entry != NULL) { /* found one. set its status and queue some work to run on the signaled thread */ pthread_t target = wait_entry->thread; @@ -238,7 +238,7 @@ mono_lifo_js_semaphore_release (LifoJSSemaphore *sem, /* if we're on the same thread, don't run the callback while holding the lock */ emscripten_dispatch_to_thread_async (target, EM_FUNC_SIG_VI, lifo_js_wait_entry_on_success, NULL, wait_entry); } else { - sem->pending_signals += count; + sem->base.pending_signals += count; count = 0; } } @@ -249,12 +249,12 @@ mono_lifo_js_semaphore_release (LifoJSSemaphore *sem, static void lifo_js_wait_entry_on_timeout (void *wait_entry_as_user_data) { - LifoJSSemaphoreWaitEntry *wait_entry = (LifoJSSemaphoreWaitEntry *)wait_entry_as_user_data; - g_assert (pthread_equal (wait_entry->thread, pthread_self())); // FIXME: failing here sometimes - thread already exited? + LifoSemaphoreAsyncWaitWaitEntry *wait_entry = (LifoSemaphoreAsyncWaitWaitEntry *)wait_entry_as_user_data; + g_assert (pthread_equal (wait_entry->thread, pthread_self())); g_assert (wait_entry->sem != NULL); - LifoJSSemaphore *sem = wait_entry->sem; + LifoSemaphoreAsyncWait *sem = wait_entry->sem; gboolean call_timeout_cb = FALSE; - LifoJSSemaphoreCallbackFn timeout_cb = NULL; + LifoSemaphoreAsyncWaitCallbackFn timeout_cb = NULL; intptr_t user_data = 0; mono_coop_mutex_lock (&sem->base.mutex); switch (wait_entry->state) { @@ -288,12 +288,12 @@ lifo_js_wait_entry_on_timeout (void *wait_entry_as_user_data) static void lifo_js_wait_entry_on_success (void *wait_entry_as_user_data) { - LifoJSSemaphoreWaitEntry *wait_entry = (LifoJSSemaphoreWaitEntry *)wait_entry_as_user_data; + LifoSemaphoreAsyncWaitWaitEntry *wait_entry = (LifoSemaphoreAsyncWaitWaitEntry *)wait_entry_as_user_data; g_assert (pthread_equal (wait_entry->thread, pthread_self())); g_assert (wait_entry->sem != NULL); - LifoJSSemaphore *sem = wait_entry->sem; + LifoSemaphoreAsyncWait *sem = wait_entry->sem; gboolean call_success_cb = FALSE; - LifoJSSemaphoreCallbackFn success_cb = NULL; + LifoSemaphoreAsyncWaitCallbackFn success_cb = NULL; intptr_t user_data = 0; mono_coop_mutex_lock (&sem->base.mutex); switch (wait_entry->state) { diff --git a/src/mono/mono/utils/lifo-semaphore.h b/src/mono/mono/utils/lifo-semaphore.h index 36a73fe04e4fb..a97a560e28116 100644 --- a/src/mono/mono/utils/lifo-semaphore.h +++ b/src/mono/mono/utils/lifo-semaphore.h @@ -8,13 +8,14 @@ typedef struct _LifoSemaphoreBase LifoSemaphoreBase; struct _LifoSemaphoreBase { MonoCoopMutex mutex; + uint32_t pending_signals; uint8_t kind; }; enum { LIFO_SEMAPHORE_NORMAL = 1, #if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) - LIFO_SEMAPHORE_ASYNC_JS, + LIFO_SEMAPHORE_ASYNCWAIT, #endif }; @@ -31,7 +32,6 @@ struct _LifoSemaphoreWaitEntry { struct _LifoSemaphore { LifoSemaphoreBase base; LifoSemaphoreWaitEntry *head; - uint32_t pending_signals; }; LifoSemaphore * @@ -53,22 +53,22 @@ mono_lifo_semaphore_release (LifoSemaphore *semaphore, uint32_t count); * timeout callback. The wait function returns immediately and the callbacks will fire on the JS * event loop when the semaphore is released or the timeout expires. */ -typedef struct _LifoJSSemaphore LifoJSSemaphore; +typedef struct _LifoSemaphoreAsyncWait LifoSemaphoreAsyncWait; /* * Because the callbacks are asynchronous, it's possible for the same thread to attempt to wait * multiple times for the same semaphore. For simplicity of reasoning, we dissallow that and * assert. In principle we could support it, but we haven't implemented that. */ -typedef struct _LifoJSSemaphoreWaitEntry LifoJSSemaphoreWaitEntry; +typedef struct _LifoSemaphoreAsyncWaitWaitEntry LifoSemaphoreAsyncWaitWaitEntry; -typedef void (*LifoJSSemaphoreCallbackFn)(LifoJSSemaphore *semaphore, intptr_t user_data); +typedef void (*LifoSemaphoreAsyncWaitCallbackFn)(LifoSemaphoreAsyncWait *semaphore, intptr_t user_data); -struct _LifoJSSemaphoreWaitEntry { - LifoJSSemaphoreWaitEntry *previous; - LifoJSSemaphoreWaitEntry *next; - LifoJSSemaphoreCallbackFn success_cb; - LifoJSSemaphoreCallbackFn timeout_cb; - LifoJSSemaphore *sem; +struct _LifoSemaphoreAsyncWaitWaitEntry { + LifoSemaphoreAsyncWaitWaitEntry *previous; + LifoSemaphoreAsyncWaitWaitEntry *next; + LifoSemaphoreAsyncWaitCallbackFn success_cb; + LifoSemaphoreAsyncWaitCallbackFn timeout_cb; + LifoSemaphoreAsyncWait *sem; intptr_t user_data; pthread_t thread; int32_t js_timeout_id; // only valid to access from the waiting thread @@ -77,20 +77,19 @@ struct _LifoJSSemaphoreWaitEntry { uint16_t refcount; /* 1 if waiting, 2 if signaled, 1 if timeout fired while signaled and we're ignoring the timeout */ }; -struct _LifoJSSemaphore { +struct _LifoSemaphoreAsyncWait { LifoSemaphoreBase base; - LifoJSSemaphoreWaitEntry *head; - uint32_t pending_signals; + LifoSemaphoreAsyncWaitWaitEntry *head; }; -LifoJSSemaphore * -mono_lifo_js_semaphore_init (void); +LifoSemaphoreAsyncWait * +mono_lifo_semaphore_asyncwait_init (void); /* what to do with waiters? * might be kind of academic - we don't expect to destroy these */ void -mono_lifo_js_semaphore_delete (LifoJSSemaphore *semaphore); +mono_lifo_semaphore_asyncwait_delete (LifoSemaphoreAsyncWait *semaphore); /* * the timeout_cb is triggered by a JS setTimeout callback @@ -127,13 +126,13 @@ mono_lifo_js_semaphore_delete (LifoJSSemaphore *semaphore); * popped when the timeout runs. But emscripten_clear_timeout doesn't pop - we need to pop ourselves */ void -mono_lifo_js_semaphore_prepare_wait (LifoJSSemaphore *semaphore, int32_t timeout_ms, - LifoJSSemaphoreCallbackFn success_cb, - LifoJSSemaphoreCallbackFn timeout_cb, +mono_lifo_semaphore_asyncwait_prepare_wait (LifoSemaphoreAsyncWait *semaphore, int32_t timeout_ms, + LifoSemaphoreAsyncWaitCallbackFn success_cb, + LifoSemaphoreAsyncWaitCallbackFn timeout_cb, intptr_t user_data); void -mono_lifo_js_semaphore_release (LifoJSSemaphore *semaphore, uint32_t count); +mono_lifo_semaphore_asyncwait_release (LifoSemaphoreAsyncWait *semaphore, uint32_t count); #endif /* HOST_BROWSER && !DISABLE_THREADS */ From 3e8fee45f67587ba2da574bbc9480f35d6e1a9a1 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 30 Mar 2023 22:19:49 -0400 Subject: [PATCH 33/37] Rename managed file to LowLevelLifoSemaphore.AsyncWait.Browser.Threads.Mono.cs --- src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj | 2 +- ... => LowLevelLifoSemaphore.AsyncWait.Browser.Threads.Mono.cs} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/mono/System.Private.CoreLib/src/System/Threading/{LowLevelJSSemaphore.Browser.Mono.cs => LowLevelLifoSemaphore.AsyncWait.Browser.Threads.Mono.cs} (100%) diff --git a/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj b/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj index d2a6df7f6f9b7..6a0e3f735ea65 100644 --- a/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj +++ b/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj @@ -282,7 +282,7 @@ - + diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelJSSemaphore.Browser.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.AsyncWait.Browser.Threads.Mono.cs similarity index 100% rename from src/mono/System.Private.CoreLib/src/System/Threading/LowLevelJSSemaphore.Browser.Mono.cs rename to src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.AsyncWait.Browser.Threads.Mono.cs From 552f9a5414c8a268969e81ee359de65842aefc95 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 30 Mar 2023 22:49:17 -0400 Subject: [PATCH 34/37] Remove unnecessary indirections and allocations from managed AsyncWait semaphore --- ...emaphore.AsyncWait.Browser.Threads.Mono.cs | 71 +++++++++---------- .../LowLevelLifoSemaphore.Unix.Mono.cs | 2 +- ...dPool.WorkerThread.Browser.Threads.Mono.cs | 2 +- 3 files changed, 35 insertions(+), 40 deletions(-) diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.AsyncWait.Browser.Threads.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.AsyncWait.Browser.Threads.Mono.cs index 581c15cb2f941..2b20b1c912524 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.AsyncWait.Browser.Threads.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.AsyncWait.Browser.Threads.Mono.cs @@ -15,18 +15,18 @@ namespace System.Threading; // internal sealed partial class LowLevelLifoSemaphore : IDisposable { - internal static LowLevelLifoSemaphore CreateAsyncJS (int initialSignalCount, int maximumSignalCount, int spinCount, Action onWait) + public static LowLevelLifoSemaphore CreateAsyncWaitSemaphore (int initialSignalCount, int maximumSignalCount, int spinCount, Action onWait) { - return new LowLevelLifoSemaphore(initialSignalCount, maximumSignalCount, spinCount, onWait, asyncJS: true); + return new LowLevelLifoSemaphore(initialSignalCount, maximumSignalCount, spinCount, onWait, asyncWait: true); } - private LowLevelLifoSemaphore(int initialSignalCount, int maximumSignalCount, int spinCount, Action onWait, bool asyncJS) + private LowLevelLifoSemaphore(int initialSignalCount, int maximumSignalCount, int spinCount, Action onWait, bool asyncWait) { Debug.Assert(initialSignalCount >= 0); Debug.Assert(initialSignalCount <= maximumSignalCount); Debug.Assert(maximumSignalCount > 0); Debug.Assert(spinCount >= 0); - Debug.Assert(asyncJS); + Debug.Assert(asyncWait); _separated = default; _separated._counts.SignalCount = (uint)initialSignalCount; @@ -34,30 +34,24 @@ private LowLevelLifoSemaphore(int initialSignalCount, int maximumSignalCount, in _spinCount = spinCount; _onWait = onWait; - CreateAsyncJS(maximumSignalCount); + CreateAsyncWait(maximumSignalCount); } #pragma warning disable IDE0060 - private void CreateAsyncJS(int maximumSignalCount) + private void CreateAsyncWait(int maximumSignalCount) { - _kind = LifoSemaphoreKind.AsyncJS; + _kind = LifoSemaphoreKind.AsyncWait; lifo_semaphore = InitInternal((int)_kind); } #pragma warning restore IDE0060 - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern unsafe void PrepareAsyncWaitInternal(IntPtr semaphore, - int timeoutMs, - /*delegate* unmanaged successCallback*/ void* successCallback, - /*delegate* unmanaged timeoutCallback*/ void* timeoutCallback, - IntPtr userData); - - private sealed record WaitEntry (LowLevelLifoSemaphore Semaphore, Action OnSuccess, Action OnTimeout, object? State); + private sealed record WaitEntry (LowLevelLifoSemaphore Semaphore, int TimeoutMs, Action OnSuccess, Action OnTimeout, object? State); - internal void PrepareAsyncWait(int timeoutMs, Action onSuccess, Action onTimeout, object? state) + public void PrepareAsyncWait(int timeoutMs, Action onSuccess, Action onTimeout, object? state) { //FIXME(ak): the async wait never spins. Shoudl we spin a little? Debug.Assert(timeoutMs >= -1); + ThrowIfInvalidSemaphoreKind(LifoSemaphoreKind.AsyncWait); // Try to acquire the semaphore or // [[a) register as a spinner if false and timeoutMs > 0]] @@ -136,27 +130,22 @@ private void PrepareAsyncWaitForSignal(int timeoutMs, Action s_InternalAsyncWaitSuccess = InternalAsyncWaitSuccess; - - private static readonly Action s_InternalAsyncWaitTimeout = InternalAsyncWaitTimeout; - - internal sealed record InternalWait(int TimeoutMs, Action OnSuccess, Action OnTimeout, object? State); - - private static void InternalAsyncWaitTimeout(LowLevelLifoSemaphore self, object? internalWaitObj) + private static void InternalAsyncWaitTimeout(LowLevelLifoSemaphore self, WaitEntry internalWaitEntry) { - InternalWait i = (InternalWait)internalWaitObj!; + WaitEntry we = internalWaitEntry!; // Unregister the waiter. The wait subsystem used above guarantees that a thread that wakes due to a timeout does // not observe a signal to the object being waited upon. self._separated._counts.InterlockedDecrementWaiterCount(); - i.OnTimeout(self, i.State); + we.OnTimeout(self, we.State); } - private static void InternalAsyncWaitSuccess(LowLevelLifoSemaphore self, object? internalWaitObj) + private static void InternalAsyncWaitSuccess(LowLevelLifoSemaphore self, WaitEntry internalWaitEntry) { - InternalWait i = (InternalWait)internalWaitObj!; + WaitEntry we = internalWaitEntry!; // Unregister the waiter if this thread will not be waiting anymore, and try to acquire the semaphore Counts counts = self._separated._counts; while (true) @@ -180,7 +169,7 @@ private static void InternalAsyncWaitSuccess(LowLevelLifoSemaphore self, object? { if (counts.SignalCount != 0) { - i.OnSuccess(self, i.State); + we.OnSuccess(self, we.State); return; } break; @@ -191,14 +180,13 @@ private static void InternalAsyncWaitSuccess(LowLevelLifoSemaphore self, object? // if we get here, we need to keep waiting because the SignalCount above was 0 after we did // the CompareExchange - someone took the signal before us. // FIXME(ak): why is the timeoutMs the same as before? wouldn't we starve? why does LowLevelLifoSemaphore.WaitForSignal not decrement timeoutMs? - self.PrepareAsyncWaitCore (i.TimeoutMs, s_InternalAsyncWaitSuccess, s_InternalAsyncWaitTimeout, i); + self.PrepareAsyncWaitCore (we.TimeoutMs, we); + // on success calls InternalAsyncWaitSuccess, on timeout calls InternalAsyncWaitTimeout } - internal void PrepareAsyncWaitCore(int timeout_ms, Action onSuccess, Action onTimeout, object? state) + private void PrepareAsyncWaitCore(int timeout_ms, WaitEntry internalWaitEntry) { - ThrowIfInvalidSemaphoreKind (LifoSemaphoreKind.AsyncJS); - WaitEntry entry = new (this, onSuccess, onTimeout, state); - GCHandle gchandle = GCHandle.Alloc (entry); + GCHandle gchandle = GCHandle.Alloc (internalWaitEntry); unsafe { delegate* unmanaged successCallback = &SuccessCallback; delegate* unmanaged timeoutCallback = &TimeoutCallback; @@ -206,22 +194,29 @@ internal void PrepareAsyncWaitCore(int timeout_ms, Action successCallback*/ void* successCallback, + /*delegate* unmanaged timeoutCallback*/ void* timeoutCallback, + IntPtr userData); + [UnmanagedCallersOnly] private static void SuccessCallback(IntPtr lifoSemaphore, IntPtr userData) { GCHandle gchandle = GCHandle.FromIntPtr(userData); - WaitEntry entry = (WaitEntry)gchandle.Target!; + WaitEntry internalWaitEntry = (WaitEntry)gchandle.Target!; gchandle.Free(); - entry.OnSuccess(entry.Semaphore, entry.State); + InternalAsyncWaitSuccess(internalWaitEntry.Semaphore, internalWaitEntry); } [UnmanagedCallersOnly] private static void TimeoutCallback(IntPtr lifoSemaphore, IntPtr userData) { GCHandle gchandle = GCHandle.FromIntPtr(userData); - WaitEntry entry = (WaitEntry)gchandle.Target!; + WaitEntry internalWaitEntry = (WaitEntry)gchandle.Target!; gchandle.Free(); - entry.OnTimeout(entry.Semaphore, entry.State); + InternalAsyncWaitTimeout(internalWaitEntry.Semaphore, internalWaitEntry); } } diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.Unix.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.Unix.Mono.cs index d4ce63957e206..4b3be5af50077 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.Unix.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.Unix.Mono.cs @@ -14,7 +14,7 @@ internal sealed unsafe partial class LowLevelLifoSemaphore : IDisposable // Keep in sync with lifo-semaphore.h private enum LifoSemaphoreKind : int { Normal = 1, - AsyncJS = 2, + AsyncWait = 2, } #endif diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs index b12a04836e60f..52f7b03e699c6 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs @@ -19,7 +19,7 @@ private static partial class WorkerThread /// Semaphore for controlling how many threads are currently working. /// private static readonly LowLevelLifoSemaphore s_semaphore = - LowLevelLifoSemaphore.CreateAsyncJS( + LowLevelLifoSemaphore.CreateAsyncWaitSemaphore( 0, MaxPossibleThreadCount, AppContextConfigHelper.GetInt32Config( From 812524fd2614cc8e70d5f2f6a082f6b4456feb9e Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 30 Mar 2023 22:55:12 -0400 Subject: [PATCH 35/37] fix non-browser+threads builds --- ...emaphore.AsyncWait.Browser.Threads.Mono.cs | 4 +-- .../LowLevelLifoSemaphore.Unix.Mono.cs | 36 ++++++++++++++++--- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.AsyncWait.Browser.Threads.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.AsyncWait.Browser.Threads.Mono.cs index 2b20b1c912524..d4a32a7604b64 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.AsyncWait.Browser.Threads.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.AsyncWait.Browser.Threads.Mono.cs @@ -40,8 +40,8 @@ private LowLevelLifoSemaphore(int initialSignalCount, int maximumSignalCount, in #pragma warning disable IDE0060 private void CreateAsyncWait(int maximumSignalCount) { - _kind = LifoSemaphoreKind.AsyncWait; - lifo_semaphore = InitInternal((int)_kind); + Kind = LifoSemaphoreKind.AsyncWait; + lifo_semaphore = InitInternal((int)Kind); } #pragma warning restore IDE0060 diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.Unix.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.Unix.Mono.cs index 4b3be5af50077..7064e3091913d 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.Unix.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.Unix.Mono.cs @@ -8,15 +8,37 @@ namespace System.Threading internal sealed unsafe partial class LowLevelLifoSemaphore : IDisposable { private IntPtr lifo_semaphore; -#if FEATURE_WASM_THREADS +#if TARGET_BROWSER && FEATURE_WASM_THREADS private LifoSemaphoreKind _kind; +#endif + +#pragma warning disable CA1822 + private LifoSemaphoreKind Kind +#pragma warning restore CA1822 + { + get + { +#if TARGET_BROWSER && FEATURE_WASM_THREADS + return _kind; +#else + return LifoSemaphoreKind.Normal; +#endif + } + set + { +#if TARGET_BROWSER && FEATURE_WASM_THREADS + _kind = value; +#endif + } + } // Keep in sync with lifo-semaphore.h private enum LifoSemaphoreKind : int { Normal = 1, +#if TARGET_BROWSER && FEATURE_WASM_THREADS AsyncWait = 2, - } #endif + } [MethodImplAttribute(MethodImplOptions.InternalCall)] private static extern IntPtr InitInternal(int kind); @@ -24,8 +46,8 @@ private enum LifoSemaphoreKind : int { #pragma warning disable IDE0060 private void Create(int maximumSignalCount) { - _kind = LifoSemaphoreKind.Normal; - lifo_semaphore = InitInternal((int)_kind); + Kind = LifoSemaphoreKind.Normal; + lifo_semaphore = InitInternal((int)Kind); } #pragma warning restore IDE0060 @@ -36,7 +58,7 @@ public void Dispose() { DeleteInternal(lifo_semaphore); lifo_semaphore = IntPtr.Zero; - _kind = (LifoSemaphoreKind)0; + Kind = (LifoSemaphoreKind)0; } [MethodImplAttribute(MethodImplOptions.InternalCall)] @@ -48,10 +70,14 @@ private bool WaitCore(int timeoutMs) return TimedWaitInternal(lifo_semaphore, timeoutMs) != 0; } +#pragma warning disable CA1822 private void ThrowIfInvalidSemaphoreKind(LifoSemaphoreKind expected) +#pragma warning restore CA1822 { +#if TARGET_BROWSER && FEATURE_WASM_THREADS if (_kind != expected) throw new InvalidOperationException ($"Unexpected LowLevelLifoSemaphore kind {_kind} expected {expected}"); +#endif } [MethodImplAttribute(MethodImplOptions.InternalCall)] From 50c0f1aacbb039732169ca2821ad228dabaa8e72 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Mon, 3 Apr 2023 15:43:19 -0400 Subject: [PATCH 36/37] Keep track of unsettled JS interop promises in threadpool workers Set WorkerThread.IsIOPending when the current thread has unsettled JS interop promises. When IsIOPending is true, the worker will not exit even if it has no more work to do. Instead it will repeatedly wait for more work to arrive or for all promises to settle. --- .../System.Private.CoreLib.Shared.projitems | 2 +- .../System.Private.CoreLib.csproj | 1 + ...PortableThreadPool.Browser.Threads.Mono.cs | 19 +++++++++++++++++++ ...WebWorkerEventLoop.Browser.Threads.Mono.cs | 7 +++++++ src/mono/mono/metadata/icall-decl.h | 1 + src/mono/mono/metadata/icall-def.h | 5 +++-- src/mono/mono/metadata/threads.c | 14 ++++++++++++++ src/mono/wasm/runtime/es6/dotnet.es6.lib.js | 2 ++ src/mono/wasm/runtime/exports-linker.ts | 4 +++- src/mono/wasm/runtime/gc-handles.ts | 10 ++++++++-- 10 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.Browser.Threads.Mono.cs diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index e65522aab7633..9f1b2cbb2a1c1 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -2525,7 +2525,7 @@ - + diff --git a/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj b/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj index 6a0e3f735ea65..d05b85bbaad79 100644 --- a/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj +++ b/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj @@ -280,6 +280,7 @@ + diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.Browser.Threads.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.Browser.Threads.Mono.cs new file mode 100644 index 0000000000000..d459c992f810f --- /dev/null +++ b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.Browser.Threads.Mono.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Threading; + +internal sealed partial class PortableThreadPool +{ + private static partial class WorkerThread + { + private static bool IsIOPending => WebWorkerEventLoop.HasUnsettledInteropPromises; + } + + private struct CpuUtilizationReader + { +#pragma warning disable CA1822 + public double CurrentUtilization => 0.0; // FIXME: can we do better +#pragma warning restore CA1822 + } +} diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/WebWorkerEventLoop.Browser.Threads.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/WebWorkerEventLoop.Browser.Threads.Mono.cs index 72260e3e8fcdb..0467107bbbb1c 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/WebWorkerEventLoop.Browser.Threads.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/WebWorkerEventLoop.Browser.Threads.Mono.cs @@ -73,4 +73,11 @@ internal static void StartExitable(Thread thread, bool captureContext) throw new InvalidOperationException(); thread.UnsafeStart(); } + + /// returns true if the current thread has unsettled JS Interop promises + internal static bool HasUnsettledInteropPromises => HasUnsettledInteropPromisesNative(); + + // FIXME: this could be a qcall with a SuppressGCTransitionAttribute + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern bool HasUnsettledInteropPromisesNative(); } diff --git a/src/mono/mono/metadata/icall-decl.h b/src/mono/mono/metadata/icall-decl.h index 16e9524612fec..32ebf5cac0395 100644 --- a/src/mono/mono/metadata/icall-decl.h +++ b/src/mono/mono/metadata/icall-decl.h @@ -188,6 +188,7 @@ ICALL_EXPORT void ves_icall_System_Threading_LowLevelLifoSemaphore_ReleaseIn #if (defined(HOST_BROWSER) && !defined(DISABLE_THREADS)) || (defined(TARGET_WASM) && defined(ENABLE_ICALL_SYMBOL_MAP)) ICALL_EXPORT void ves_icall_System_Threading_LowLevelLifoSemaphore_PrepareAsyncWaitInternal (gpointer sem_ptr, gint32 timeout_ms, gpointer success_cb, gpointer timeout_cb, intptr_t user_data); +ICALL_EXPORT MonoBoolean ves_icall_System_Threading_WebWorkerEventLoop_HasUnsettledInteropPromisesNative(void); ICALL_EXPORT void ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePushInternal (void); ICALL_EXPORT void ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal (void); #endif diff --git a/src/mono/mono/metadata/icall-def.h b/src/mono/mono/metadata/icall-def.h index 0de7251b26dea..3edbaf6305854 100644 --- a/src/mono/mono/metadata/icall-def.h +++ b/src/mono/mono/metadata/icall-def.h @@ -605,8 +605,9 @@ NOHANDLES(ICALL(THREAD_14, "YieldInternal", ves_icall_System_Threading_Thread_Yi /* include these icalls if we're in the threaded wasm runtime, or if we're building a wasm-targeting cross compiler and we need to support --print-icall-table */ #if (defined(HOST_BROWSER) && !defined(DISABLE_THREADS)) || (defined(TARGET_WASM) && defined(ENABLE_ICALL_SYMBOL_MAP)) ICALL_TYPE(WEBWORKERLOOP, "System.Threading.WebWorkerEventLoop", WEBWORKERLOOP_1) -NOHANDLES(ICALL(WEBWORKERLOOP_1, "KeepalivePopInternal", ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal)) -NOHANDLES(ICALL(WEBWORKERLOOP_2, "KeepalivePushInternal", ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePushInternal)) +NOHANDLES(ICALL(WEBWORKERLOOP_1, "HasUnsettledInteropPromisesNative", ves_icall_System_Threading_WebWorkerEventLoop_HasUnsettledInteropPromisesNative)) +NOHANDLES(ICALL(WEBWORKERLOOP_2, "KeepalivePopInternal", ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal)) +NOHANDLES(ICALL(WEBWORKERLOOP_3, "KeepalivePushInternal", ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePushInternal)) #endif ICALL_TYPE(TYPE, "System.Type", TYPE_1) diff --git a/src/mono/mono/metadata/threads.c b/src/mono/mono/metadata/threads.c index ac143daf981ad..869b8484c473c 100644 --- a/src/mono/mono/metadata/threads.c +++ b/src/mono/mono/metadata/threads.c @@ -5100,6 +5100,14 @@ ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal (void) #endif } +extern int mono_wasm_eventloop_has_unsettled_interop_promises(void); + +MonoBoolean +ves_icall_System_Threading_WebWorkerEventLoop_HasUnsettledInteropPromisesNative(void) +{ + return !!mono_wasm_eventloop_has_unsettled_interop_promises(); +} + #endif /* HOST_BROWSER && !DISABLE_THREADS */ /* for the AOT cross compiler with --print-icall-table these don't need to be callable, they just @@ -5123,4 +5131,10 @@ ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal (void) g_assert_not_reached(); } +MonoBoolean +ves_icall_System_Threading_WebWorkerEventLoop_HasUnsettledInteropPromisesNative(void) +{ + g_assert_not_reached(); +} + #endif /* defined(TARGET_WASM) && defined(ENABLE_ICALL_SYMBOL_MAP) */ diff --git a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js index 4e446e8dce62b..d2fe6874dc69e 100644 --- a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js +++ b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js @@ -105,6 +105,8 @@ if (monoWasmThreads) { linked_functions = [...linked_functions, /// mono-threads-wasm.c "mono_wasm_pthread_on_pthread_attached", + // threads.c + "mono_wasm_eventloop_has_unsettled_interop_promises", // diagnostics_server.c "mono_wasm_diagnostic_server_on_server_thread_created", "mono_wasm_diagnostic_server_on_runtime_server_init", diff --git a/src/mono/wasm/runtime/exports-linker.ts b/src/mono/wasm/runtime/exports-linker.ts index f5f55de63f128..0cd5e1def740b 100644 --- a/src/mono/wasm/runtime/exports-linker.ts +++ b/src/mono/wasm/runtime/exports-linker.ts @@ -4,7 +4,7 @@ import MonoWasmThreads from "consts:monoWasmThreads"; import WasmEnableLegacyJsInterop from "consts:WasmEnableLegacyJsInterop"; import { mono_wasm_debugger_log, mono_wasm_add_dbg_command_received, mono_wasm_set_entrypoint_breakpoint, mono_wasm_fire_debugger_agent_message_with_data, mono_wasm_fire_debugger_agent_message_with_data_to_pause } from "./debug"; -import { mono_wasm_release_cs_owned_object } from "./gc-handles"; +import { mono_wasm_release_cs_owned_object, mono_wasm_eventloop_has_unsettled_interop_promises } from "./gc-handles"; import { mono_wasm_bind_cs_function } from "./invoke-cs"; import { mono_wasm_bind_js_function, mono_wasm_invoke_bound_function, mono_wasm_invoke_import } from "./invoke-js"; import { mono_interp_tier_prepare_jiterpreter } from "./jiterpreter"; @@ -32,6 +32,8 @@ import { const mono_wasm_threads_exports = !MonoWasmThreads ? undefined : { // mono-threads-wasm.c mono_wasm_pthread_on_pthread_attached, + // threads.c + mono_wasm_eventloop_has_unsettled_interop_promises, // diagnostics_server.c mono_wasm_diagnostic_server_on_server_thread_created, mono_wasm_diagnostic_server_on_runtime_server_init, diff --git a/src/mono/wasm/runtime/gc-handles.ts b/src/mono/wasm/runtime/gc-handles.ts index 465bfc9264c07..8cdd824e04668 100644 --- a/src/mono/wasm/runtime/gc-handles.ts +++ b/src/mono/wasm/runtime/gc-handles.ts @@ -49,8 +49,8 @@ export function mono_wasm_get_js_handle(js_obj: any): JSHandle { js_obj[cs_owned_js_handle_symbol] = js_handle; } // else - // The consequence of not adding the cs_owned_js_handle_symbol is, that we could have multiple JSHandles and multiple proxy instances. - // Throwing exception would prevent us from creating any proxy of non-extensible things. + // The consequence of not adding the cs_owned_js_handle_symbol is, that we could have multiple JSHandles and multiple proxy instances. + // Throwing exception would prevent us from creating any proxy of non-extensible things. // If we have weakmap instead, we would pay the price of the lookup for all proxies, not just non-extensible objects. return js_handle as JSHandle; @@ -131,3 +131,9 @@ export function _lookup_js_owned_object(gc_handle: GCHandle): any { } return null; } + +/// Called from the C# threadpool worker loop to find out if there are any +/// unsettled JS promises that need to keep the worker alive +export function mono_wasm_eventloop_has_unsettled_interop_promises(): boolean { + return _js_owned_object_table.size > 0; +} From c8afabadae354c5a0697cca4d696e1b3e0db3364 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Mon, 3 Apr 2023 15:57:53 -0400 Subject: [PATCH 37/37] change minimal sample's fetch helper to artificially delay the delay is longer that the threadpool worker's semaphore timeout, in order to validate that the worker stays alive while there are unsettled promises --- .../wasm/browser-threads-minimal/fetchhelper.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/mono/sample/wasm/browser-threads-minimal/fetchhelper.js b/src/mono/sample/wasm/browser-threads-minimal/fetchhelper.js index 0ed160165e926..928492378fc6c 100644 --- a/src/mono/sample/wasm/browser-threads-minimal/fetchhelper.js +++ b/src/mono/sample/wasm/browser-threads-minimal/fetchhelper.js @@ -1,4 +1,11 @@ -export function responseText(response) /* Promise */ { - return response.text(); +function delay(timeoutMs) { + return new Promise(resolve => setTimeout(resolve, timeoutMs)); +} + +export async function responseText(response) /* Promise */ { + console.log("artificially waiting for response for 25 seconds"); + await delay(25000); + console.log("artificial waiting done"); + return await response.text(); }