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;
+}