Skip to content

Commit bd37436

Browse files
authored
[mono] add internal WebWorkerEventLoop utility class (#84492)
This is part of #84489 - landing support for async JS interop on threadpool threads in multi-threaded WebAssembly. Provides two pieces of functionality: 1. A keepalive token that can be used to prevent the current POSIX thread from terminating when it returns from its thread start function, or from an invocation from the JS event loop. When the last keepalive token is destroyed (assuming Emscripten isn't keeping the thread alive for other reasons) it will terminate as if by calling `pthread_exit` and the webworker will be made available to other threads 2. A `HasUnsettledInteropPromises` property that peeks `_js_owned_object_table` to see if there are any promises created by the interop subsystem that have not been fulfilled or rejected yet. * [mono] add internal WebWorkerEventLoop utility class Provides two pieces of functionality: 1. A keepalive token that can be used to prevent the current POSIX thread from terminating when it returns from its thread start function, or from an invocation from the JS event loop. When the last keepalive token is destroyed (assuming Emscripten isn't keeping the thread alive for other reasons) it will terminate as if by calling `pthread_exit` and the webworker will be made available to other threads 2. A `HasUnsettledInteropPromises` property that peeks `_js_owned_object_table` to see if there are any promises created by the interop subsystem that have not been fulfilled or rejected yet. * Use a per-thread unsettled promise count for mono_wasm_eventloop_has_unsettled_interop_promises we can't use the _js_owned_object_table size because it contains other interop objects, not just promises * remove old emscripten keepalive workaround hack * Add a more general getter for JS interop to keep a webworker alive And link to #85052 for more details * fixup docs
1 parent 9aac647 commit bd37436

File tree

10 files changed

+205
-2
lines changed

10 files changed

+205
-2
lines changed

src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@
280280
</ItemGroup>
281281
<ItemGroup Condition="('$(TargetsBrowser)' == 'true' or '$(TargetsWasi)' == 'true') and '$(FeatureWasmThreads)' == 'true'">
282282
<Compile Include="$(BclSourcesRoot)\System\Threading\ThreadPoolBoundHandle.Browser.Threads.Mono.cs" />
283+
<Compile Include="$(BclSourcesRoot)\System\Threading\WebWorkerEventLoop.Browser.Threads.Mono.cs" />
283284
</ItemGroup>
284285
<ItemGroup Condition="('$(TargetsBrowser)' == 'true' or '$(TargetsWasi)' == 'true') and '$(FeatureWasmThreads)' != 'true'">
285286
<Compile Include="$(BclSourcesRoot)\System\Threading\ThreadPoolBoundHandle.Browser.Mono.cs" />
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
using System.Diagnostics.Tracing;
6+
using System.Runtime.CompilerServices;
7+
8+
namespace System.Threading;
9+
10+
/// <summary>
11+
/// Keep a pthread alive in its WebWorker after its pthread start function returns.
12+
/// </summary>
13+
internal static class WebWorkerEventLoop
14+
{
15+
// FIXME: these keepalive calls could be qcalls with a SuppressGCTransitionAttribute
16+
[MethodImpl(MethodImplOptions.InternalCall)]
17+
private static extern void KeepalivePushInternal();
18+
[MethodImpl(MethodImplOptions.InternalCall)]
19+
private static extern void KeepalivePopInternal();
20+
21+
/// <summary>
22+
/// A keepalive token prevents a thread from shutting down even if it returns to the JS event
23+
/// loop. A thread may want a keepalive token if it needs to allow JS code to run to settle JS
24+
/// promises or execute JS timeout callbacks.
25+
/// </summary>
26+
internal sealed class KeepaliveToken
27+
{
28+
public bool Valid {get; private set; }
29+
30+
private KeepaliveToken() { Valid = true; }
31+
32+
/// <summary>
33+
/// Decrement the Emscripten keepalive count. A thread with a zero keepalive count will
34+
/// terminate when it returns from its start function or from an async invocation from the
35+
/// JS event loop.
36+
/// </summary>
37+
internal void Pop() {
38+
if (!Valid)
39+
throw new InvalidOperationException();
40+
Valid = false;
41+
KeepalivePopInternal();
42+
}
43+
44+
internal static KeepaliveToken Create()
45+
{
46+
KeepalivePushInternal();
47+
return new KeepaliveToken();
48+
}
49+
}
50+
51+
/// <summary>
52+
/// Increment the Emscripten keepalive count. A thread with a positive keepalive can return from its
53+
/// thread start function or a JS event loop invocation and continue running in the JS event
54+
/// loop.
55+
/// </summary>
56+
internal static KeepaliveToken KeepalivePush() => KeepaliveToken.Create();
57+
58+
/// <summary>
59+
/// Start a thread that may be kept alive on its webworker after the start function returns,
60+
/// if the emscripten keepalive count is positive. Once the thread returns to the JS event
61+
/// loop it will be able to settle JS promises as well as run any queued managed async
62+
/// callbacks.
63+
/// </summary>
64+
internal static void StartExitable(Thread thread, bool captureContext)
65+
{
66+
// don't support captureContext == true, for now, since it's
67+
// not needed by PortableThreadPool.WorkerThread
68+
if (captureContext)
69+
throw new InvalidOperationException();
70+
// hack: threadpool threads are exitable, and nothing else is.
71+
// see create_thread() in mono/metadata/threads.c
72+
if (!thread.IsThreadPoolThread)
73+
throw new InvalidOperationException();
74+
thread.UnsafeStart();
75+
}
76+
77+
/// returns true if the current thread has unsettled JS Interop promises
78+
private static bool HasUnsettledInteropPromises => HasUnsettledInteropPromisesNative();
79+
80+
// FIXME: this could be a qcall with a SuppressGCTransitionAttribute
81+
[MethodImpl(MethodImplOptions.InternalCall)]
82+
private static extern bool HasUnsettledInteropPromisesNative();
83+
84+
/// <summary>returns true if the current WebWorker has JavaScript objects that depend on the
85+
/// current managed thread.</summary>
86+
///
87+
/// <remarks>If this returns false, the runtime is allowed to allow the current managed thread
88+
/// to exit and for the WebWorker to be recycled by Emscripten for another managed
89+
/// thread.</remarks>
90+
internal static bool HasJavaScriptInteropDependents
91+
{
92+
//
93+
// FIXME:
94+
// https://github.com/dotnet/runtime/issues/85052 - unsettled promises are not the only relevant
95+
// reasons for keeping a worker thread alive. We will need to add other conditions here.
96+
get => HasUnsettledInteropPromises;
97+
}
98+
}

src/mono/mono/metadata/icall-decl.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,13 @@ ICALL_EXPORT void ves_icall_System_Threading_LowLevelLifoSemaphore_DeleteInt
184184
ICALL_EXPORT gint32 ves_icall_System_Threading_LowLevelLifoSemaphore_TimedWaitInternal (gpointer sem_ptr, gint32 timeout_ms);
185185
ICALL_EXPORT void ves_icall_System_Threading_LowLevelLifoSemaphore_ReleaseInternal (gpointer sem_ptr, gint32 count);
186186

187+
/* 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 */
188+
#if (defined(HOST_BROWSER) && !defined(DISABLE_THREADS)) || (defined(TARGET_WASM) && defined(ENABLE_ICALL_SYMBOL_MAP))
189+
ICALL_EXPORT MonoBoolean ves_icall_System_Threading_WebWorkerEventLoop_HasUnsettledInteropPromisesNative(void);
190+
ICALL_EXPORT void ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePushInternal (void);
191+
ICALL_EXPORT void ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal (void);
192+
#endif
193+
187194
#ifdef TARGET_AMD64
188195
ICALL_EXPORT void ves_icall_System_Runtime_Intrinsics_X86_X86Base___cpuidex (int abcd[4], int function_id, int subfunction_id);
189196
#endif

src/mono/mono/metadata/icall-def.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,7 @@ NOHANDLES(ICALL(LIFOSEM_2, "InitInternal", ves_icall_System_Threading_LowLevelLi
573573
NOHANDLES(ICALL(LIFOSEM_3, "ReleaseInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_ReleaseInternal))
574574
NOHANDLES(ICALL(LIFOSEM_4, "TimedWaitInternal", ves_icall_System_Threading_LowLevelLifoSemaphore_TimedWaitInternal))
575575

576+
576577
ICALL_TYPE(MONIT, "System.Threading.Monitor", MONIT_0)
577578
HANDLES(MONIT_0, "Enter", ves_icall_System_Threading_Monitor_Monitor_Enter, void, 1, (MonoObject))
578579
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,
597598
HANDLES(THREAD_13, "StartInternal", ves_icall_System_Threading_Thread_StartInternal, void, 2, (MonoThreadObject, gint32))
598599
NOHANDLES(ICALL(THREAD_14, "YieldInternal", ves_icall_System_Threading_Thread_YieldInternal))
599600

601+
/* 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 */
602+
#if (defined(HOST_BROWSER) && !defined(DISABLE_THREADS)) || (defined(TARGET_WASM) && defined(ENABLE_ICALL_SYMBOL_MAP))
603+
ICALL_TYPE(WEBWORKERLOOP, "System.Threading.WebWorkerEventLoop", WEBWORKERLOOP_1)
604+
NOHANDLES(ICALL(WEBWORKERLOOP_1, "HasUnsettledInteropPromisesNative", ves_icall_System_Threading_WebWorkerEventLoop_HasUnsettledInteropPromisesNative))
605+
NOHANDLES(ICALL(WEBWORKERLOOP_2, "KeepalivePopInternal", ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal))
606+
NOHANDLES(ICALL(WEBWORKERLOOP_3, "KeepalivePushInternal", ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePushInternal))
607+
#endif
608+
600609
ICALL_TYPE(TYPE, "System.Type", TYPE_1)
601610
HANDLES(TYPE_1, "internal_from_handle", ves_icall_System_Type_internal_from_handle, MonoReflectionType, 1, (MonoType_ref))
602611

src/mono/mono/metadata/threads.c

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ mono_native_thread_join_handle (HANDLE thread_handle, gboolean close_handle);
9191
#include <errno.h>
9292
#endif
9393

94+
#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS)
95+
#include <mono/utils/mono-threads-wasm.h>
96+
#include <emscripten/eventloop.h>
97+
#endif
98+
9499
#include "icall-decl.h"
95100

96101
/*#define THREAD_DEBUG(a) do { a; } while (0)*/
@@ -1110,6 +1115,7 @@ fire_attach_profiler_events (MonoNativeThreadId tid)
11101115
"Handle Stack"));
11111116
}
11121117

1118+
11131119
static guint32 WINAPI
11141120
start_wrapper_internal (StartInfo *start_info, gsize *stack_ptr)
11151121
{
@@ -4963,3 +4969,50 @@ ves_icall_System_Threading_LowLevelLifoSemaphore_ReleaseInternal (gpointer sem_p
49634969
LifoSemaphore *sem = (LifoSemaphore *)sem_ptr;
49644970
mono_lifo_semaphore_release (sem, count);
49654971
}
4972+
4973+
#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS)
4974+
void
4975+
ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePushInternal (void)
4976+
{
4977+
emscripten_runtime_keepalive_push();
4978+
}
4979+
4980+
void
4981+
ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal (void)
4982+
{
4983+
emscripten_runtime_keepalive_pop();
4984+
}
4985+
4986+
extern int mono_wasm_eventloop_has_unsettled_interop_promises(void);
4987+
4988+
MonoBoolean
4989+
ves_icall_System_Threading_WebWorkerEventLoop_HasUnsettledInteropPromisesNative(void)
4990+
{
4991+
return !!mono_wasm_eventloop_has_unsettled_interop_promises();
4992+
}
4993+
4994+
#endif /* HOST_BROWSER && !DISABLE_THREADS */
4995+
4996+
/* for the AOT cross compiler with --print-icall-table these don't need to be callable, they just
4997+
* need to be defined */
4998+
#if defined(TARGET_WASM) && defined(ENABLE_ICALL_SYMBOL_MAP)
4999+
void
5000+
ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePushInternal (void)
5001+
{
5002+
g_assert_not_reached();
5003+
}
5004+
5005+
void
5006+
ves_icall_System_Threading_WebWorkerEventLoop_KeepalivePopInternal (void)
5007+
{
5008+
g_assert_not_reached();
5009+
}
5010+
5011+
MonoBoolean
5012+
ves_icall_System_Threading_WebWorkerEventLoop_HasUnsettledInteropPromisesNative(void)
5013+
{
5014+
g_assert_not_reached();
5015+
}
5016+
5017+
#endif /* defined(TARGET_WASM) && defined(ENABLE_ICALL_SYMBOL_MAP) */
5018+

src/mono/wasm/runtime/es6/dotnet.es6.lib.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ if (monoWasmThreads) {
109109
linked_functions = [...linked_functions,
110110
/// mono-threads-wasm.c
111111
"mono_wasm_pthread_on_pthread_attached",
112+
// threads.c
113+
"mono_wasm_eventloop_has_unsettled_interop_promises",
112114
// diagnostics_server.c
113115
"mono_wasm_diagnostic_server_on_server_thread_created",
114116
"mono_wasm_diagnostic_server_on_runtime_server_init",

src/mono/wasm/runtime/exports-linker.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { mono_interp_tier_prepare_jiterpreter } from "./jiterpreter";
1111
import { mono_interp_jit_wasm_entry_trampoline, mono_interp_record_interp_entry } from "./jiterpreter-interp-entry";
1212
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";
1313
import { mono_wasm_marshal_promise } from "./marshal-to-js";
14+
import { mono_wasm_eventloop_has_unsettled_interop_promises } from "./pthreads/shared/eventloop";
1415
import { mono_wasm_pthread_on_pthread_attached } from "./pthreads/worker";
1516
import { mono_set_timeout, schedule_background_exec } from "./scheduling";
1617
import { mono_wasm_asm_loaded } from "./startup";
@@ -33,6 +34,8 @@ import { mono_wasm_change_case, mono_wasm_change_case_invariant, mono_wasm_compa
3334
const mono_wasm_threads_exports = !MonoWasmThreads ? undefined : {
3435
// mono-threads-wasm.c
3536
mono_wasm_pthread_on_pthread_attached,
37+
// threads.c
38+
mono_wasm_eventloop_has_unsettled_interop_promises,
3639
// diagnostics_server.c
3740
mono_wasm_diagnostic_server_on_server_thread_created,
3841
mono_wasm_diagnostic_server_on_runtime_server_init,

src/mono/wasm/runtime/gc-handles.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ export function mono_wasm_get_js_handle(js_obj: any): JSHandle {
4949
js_obj[cs_owned_js_handle_symbol] = js_handle;
5050
}
5151
// else
52-
// The consequence of not adding the cs_owned_js_handle_symbol is, that we could have multiple JSHandles and multiple proxy instances.
53-
// Throwing exception would prevent us from creating any proxy of non-extensible things.
52+
// The consequence of not adding the cs_owned_js_handle_symbol is, that we could have multiple JSHandles and multiple proxy instances.
53+
// Throwing exception would prevent us from creating any proxy of non-extensible things.
5454
// If we have weakmap instead, we would pay the price of the lookup for all proxies, not just non-extensible objects.
5555

5656
return js_handle as JSHandle;
@@ -131,3 +131,4 @@ export function _lookup_js_owned_object(gc_handle: GCHandle): any {
131131
}
132132
return null;
133133
}
134+

src/mono/wasm/runtime/marshal-to-cs.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
import monoWasmThreads from "consts:monoWasmThreads";
45
import { isThenable } from "./cancelable-promise";
56
import cwraps from "./cwraps";
67
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";
1819
import { js_string_to_mono_string_root } from "./strings";
1920
import { mono_assert, GCHandle, GCHandleNull, JSMarshalerArgument, JSMarshalerArguments, JSMarshalerType, MarshalerToCs, MarshalerToJs, BoundMarshalerToCs, MarshalerType } from "./types";
2021
import { TypedArray } from "./types/emscripten";
22+
import { addUnsettledPromise, settleUnsettledPromise } from "./pthreads/shared/eventloop";
23+
2124

2225
export function initialize_marshalers_to_cs(): void {
2326
if (js_to_cs_marshalers.size == 0) {
@@ -306,10 +309,17 @@ function _marshal_task_to_cs(arg: JSMarshalerArgument, value: Promise<any>, _?:
306309
const holder = new TaskCallbackHolder(value);
307310
setup_managed_proxy(holder, gc_handle);
308311

312+
if (monoWasmThreads)
313+
addUnsettledPromise();
314+
309315
value.then(data => {
316+
if (monoWasmThreads)
317+
settleUnsettledPromise();
310318
runtimeHelpers.javaScriptExports.complete_task(gc_handle, null, data, res_converter || _marshal_cs_object_to_cs);
311319
teardown_managed_proxy(holder, gc_handle); // this holds holder alive for finalizer, until the promise is freed, (holding promise instead would not work)
312320
}).catch(reason => {
321+
if (monoWasmThreads)
322+
settleUnsettledPromise();
313323
runtimeHelpers.javaScriptExports.complete_task(gc_handle, reason, null, undefined);
314324
teardown_managed_proxy(holder, gc_handle); // this holds holder alive for finalizer, until the promise is freed
315325
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
5+
let _per_thread_unsettled_promise_count = 0;
6+
7+
export function addUnsettledPromise() {
8+
_per_thread_unsettled_promise_count++;
9+
}
10+
11+
export function settleUnsettledPromise() {
12+
_per_thread_unsettled_promise_count--;
13+
}
14+
15+
/// Called from the C# threadpool worker loop to find out if there are any
16+
/// unsettled JS promises that need to keep the worker alive
17+
export function mono_wasm_eventloop_has_unsettled_interop_promises(): boolean {
18+
return _per_thread_unsettled_promise_count > 0;
19+
}

0 commit comments

Comments
 (0)