Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[mono] add internal WebWorkerEventLoop utility class #84492

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@
</ItemGroup>
<ItemGroup Condition="('$(TargetsBrowser)' == 'true' or '$(TargetsWasi)' == 'true') and '$(FeatureWasmThreads)' == 'true'">
<Compile Include="$(BclSourcesRoot)\System\Threading\ThreadPoolBoundHandle.Browser.Threads.Mono.cs" />
<Compile Include="$(BclSourcesRoot)\System\Threading\WebWorkerEventLoop.Browser.Threads.Mono.cs" />
</ItemGroup>
<ItemGroup Condition="('$(TargetsBrowser)' == 'true' or '$(TargetsWasi)' == 'true') and '$(FeatureWasmThreads)' != 'true'">
<Compile Include="$(BclSourcesRoot)\System\Threading\ThreadPoolBoundHandle.Browser.Mono.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Keep a pthread alive in its WebWorker after its pthread start function returns.
/// </summary>
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();

/// <summary>
/// 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.
/// </summary>
internal sealed class KeepaliveToken
{
public bool Valid {get; private set; }

private KeepaliveToken() { Valid = true; }

/// <summary>
/// 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.
/// </summary>
internal void Pop() {
if (!Valid)
throw new InvalidOperationException();
Valid = false;
KeepalivePopInternal();
}

internal static KeepaliveToken Create()
{
KeepalivePushInternal();
return new KeepaliveToken();
}
}

/// <summary>
/// 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.
/// </summary>
internal static KeepaliveToken KeepalivePush() => KeepaliveToken.Create();

/// <summary>
/// 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.
/// </summary>
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();

/// <summary>returns true if the current WebWorker has JavaScript objects that depend on the
/// current managed thread.</summary>
///
/// <remarks>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.</remarks>
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;
}
}
7 changes: 7 additions & 0 deletions src/mono/mono/metadata/icall-decl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/mono/mono/metadata/icall-def.h
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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))

Expand Down
53 changes: 53 additions & 0 deletions src/mono/mono/metadata/threads.c
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ mono_native_thread_join_handle (HANDLE thread_handle, gboolean close_handle);
#include <errno.h>
#endif

#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS)
#include <mono/utils/mono-threads-wasm.h>
#include <emscripten/eventloop.h>
#endif

#include "icall-decl.h"

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


static guint32 WINAPI
start_wrapper_internal (StartInfo *start_info, gsize *stack_ptr)
{
Expand Down Expand Up @@ -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) */

2 changes: 2 additions & 0 deletions src/mono/wasm/runtime/es6/dotnet.es6.lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,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",
Expand Down
3 changes: 3 additions & 0 deletions src/mono/wasm/runtime/exports-linker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions src/mono/wasm/runtime/gc-handles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -131,3 +131,4 @@ export function _lookup_js_owned_object(gc_handle: GCHandle): any {
}
return null;
}

10 changes: 10 additions & 0 deletions src/mono/wasm/runtime/marshal-to-cs.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 } 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) {
Expand Down Expand Up @@ -305,10 +308,17 @@ function _marshal_task_to_cs(arg: JSMarshalerArgument, value: Promise<any>, _?:
const holder = new TaskCallbackHolder(value);
setup_managed_proxy(holder, gc_handle);

if (monoWasmThreads)
addUnsettledPromise();
pavelsavara marked this conversation as resolved.
Show resolved Hide resolved

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
});
Expand Down
19 changes: 19 additions & 0 deletions src/mono/wasm/runtime/pthreads/shared/eventloop.ts
Original file line number Diff line number Diff line change
@@ -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;
}