diff --git a/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj b/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj index 0e3a627d8f961..709f018ad2d47 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/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..5a49c076271d1 --- /dev/null +++ b/src/mono/System.Private.CoreLib/src/System/Threading/WebWorkerEventLoop.Browser.Threads.Mono.cs @@ -0,0 +1,98 @@ +// 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(); + + /// + /// 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; } + + 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 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 KeepaliveToken KeepalivePush() => KeepaliveToken.Create(); + + /// + /// 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. + /// + 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(); + } + + /// returns true if the current thread has unsettled JS Interop promises + private static bool HasUnsettledInteropPromises => HasUnsettledInteropPromisesNative(); + + // FIXME: this could be a qcall with a SuppressGCTransitionAttribute + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern bool HasUnsettledInteropPromisesNative(); + + /// returns true if the current WebWorker has JavaScript objects that depend on the + /// current managed thread. + /// + /// If this returns false, the runtime is allowed to allow the current managed thread + /// to exit and for the WebWorker to be recycled by Emscripten for another managed + /// thread. + internal static bool HasJavaScriptInteropDependents + { + // + // FIXME: + // https://github.com/dotnet/runtime/issues/85052 - unsettled promises are not the only relevant + // reasons for keeping a worker thread alive. We will need to add other conditions here. + get => HasUnsettledInteropPromises; + } +} diff --git a/src/mono/mono/metadata/icall-decl.h b/src/mono/mono/metadata/icall-decl.h index 5947224866181..7d59362968905 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); +/* 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 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 + #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..33bf8de3e0fe3 100644 --- a/src/mono/mono/metadata/icall-def.h +++ b/src/mono/mono/metadata/icall-def.h @@ -573,6 +573,7 @@ NOHANDLES(ICALL(LIFOSEM_2, "InitInternal", ves_icall_System_Threading_LowLevelLi 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)) @@ -597,6 +598,14 @@ 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)) +/* 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, "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) 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 a6bb38f55fea8..abe6cae20c51e 100644 --- a/src/mono/mono/metadata/threads.c +++ b/src/mono/mono/metadata/threads.c @@ -91,6 +91,11 @@ mono_native_thread_join_handle (HANDLE thread_handle, gboolean close_handle); #include #endif +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) +#include +#include +#endif + #include "icall-decl.h" /*#define THREAD_DEBUG(a) do { a; } while (0)*/ @@ -1110,6 +1115,7 @@ fire_attach_profiler_events (MonoNativeThreadId tid) "Handle Stack")); } + static guint32 WINAPI start_wrapper_internal (StartInfo *start_info, gsize *stack_ptr) { @@ -4963,3 +4969,50 @@ 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) +void +ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePushInternal (void) +{ + emscripten_runtime_keepalive_push(); +} + +void +ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal (void) +{ + emscripten_runtime_keepalive_pop(); +} + +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 + * need to be defined */ +#if defined(TARGET_WASM) && defined(ENABLE_ICALL_SYMBOL_MAP) +void +ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePushInternal (void) +{ + g_assert_not_reached(); +} + +void +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 e6139d3537519..2403db26b3205 100644 --- a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js +++ b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js @@ -109,6 +109,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 ae0305551a42c..ed75ea9df9a66 100644 --- a/src/mono/wasm/runtime/exports-linker.ts +++ b/src/mono/wasm/runtime/exports-linker.ts @@ -11,6 +11,7 @@ import { mono_interp_tier_prepare_jiterpreter } from "./jiterpreter"; import { mono_interp_jit_wasm_entry_trampoline, mono_interp_record_interp_entry } from "./jiterpreter-interp-entry"; import { mono_interp_jit_wasm_jit_call_trampoline, mono_interp_invoke_wasm_jit_call_trampoline, mono_interp_flush_jitcall_queue, mono_jiterp_do_jit_call_indirect } from "./jiterpreter-jit-call"; import { mono_wasm_marshal_promise } from "./marshal-to-js"; +import { mono_wasm_eventloop_has_unsettled_interop_promises } from "./pthreads/shared/eventloop"; import { mono_wasm_pthread_on_pthread_attached } from "./pthreads/worker"; import { mono_set_timeout, schedule_background_exec } from "./scheduling"; import { mono_wasm_asm_loaded } from "./startup"; @@ -33,6 +34,8 @@ import { mono_wasm_change_case, mono_wasm_change_case_invariant, mono_wasm_compa 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..ce83039e4e366 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,4 @@ export function _lookup_js_owned_object(gc_handle: GCHandle): any { } return null; } + diff --git a/src/mono/wasm/runtime/marshal-to-cs.ts b/src/mono/wasm/runtime/marshal-to-cs.ts index 607cfdcb85586..2e1ff002741a2 100644 --- a/src/mono/wasm/runtime/marshal-to-cs.ts +++ b/src/mono/wasm/runtime/marshal-to-cs.ts @@ -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. +import monoWasmThreads from "consts:monoWasmThreads"; import { isThenable } from "./cancelable-promise"; import cwraps from "./cwraps"; import { assert_not_disposed, cs_owned_js_handle_symbol, js_owned_gc_handle_symbol, mono_wasm_get_js_handle, setup_managed_proxy, teardown_managed_proxy } from "./gc-handles"; @@ -18,6 +19,8 @@ import { _zero_region } from "./memory"; import { js_string_to_mono_string_root } from "./strings"; import { mono_assert, GCHandle, GCHandleNull, JSMarshalerArgument, JSMarshalerArguments, JSMarshalerType, MarshalerToCs, MarshalerToJs, BoundMarshalerToCs, MarshalerType } from "./types"; import { TypedArray } from "./types/emscripten"; +import { addUnsettledPromise, settleUnsettledPromise } from "./pthreads/shared/eventloop"; + export function initialize_marshalers_to_cs(): void { if (js_to_cs_marshalers.size == 0) { @@ -306,10 +309,17 @@ function _marshal_task_to_cs(arg: JSMarshalerArgument, value: Promise, _?: const holder = new TaskCallbackHolder(value); setup_managed_proxy(holder, gc_handle); + if (monoWasmThreads) + addUnsettledPromise(); + value.then(data => { + if (monoWasmThreads) + settleUnsettledPromise(); runtimeHelpers.javaScriptExports.complete_task(gc_handle, null, data, res_converter || _marshal_cs_object_to_cs); teardown_managed_proxy(holder, gc_handle); // this holds holder alive for finalizer, until the promise is freed, (holding promise instead would not work) }).catch(reason => { + if (monoWasmThreads) + settleUnsettledPromise(); runtimeHelpers.javaScriptExports.complete_task(gc_handle, reason, null, undefined); teardown_managed_proxy(holder, gc_handle); // this holds holder alive for finalizer, until the promise is freed }); diff --git a/src/mono/wasm/runtime/pthreads/shared/eventloop.ts b/src/mono/wasm/runtime/pthreads/shared/eventloop.ts new file mode 100644 index 0000000000000..19533a27ee5f8 --- /dev/null +++ b/src/mono/wasm/runtime/pthreads/shared/eventloop.ts @@ -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. + + +let _per_thread_unsettled_promise_count = 0; + +export function addUnsettledPromise() { + _per_thread_unsettled_promise_count++; +} + +export function settleUnsettledPromise() { + _per_thread_unsettled_promise_count--; +} + +/// 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 _per_thread_unsettled_promise_count > 0; +}