From b3ed99bab23485ba7a172c2d858d98f3e0d5d726 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 6 Jun 2023 12:21:12 +0200 Subject: [PATCH 01/15] wip --- eng/Subsets.props | 1 + .../src/Interop/Browser/Interop.Runtime.cs | 7 + ....Runtime.InteropServices.JavaScript.csproj | 8 +- .../JavaScript/Interop/JavaScriptExports.cs | 4 +- .../JavaScript/JSHostImplementation.cs | 64 ++++++++- .../JavaScript/JSSynchronizationContext.cs | 65 +++++---- .../InteropServices/JavaScript/WebWorker.cs | 128 +++++++++++++++++ .../src/System/Threading/Thread.Mono.cs | 9 +- .../Threading/ThreadPool.Browser.Mono.cs | 5 +- src/mono/mono/metadata/threads.c | 5 +- src/mono/mono/mini/mini-wasm.c | 7 +- src/mono/mono/utils/lifo-semaphore.c | 4 + src/mono/mono/utils/lifo-semaphore.h | 3 - src/mono/mono/utils/mono-threads-wasm.c | 21 ++- src/mono/mono/utils/mono-threads.h | 1 + .../wasm/browser-threads-minimal/Program.cs | 134 +++++++++++++++--- .../wasm/browser-threads-minimal/WebWorker.cs | 58 ++++++++ .../wasm/browser-threads-minimal/main.js | 44 +++++- src/mono/wasm/runtime/corebindings.c | 11 ++ src/mono/wasm/runtime/dotnet.d.ts | 2 +- src/mono/wasm/runtime/driver.c | 8 +- src/mono/wasm/runtime/es6/dotnet.es6.lib.js | 5 +- src/mono/wasm/runtime/exports-linker.ts | 16 ++- src/mono/wasm/runtime/gc-handles.ts | 27 ++++ src/mono/wasm/runtime/invoke-cs.ts | 16 ++- src/mono/wasm/runtime/invoke-js.ts | 19 ++- src/mono/wasm/runtime/logging.ts | 6 +- src/mono/wasm/runtime/net6-legacy/cs-to-js.ts | 3 +- src/mono/wasm/runtime/net6-legacy/js-to-cs.ts | 2 +- .../runtime/net6-legacy/method-binding.ts | 13 +- .../wasm/runtime/net6-legacy/method-calls.ts | 7 +- src/mono/wasm/runtime/net6-legacy/strings.ts | 2 +- .../wasm/runtime/pthreads/shared/index.ts | 42 ++++-- .../wasm/runtime/pthreads/worker/index.ts | 6 +- src/mono/wasm/runtime/run.ts | 3 +- src/mono/wasm/runtime/scheduling.ts | 10 +- src/mono/wasm/runtime/startup.ts | 22 ++- src/mono/wasm/runtime/types/internal.ts | 3 + src/mono/wasm/threads.md | 9 +- src/mono/wasm/wasm.proj | 6 + src/native/libs/System.Native/pal_time.c | 2 +- 41 files changed, 680 insertions(+), 128 deletions(-) create mode 100644 src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/WebWorker.cs create mode 100644 src/mono/sample/wasm/browser-threads-minimal/WebWorker.cs diff --git a/eng/Subsets.props b/eng/Subsets.props index 211dc6a8a180c0..daa926ed523f3b 100644 --- a/eng/Subsets.props +++ b/eng/Subsets.props @@ -440,6 +440,7 @@ + diff --git a/src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs b/src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs index 202afdf004e271..edf075ab2867d9 100644 --- a/src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs +++ b/src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs @@ -28,6 +28,13 @@ internal static unsafe partial class Runtime [MethodImpl(MethodImplOptions.InternalCall)] public static extern void DeregisterGCRoot(IntPtr handle); +#if FEATURE_WASM_THREADS + [MethodImpl(MethodImplOptions.InternalCall)] + public static extern void InstallWebWorkerInterop(bool installJSSynchronizationContext); + [MethodImpl(MethodImplOptions.InternalCall)] + public static extern void UninstallWebWorkerInterop(bool uninstallJSSynchronizationContext); +#endif + #region Legacy [MethodImplAttribute(MethodImplOptions.InternalCall)] diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj index a77e9bf07b3400..43251dd4d5952a 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj @@ -58,8 +58,6 @@ - - @@ -74,6 +72,12 @@ + + + + + + diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs index 9a5ec0d21f24fa..9a0c75d78aeacd 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs @@ -4,6 +4,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; namespace System.Runtime.InteropServices.JavaScript { @@ -219,11 +220,12 @@ public static void GetManagedStackTrace(JSMarshalerArgument* arguments_buffer) // the marshaled signature is: // void InstallSynchronizationContext() + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, "System.Runtime.InteropServices.JavaScript.WebWorker", "System.Runtime.InteropServices.JavaScript")] public static void InstallSynchronizationContext (JSMarshalerArgument* arguments_buffer) { ref JSMarshalerArgument arg_exc = ref arguments_buffer[0]; // initialized by caller in alloc_stack_frame() try { - JSSynchronizationContext.Install(); + JSHostImplementation.InstallWebWorkerInterop(true); } catch (Exception ex) { diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs index a333310a7d5b51..e949c5532a2b81 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs @@ -24,7 +24,7 @@ public static Dictionary> ThreadCsOwnedObjects { get { - s_csOwnedObjects ??= new (); + s_csOwnedObjects ??= new(); return s_csOwnedObjects; } } @@ -197,5 +197,67 @@ public static JSObject CreateCSOwnedProxy(nint jsHandle) } return res; } + +#if FEATURE_WASM_THREADS + public static void InstallWebWorkerInterop(bool installJSSynchronizationContext) + { + Interop.Runtime.InstallWebWorkerInterop(installJSSynchronizationContext); + if (installJSSynchronizationContext) + { + var currentThreadId = GetNativeThreadId(); + var ctx = JSSynchronizationContext.CurrentJSSynchronizationContext; + if (ctx == null) + { + ctx = new JSSynchronizationContext(Thread.CurrentThread, currentThreadId); + ctx.previousSynchronizationContext = SynchronizationContext.Current; + JSSynchronizationContext.CurrentJSSynchronizationContext = ctx; + SynchronizationContext.SetSynchronizationContext(ctx); + } + else if (ctx.TargetThreadId != currentThreadId) + { + Environment.FailFast($"JSSynchronizationContext.Install failed has wrong native thread id {ctx.TargetThreadId} != {currentThreadId}"); + } + ctx.AwaitNewData(); + } + } + + public static void UninstallWebWorkerInterop() + { + var ctx = SynchronizationContext.Current as JSSynchronizationContext; + var uninstallJSSynchronizationContext = ctx != null; + if (uninstallJSSynchronizationContext) + { + SynchronizationContext.SetSynchronizationContext(ctx!.previousSynchronizationContext); + JSSynchronizationContext.CurrentJSSynchronizationContext = null; + ctx.isDisposed = true; + } + Interop.Runtime.UninstallWebWorkerInterop(uninstallJSSynchronizationContext); + } + + private static FieldInfo? thread_id_Field; + private static FieldInfo? external_eventloop_Field; + + [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, "System.Threading.Thread", "System.Private.CoreLib")] + public static void SetHasExternalEventLoop(Thread thread) + { + if (external_eventloop_Field == null) + { + external_eventloop_Field = typeof(Thread).GetField("external_eventloop", BindingFlags.NonPublic | BindingFlags.Instance)!; + } + external_eventloop_Field.SetValue(thread, true); + } + + [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicFields, "System.Threading.Thread", "System.Private.CoreLib")] + public static IntPtr GetNativeThreadId() + { + if (thread_id_Field == null) + { + thread_id_Field = typeof(Thread).GetField("thread_id", BindingFlags.NonPublic | BindingFlags.Instance)!; + } + return (int)(long)thread_id_Field.GetValue(Thread.CurrentThread)!; + } + +#endif + } } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSSynchronizationContext.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSSynchronizationContext.cs index 6c8a00bd00eebc..c657eba63efb54 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSSynchronizationContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSSynchronizationContext.cs @@ -3,25 +3,31 @@ #if FEATURE_WASM_THREADS -using System; using System.Threading; using System.Threading.Channels; -using System.Runtime; -using System.Runtime.InteropServices; using System.Runtime.CompilerServices; -using QueueType = System.Threading.Channels.Channel; +using WorkItemQueueType = System.Threading.Channels.Channel; namespace System.Runtime.InteropServices.JavaScript { /// /// Provides a thread-safe default SynchronizationContext for the browser that will automatically - /// route callbacks to the main browser thread where they can interact with the DOM and other + /// route callbacks to the original browser thread where they can interact with the DOM and other /// thread-affinity-having APIs like WebSockets, fetch, WebGL, etc. /// Callbacks are processed during event loop turns via the runtime's background job system. + /// See also https://github.com/dotnet/runtime/blob/main/src/mono/wasm/threads.md#JS-interop-on-dedicated-threads /// internal sealed class JSSynchronizationContext : SynchronizationContext { - public readonly Thread MainThread; + private readonly Action _DataIsAvailable;// don't allocate Action on each call to UnsafeOnCompleted + public readonly Thread TargetThread; + public readonly IntPtr TargetThreadId; + private readonly WorkItemQueueType Queue; + + [ThreadStatic] + internal static JSSynchronizationContext? CurrentJSSynchronizationContext; + internal SynchronizationContext? previousSynchronizationContext; + internal bool isDisposed; internal readonly struct WorkItem { @@ -37,13 +43,9 @@ public WorkItem(SendOrPostCallback callback, object? data, ManualResetEventSlim? } } - private static JSSynchronizationContext? MainThreadSynchronizationContext; - private readonly QueueType Queue; - private readonly Action _DataIsAvailable;// don't allocate Action on each call to UnsafeOnCompleted - - private JSSynchronizationContext() + internal JSSynchronizationContext(Thread targetThread, IntPtr targetThreadId) : this( - Thread.CurrentThread, + targetThread, targetThreadId, Channel.CreateUnbounded( new UnboundedChannelOptions { SingleWriter = false, SingleReader = true, AllowSynchronousContinuations = true } ) @@ -51,20 +53,23 @@ private JSSynchronizationContext() { } - private JSSynchronizationContext(Thread mainThread, QueueType queue) + private JSSynchronizationContext(Thread targetThread, IntPtr targetThreadId, WorkItemQueueType queue) { - MainThread = mainThread; + TargetThread = targetThread; + TargetThreadId = targetThreadId; Queue = queue; _DataIsAvailable = DataIsAvailable; } public override SynchronizationContext CreateCopy() { - return new JSSynchronizationContext(MainThread, Queue); + return new JSSynchronizationContext(TargetThread, TargetThreadId, Queue); } - private void AwaitNewData() + internal void AwaitNewData() { + ObjectDisposedException.ThrowIf(isDisposed, this); + var vt = Queue.Reader.WaitToReadAsync(); if (vt.IsCompleted) { @@ -84,11 +89,13 @@ private unsafe void DataIsAvailable() { // While we COULD pump here, we don't want to. We want the pump to happen on the next event loop turn. // Otherwise we could get a chain where a pump generates a new work item and that makes us pump again, forever. - MainThreadScheduleBackgroundJob((void*)(delegate* unmanaged[Cdecl])&BackgroundJobHandler); + TargetThreadScheduleBackgroundJob(TargetThreadId, (void*)(delegate* unmanaged[Cdecl])&BackgroundJobHandler); } public override void Post(SendOrPostCallback d, object? state) { + ObjectDisposedException.ThrowIf(isDisposed, this); + var workItem = new WorkItem(d, state, null); if (!Queue.Writer.TryWrite(workItem)) throw new Exception("Internal error"); @@ -99,7 +106,9 @@ public override void Post(SendOrPostCallback d, object? state) public override void Send(SendOrPostCallback d, object? state) { - if (Thread.CurrentThread == MainThread) + ObjectDisposedException.ThrowIf(isDisposed, this); + + if (Thread.CurrentThread == TargetThread) { d(state); return; @@ -115,27 +124,25 @@ public override void Send(SendOrPostCallback d, object? state) } } - internal static void Install() - { - MainThreadSynchronizationContext ??= new JSSynchronizationContext(); - SynchronizationContext.SetSynchronizationContext(MainThreadSynchronizationContext); - MainThreadSynchronizationContext.AwaitNewData(); - } - [MethodImplAttribute(MethodImplOptions.InternalCall)] - internal static extern unsafe void MainThreadScheduleBackgroundJob(void* callback); + internal static extern unsafe void TargetThreadScheduleBackgroundJob(IntPtr targetThread, void* callback); #pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] #pragma warning restore CS3016 - // this callback will arrive on the bound thread, called from mono_background_exec + // this callback will arrive on the target thread, called from mono_background_exec private static void BackgroundJobHandler() { - MainThreadSynchronizationContext!.Pump(); + CurrentJSSynchronizationContext!.Pump(); } private void Pump() { + if (isDisposed) + { + // FIXME: there could be abandoned work, but here we have no way how to propagate the failure + return; + } try { while (Queue.Reader.TryRead(out var item)) @@ -160,7 +167,7 @@ private void Pump() finally { // If an item throws, we want to ensure that the next pump gets scheduled appropriately regardless. - AwaitNewData(); + if(!isDisposed) AwaitNewData(); } } } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/WebWorker.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/WebWorker.cs new file mode 100644 index 00000000000000..31b7508b0ffd6a --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/WebWorker.cs @@ -0,0 +1,128 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if FEATURE_WASM_THREADS + +#pragma warning disable CA1416 + +using System.Threading; +using System.Threading.Tasks; + +namespace System.Runtime.InteropServices.JavaScript +{ + /// + /// This is draft for possible public API of browser thread (web worker) dedicated to JS interop workloads + /// + internal static class WebWorker + { + public static Task RunAsync(Func> body) + { + var parentScheduler = TaskScheduler.FromCurrentSynchronizationContext(); + var tcs = new TaskCompletionSource(); + var capturedContext = SynchronizationContext.Current; + var t = new Thread(() => + { + try + { + JSHostImplementation.InstallWebWorkerInterop(true); + var childScheduler = TaskScheduler.FromCurrentSynchronizationContext(); + Task res = body(); + // This code is exiting thread main() before all promises are resolved. + // the continuation is executed by setTimeout() callback of the thread. + res.ContinueWith(t => + { + JSHostImplementation.UninstallWebWorkerInterop(); + return t; + }, childScheduler) + .ContinueWith((t) => + { + if (res.IsFaulted) + tcs.SetException(res.Exception); + else if (t.IsCanceled) + tcs.SetCanceled(); + else + tcs.SetResult(res.Result); + + }, parentScheduler); + } + catch (Exception e) + { + Environment.FailFast("WebWorker.RunAsync failed", e); + } + + }); + JSHostImplementation.SetHasExternalEventLoop(t); + t.Start(); + return tcs.Task; + } + + public static Task RunAsyncVoid(Func body) + { + var parentScheduler = TaskScheduler.FromCurrentSynchronizationContext(); + var tcs = new TaskCompletionSource(); + var capturedContext = SynchronizationContext.Current; + var t = new Thread(() => + { + try + { + JSHostImplementation.InstallWebWorkerInterop(true); + var childScheduler = TaskScheduler.FromCurrentSynchronizationContext(); + Task res = body(); + // This code is exiting thread main() before all promises are resolved. + // the continuation is executed by setTimeout() callback of the thread. + res.ContinueWith(t => + { + JSHostImplementation.UninstallWebWorkerInterop(); + return t; + }, childScheduler) + .ContinueWith((t) => + { + if (res.IsFaulted) + tcs.SetException(res.Exception); + else if (t.IsCanceled) + tcs.SetCanceled(); + else + tcs.SetResult(); + }, parentScheduler); + } + catch (Exception e) + { + Environment.FailFast("WebWorker.RunAsync failed", e); + } + + }); + JSHostImplementation.SetHasExternalEventLoop(t); + t.Start(); + return tcs.Task; + } + + public static Task Run(Action body) + { + var parentContext = SynchronizationContext.Current ?? new SynchronizationContext(); + var tcs = new TaskCompletionSource(); + var capturedContext = SynchronizationContext.Current; + var t = new Thread(() => + { + try + { + JSHostImplementation.InstallWebWorkerInterop(false); + body(); + parentContext.Post((_) => { + tcs.SetResult(); + }, null); + JSHostImplementation.UninstallWebWorkerInterop(); + } + catch (Exception e) + { + tcs.SetException(e); + } + + }); + JSHostImplementation.SetHasExternalEventLoop(t); + t.Start(); + return tcs.Task; + } + } +} + +#endif diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs index d5b1918608b77f..5b58f776a8c389 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs @@ -29,7 +29,6 @@ public partial class Thread private ThreadState state; private object? abort_exc; private int abort_state_handle; - /* thread_id is only accessed from unmanaged code */ internal long thread_id; private IntPtr debugger_thread; // FIXME switch to bool as soon as CI testing with corlib version bump works private UIntPtr static_data; /* GC-tracked */ @@ -365,5 +364,13 @@ internal bool HasExternalEventLoop external_eventloop = value; } } + + internal long NativeThreadId + { + get + { + return thread_id; + } + } } } diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/ThreadPool.Browser.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/ThreadPool.Browser.Mono.cs index 96ca4d4c939f3f..2ad06cd7b49782 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/ThreadPool.Browser.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/ThreadPool.Browser.Mono.cs @@ -13,9 +13,10 @@ namespace System.Threading { -#if !FEATURE_WASM_THREADS - [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] +#if FEATURE_WASM_THREADS +#error when compiled with FEATURE_WASM_THREADS, we use PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs #endif + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] public sealed class RegisteredWaitHandle : MarshalByRefObject { internal RegisteredWaitHandle() diff --git a/src/mono/mono/metadata/threads.c b/src/mono/mono/metadata/threads.c index 0788d9ccdac13c..6c1fc2df0dd515 100644 --- a/src/mono/mono/metadata/threads.c +++ b/src/mono/mono/metadata/threads.c @@ -46,6 +46,7 @@ #include #include #include +#include #include #include #include @@ -1078,7 +1079,7 @@ mono_thread_detach_internal (MonoInternalThread *thread) /* Don't need to close the handle to this thread, even though we took a * reference in mono_thread_attach (), because the GC will do it - * when the Thread object is finalised. + * when the Thread object is finalized. */ } @@ -1283,7 +1284,7 @@ start_wrapper (gpointer data) /* if the thread wants to stay alive, don't clean up after it */ if (mono_thread_platform_external_eventloop_keepalive_check ()) { /* while we wait in the external eventloop, we're GC safe */ - MONO_REQ_GC_SAFE_MODE; + MONO_ENTER_GC_SAFE_UNBALANCED; return 0; } } diff --git a/src/mono/mono/mini/mini-wasm.c b/src/mono/mono/mini/mini-wasm.c index 8aa7934570b5c3..cab202a1fa326e 100644 --- a/src/mono/mono/mini/mini-wasm.c +++ b/src/mono/mono/mini/mini-wasm.c @@ -597,6 +597,8 @@ mono_wasm_execute_timer (void) void mono_wasm_main_thread_schedule_timer (void *timerHandler, int shortestDueTimeMs) { + // NOTE: here the `timerHandler` callback is [UnmanagedCallersOnly] which wraps it with MONO_ENTER_GC_UNSAFE/MONO_EXIT_GC_UNSAFE + g_assert (timerHandler); timer_handler = timerHandler; #ifdef HOST_BROWSER @@ -615,9 +617,10 @@ mono_arch_register_icall (void) { #ifdef HOST_BROWSER mono_add_internal_call_internal ("System.Threading.TimerQueue::MainThreadScheduleTimer", mono_wasm_main_thread_schedule_timer); +#ifdef DISABLE_THREADS mono_add_internal_call_internal ("System.Threading.ThreadPool::MainThreadScheduleBackgroundJob", mono_main_thread_schedule_background_job); -#ifndef DISABLE_THREADS - mono_add_internal_call_internal ("System.Runtime.InteropServices.JavaScript.JSSynchronizationContext::MainThreadScheduleBackgroundJob", mono_main_thread_schedule_background_job); +#else + mono_add_internal_call_internal ("System.Runtime.InteropServices.JavaScript.JSSynchronizationContext::TargetThreadScheduleBackgroundJob", mono_target_thread_schedule_background_job); #endif /* DISABLE_THREADS */ #endif /* HOST_BROWSER */ } diff --git a/src/mono/mono/utils/lifo-semaphore.c b/src/mono/mono/utils/lifo-semaphore.c index 51117ab972a66a..dce67c48e8b37d 100644 --- a/src/mono/mono/utils/lifo-semaphore.c +++ b/src/mono/mono/utils/lifo-semaphore.c @@ -257,6 +257,7 @@ lifo_js_wait_entry_on_timeout (void *wait_entry_as_user_data) gboolean call_timeout_cb = FALSE; LifoSemaphoreAsyncWaitCallbackFn timeout_cb = NULL; intptr_t user_data = 0; + MONO_ENTER_GC_UNSAFE; mono_coop_mutex_lock (&sem->base.mutex); switch (wait_entry->state) { case LIFO_JS_WAITING: @@ -284,6 +285,7 @@ lifo_js_wait_entry_on_timeout (void *wait_entry_as_user_data) if (call_timeout_cb) { timeout_cb (sem, user_data); } + MONO_EXIT_GC_UNSAFE; } static void @@ -296,6 +298,7 @@ lifo_js_wait_entry_on_success (void *wait_entry_as_user_data) gboolean call_success_cb = FALSE; LifoSemaphoreAsyncWaitCallbackFn success_cb = NULL; intptr_t user_data = 0; + MONO_ENTER_GC_UNSAFE; mono_coop_mutex_lock (&sem->base.mutex); switch (wait_entry->state) { case LIFO_JS_SIGNALED: @@ -321,6 +324,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, user_data); + MONO_EXIT_GC_UNSAFE; } #endif /* HOST_BROWSER && !DISABLE_THREADS */ diff --git a/src/mono/mono/utils/lifo-semaphore.h b/src/mono/mono/utils/lifo-semaphore.h index a97a560e281161..1a91a6f4d7c39b 100644 --- a/src/mono/mono/utils/lifo-semaphore.h +++ b/src/mono/mono/utils/lifo-semaphore.h @@ -121,9 +121,6 @@ mono_lifo_semaphore_asyncwait_delete (LifoSemaphoreAsyncWait *semaphore); * 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_semaphore_asyncwait_prepare_wait (LifoSemaphoreAsyncWait *semaphore, int32_t timeout_ms, diff --git a/src/mono/mono/utils/mono-threads-wasm.c b/src/mono/mono/utils/mono-threads-wasm.c index dd49bf4205e3a3..efc02389d3ecc7 100644 --- a/src/mono/mono/utils/mono-threads-wasm.c +++ b/src/mono/mono/utils/mono-threads-wasm.c @@ -23,6 +23,10 @@ #include #endif +#ifdef ENABLE_CHECKED_BUILD +#include +#endif + #define round_down(addr, val) ((void*)((addr) & ~((val) - 1))) EMSCRIPTEN_KEEPALIVE @@ -305,6 +309,9 @@ gboolean mono_thread_platform_external_eventloop_keepalive_check (void) { #if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) +#ifdef ENABLE_CHECKED_BUILD + MONO_REQ_GC_SAFE_MODE; +#endif /* if someone called emscripten_runtime_keepalive_push (), the * thread will stay alive in the JS event loop after returning * from the thread's main function. @@ -402,6 +409,16 @@ mono_current_thread_schedule_background_job (background_job_cb cb) #endif /*DISABLE_THREADS*/ } +#ifndef DISABLE_THREADS +void +mono_target_thread_schedule_background_job (MonoNativeThreadId target_thread, background_job_cb cb) +{ + THREADS_DEBUG ("worker %p queued job %p to worker %p \n", (gpointer)pthread_self(), (gpointer) cb, (gpointer) target_thread); + // NOTE: here the cb is [UnmanagedCallersOnly] which wraps it with MONO_ENTER_GC_UNSAFE/MONO_EXIT_GC_UNSAFE + mono_threads_wasm_async_run_in_target_thread_vi ((pthread_t) target_thread, (void*)mono_current_thread_schedule_background_job, (gpointer)cb); +} +#endif /*DISABLE_THREADS*/ + G_EXTERN_C EMSCRIPTEN_KEEPALIVE void mono_background_exec (void); @@ -463,8 +480,7 @@ mono_threads_wasm_browser_thread_tid (void) } #ifndef DISABLE_THREADS -extern void -mono_wasm_pthread_on_pthread_attached (gpointer pthread_id); +extern void mono_wasm_pthread_on_pthread_attached (MonoNativeThreadId pthread_id); #endif void @@ -484,7 +500,6 @@ mono_threads_wasm_on_thread_attached (void) #endif } - #ifndef DISABLE_THREADS void mono_threads_wasm_async_run_in_main_thread (void (*func) (void)) diff --git a/src/mono/mono/utils/mono-threads.h b/src/mono/mono/utils/mono-threads.h index 80d6f198a16b97..0d3d8027006a11 100644 --- a/src/mono/mono/utils/mono-threads.h +++ b/src/mono/mono/utils/mono-threads.h @@ -848,6 +848,7 @@ void mono_threads_join_unlock (void); typedef void (*background_job_cb)(void); void mono_main_thread_schedule_background_job (background_job_cb cb); void mono_current_thread_schedule_background_job (background_job_cb cb); +void mono_target_thread_schedule_background_job (MonoNativeThreadId target_thread, background_job_cb cb); #endif #ifdef USE_WINDOWS_BACKEND diff --git a/src/mono/sample/wasm/browser-threads-minimal/Program.cs b/src/mono/sample/wasm/browser-threads-minimal/Program.cs index 0e9e5584c7c9d9..bd2646a34d2745 100644 --- a/src/mono/sample/wasm/browser-threads-minimal/Program.cs +++ b/src/mono/sample/wasm/browser-threads-minimal/Program.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices.JavaScript; using System.Threading; +using System.Reflection; using System.Threading.Tasks; using System.Collections.Generic; @@ -18,6 +19,41 @@ public static int Main(string[] args) return 0; } + [JSImport("globalThis.setTimeout")] + static partial void GlobalThisSetTimeout([JSMarshalAs] Action cb, int timeoutMs); + + [JSImport("globalThis.fetch")] + private static partial Task GlobalThisFetch(string url); + + [JSImport("globalThis.console.log")] + private static partial void GlobalThisConsoleLog(string text); + + const string fetchhelper = "./fetchelper.js"; + + [JSImport("responseText", fetchhelper)] + private static partial Task FetchHelperResponseText(JSObject response, int delayMs); + + [JSImport("delay", fetchhelper)] + private static partial Task Delay(int timeoutMs); + + [JSExport] + internal static Task TestHelloWebWorker() + { + Console.WriteLine($"smoke: TestHelloWebWorker 1 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}"); + Task t= WebWorker.RunAsync(() => + { + Console.WriteLine($"smoke: TestHelloWebWorker 2 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}"); + GlobalThisConsoleLog($"smoke: TestHelloWebWorker 3 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}"); + Console.WriteLine($"smoke: TestHelloWebWorker 4 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}"); + }); + Console.WriteLine($"smoke: TestHelloWebWorker 5 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}"); + return t.ContinueWith(Gogo); + } + + private static void Gogo(Task t){ + Console.WriteLine($"smoke: TestHelloWebWorker 6 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}"); + } + [JSExport] public static async Task TestCanStartThread() { @@ -40,30 +76,63 @@ public static async Task TestCanStartThread() throw new Exception("Child thread ran on same thread as parent"); } - [JSImport("globalThis.setTimeout")] - static partial void GlobalThisSetTimeout([JSMarshalAs] Action cb, int timeoutMs); + static bool _timerDone = false; - [JSImport("globalThis.fetch")] - private static partial Task GlobalThisFetch(string url); + [JSExport] + internal static void StartTimerFromWorker() + { + Console.WriteLine("smoke: StartTimerFromWorker 1 utc {0}", DateTime.UtcNow.ToUniversalTime()); + WebWorker.RunAsync(async () => + { + while (!_timerDone) + { + await Task.Delay(1 * 1000); + Console.WriteLine("smoke: StartTimerFromWorker 2 utc {0}", DateTime.UtcNow.ToUniversalTime()); + } + Console.WriteLine("smoke: StartTimerFromWorker done utc {0}", DateTime.UtcNow.ToUniversalTime()); + }); + } [JSExport] - public static async Task TestCallSetTimeoutOnWorker() + internal static void StartAllocatorFromWorker() { - var t = Task.Run(TimeOutThenComplete); - await t; - Console.WriteLine ($"XYZ: Main Thread caught task tid:{Thread.CurrentThread.ManagedThreadId}"); + Console.WriteLine("smoke: StartAllocatorFromWorker 1 utc {0}", DateTime.UtcNow.ToUniversalTime()); + WebWorker.RunAsync(async () => + { + while (!_timerDone) + { + await Task.Delay(1 * 100); + var x = new List(); + for (int i = 0; i < 1000; i++) + { + var v=new int[1000]; + v[i] = i; + x.Add(v); + } + Console.WriteLine("smoke: StartAllocatorFromWorker 2 utc {0} {1} {2}", DateTime.UtcNow.ToUniversalTime(),x[1][1], GC.GetTotalAllocatedBytes()); + } + Console.WriteLine("smoke: StartAllocatorFromWorker done utc {0}", DateTime.UtcNow.ToUniversalTime()); + }); } - const string fetchhelper = "./fetchelper.js"; + [JSExport] + internal static void StopTimerFromWorker() + { + _timerDone = true; + } - [JSImport("responseText", fetchhelper)] - private static partial Task FetchHelperResponseText(JSObject response, int delayMs); + [JSExport] + public static async Task TestCallSetTimeoutOnWorker() + { + await WebWorker.RunAsync(() => TimeOutThenComplete()); + Console.WriteLine ($"XYZ: Main Thread caught task tid:{Thread.CurrentThread.ManagedThreadId}"); + } [JSExport] public static async Task FetchBackground(string url) { Console.WriteLine($"smoke: FetchBackground 1 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}"); - var t = Task.Run(async () => + var t = WebWorker.RunAsync(async () => { Console.WriteLine($"smoke: FetchBackground 2 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}"); var x=JSHost.ImportAsync(fetchhelper, "./fetchhelper.js"); @@ -89,21 +158,44 @@ public static async Task FetchBackground(string url) return "not-ok"; }); var r = await t; - Console.WriteLine($"XYZ: FetchBackground thread:{Thread.CurrentThread.ManagedThreadId} background thread returned"); + Console.WriteLine($"smoke: FetchBackground thread:{Thread.CurrentThread.ManagedThreadId} background thread returned"); return r; } + [ThreadStatic] + public static int meaning = 42; + + [JSExport] + public static async Task TestTLS() + { + Console.WriteLine($"smoke {meaning}: TestTLS 1 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}"); + meaning = 40; + await WebWorker.RunAsync(async () => + { + Console.WriteLine($"smoke {meaning}: TestTLS 2 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}"); + meaning = 41; + await JSHost.ImportAsync(fetchhelper, "./fetchhelper.js"); + Console.WriteLine($"smoke {meaning}: TestTLS 3 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}"); + meaning = 43; + Console.WriteLine($"smoke {meaning}: TestTLS 4 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}"); + await Delay(100); + meaning = 44; + Console.WriteLine($"smoke {meaning}: TestTLS 5 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}"); + }); + Console.WriteLine($"smoke {meaning}: TestTLS 9 ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}, SynchronizationContext: {SynchronizationContext.Current?.GetType().FullName ?? "null"}"); + } + private static async Task TimeOutThenComplete() { var tcs = new TaskCompletionSource(); - Console.WriteLine ($"XYZ: Task running tid:{Thread.CurrentThread.ManagedThreadId}"); + Console.WriteLine ($"smoke: Task running tid:{Thread.CurrentThread.ManagedThreadId}"); GlobalThisSetTimeout(() => { tcs.SetResult(); - Console.WriteLine ($"XYZ: Timeout fired tid:{Thread.CurrentThread.ManagedThreadId}"); + Console.WriteLine ($"smoke: Timeout fired tid:{Thread.CurrentThread.ManagedThreadId}"); }, 250); - Console.WriteLine ($"XYZ: Task sleeping tid:{Thread.CurrentThread.ManagedThreadId}"); + Console.WriteLine ($"smoke: Task sleeping tid:{Thread.CurrentThread.ManagedThreadId}"); await tcs.Task; - Console.WriteLine ($"XYZ: Task resumed tid:{Thread.CurrentThread.ManagedThreadId}"); + Console.WriteLine ($"smoke: Task resumed tid:{Thread.CurrentThread.ManagedThreadId}"); } [JSExport] @@ -146,6 +238,14 @@ public static async Task RunBackgroundTaskRunCompute() return rs[0]; } + [JSExport] + internal static void GCCollect() + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + + public static int CountingCollatzTest() { const int limit = 5000; diff --git a/src/mono/sample/wasm/browser-threads-minimal/WebWorker.cs b/src/mono/sample/wasm/browser-threads-minimal/WebWorker.cs new file mode 100644 index 00000000000000..d3c1616f3c1940 --- /dev/null +++ b/src/mono/sample/wasm/browser-threads-minimal/WebWorker.cs @@ -0,0 +1,58 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices.JavaScript; +using System.Threading; +using System.Reflection; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace System.Runtime.InteropServices.JavaScript +{ + // this is just temporary thin wrapper to expose future public API + public partial class WebWorker + { + private static MethodInfo runAsyncMethod; + private static MethodInfo runAsyncVoidMethod; + private static MethodInfo runMethod; + + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, "System.Runtime.InteropServices.JavaScript.WebWorker", "System.Runtime.InteropServices.JavaScript")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern", Justification = "work in progress")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", Justification = "work in progress")] + public static Task RunAsync(Func> body) + { + if(runAsyncMethod == null) + { + var webWorker = typeof(JSObject).Assembly.GetType("System.Runtime.InteropServices.JavaScript.WebWorker"); + runAsyncMethod = webWorker.GetMethod("RunAsync", BindingFlags.Public|BindingFlags.Static); + } + return (Task)runAsyncMethod.Invoke(null, new object[] { body }); + } + + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, "System.Runtime.InteropServices.JavaScript.WebWorker", "System.Runtime.InteropServices.JavaScript")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern", Justification = "work in progress")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", Justification = "work in progress")] + public static Task RunAsync(Func body) + { + if(runAsyncVoidMethod == null) + { + var webWorker = typeof(JSObject).Assembly.GetType("System.Runtime.InteropServices.JavaScript.WebWorker"); + runAsyncVoidMethod = webWorker.GetMethod("RunAsyncVoid", BindingFlags.Public|BindingFlags.Static); + } + return (Task)runAsyncVoidMethod.Invoke(null, new object[] { body }); + } + + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, "System.Runtime.InteropServices.JavaScript.WebWorker", "System.Runtime.InteropServices.JavaScript")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern", Justification = "work in progress")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", Justification = "work in progress")] + public static Task RunAsync(Action body) + { + if(runMethod == null) + { + var webWorker = typeof(JSObject).Assembly.GetType("System.Runtime.InteropServices.JavaScript.WebWorker"); + runMethod = webWorker.GetMethod("Run", BindingFlags.Public|BindingFlags.Static); + } + return (Task)runMethod.Invoke(null, new object[] { body }); + } + } +} \ No newline at end of file diff --git a/src/mono/sample/wasm/browser-threads-minimal/main.js b/src/mono/sample/wasm/browser-threads-minimal/main.js index b90abdbdf51044..7fc5a65cf95e86 100644 --- a/src/mono/sample/wasm/browser-threads-minimal/main.js +++ b/src/mono/sample/wasm/browser-threads-minimal/main.js @@ -8,17 +8,39 @@ const assemblyName = "Wasm.Browser.Threads.Minimal.Sample.dll"; try { const { setModuleImports, getAssemblyExports, runMain } = await dotnet - .withEnvironmentVariable("MONO_LOG_LEVEL", "debug") + //.withEnvironmentVariable("MONO_LOG_LEVEL", "debug") + .withDiagnosticTracing(true) + .withConfig({ + pthreadPoolSize: 4, + }) .withElementOnExit() .withExitCodeLogging() .create(); const exports = await getAssemblyExports(assemblyName); + console.log("smoke: running TestHelloWebWorker"); + await exports.Sample.Test.TestHelloWebWorker(); + await exports.Sample.Test.TestHelloWebWorker(); + await exports.Sample.Test.TestHelloWebWorker(); + await exports.Sample.Test.TestHelloWebWorker(); + await exports.Sample.Test.TestHelloWebWorker(); + await exports.Sample.Test.TestHelloWebWorker(); + await exports.Sample.Test.TestHelloWebWorker(); + await exports.Sample.Test.TestHelloWebWorker(); + console.log("smoke: TestHelloWebWorker done"); + console.log("smoke: running TestCanStartThread"); await exports.Sample.Test.TestCanStartThread(); console.log("smoke: TestCanStartThread done"); + console.log("smoke: running TestTLS"); + await exports.Sample.Test.TestTLS(); + console.log("smoke: TestTLS done"); + + console.log("smoke: running StartTimerFromWorker"); + exports.Sample.Test.StartTimerFromWorker(); + console.log("smoke: running TestCallSetTimeoutOnWorker"); await exports.Sample.Test.TestCallSetTimeoutOnWorker(); console.log("smoke: TestCallSetTimeoutOnWorker done"); @@ -50,9 +72,29 @@ try { } console.log("smoke: TaskRunCompute done"); + console.log("smoke: running StartAllocatorFromWorker"); + exports.Sample.Test.StartAllocatorFromWorker(); + + await delay(5000); + + console.log("smoke: running GCCollect"); + exports.Sample.Test.GCCollect(); + + await delay(5000); + + console.log("smoke: running GCCollect"); + exports.Sample.Test.GCCollect(); + + console.log("smoke: running StopTimerFromWorker"); + exports.Sample.Test.StopTimerFromWorker(); let exit_code = await runMain(assemblyName, []); exit(exit_code); } catch (err) { exit(2, err); } + +function delay(timeoutMs) { + return new Promise(resolve => setTimeout(resolve, timeoutMs)); +} + diff --git a/src/mono/wasm/runtime/corebindings.c b/src/mono/wasm/runtime/corebindings.c index a6d0f39deb39d6..0dd7f9f3a15daa 100644 --- a/src/mono/wasm/runtime/corebindings.c +++ b/src/mono/wasm/runtime/corebindings.c @@ -43,6 +43,11 @@ extern void mono_wasm_typed_array_from_ref (int ptr, int begin, int end, int byt extern void* mono_wasm_invoke_js_blazor (MonoString **exceptionMessage, void *callInfo, void* arg0, void* arg1, void* arg2); #endif /* DISABLE_LEGACY_JS_INTEROP */ +#ifndef DISABLE_THREADS +extern void mono_wasm_install_js_worker_interop (int install_js_synchronization_context); +extern void mono_wasm_uninstall_js_worker_interop (int uninstall_js_synchronization_context); +#endif /* DISABLE_THREADS */ + // HybridGlobalization extern void mono_wasm_change_case_invariant(const uint16_t* src, int32_t srcLength, uint16_t* dst, int32_t dstLength, mono_bool bToUpper, int *is_exception, MonoObject** ex_result); extern void mono_wasm_change_case(MonoString **culture, const uint16_t* src, int32_t srcLength, uint16_t* dst, int32_t dstLength, mono_bool bToUpper, int *is_exception, MonoObject** ex_result); @@ -61,6 +66,12 @@ void bindings_initialize_internals (void) mono_add_internal_call ("Interop/Runtime::MarshalPromise", mono_wasm_marshal_promise); mono_add_internal_call ("Interop/Runtime::RegisterGCRoot", mono_wasm_register_root); mono_add_internal_call ("Interop/Runtime::DeregisterGCRoot", mono_wasm_deregister_root); + +#ifndef DISABLE_THREADS + mono_add_internal_call ("Interop/Runtime::InstallWebWorkerInterop", mono_wasm_install_js_worker_interop); + mono_add_internal_call ("Interop/Runtime::UninstallWebWorkerInterop", mono_wasm_uninstall_js_worker_interop); +#endif /* DISABLE_THREADS */ + #ifndef DISABLE_LEGACY_JS_INTEROP // legacy mono_add_internal_call ("Interop/Runtime::InvokeJSWithArgsRef", mono_wasm_invoke_js_with_args_ref); diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index 8a0dfc26cf6f87..7e055b7d85a8dd 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -3,7 +3,7 @@ //! //! This is generated file, see src/mono/wasm/runtime/rollup.config.js -//! This is not considered public API with backward compatibility guarantees. +//! This is not considered public API with backward compatibility guarantees. declare interface NativePointer { __brandNativePointer: "NativePointer"; diff --git a/src/mono/wasm/runtime/driver.c b/src/mono/wasm/runtime/driver.c index 6e0d65f7e98245..cf26afb292ed05 100644 --- a/src/mono/wasm/runtime/driver.c +++ b/src/mono/wasm/runtime/driver.c @@ -689,19 +689,19 @@ EMSCRIPTEN_KEEPALIVE MonoObject* mono_wasm_invoke_method_bound (MonoMethod *method, void* args)// JSMarshalerArguments { MonoObject *exc = NULL; - MonoObject *res; + MonoObject *res = NULL; void *invoke_args[1] = { args }; - + MONO_ENTER_GC_UNSAFE; mono_runtime_invoke (method, NULL, invoke_args, &exc); if (exc) { MonoObject *exc2 = NULL; res = (MonoObject*)mono_object_to_string (exc, &exc2); if (exc2) res = (MonoObject*) mono_string_new (root_domain, "Exception Double Fault"); - return res; } - return NULL; + MONO_EXIT_GC_UNSAFE; + return res; } EMSCRIPTEN_KEEPALIVE MonoMethod* diff --git a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js index 3f5fcabf626565..ded8f070602739 100644 --- a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js +++ b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js @@ -118,7 +118,7 @@ let linked_functions = [ #if USE_PTHREADS linked_functions = [...linked_functions, - /// mono-threads-wasm.c + // mono-threads-wasm.c "mono_wasm_pthread_on_pthread_attached", // threads.c "mono_wasm_eventloop_has_unsettled_interop_promises", @@ -126,6 +126,9 @@ linked_functions = [...linked_functions, "mono_wasm_diagnostic_server_on_server_thread_created", "mono_wasm_diagnostic_server_on_runtime_server_init", "mono_wasm_diagnostic_server_stream_signal_work_available", + // corebindings.c + "mono_wasm_install_js_worker_interop", + "mono_wasm_uninstall_js_worker_interop", ] #endif if (!DISABLE_LEGACY_JS_INTEROP) { diff --git a/src/mono/wasm/runtime/exports-linker.ts b/src/mono/wasm/runtime/exports-linker.ts index b56fe3684fcc06..0bd8496e6a8061 100644 --- a/src/mono/wasm/runtime/exports-linker.ts +++ b/src/mono/wasm/runtime/exports-linker.ts @@ -20,15 +20,17 @@ import { mono_wasm_diagnostic_server_on_runtime_server_init, mono_wasm_event_pip import { mono_wasm_diagnostic_server_stream_signal_work_available } from "./diagnostics/server_pthread/stream-queue"; import { mono_wasm_trace_logger } from "./logging"; import { mono_wasm_profiler_leave, mono_wasm_profiler_enter } from "./profiler"; -import { mono_wasm_create_cs_owned_object_ref } from "./net6-legacy/cs-to-js"; -import { mono_wasm_typed_array_to_array_ref } from "./net6-legacy/js-to-cs"; -import { mono_wasm_typed_array_from_ref } from "./net6-legacy/buffers"; +import { mono_wasm_change_case, mono_wasm_change_case_invariant } from "./hybrid-globalization/change-case"; +import { mono_wasm_compare_string, mono_wasm_ends_with, mono_wasm_starts_with, mono_wasm_index_of } from "./hybrid-globalization/collations"; +import { mono_wasm_install_js_worker_interop, mono_wasm_uninstall_js_worker_interop } from "./pthreads/shared"; + import { mono_wasm_invoke_js_blazor, mono_wasm_invoke_js_with_args_ref, mono_wasm_get_object_property_ref, mono_wasm_set_object_property_ref, mono_wasm_get_by_index_ref, mono_wasm_set_by_index_ref, mono_wasm_get_global_object_ref } from "./net6-legacy/method-calls"; -import { mono_wasm_change_case, mono_wasm_change_case_invariant } from "./hybrid-globalization/change-case"; -import { mono_wasm_compare_string, mono_wasm_ends_with, mono_wasm_starts_with, mono_wasm_index_of } from "./hybrid-globalization/collations"; +import { mono_wasm_create_cs_owned_object_ref } from "./net6-legacy/cs-to-js"; +import { mono_wasm_typed_array_to_array_ref } from "./net6-legacy/js-to-cs"; +import { mono_wasm_typed_array_from_ref } from "./net6-legacy/buffers"; // the methods would be visible to EMCC linker // --- keep in sync with dotnet.cjs.lib.js --- @@ -41,6 +43,10 @@ const mono_wasm_threads_exports = !MonoWasmThreads ? undefined : { mono_wasm_diagnostic_server_on_server_thread_created, mono_wasm_diagnostic_server_on_runtime_server_init, mono_wasm_diagnostic_server_stream_signal_work_available, + + // corebindings.c + mono_wasm_install_js_worker_interop, + mono_wasm_uninstall_js_worker_interop, }; const mono_wasm_legacy_interop_exports = !WasmEnableLegacyJsInterop ? undefined : { diff --git a/src/mono/wasm/runtime/gc-handles.ts b/src/mono/wasm/runtime/gc-handles.ts index a4e8117f73a446..b5b6382ab27adb 100644 --- a/src/mono/wasm/runtime/gc-handles.ts +++ b/src/mono/wasm/runtime/gc-handles.ts @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import { runtimeHelpers } from "./globals"; +import { mono_log_warn } from "./logging"; import { GCHandle, GCHandleNull, JSHandle, JSHandleDisposed, JSHandleNull } from "./types/internal"; import { create_weak_ref } from "./weak-ref"; @@ -128,3 +129,29 @@ export function _lookup_js_owned_object(gc_handle: GCHandle): any { return null; } +export function forceDisposeProxies(dump: boolean): void { + // dispose all proxies to C# objects + const gchandles = [..._js_owned_object_table.keys()]; + for (const gchandle of gchandles) { + const wr = _js_owned_object_table.get(gchandle); + const obj = wr.deref(); + if (obj) { + if (dump) { + mono_log_warn(`Proxy of C# object with GCHandle ${gchandle} was still alive`); + } + teardown_managed_proxy(obj, gchandle); + } + } + // TODO: call C# to iterate and release all in JSHostImplementation.ThreadCsOwnedObjects + + // dispose all proxies to JS objects + for (const js_obj of _cs_owned_objects_by_js_handle) { + if (js_obj) { + const js_handle = js_obj[cs_owned_js_handle_symbol]; + if (js_handle) { + mono_log_warn(`Proxy of JS object with JSHandleandle ${js_handle} was still alive`); + mono_wasm_release_cs_owned_object(js_handle); + } + } + } +} \ No newline at end of file diff --git a/src/mono/wasm/runtime/invoke-cs.ts b/src/mono/wasm/runtime/invoke-cs.ts index c17987ebbf976d..c136d1bbf2f68f 100644 --- a/src/mono/wasm/runtime/invoke-cs.ts +++ b/src/mono/wasm/runtime/invoke-cs.ts @@ -13,17 +13,17 @@ import { } from "./marshal"; import { mono_wasm_new_external_root, mono_wasm_new_root } from "./roots"; import { monoStringToString, monoStringToStringUnsafe } from "./strings"; -import { MonoObjectRef, MonoStringRef, MonoString, MonoObject, MonoMethod, JSMarshalerArguments, JSFunctionSignature, BoundMarshalerToCs, BoundMarshalerToJs, VoidPtrNull, MonoObjectRefNull, MonoObjectNull } from "./types/internal"; +import { MonoObjectRef, MonoStringRef, MonoString, MonoObject, MonoMethod, JSMarshalerArguments, JSFunctionSignature, BoundMarshalerToCs, BoundMarshalerToJs, VoidPtrNull, MonoObjectRefNull, MonoObjectNull, MarshalerType } from "./types/internal"; import { Int32Ptr } from "./types/emscripten"; import cwraps from "./cwraps"; import { assembly_load } from "./class-loader"; -import { wrap_error_root, wrap_no_error_root } from "./invoke-js"; +import { assert_bindings, wrap_error_root, wrap_no_error_root } from "./invoke-js"; import { startMeasure, MeasuredBlock, endMeasure } from "./profiler"; import { mono_log_debug } from "./logging"; import { assert_synchronization_context } from "./pthreads/shared"; export function mono_wasm_bind_cs_function(fully_qualified_name: MonoStringRef, signature_hash: number, signature: JSFunctionSignature, is_exception: Int32Ptr, result_address: MonoObjectRef): void { - assert_synchronization_context(); + assert_bindings(); const fqn_root = mono_wasm_new_external_root(fully_qualified_name), resultRoot = mono_wasm_new_external_root(result_address); const mark = startMeasure(); try { @@ -55,6 +55,9 @@ export function mono_wasm_bind_cs_function(fully_qualified_name: MonoStringRef, for (let index = 0; index < args_count; index++) { const sig = get_sig(signature, index + 2); const marshaler_type = get_signature_type(sig); + if (marshaler_type == MarshalerType.Task) { + assert_synchronization_context(); + } const arg_marshaler = bind_arg_marshal_to_cs(sig, marshaler_type, index + 2); mono_assert(arg_marshaler, "ERR43: argument marshaler must be resolved"); arg_marshalers[index] = arg_marshaler; @@ -62,6 +65,9 @@ export function mono_wasm_bind_cs_function(fully_qualified_name: MonoStringRef, const res_sig = get_sig(signature, 1); const res_marshaler_type = get_signature_type(res_sig); + if (res_marshaler_type == MarshalerType.Task) { + assert_synchronization_context(); + } const res_converter = bind_arg_marshal_to_js(res_sig, res_marshaler_type, 1); const closure: BindingClosure = { @@ -244,7 +250,7 @@ type BindingClosure = { } export function invoke_method_and_handle_exception(method: MonoMethod, args: JSMarshalerArguments): void { - assert_synchronization_context(); + assert_bindings(); const fail = cwraps.mono_wasm_invoke_method_bound(method, args); if (fail) throw new Error("ERR24: Unexpected error: " + monoStringToStringUnsafe(fail)); if (is_args_exception(args)) { @@ -284,7 +290,7 @@ function _walk_exports_to_set_function(assembly: string, namespace: string, clas } export async function mono_wasm_get_assembly_exports(assembly: string): Promise { - mono_assert(runtimeHelpers.mono_wasm_bindings_is_ready, "The runtime must be initialized."); + assert_bindings(); const result = exportsByAssembly.get(assembly); if (!result) { const mark = startMeasure(); diff --git a/src/mono/wasm/runtime/invoke-js.ts b/src/mono/wasm/runtime/invoke-js.ts index e1caf2475d754a..f2ce876af5fb6c 100644 --- a/src/mono/wasm/runtime/invoke-js.ts +++ b/src/mono/wasm/runtime/invoke-js.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 BuildConfiguration from "consts:configuration"; import { marshal_exception_to_cs, bind_arg_marshal_to_cs } from "./marshal-to-cs"; @@ -9,7 +10,7 @@ import { setI32, setI32_unchecked, receiveWorkerHeapViews } from "./memory"; import { monoStringToString, stringToMonoStringRoot } from "./strings"; import { MonoObject, MonoObjectRef, MonoString, MonoStringRef, JSFunctionSignature, JSMarshalerArguments, WasmRoot, BoundMarshalerToJs, JSFnHandle, BoundMarshalerToCs, JSHandle, MarshalerType } from "./types/internal"; import { Int32Ptr } from "./types/emscripten"; -import { INTERNAL, Module } from "./globals"; +import { INTERNAL, Module, runtimeHelpers } from "./globals"; import { bind_arg_marshal_to_js } from "./marshal-to-js"; import { mono_wasm_new_external_root } from "./roots"; import { mono_log_debug, mono_wasm_symbolicate_string } from "./logging"; @@ -21,7 +22,7 @@ import { assert_synchronization_context } from "./pthreads/shared"; const fn_wrapper_by_fn_handle: Function[] = [null];// 0th slot is dummy, we never free bound functions export function mono_wasm_bind_js_function(function_name: MonoStringRef, module_name: MonoStringRef, signature: JSFunctionSignature, function_js_handle: Int32Ptr, is_exception: Int32Ptr, result_address: MonoObjectRef): void { - assert_synchronization_context(); + assert_bindings(); const function_name_root = mono_wasm_new_external_root(function_name), module_name_root = mono_wasm_new_external_root(module_name), resultRoot = mono_wasm_new_external_root(result_address); @@ -54,9 +55,15 @@ export function mono_wasm_bind_js_function(function_name: MonoStringRef, module_ }; has_cleanup = true; } + else if (marshaler_type == MarshalerType.Task) { + assert_synchronization_context(); + } } const res_sig = get_sig(signature, 1); const res_marshaler_type = get_signature_type(res_sig); + if (res_marshaler_type == MarshalerType.Task) { + assert_synchronization_context(); + } const res_converter = bind_arg_marshal_to_cs(res_sig, res_marshaler_type, 1); const closure: BindingClosure = { @@ -383,3 +390,11 @@ export function wrap_no_error_root(is_exception: Int32Ptr | null, result?: WasmR result.clear(); } } + +export function assert_bindings(): void { + if (MonoWasmThreads) { + mono_assert(runtimeHelpers.mono_wasm_bindings_is_ready, "Please use dedicated worker for working with JavaScript interop. See https://github.com/dotnet/runtime/blob/main/src/mono/wasm/threads.md#JS-interop-on-dedicated-threads"); + } else { + mono_assert(runtimeHelpers.mono_wasm_bindings_is_ready, "The runtime must be initialized."); + } +} diff --git a/src/mono/wasm/runtime/logging.ts b/src/mono/wasm/runtime/logging.ts index 305fe6a9e22bab..ad1da8788e3961 100644 --- a/src/mono/wasm/runtime/logging.ts +++ b/src/mono/wasm/runtime/logging.ts @@ -6,7 +6,11 @@ import { INTERNAL, runtimeHelpers } from "./globals"; import { utf8ToString } from "./strings"; import { CharPtr, VoidPtr } from "./types/emscripten"; -const prefix = "MONO_WASM: "; +let prefix = "MONO_WASM: "; + +export function mono_set_thread_id(tid: string) { + prefix = `MONO_WASM [${tid}]: `; +} export function mono_log_debug(msg: string, ...data: any) { if (runtimeHelpers.diagnosticTracing) { diff --git a/src/mono/wasm/runtime/net6-legacy/cs-to-js.ts b/src/mono/wasm/runtime/net6-legacy/cs-to-js.ts index 009aba9fb196f7..f93a3d13c53fd5 100644 --- a/src/mono/wasm/runtime/net6-legacy/cs-to-js.ts +++ b/src/mono/wasm/runtime/net6-legacy/cs-to-js.ts @@ -14,9 +14,8 @@ import { monoStringToString, monoStringToStringUnsafe } from "../strings"; import { legacyManagedExports } from "./corebindings"; import { legacyHelpers } from "./globals"; import { js_to_mono_obj_root } from "./js-to-cs"; -import { mono_bind_method, mono_method_get_call_signature_ref } from "./method-binding"; +import { assert_legacy_interop, mono_bind_method, mono_method_get_call_signature_ref } from "./method-binding"; import { createPromiseController } from "../globals"; -import { assert_legacy_interop } from "../pthreads/shared"; const delegate_invoke_symbol = Symbol.for("wasm delegate_invoke"); diff --git a/src/mono/wasm/runtime/net6-legacy/js-to-cs.ts b/src/mono/wasm/runtime/net6-legacy/js-to-cs.ts index 83df9bd39b97d2..9e9f5d9bb7c028 100644 --- a/src/mono/wasm/runtime/net6-legacy/js-to-cs.ts +++ b/src/mono/wasm/runtime/net6-legacy/js-to-cs.ts @@ -15,7 +15,7 @@ import { has_backing_array_buffer } from "./buffers"; import { legacyManagedExports } from "./corebindings"; import { get_js_owned_object_by_gc_handle_ref } from "./cs-to-js"; import { legacyHelpers, wasm_type_symbol } from "./globals"; -import { assert_legacy_interop } from "../pthreads/shared"; +import { assert_legacy_interop } from "./method-binding"; export function _js_to_mono_uri_root(should_add_in_flight: boolean, js_obj: any, result: WasmRoot): void { switch (true) { diff --git a/src/mono/wasm/runtime/net6-legacy/method-binding.ts b/src/mono/wasm/runtime/net6-legacy/method-binding.ts index bd1ded599d71d3..5764e9afa1161d 100644 --- a/src/mono/wasm/runtime/net6-legacy/method-binding.ts +++ b/src/mono/wasm/runtime/net6-legacy/method-binding.ts @@ -1,8 +1,10 @@ // 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 { legacy_c_functions as cwraps } from "../cwraps"; -import { Module } from "../globals"; +import { ENVIRONMENT_IS_PTHREAD, Module } from "../globals"; import { parseFQN } from "../invoke-cs"; import { setI32, setU32, setF32, setF64, setU52, setI52, setB32, setI32_unchecked, setU32_unchecked, _zero_region, _create_temp_frame, getB32, getI32, getU32, getF32, getF64 } from "../memory"; import { mono_wasm_new_external_root, mono_wasm_new_root } from "../roots"; @@ -15,7 +17,7 @@ import { legacyHelpers } from "./globals"; import { js_to_mono_obj_root, _js_to_mono_uri_root, js_to_mono_enum } from "./js-to-cs"; import { _teardown_after_call } from "./method-calls"; import { mono_log_warn } from "../logging"; -import { assert_legacy_interop } from "../pthreads/shared"; +import { assert_bindings } from "../invoke-js"; const escapeRE = /[^A-Za-z0-9_$]/g; @@ -671,3 +673,10 @@ export function mono_method_resolve(fqn: string): MonoMethod { export function mono_method_get_call_signature_ref(method: MonoMethod, mono_obj?: WasmRoot): string/*ArgsMarshalString*/ { return legacyManagedExports._get_call_sig_ref(method, mono_obj ? mono_obj.address : legacyHelpers._null_root.address); } + +export function assert_legacy_interop(): void { + if (MonoWasmThreads) { + mono_assert(!ENVIRONMENT_IS_PTHREAD, "Legacy interop is not supported with WebAssembly threads."); + } + assert_bindings(); +} \ No newline at end of file diff --git a/src/mono/wasm/runtime/net6-legacy/method-calls.ts b/src/mono/wasm/runtime/net6-legacy/method-calls.ts index e69883bd8f67e1..753c5851ab3d18 100644 --- a/src/mono/wasm/runtime/net6-legacy/method-calls.ts +++ b/src/mono/wasm/runtime/net6-legacy/method-calls.ts @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import { get_js_obj, mono_wasm_get_jsobj_from_js_handle } from "../gc-handles"; -import { Module, runtimeHelpers, INTERNAL } from "../globals"; +import { Module, INTERNAL } from "../globals"; import { wrap_error_root, wrap_no_error_root } from "../invoke-js"; import { _release_temp_frame } from "../memory"; import { mono_wasm_new_external_root, mono_wasm_new_root } from "../roots"; @@ -12,8 +12,7 @@ import { JSHandle, MonoStringRef, MonoObjectRef, MonoArray, MonoString, MonoObje import { Int32Ptr, VoidPtr } from "../types/emscripten"; import { mono_array_root_to_js_array, unbox_mono_obj_root } from "./cs-to-js"; import { js_array_to_mono_array, js_to_mono_obj_root } from "./js-to-cs"; -import { Converter, BoundMethodToken, mono_method_resolve, mono_method_get_call_signature_ref, mono_bind_method } from "./method-binding"; -import { assert_legacy_interop } from "../pthreads/shared"; +import { Converter, BoundMethodToken, mono_method_resolve, mono_method_get_call_signature_ref, mono_bind_method, assert_legacy_interop } from "./method-binding"; const boundMethodsByFqn: Map = new Map(); @@ -52,7 +51,6 @@ export function _teardown_after_call( } export function mono_bind_static_method(fqn: string, signature?: string/*ArgsMarshalString*/): Function { - mono_assert(runtimeHelpers.mono_wasm_bindings_is_ready, "The runtime must be initialized."); assert_legacy_interop(); const key = `${fqn}-${signature}`; @@ -85,7 +83,6 @@ export function mono_bind_assembly_entry_point(assembly: string, signature?: str } export function mono_call_assembly_entry_point(assembly: string, args?: any[], signature?: string/*ArgsMarshalString*/): number { - mono_assert(runtimeHelpers.mono_wasm_bindings_is_ready, "The runtime must be initialized."); assert_legacy_interop(); if (!args) { args = [[]]; diff --git a/src/mono/wasm/runtime/net6-legacy/strings.ts b/src/mono/wasm/runtime/net6-legacy/strings.ts index ba01a2691de553..e30324ce08ee96 100644 --- a/src/mono/wasm/runtime/net6-legacy/strings.ts +++ b/src/mono/wasm/runtime/net6-legacy/strings.ts @@ -1,10 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { assert_legacy_interop } from "../pthreads/shared"; import { mono_wasm_new_root } from "../roots"; import { interned_string_table, mono_wasm_empty_string, stringToInternedMonoStringRoot, stringToMonoStringRoot } from "../strings"; import { MonoString, is_nullish } from "../types/internal"; +import { assert_legacy_interop } from "./method-binding"; /** * @deprecated Not GC or thread safe diff --git a/src/mono/wasm/runtime/pthreads/shared/index.ts b/src/mono/wasm/runtime/pthreads/shared/index.ts index 6fa9789bd68654..ce188a7e673bbc 100644 --- a/src/mono/wasm/runtime/pthreads/shared/index.ts +++ b/src/mono/wasm/runtime/pthreads/shared/index.ts @@ -3,9 +3,12 @@ import MonoWasmThreads from "consts:monoWasmThreads"; -import { ENVIRONMENT_IS_PTHREAD, Module, runtimeHelpers } from "../../globals"; +import { Module, runtimeHelpers } from "../../globals"; import { MonoConfig } from "../../types"; import { pthreadPtr } from "./types"; +import { mono_log_debug } from "../../logging"; +import { bindings_init } from "../../startup"; +import { forceDisposeProxies } from "../../gc-handles"; export interface PThreadInfo { readonly pthreadId: pthreadPtr; @@ -131,23 +134,38 @@ export function isMonoWorkerMessagePreload(message: MonoWorkerMessage { + Module.runtimeKeepalivePop(); + }, 0); } + + worker_js_synchronization_context_installed = false; + runtimeHelpers.mono_wasm_bindings_is_ready = false; } -export function assert_legacy_interop(): void { +export function assert_synchronization_context(): void { if (MonoWasmThreads) { - mono_assert(!ENVIRONMENT_IS_PTHREAD, "Legacy interop is not supported with WebAssembly threads."); + mono_assert(worker_js_synchronization_context_installed, "Please use dedicated worker for working with JavaScript interop. See https://github.com/dotnet/runtime/blob/main/src/mono/wasm/threads.md#JS-interop-on-dedicated-threads"); } } - diff --git a/src/mono/wasm/runtime/pthreads/worker/index.ts b/src/mono/wasm/runtime/pthreads/worker/index.ts index 2d44436507d092..090a468db82026 100644 --- a/src/mono/wasm/runtime/pthreads/worker/index.ts +++ b/src/mono/wasm/runtime/pthreads/worker/index.ts @@ -18,6 +18,7 @@ import { } from "./events"; import { preRunWorker } from "../../startup"; import { mono_log_debug } from "../../logging"; +import { mono_set_thread_id } from "../../logging"; // re-export some of the events types export { @@ -78,10 +79,11 @@ function setupChannelToMainThread(pthread_ptr: pthreadPtr): PThreadSelf { /// This is an implementation detail function. -/// Called in the worker thread from mono when a pthread becomes attached to the mono runtime. -export function mono_wasm_pthread_on_pthread_attached(pthread_id: pthreadPtr): void { +/// Called in the worker thread (not main thread) from mono when a pthread becomes attached to the mono runtime. +export function mono_wasm_pthread_on_pthread_attached(pthread_id: number): void { const self = pthread_self; mono_assert(self !== null && self.pthreadId == pthread_id, "expected pthread_self to be set already when attaching"); + mono_set_thread_id("0x" + pthread_id.toString(16)); mono_log_debug("attaching pthread to runtime 0x" + pthread_id.toString(16)); preRunWorker(); currentWorkerThreadEvents.dispatchEvent(makeWorkerThreadEvent(dotnetPthreadAttached, self)); diff --git a/src/mono/wasm/runtime/run.ts b/src/mono/wasm/runtime/run.ts index 5839b4538e8680..3c8e46df5d80f1 100644 --- a/src/mono/wasm/runtime/run.ts +++ b/src/mono/wasm/runtime/run.ts @@ -7,6 +7,7 @@ import { mono_wasm_set_main_args } from "./startup"; import cwraps from "./cwraps"; import { assembly_load } from "./class-loader"; import { mono_log_info } from "./logging"; +import { assert_bindings } from "./invoke-js"; /** * Possible signatures are described here https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/main-command-line @@ -39,7 +40,7 @@ export async function mono_run_main(main_assembly_name: string, args: string[]): } export function find_entry_point(assembly: string) { - mono_assert(runtimeHelpers.mono_wasm_bindings_is_ready, "The runtime must be initialized."); + assert_bindings(); const asm = assembly_load(assembly); if (!asm) throw new Error("Could not find assembly: " + assembly); diff --git a/src/mono/wasm/runtime/scheduling.ts b/src/mono/wasm/runtime/scheduling.ts index 7b0755b3148e2b..b2c2190d57dd38 100644 --- a/src/mono/wasm/runtime/scheduling.ts +++ b/src/mono/wasm/runtime/scheduling.ts @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import cwraps from "./cwraps"; +import { Module } from "./globals"; let spread_timers_maximum = 0; export let isChromium = false; @@ -50,7 +51,7 @@ function mono_background_exec_until_done() { export function schedule_background_exec(): void { ++pump_count; - globalThis.setTimeout(mono_background_exec_until_done, 0); + Module.safeSetTimeout(mono_background_exec_until_done, 0); } let lastScheduledTimeoutId: any = undefined; @@ -58,10 +59,13 @@ export function mono_wasm_schedule_timer(shortestDueTimeMs: number): void { if (lastScheduledTimeoutId) { globalThis.clearTimeout(lastScheduledTimeoutId); lastScheduledTimeoutId = undefined; + // NOTE: Module.safeSetTimeout() does the runtimeKeepalivePush() but clearTimeout is asymmetric. + Module.runtimeKeepalivePop(); } - lastScheduledTimeoutId = globalThis.setTimeout(mono_wasm_schedule_timer_tick, shortestDueTimeMs); + lastScheduledTimeoutId = Module.safeSetTimeout(mono_wasm_schedule_timer_tick, shortestDueTimeMs); } function mono_wasm_schedule_timer_tick() { + lastScheduledTimeoutId = undefined; cwraps.mono_wasm_execute_timer(); -} +} \ No newline at end of file diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index bbb7816c94eb73..e9eee44f0de192 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -24,16 +24,15 @@ import { preAllocatePThreadWorkerPool, instantiateWasmPThreadWorkerPool } from " import { export_linker } from "./exports-linker"; import { endMeasure, MeasuredBlock, startMeasure } from "./profiler"; import { getMemorySnapshot, storeMemorySnapshot, getMemorySnapshotSize } from "./snapshot"; +import { mono_log_debug, mono_log_warn, mono_set_thread_id } from "./logging"; +import { getBrowserThreadID } from "./pthreads/shared"; // legacy import { init_legacy_exports } from "./net6-legacy/corebindings"; import { cwraps_binding_api, cwraps_mono_api } from "./net6-legacy/exports-legacy"; import { BINDING, MONO } from "./net6-legacy/globals"; -import { mono_log_debug, mono_log_warn } from "./logging"; -import { install_synchronization_context } from "./pthreads/shared"; import { localHeapViewU8 } from "./memory"; - // default size if MonoConfig.pthreadPoolSize is undefined const MONO_PTHREAD_POOL_SIZE = 4; @@ -197,14 +196,6 @@ async function preInitWorkerAsync() { } export function preRunWorker() { - const mark = startMeasure(); - try { - bindings_init(); - endMeasure(mark, MeasuredBlock.preRunWorker); - } catch (err) { - loaderHelpers.abort_startup(err, true); - throw err; - } // signal next stage runtimeHelpers.afterPreRun.promise_control.resolve(); } @@ -265,10 +256,16 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { // we could enable diagnostics after the snapshot is taken await mono_wasm_init_diagnostics(); } + const tid = getBrowserThreadID(); + mono_set_thread_id(`0x${tid.toString(16)}-main`); await instantiateWasmPThreadWorkerPool(); } bindings_init(); + if (MonoWasmThreads) { + runtimeHelpers.javaScriptExports.install_synchronization_context(); + } + if (!runtimeHelpers.mono_wasm_runtime_is_ready) mono_wasm_runtime_ready(); if (runtimeHelpers.config.startupOptions && INTERNAL.resourceLoader) { @@ -587,9 +584,6 @@ export function bindings_init(): void { if (WasmEnableLegacyJsInterop && !disableLegacyJsInterop && !ENVIRONMENT_IS_PTHREAD) { init_legacy_exports(); } - if (MonoWasmThreads && !ENVIRONMENT_IS_PTHREAD) { - install_synchronization_context(); - } initialize_marshalers_to_js(); initialize_marshalers_to_cs(); runtimeHelpers._i52_error_scratch_buffer = Module._malloc(4); diff --git a/src/mono/wasm/runtime/types/internal.ts b/src/mono/wasm/runtime/types/internal.ts index f9a16235228ca2..64057d7a3ce973 100644 --- a/src/mono/wasm/runtime/types/internal.ts +++ b/src/mono/wasm/runtime/types/internal.ts @@ -413,6 +413,9 @@ export declare interface EmscriptenModuleInternal { removeRunDependency(id: string): void; addRunDependency(id: string): void; onConfigLoaded?: (config: MonoConfig, api: RuntimeAPI) => void | Promise; + safeSetTimeout(func: Function, timeout: number): number; + runtimeKeepalivePush(): void; + runtimeKeepalivePop(): void; } /// A PromiseController encapsulates a Promise together with easy access to its resolve and reject functions. diff --git a/src/mono/wasm/threads.md b/src/mono/wasm/threads.md index 697c8c2dc4bb3d..bf75f46137b6aa 100644 --- a/src/mono/wasm/threads.md +++ b/src/mono/wasm/threads.md @@ -90,4 +90,11 @@ a worker thread will use `async_run_in_main_thread` to queue up work for the mai To run the debugger tests in the runtime [built with enabled support for multi-threading](#building-the-runtime) we use: ``` dotnet test src/mono/wasm/debugger/DebuggerTestSuite -e RuntimeConfiguration=Debug -e Configuration=Debug -e DebuggerHost=chrome -e WasmEnableThreads=true -e WASM_TESTS_USING_VARIANT=multithreaded -``` \ No newline at end of file +``` + +## JS interop on dedicated threads ## +FIXME: better documentation, better public API. +The JavaScript objects have thread (web worker) affinity. You can't use DOM, WebSocket or their promises on any other web worker than the original one. +Therefore we have JSSynchronizationContext which is helping the user code to stay on that thread. Instead of finishing the `await` continuation on any threadpool thread. +Because browser events (for example incoming web socket message) could be fired after any synchronous code of the thread finished, we have to treat threads (web workers) which want to do JS interop as un-managed resource. It's lifetime should be managed by the user. +As we are prototyping it, we have [WebWorker](..\..\libraries\System.Runtime.InteropServices.JavaScript\src\System\Runtime\InteropServices\JavaScript\WebWorker.cs) as tentative API which should be used to start such dedicated threads. diff --git a/src/mono/wasm/wasm.proj b/src/mono/wasm/wasm.proj index 771208e5133d65..f0f606aa52e8bf 100644 --- a/src/mono/wasm/wasm.proj +++ b/src/mono/wasm/wasm.proj @@ -199,6 +199,9 @@ + + + @@ -260,6 +263,9 @@ + + + <_EmccExportedLibraryFunction>"[@(EmccExportedLibraryFunction -> '%27%(Identity)%27', ',')]" <_EmccExportedRuntimeMethods>"[@(EmccExportedRuntimeMethod -> '%27%(Identity)%27', ',')]" diff --git a/src/native/libs/System.Native/pal_time.c b/src/native/libs/System.Native/pal_time.c index 51b463c76a605d..a249fe653be1c0 100644 --- a/src/native/libs/System.Native/pal_time.c +++ b/src/native/libs/System.Native/pal_time.c @@ -121,7 +121,7 @@ int64_t SystemNative_GetBootTimeTicks(void) double SystemNative_GetCpuUtilization(ProcessCpuInformation* previousCpuInfo) { -#ifdef HAVE_GETRUSAGE +#if defined(HAVE_GETRUSAGE) && !defined(HOST_BROWSER) uint64_t kernelTime = 0; uint64_t userTime = 0; From ca4a03aa78e2a5a742ac9794d330a5553f5985e8 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 6 Jun 2023 20:28:43 +0200 Subject: [PATCH 02/15] wip --- src/mono/mono/utils/mono-threads-wasm.c | 17 +++++++++++++++++ src/mono/mono/utils/mono-threads-wasm.h | 3 +++ src/mono/mono/utils/mono-threads.c | 4 ++++ src/mono/wasm/runtime/es6/dotnet.es6.lib.js | 1 + src/mono/wasm/runtime/exports-linker.ts | 3 ++- src/mono/wasm/runtime/pthreads/shared/index.ts | 12 ++++++++++++ src/mono/wasm/runtime/pthreads/worker/index.ts | 13 +++++++++++-- src/mono/wasm/runtime/scheduling.ts | 1 + src/mono/wasm/runtime/types/internal.ts | 1 + src/mono/wasm/wasm.proj | 1 + 10 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/mono/mono/utils/mono-threads-wasm.c b/src/mono/mono/utils/mono-threads-wasm.c index efc02389d3ecc7..1438defba4b112 100644 --- a/src/mono/mono/utils/mono-threads-wasm.c +++ b/src/mono/mono/utils/mono-threads-wasm.c @@ -481,6 +481,7 @@ mono_threads_wasm_browser_thread_tid (void) #ifndef DISABLE_THREADS extern void mono_wasm_pthread_on_pthread_attached (MonoNativeThreadId pthread_id); +extern void mono_wasm_pthread_on_pthread_detached (MonoNativeThreadId pthread_id); #endif void @@ -500,6 +501,22 @@ mono_threads_wasm_on_thread_attached (void) #endif } +void +mono_threads_wasm_on_thread_detached (void) +{ +#ifdef DISABLE_THREADS + return; +#else + if (mono_threads_wasm_is_browser_thread ()) { + return; + } + // Notify JS that the pthread attachd to Mono + pthread_t id = pthread_self (); + + mono_wasm_pthread_on_pthread_detached (id); +#endif +} + #ifndef DISABLE_THREADS void mono_threads_wasm_async_run_in_main_thread (void (*func) (void)) diff --git a/src/mono/mono/utils/mono-threads-wasm.h b/src/mono/mono/utils/mono-threads-wasm.h index 95f8e6392906b8..38b63118c4acc8 100644 --- a/src/mono/mono/utils/mono-threads-wasm.h +++ b/src/mono/mono/utils/mono-threads-wasm.h @@ -75,6 +75,9 @@ extern GSList *jobs; void mono_threads_wasm_on_thread_attached (void); +void +mono_threads_wasm_on_thread_detached (void); + #endif /* HOST_WASM*/ #endif /* __MONO_THREADS_WASM_H__ */ diff --git a/src/mono/mono/utils/mono-threads.c b/src/mono/mono/utils/mono-threads.c index b364d9467b1d52..e453030827c44c 100644 --- a/src/mono/mono/utils/mono-threads.c +++ b/src/mono/mono/utils/mono-threads.c @@ -648,6 +648,10 @@ unregister_thread (void *arg) mono_thread_info_suspend_unlock (); +#ifdef HOST_BROWSER + mono_threads_wasm_on_thread_detached (); +#endif + g_byte_array_free (info->stackdata, /*free_segment=*/TRUE); /*now it's safe to free the thread info.*/ diff --git a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js index ded8f070602739..66477cc88d8deb 100644 --- a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js +++ b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js @@ -120,6 +120,7 @@ let linked_functions = [ linked_functions = [...linked_functions, // mono-threads-wasm.c "mono_wasm_pthread_on_pthread_attached", + "mono_wasm_pthread_on_pthread_detached", // threads.c "mono_wasm_eventloop_has_unsettled_interop_promises", // diagnostics_server.c diff --git a/src/mono/wasm/runtime/exports-linker.ts b/src/mono/wasm/runtime/exports-linker.ts index 0bd8496e6a8061..857c62026adfdc 100644 --- a/src/mono/wasm/runtime/exports-linker.ts +++ b/src/mono/wasm/runtime/exports-linker.ts @@ -12,7 +12,7 @@ import { mono_interp_jit_wasm_entry_trampoline, mono_interp_record_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_wasm_pthread_on_pthread_attached, mono_wasm_pthread_on_pthread_detached } from "./pthreads/worker"; import { mono_wasm_schedule_timer, schedule_background_exec } from "./scheduling"; import { mono_wasm_asm_loaded } from "./startup"; import { mono_wasm_diagnostic_server_on_server_thread_created } from "./diagnostics/server_pthread"; @@ -37,6 +37,7 @@ import { mono_wasm_typed_array_from_ref } from "./net6-legacy/buffers"; const mono_wasm_threads_exports = !MonoWasmThreads ? undefined : { // mono-threads-wasm.c mono_wasm_pthread_on_pthread_attached, + mono_wasm_pthread_on_pthread_detached, // threads.c mono_wasm_eventloop_has_unsettled_interop_promises, // diagnostics_server.c diff --git a/src/mono/wasm/runtime/pthreads/shared/index.ts b/src/mono/wasm/runtime/pthreads/shared/index.ts index ce188a7e673bbc..673f55af4fbb0c 100644 --- a/src/mono/wasm/runtime/pthreads/shared/index.ts +++ b/src/mono/wasm/runtime/pthreads/shared/index.ts @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import MonoWasmThreads from "consts:monoWasmThreads"; +import BuildConfiguration from "consts:configuration"; import { Module, runtimeHelpers } from "../../globals"; import { MonoConfig } from "../../types"; @@ -9,6 +10,7 @@ import { pthreadPtr } from "./types"; import { mono_log_debug } from "../../logging"; import { bindings_init } from "../../startup"; import { forceDisposeProxies } from "../../gc-handles"; +import { pthread_self } from "../worker"; export interface PThreadInfo { readonly pthreadId: pthreadPtr; @@ -146,6 +148,8 @@ export function mono_wasm_install_js_worker_interop(install_js_synchronization_c if (install_js_synchronization_context) { Module.runtimeKeepalivePush(); } + + set_thread_info(pthread_self ? pthread_self.pthreadId : 0, true, true, !!install_js_synchronization_context); } export function mono_wasm_uninstall_js_worker_interop(uninstall_js_synchronization_context: number): void { @@ -162,6 +166,7 @@ export function mono_wasm_uninstall_js_worker_interop(uninstall_js_synchronizati worker_js_synchronization_context_installed = false; runtimeHelpers.mono_wasm_bindings_is_ready = false; + set_thread_info(pthread_self ? pthread_self.pthreadId : 0, true, false, false); } export function assert_synchronization_context(): void { @@ -169,3 +174,10 @@ export function assert_synchronization_context(): void { mono_assert(worker_js_synchronization_context_installed, "Please use dedicated worker for working with JavaScript interop. See https://github.com/dotnet/runtime/blob/main/src/mono/wasm/threads.md#JS-interop-on-dedicated-threads"); } } + +// this is just for Debug build of the runtime, making it easier to debug worker threads +export function set_thread_info(pthread_ptr: number, isAttached: boolean, hasInterop: boolean, hasSynchronization: boolean): void { + if (MonoWasmThreads && BuildConfiguration === "Debug") { + (globalThis as any).monoThreadInfo = new Function(`//# sourceURL=https://WorkerInfo/\r\nconsole.log("tid:0x${pthread_ptr.toString(16)} isAttached:${isAttached} hasInterop:${!!hasInterop} hasSynchronization:${hasSynchronization}" );`); + } +} diff --git a/src/mono/wasm/runtime/pthreads/worker/index.ts b/src/mono/wasm/runtime/pthreads/worker/index.ts index 090a468db82026..9ff83946079149 100644 --- a/src/mono/wasm/runtime/pthreads/worker/index.ts +++ b/src/mono/wasm/runtime/pthreads/worker/index.ts @@ -4,8 +4,9 @@ /// import MonoWasmThreads from "consts:monoWasmThreads"; + import { Module, ENVIRONMENT_IS_PTHREAD } from "../../globals"; -import { makeChannelCreatedMonoMessage } from "../shared"; +import { makeChannelCreatedMonoMessage, set_thread_info } from "../shared"; import type { pthreadPtr } from "../shared/types"; import { is_nullish } from "../../types/internal"; import type { MonoThreadMessage } from "../shared"; @@ -84,11 +85,19 @@ export function mono_wasm_pthread_on_pthread_attached(pthread_id: number): void const self = pthread_self; mono_assert(self !== null && self.pthreadId == pthread_id, "expected pthread_self to be set already when attaching"); mono_set_thread_id("0x" + pthread_id.toString(16)); - mono_log_debug("attaching pthread to runtime 0x" + pthread_id.toString(16)); + mono_log_debug("attaching pthread to mono runtime 0x" + pthread_id.toString(16)); preRunWorker(); + set_thread_info(pthread_id, true, false, false); currentWorkerThreadEvents.dispatchEvent(makeWorkerThreadEvent(dotnetPthreadAttached, self)); } +/// Called in the worker thread (not main thread) from mono when a pthread becomes detached from the mono runtime. +export function mono_wasm_pthread_on_pthread_detached(pthread_id: number): void { + mono_log_debug("detaching pthread from mono runtime 0x" + pthread_id.toString(16)); + set_thread_info(pthread_id, false, false, false); + mono_set_thread_id(""); +} + /// This is an implementation detail function. /// Called by emscripten when a pthread is setup to run on a worker. Can be called multiple times /// for the same worker, since emscripten can reuse workers. This is an implementation detail, that shouldn't be used directly. diff --git a/src/mono/wasm/runtime/scheduling.ts b/src/mono/wasm/runtime/scheduling.ts index b2c2190d57dd38..29c16be06e9f31 100644 --- a/src/mono/wasm/runtime/scheduling.ts +++ b/src/mono/wasm/runtime/scheduling.ts @@ -37,6 +37,7 @@ export function prevent_timer_throttling(): void { } function prevent_timer_throttling_tick() { + Module.maybeExit(); cwraps.mono_wasm_execute_timer(); pump_count++; mono_background_exec_until_done(); diff --git a/src/mono/wasm/runtime/types/internal.ts b/src/mono/wasm/runtime/types/internal.ts index 64057d7a3ce973..f1945eb4e05fa0 100644 --- a/src/mono/wasm/runtime/types/internal.ts +++ b/src/mono/wasm/runtime/types/internal.ts @@ -416,6 +416,7 @@ export declare interface EmscriptenModuleInternal { safeSetTimeout(func: Function, timeout: number): number; runtimeKeepalivePush(): void; runtimeKeepalivePop(): void; + maybeExit(): void; } /// A PromiseController encapsulates a Promise together with easy access to its resolve and reject functions. diff --git a/src/mono/wasm/wasm.proj b/src/mono/wasm/wasm.proj index f0f606aa52e8bf..7bc98cdff5152c 100644 --- a/src/mono/wasm/wasm.proj +++ b/src/mono/wasm/wasm.proj @@ -202,6 +202,7 @@ + From c717276f3f6fd6dea6a0ad37bfd819803fd9243e Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 6 Jun 2023 20:40:38 +0200 Subject: [PATCH 03/15] wip --- src/mono/sample/wasm/browser-threads-minimal/WebWorker.cs | 4 +++- src/mono/sample/wasm/browser-threads-minimal/main.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/mono/sample/wasm/browser-threads-minimal/WebWorker.cs b/src/mono/sample/wasm/browser-threads-minimal/WebWorker.cs index d3c1616f3c1940..daf84e66e21a16 100644 --- a/src/mono/sample/wasm/browser-threads-minimal/WebWorker.cs +++ b/src/mono/sample/wasm/browser-threads-minimal/WebWorker.cs @@ -26,7 +26,9 @@ public static Task RunAsync(Func> body) var webWorker = typeof(JSObject).Assembly.GetType("System.Runtime.InteropServices.JavaScript.WebWorker"); runAsyncMethod = webWorker.GetMethod("RunAsync", BindingFlags.Public|BindingFlags.Static); } - return (Task)runAsyncMethod.Invoke(null, new object[] { body }); + + var genericRunAsyncMethod = runAsyncMethod.MakeGenericMethod(typeof(T)); + return (Task)genericRunAsyncMethod.Invoke(null, new object[] { body }); } [DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, "System.Runtime.InteropServices.JavaScript.WebWorker", "System.Runtime.InteropServices.JavaScript")] diff --git a/src/mono/sample/wasm/browser-threads-minimal/main.js b/src/mono/sample/wasm/browser-threads-minimal/main.js index 7fc5a65cf95e86..1792f3d4582682 100644 --- a/src/mono/sample/wasm/browser-threads-minimal/main.js +++ b/src/mono/sample/wasm/browser-threads-minimal/main.js @@ -11,7 +11,7 @@ try { //.withEnvironmentVariable("MONO_LOG_LEVEL", "debug") .withDiagnosticTracing(true) .withConfig({ - pthreadPoolSize: 4, + pthreadPoolSize: 6, }) .withElementOnExit() .withExitCodeLogging() From 6a904cbd2a35ddc037756d5e57917cff80e1e6cb Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 6 Jun 2023 21:20:11 +0200 Subject: [PATCH 04/15] wip --- src/mono/sample/wasm/browser-threads-minimal/WebWorker.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mono/sample/wasm/browser-threads-minimal/WebWorker.cs b/src/mono/sample/wasm/browser-threads-minimal/WebWorker.cs index daf84e66e21a16..851144a868cc41 100644 --- a/src/mono/sample/wasm/browser-threads-minimal/WebWorker.cs +++ b/src/mono/sample/wasm/browser-threads-minimal/WebWorker.cs @@ -19,6 +19,7 @@ public partial class WebWorker [DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, "System.Runtime.InteropServices.JavaScript.WebWorker", "System.Runtime.InteropServices.JavaScript")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern", Justification = "work in progress")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", Justification = "work in progress")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060:UnrecognizedReflectionPattern", Justification = "work in progress")] public static Task RunAsync(Func> body) { if(runAsyncMethod == null) From 9eacc4a0f6915d5ec20d1b9ef039ccd9898eeaee Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 6 Jun 2023 22:48:06 +0200 Subject: [PATCH 05/15] faster runtimeKeepalivePop --- .../InteropServices/JavaScript/WebWorker.cs | 45 +++++++++---------- .../wasm/runtime/pthreads/shared/index.ts | 4 +- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/WebWorker.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/WebWorker.cs index 31b7508b0ffd6a..10cb22bf6eb92d 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/WebWorker.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/WebWorker.cs @@ -17,7 +17,7 @@ internal static class WebWorker { public static Task RunAsync(Func> body) { - var parentScheduler = TaskScheduler.FromCurrentSynchronizationContext(); + var parentContext = SynchronizationContext.Current ?? new SynchronizationContext(); var tcs = new TaskCompletionSource(); var capturedContext = SynchronizationContext.Current; var t = new Thread(() => @@ -31,19 +31,16 @@ public static Task RunAsync(Func> body) // the continuation is executed by setTimeout() callback of the thread. res.ContinueWith(t => { + parentContext.Post((_) => { + if (res.IsFaulted) + tcs.SetException(res.Exception); + else if (t.IsCanceled) + tcs.SetCanceled(); + else + tcs.SetResult(res.Result); + }, null); JSHostImplementation.UninstallWebWorkerInterop(); - return t; - }, childScheduler) - .ContinueWith((t) => - { - if (res.IsFaulted) - tcs.SetException(res.Exception); - else if (t.IsCanceled) - tcs.SetCanceled(); - else - tcs.SetResult(res.Result); - - }, parentScheduler); + }, childScheduler); } catch (Exception e) { @@ -58,7 +55,7 @@ public static Task RunAsync(Func> body) public static Task RunAsyncVoid(Func body) { - var parentScheduler = TaskScheduler.FromCurrentSynchronizationContext(); + var parentContext = SynchronizationContext.Current ?? new SynchronizationContext(); var tcs = new TaskCompletionSource(); var capturedContext = SynchronizationContext.Current; var t = new Thread(() => @@ -72,18 +69,16 @@ public static Task RunAsyncVoid(Func body) // the continuation is executed by setTimeout() callback of the thread. res.ContinueWith(t => { + parentContext.Post((_) => { + if (res.IsFaulted) + tcs.SetException(res.Exception); + else if (t.IsCanceled) + tcs.SetCanceled(); + else + tcs.SetResult(); + }, null); JSHostImplementation.UninstallWebWorkerInterop(); - return t; - }, childScheduler) - .ContinueWith((t) => - { - if (res.IsFaulted) - tcs.SetException(res.Exception); - else if (t.IsCanceled) - tcs.SetCanceled(); - else - tcs.SetResult(); - }, parentScheduler); + }, childScheduler); } catch (Exception e) { diff --git a/src/mono/wasm/runtime/pthreads/shared/index.ts b/src/mono/wasm/runtime/pthreads/shared/index.ts index 673f55af4fbb0c..6450dd84b196cd 100644 --- a/src/mono/wasm/runtime/pthreads/shared/index.ts +++ b/src/mono/wasm/runtime/pthreads/shared/index.ts @@ -159,9 +159,7 @@ export function mono_wasm_uninstall_js_worker_interop(uninstall_js_synchronizati forceDisposeProxies(false); if (uninstall_js_synchronization_context) { - Module.safeSetTimeout(() => { - Module.runtimeKeepalivePop(); - }, 0); + Module.runtimeKeepalivePop(); } worker_js_synchronization_context_installed = false; From 0eefea380fc53b572cac9ea9d3cf763a0330785f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 7 Jun 2023 18:49:04 +0200 Subject: [PATCH 06/15] feedback --- .../JavaScript/JSHostImplementation.cs | 4 + .../InteropServices/JavaScript/WebWorker.cs | 153 +++++++++++++++--- .../src/System/Threading/Thread.Mono.cs | 8 - src/mono/mono/utils/mono-threads-wasm.c | 6 +- .../wasm/browser-threads-minimal/WebWorker.cs | 27 +++- 5 files changed, 155 insertions(+), 43 deletions(-) diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs index e949c5532a2b81..e844a5d6ac0df9 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs @@ -237,6 +237,9 @@ public static void UninstallWebWorkerInterop() private static FieldInfo? thread_id_Field; private static FieldInfo? external_eventloop_Field; + // FIXME: after https://github.com/dotnet/runtime/issues/86040 replace with + // [UnsafeAccessor(UnsafeAccessorKind.Field, Name="external_eventloop")] + // static extern ref bool ThreadExternalEventloop(Thread @this); [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, "System.Threading.Thread", "System.Private.CoreLib")] public static void SetHasExternalEventLoop(Thread thread) { @@ -247,6 +250,7 @@ public static void SetHasExternalEventLoop(Thread thread) external_eventloop_Field.SetValue(thread, true); } + // FIXME: after https://github.com/dotnet/runtime/issues/86040 [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicFields, "System.Threading.Thread", "System.Private.CoreLib")] public static IntPtr GetNativeThreadId() { diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/WebWorker.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/WebWorker.cs index 10cb22bf6eb92d..9b95c18678687d 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/WebWorker.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/WebWorker.cs @@ -11,11 +11,12 @@ namespace System.Runtime.InteropServices.JavaScript { /// - /// This is draft for possible public API of browser thread (web worker) dedicated to JS interop workloads + /// This is draft for possible public API of browser thread (web worker) dedicated to JS interop workloads. + /// The method names are unique to make it easy to call them via reflection for now. All of them should be just `RunAsync` probably. /// internal static class WebWorker { - public static Task RunAsync(Func> body) + public static Task RunAsync(Func> body, CancellationToken cancellationToken) { var parentContext = SynchronizationContext.Current ?? new SynchronizationContext(); var tcs = new TaskCompletionSource(); @@ -24,6 +25,12 @@ public static Task RunAsync(Func> body) { try { + if (cancellationToken.IsCancellationRequested) + { + PostWhenCancellation(parentContext, tcs); + return; + } + JSHostImplementation.InstallWebWorkerInterop(true); var childScheduler = TaskScheduler.FromCurrentSynchronizationContext(); Task res = body(); @@ -31,14 +38,7 @@ public static Task RunAsync(Func> body) // the continuation is executed by setTimeout() callback of the thread. res.ContinueWith(t => { - parentContext.Post((_) => { - if (res.IsFaulted) - tcs.SetException(res.Exception); - else if (t.IsCanceled) - tcs.SetCanceled(); - else - tcs.SetResult(res.Result); - }, null); + PostWhenDone(parentContext, tcs, res); JSHostImplementation.UninstallWebWorkerInterop(); }, childScheduler); } @@ -53,7 +53,7 @@ public static Task RunAsync(Func> body) return tcs.Task; } - public static Task RunAsyncVoid(Func body) + public static Task RunAsyncVoid(Func body, CancellationToken cancellationToken) { var parentContext = SynchronizationContext.Current ?? new SynchronizationContext(); var tcs = new TaskCompletionSource(); @@ -62,6 +62,12 @@ public static Task RunAsyncVoid(Func body) { try { + if (cancellationToken.IsCancellationRequested) + { + PostWhenCancellation(parentContext, tcs); + return; + } + JSHostImplementation.InstallWebWorkerInterop(true); var childScheduler = TaskScheduler.FromCurrentSynchronizationContext(); Task res = body(); @@ -69,14 +75,7 @@ public static Task RunAsyncVoid(Func body) // the continuation is executed by setTimeout() callback of the thread. res.ContinueWith(t => { - parentContext.Post((_) => { - if (res.IsFaulted) - tcs.SetException(res.Exception); - else if (t.IsCanceled) - tcs.SetCanceled(); - else - tcs.SetResult(); - }, null); + PostWhenDone(parentContext, tcs, res); JSHostImplementation.UninstallWebWorkerInterop(); }, childScheduler); } @@ -91,7 +90,7 @@ public static Task RunAsyncVoid(Func body) return tcs.Task; } - public static Task Run(Action body) + public static Task Run(Action body, CancellationToken cancellationToken) { var parentContext = SynchronizationContext.Current ?? new SynchronizationContext(); var tcs = new TaskCompletionSource(); @@ -100,11 +99,22 @@ public static Task Run(Action body) { try { + if (cancellationToken.IsCancellationRequested) + { + PostWhenCancellation(parentContext, tcs); + return; + } + JSHostImplementation.InstallWebWorkerInterop(false); - body(); - parentContext.Post((_) => { - tcs.SetResult(); - }, null); + try + { + body(); + PostWhenDone(parentContext, tcs); + } + catch (Exception ex) + { + PostWhenException(parentContext, tcs, ex); + } JSHostImplementation.UninstallWebWorkerInterop(); } catch (Exception e) @@ -117,6 +127,101 @@ public static Task Run(Action body) t.Start(); return tcs.Task; } + + #region posting result to the original thread when handling exception + + private static void PostWhenCancellation(SynchronizationContext ctx, TaskCompletionSource tcs) + { + try + { + ctx.Post((_) => tcs.SetCanceled(), null); + } + catch (Exception e) + { + Environment.FailFast("WebWorker.RunAsync failed", e); + } + } + + private static void PostWhenCancellation(SynchronizationContext ctx, TaskCompletionSource tcs) + { + try + { + ctx.Post((_) => tcs.SetCanceled(), null); + } + catch (Exception e) + { + Environment.FailFast("WebWorker.RunAsync failed", e); + } + } + + private static void PostWhenDone(SynchronizationContext ctx, TaskCompletionSource tcs, Task done) + { + try + { + ctx.Post((_) => + { + if (done.IsFaulted) + tcs.SetException(done.Exception); + else if (done.IsCanceled) + tcs.SetCanceled(); + else + tcs.SetResult(); + + }, null); + } + catch (Exception e) + { + Environment.FailFast("WebWorker.RunAsync failed", e); + } + } + + private static void PostWhenDone(SynchronizationContext ctx, TaskCompletionSource tcs) + { + try + { + ctx.Post((_) => tcs.SetResult(), null); + } + catch (Exception e) + { + Environment.FailFast("WebWorker.RunAsync failed", e); + } + } + + private static void PostWhenException(SynchronizationContext ctx, TaskCompletionSource tcs, Exception ex) + { + try + { + ctx.Post((_) => tcs.SetException(ex), null); + } + catch (Exception e) + { + Environment.FailFast("WebWorker.RunAsync failed", e); + } + } + + private static void PostWhenDone(SynchronizationContext ctx, TaskCompletionSource tcs, Task done) + { + try + { + ctx.Post((_) => + { + if (done.IsFaulted) + tcs.SetException(done.Exception); + else if (done.IsCanceled) + tcs.SetCanceled(); + else + tcs.SetResult(done.Result); + + }, null); + } + catch (Exception e) + { + Environment.FailFast("WebWorker.RunAsync failed", e); + } + } + + #endregion + } } diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs index 5b58f776a8c389..74f37e6a1e82b8 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs @@ -364,13 +364,5 @@ internal bool HasExternalEventLoop external_eventloop = value; } } - - internal long NativeThreadId - { - get - { - return thread_id; - } - } } } diff --git a/src/mono/mono/utils/mono-threads-wasm.c b/src/mono/mono/utils/mono-threads-wasm.c index 1438defba4b112..daf5967de030b0 100644 --- a/src/mono/mono/utils/mono-threads-wasm.c +++ b/src/mono/mono/utils/mono-threads-wasm.c @@ -10,6 +10,7 @@ #include #include #include +#include #include @@ -23,9 +24,6 @@ #include #endif -#ifdef ENABLE_CHECKED_BUILD -#include -#endif #define round_down(addr, val) ((void*)((addr) & ~((val) - 1))) @@ -309,9 +307,7 @@ gboolean mono_thread_platform_external_eventloop_keepalive_check (void) { #if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) -#ifdef ENABLE_CHECKED_BUILD MONO_REQ_GC_SAFE_MODE; -#endif /* if someone called emscripten_runtime_keepalive_push (), the * thread will stay alive in the JS event loop after returning * from the thread's main function. diff --git a/src/mono/sample/wasm/browser-threads-minimal/WebWorker.cs b/src/mono/sample/wasm/browser-threads-minimal/WebWorker.cs index 851144a868cc41..b1036898f24668 100644 --- a/src/mono/sample/wasm/browser-threads-minimal/WebWorker.cs +++ b/src/mono/sample/wasm/browser-threads-minimal/WebWorker.cs @@ -20,7 +20,7 @@ public partial class WebWorker [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern", Justification = "work in progress")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", Justification = "work in progress")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060:UnrecognizedReflectionPattern", Justification = "work in progress")] - public static Task RunAsync(Func> body) + public static Task RunAsync(Func> body, CancellationToken cancellationToken) { if(runAsyncMethod == null) { @@ -29,33 +29,48 @@ public static Task RunAsync(Func> body) } var genericRunAsyncMethod = runAsyncMethod.MakeGenericMethod(typeof(T)); - return (Task)genericRunAsyncMethod.Invoke(null, new object[] { body }); + return (Task)genericRunAsyncMethod.Invoke(null, new object[] { body, cancellationToken }); + } + + public static Task RunAsync(Func> body) + { + return RunAsync(body, CancellationToken.None); } [DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, "System.Runtime.InteropServices.JavaScript.WebWorker", "System.Runtime.InteropServices.JavaScript")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern", Justification = "work in progress")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", Justification = "work in progress")] - public static Task RunAsync(Func body) + public static Task RunAsync(Func body, CancellationToken cancellationToken) { if(runAsyncVoidMethod == null) { var webWorker = typeof(JSObject).Assembly.GetType("System.Runtime.InteropServices.JavaScript.WebWorker"); runAsyncVoidMethod = webWorker.GetMethod("RunAsyncVoid", BindingFlags.Public|BindingFlags.Static); } - return (Task)runAsyncVoidMethod.Invoke(null, new object[] { body }); + return (Task)runAsyncVoidMethod.Invoke(null, new object[] { body, cancellationToken }); + } + + public static Task RunAsync(Func body) + { + return RunAsync(body, CancellationToken.None); } [DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, "System.Runtime.InteropServices.JavaScript.WebWorker", "System.Runtime.InteropServices.JavaScript")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern", Justification = "work in progress")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", Justification = "work in progress")] - public static Task RunAsync(Action body) + public static Task RunAsync(Action body, CancellationToken cancellationToken) { if(runMethod == null) { var webWorker = typeof(JSObject).Assembly.GetType("System.Runtime.InteropServices.JavaScript.WebWorker"); runMethod = webWorker.GetMethod("Run", BindingFlags.Public|BindingFlags.Static); } - return (Task)runMethod.Invoke(null, new object[] { body }); + return (Task)runMethod.Invoke(null, new object[] { body, cancellationToken }); + } + + public static Task RunAsync(Action body) + { + return RunAsync(body, CancellationToken.None); } } } \ No newline at end of file From 17ffe4973e4bc10ebcbe08d1c97335aeec27c378 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 8 Jun 2023 13:14:14 +0200 Subject: [PATCH 07/15] fix merge --- src/mono/wasm/runtime/scheduling.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mono/wasm/runtime/scheduling.ts b/src/mono/wasm/runtime/scheduling.ts index 8a8525cffc456b..885b7f8b7493ea 100644 --- a/src/mono/wasm/runtime/scheduling.ts +++ b/src/mono/wasm/runtime/scheduling.ts @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import cwraps from "./cwraps"; +import { Module, loaderHelpers } from "./globals"; let spread_timers_maximum = 0; let pump_count = 0; From 563c9e50fb646ad48d3ffc1d835707a46a68e4bd Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 8 Jun 2023 19:44:29 +0200 Subject: [PATCH 08/15] feedback from @kg --- src/mono/wasm/runtime/driver.c | 13 +++++++------ src/mono/wasm/runtime/invoke-cs.ts | 2 +- src/mono/wasm/runtime/net6-legacy/cs-to-js.ts | 1 - 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/mono/wasm/runtime/driver.c b/src/mono/wasm/runtime/driver.c index 01e0e62a8adfec..6f2bee81e07d8b 100644 --- a/src/mono/wasm/runtime/driver.c +++ b/src/mono/wasm/runtime/driver.c @@ -686,22 +686,23 @@ mono_wasm_invoke_method_ref (MonoMethod *method, MonoObject **this_arg_in, void } EMSCRIPTEN_KEEPALIVE int -mono_wasm_invoke_method_bound (MonoMethod *method, void* args /*JSMarshalerArguments*/, MonoObject **_out_exc) +mono_wasm_invoke_method_bound (MonoMethod *method, void* args /*JSMarshalerArguments*/, MonoString **out_exc) { - PPVOLATILE(MonoObject) out_exc = _out_exc; + PVOLATILE(MonoObject) temp_exc = NULL; + void *invoke_args[1] = { args }; int is_err = 0; MONO_ENTER_GC_UNSAFE; - mono_runtime_invoke (method, NULL, invoke_args, (MonoObject **)out_exc); + mono_runtime_invoke (method, NULL, invoke_args, (MonoObject **)&temp_exc); // this failure is unlikely because it would be runtime error, not application exception. // the application exception is passed inside JSMarshalerArguments `args` - if (*_out_exc) { + if (temp_exc) { PVOLATILE(MonoObject) exc2 = NULL; - store_volatile(_out_exc, (MonoObject*)mono_object_to_string (*out_exc, (MonoObject **)&exc2)); + store_volatile((MonoObject**)out_exc, (MonoObject*)mono_object_to_string ((MonoObject*)temp_exc, (MonoObject **)&exc2)); if (exc2) - store_volatile(_out_exc, (MonoObject*)mono_string_new (root_domain, "Exception Double Fault")); + store_volatile((MonoObject**)out_exc, (MonoObject*)mono_string_new (root_domain, "Exception Double Fault")); is_err = 1; } MONO_EXIT_GC_UNSAFE; diff --git a/src/mono/wasm/runtime/invoke-cs.ts b/src/mono/wasm/runtime/invoke-cs.ts index cef3b1c51b441c..e0f0b902cc5c64 100644 --- a/src/mono/wasm/runtime/invoke-cs.ts +++ b/src/mono/wasm/runtime/invoke-cs.ts @@ -12,7 +12,7 @@ import { bound_cs_function_symbol, get_signature_version, alloc_stack_frame, get_signature_type, } from "./marshal"; import { mono_wasm_new_external_root, mono_wasm_new_root } from "./roots"; -import { monoStringToString, monoStringToStringUnsafe } from "./strings"; +import { monoStringToString } from "./strings"; import { MonoObjectRef, MonoStringRef, MonoString, MonoObject, MonoMethod, JSMarshalerArguments, JSFunctionSignature, BoundMarshalerToCs, BoundMarshalerToJs, VoidPtrNull, MonoObjectRefNull, MonoObjectNull, MarshalerType } from "./types/internal"; import { Int32Ptr } from "./types/emscripten"; import cwraps from "./cwraps"; diff --git a/src/mono/wasm/runtime/net6-legacy/cs-to-js.ts b/src/mono/wasm/runtime/net6-legacy/cs-to-js.ts index ba61a8f52a002f..1400f7f84ed263 100644 --- a/src/mono/wasm/runtime/net6-legacy/cs-to-js.ts +++ b/src/mono/wasm/runtime/net6-legacy/cs-to-js.ts @@ -16,7 +16,6 @@ import { legacyHelpers } from "./globals"; import { js_to_mono_obj_root } from "./js-to-cs"; import { assert_legacy_interop, mono_bind_method, mono_method_get_call_signature_ref } from "./method-binding"; import { createPromiseController } from "../globals"; -import { assert_legacy_interop } from "../pthreads/shared"; import { monoStringToStringUnsafe } from "./strings"; const delegate_invoke_symbol = Symbol.for("wasm delegate_invoke"); From ccd2c5e27bc74166380d0ffa94cac19b491ced30 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 8 Jun 2023 19:47:22 +0200 Subject: [PATCH 09/15] fix merge --- src/mono/wasm/runtime/invoke-cs.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/mono/wasm/runtime/invoke-cs.ts b/src/mono/wasm/runtime/invoke-cs.ts index e0f0b902cc5c64..547e6a48cedc70 100644 --- a/src/mono/wasm/runtime/invoke-cs.ts +++ b/src/mono/wasm/runtime/invoke-cs.ts @@ -253,7 +253,6 @@ export function invoke_method_and_handle_exception(method: MonoMethod, args: JSM assert_bindings(); const fail_root = mono_wasm_new_root(); try { - assert_synchronization_context(); const fail = cwraps.mono_wasm_invoke_method_bound(method, args, fail_root.address); if (fail) throw new Error("ERR24: Unexpected error: " + monoStringToString(fail_root)); if (is_args_exception(args)) { From 5ae5db0c16355adc78db97f5cfae595d6b0f9383 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 9 Jun 2023 14:44:53 +0200 Subject: [PATCH 10/15] fix csp in debug --- src/mono/wasm/runtime/invoke-cs.ts | 9 +++++++-- src/mono/wasm/runtime/invoke-js.ts | 9 +++++++-- src/mono/wasm/runtime/pthreads/shared/index.ts | 9 +++++++-- src/mono/wasm/runtime/types/internal.ts | 1 + 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/mono/wasm/runtime/invoke-cs.ts b/src/mono/wasm/runtime/invoke-cs.ts index 547e6a48cedc70..259867305fe235 100644 --- a/src/mono/wasm/runtime/invoke-cs.ts +++ b/src/mono/wasm/runtime/invoke-cs.ts @@ -97,8 +97,13 @@ export function mono_wasm_bind_cs_function(fully_qualified_name: MonoStringRef, // this is just to make debugging easier. // It's not CSP compliant and possibly not performant, that's why it's only enabled in debug builds // in Release configuration, it would be a trimmed by rollup - if (BuildConfiguration === "Debug") { - bound_fn = new Function("fn", "return (function JSExport_" + methodname + "(){ return fn.apply(this, arguments)});")(bound_fn); + if (BuildConfiguration === "Debug" && !runtimeHelpers.cspPolicy) { + try { + bound_fn = new Function("fn", "return (function JSExport_" + methodname + "(){ return fn.apply(this, arguments)});")(bound_fn); + } + catch (ex) { + runtimeHelpers.cspPolicy = true; + } } (bound_fn)[bound_cs_function_symbol] = true; diff --git a/src/mono/wasm/runtime/invoke-js.ts b/src/mono/wasm/runtime/invoke-js.ts index f2ce876af5fb6c..98ca18a31d8fb4 100644 --- a/src/mono/wasm/runtime/invoke-js.ts +++ b/src/mono/wasm/runtime/invoke-js.ts @@ -95,8 +95,13 @@ export function mono_wasm_bind_js_function(function_name: MonoStringRef, module_ // this is just to make debugging easier. // It's not CSP compliant and possibly not performant, that's why it's only enabled in debug builds // in Release configuration, it would be a trimmed by rollup - if (BuildConfiguration === "Debug") { - bound_fn = new Function("fn", "return (function JSImport_" + js_function_name.replaceAll(".", "_") + "(){ return fn.apply(this, arguments)});")(bound_fn); + if (BuildConfiguration === "Debug" && !runtimeHelpers.cspPolicy) { + try { + bound_fn = new Function("fn", "return (function JSImport_" + js_function_name.replaceAll(".", "_") + "(){ return fn.apply(this, arguments)});")(bound_fn); + } + catch (ex) { + runtimeHelpers.cspPolicy = true; + } } (bound_fn)[imported_js_function_symbol] = true; diff --git a/src/mono/wasm/runtime/pthreads/shared/index.ts b/src/mono/wasm/runtime/pthreads/shared/index.ts index 6450dd84b196cd..6ab053516ad007 100644 --- a/src/mono/wasm/runtime/pthreads/shared/index.ts +++ b/src/mono/wasm/runtime/pthreads/shared/index.ts @@ -175,7 +175,12 @@ export function assert_synchronization_context(): void { // this is just for Debug build of the runtime, making it easier to debug worker threads export function set_thread_info(pthread_ptr: number, isAttached: boolean, hasInterop: boolean, hasSynchronization: boolean): void { - if (MonoWasmThreads && BuildConfiguration === "Debug") { - (globalThis as any).monoThreadInfo = new Function(`//# sourceURL=https://WorkerInfo/\r\nconsole.log("tid:0x${pthread_ptr.toString(16)} isAttached:${isAttached} hasInterop:${!!hasInterop} hasSynchronization:${hasSynchronization}" );`); + if (MonoWasmThreads && BuildConfiguration === "Debug" && !runtimeHelpers.cspPolicy) { + try { + (globalThis as any).monoThreadInfo = new Function(`//# sourceURL=https://WorkerInfo/\r\nconsole.log("tid:0x${pthread_ptr.toString(16)} isAttached:${isAttached} hasInterop:${!!hasInterop} hasSynchronization:${hasSynchronization}" );`); + } + catch (ex) { + runtimeHelpers.cspPolicy = true; + } } } diff --git a/src/mono/wasm/runtime/types/internal.ts b/src/mono/wasm/runtime/types/internal.ts index 47f140049bab0b..f2dba23fe73892 100644 --- a/src/mono/wasm/runtime/types/internal.ts +++ b/src/mono/wasm/runtime/types/internal.ts @@ -159,6 +159,7 @@ export type RuntimeHelpers = { subtle: SubtleCrypto | null, updateMemoryViews: () => void runtimeReady: boolean, + cspPolicy: boolean, runtimeModuleUrl: string nativeModuleUrl: string From 346c4ff260ff9393418394316c4c6a874ae56afc Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 9 Jun 2023 15:45:29 +0200 Subject: [PATCH 11/15] fix early exit in V8 caused by use of Module.safeSetTimeout --- src/mono/wasm/runtime/managed-exports.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/mono/wasm/runtime/managed-exports.ts b/src/mono/wasm/runtime/managed-exports.ts index 0dbe70b4106084..4fa83a32ea93cf 100644 --- a/src/mono/wasm/runtime/managed-exports.ts +++ b/src/mono/wasm/runtime/managed-exports.ts @@ -38,9 +38,10 @@ export function init_managed_exports(): void { const get_managed_stack_trace_method = get_method("GetManagedStackTrace"); mono_assert(get_managed_stack_trace_method, "Can't find GetManagedStackTrace method"); - runtimeHelpers.javaScriptExports.call_entry_point = (entry_point: MonoMethod, program_args?: string[]) => { + runtimeHelpers.javaScriptExports.call_entry_point = async (entry_point: MonoMethod, program_args?: string[]): Promise => { const sp = Module.stackSave(); try { + Module.runtimeKeepalivePush(); const args = alloc_stack_frame(4); const res = get_arg(args, 1); const arg1 = get_arg(args, 2); @@ -51,12 +52,13 @@ export function init_managed_exports(): void { } marshal_array_to_cs_impl(arg2, program_args, MarshalerType.String); invoke_method_and_handle_exception(call_entry_point, args); - const promise = marshal_task_to_js(res, undefined, marshal_int32_to_js); + let promise = marshal_task_to_js(res, undefined, marshal_int32_to_js); if (!promise) { - return Promise.resolve(0); + promise = Promise.resolve(0); } - return promise; + return await promise; } finally { + Module.runtimeKeepalivePop();// after await promise ! Module.stackRestore(sp); } }; From fb7f47a3040e98c1b514dfae3b425f2011334005 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 9 Jun 2023 15:50:15 +0200 Subject: [PATCH 12/15] improve logging with no WS proxy --- src/mono/wasm/runtime/loader/logging.ts | 41 ++++++++++++++----------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/mono/wasm/runtime/loader/logging.ts b/src/mono/wasm/runtime/loader/logging.ts index 72a62f97487fb5..f148653051f23d 100644 --- a/src/mono/wasm/runtime/loader/logging.ts +++ b/src/mono/wasm/runtime/loader/logging.ts @@ -81,25 +81,9 @@ export function setup_proxy_console(id: string, console: Console, origin: string } const methods = ["debug", "trace", "warn", "info", "error"]; - for (const m of methods) { - if (typeof (anyConsole[m]) !== "function") { - anyConsole[m] = proxyConsoleMethod(`console.${m}: `, console.log, false); - } - } const consoleUrl = `${origin}/console`.replace("https://", "wss://").replace("http://", "ws://"); - consoleWebSocket = new WebSocket(consoleUrl); - consoleWebSocket.addEventListener("open", () => { - originalConsole.log(`browser: [${id}] Console websocket connected.`); - }); - consoleWebSocket.addEventListener("error", (event) => { - originalConsole.error(`[${id}] websocket error: ${event}`, event); - }); - consoleWebSocket.addEventListener("close", (event) => { - originalConsole.error(`[${id}] websocket closed: ${event}`, event); - }); - const send = (msg: string) => { if (consoleWebSocket.readyState === WebSocket.OPEN) { consoleWebSocket.send(msg); @@ -109,6 +93,27 @@ export function setup_proxy_console(id: string, console: Console, origin: string } }; - for (const m of ["log", ...methods]) - anyConsole[m] = proxyConsoleMethod(`console.${m}`, send, true); + try { + for (const m of methods) { + if (typeof (anyConsole[m]) !== "function") { + anyConsole[m] = proxyConsoleMethod(`console.${m}: `, console.log, false); + } + } + + consoleWebSocket = new WebSocket(consoleUrl); + consoleWebSocket.addEventListener("open", () => { + for (const m of ["log", ...methods]) + anyConsole[m] = proxyConsoleMethod(`console.${m}`, send, true); + + originalConsole.log(`browser: [${id}] Console websocket connected.`); + }); + consoleWebSocket.addEventListener("error", (event) => { + originalConsole.error(`[${id}] websocket error: ${event}`, event); + }); + consoleWebSocket.addEventListener("close", (event) => { + originalConsole.error(`[${id}] websocket closed: ${event}`, event); + }); + } catch (e) { + originalConsole.error(`[${id}] setup_proxy_console failed: ${e}`, e); + } } From 88c939acee5486d9a6699ca0b0a2ee06f5debcf0 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 9 Jun 2023 17:06:10 +0200 Subject: [PATCH 13/15] fix timer test & non MT runtimeKeepalivePop --- .../timers.mjs | 11 +++++++---- src/mono/wasm/runtime/managed-exports.ts | 4 ++-- src/mono/wasm/runtime/scheduling.ts | 8 ++++++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.Legacy.UnitTests/timers.mjs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.Legacy.UnitTests/timers.mjs index f0804845e72501..c2895c38bdcaa6 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.Legacy.UnitTests/timers.mjs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.Legacy.UnitTests/timers.mjs @@ -7,14 +7,16 @@ export function log(message) { } export function install() { + const Module = globalThis.App.runtime.Module; const measuredCallbackName = "mono_wasm_schedule_timer_tick"; globalThis.registerCount = 0; globalThis.hitCount = 0; log("install") if (!globalThis.originalSetTimeout) { - globalThis.originalSetTimeout = globalThis.setTimeout; + globalThis.originalSetTimeout = Module.safeSetTimeout; } - globalThis.setTimeout = (cb, time) => { + + Module.safeSetTimeout = (cb, time) => { var start = Date.now().valueOf(); if (cb.name === measuredCallbackName) { globalThis.registerCount++; @@ -26,7 +28,7 @@ export function install() { globalThis.hitCount++; log(`hitCount: ${globalThis.hitCount} now:${hit} delay:${time} delta:${hit - start}`) } - cb(); + return cb(); }, time); }; } @@ -43,5 +45,6 @@ export function getHitCount() { export function cleanup() { log(`cleanup registerCount: ${globalThis.registerCount} hitCount: ${globalThis.hitCount} `) - globalThis.setTimeout = globalThis.originalSetTimeout; + const Module = globalThis.App.runtime.Module; + Module.safeSetTimeout = globalThis.originalSetTimeout; } diff --git a/src/mono/wasm/runtime/managed-exports.ts b/src/mono/wasm/runtime/managed-exports.ts index 4fa83a32ea93cf..9101523eda7a00 100644 --- a/src/mono/wasm/runtime/managed-exports.ts +++ b/src/mono/wasm/runtime/managed-exports.ts @@ -41,7 +41,7 @@ export function init_managed_exports(): void { runtimeHelpers.javaScriptExports.call_entry_point = async (entry_point: MonoMethod, program_args?: string[]): Promise => { const sp = Module.stackSave(); try { - Module.runtimeKeepalivePush(); + if (MonoWasmThreads) Module.runtimeKeepalivePush(); const args = alloc_stack_frame(4); const res = get_arg(args, 1); const arg1 = get_arg(args, 2); @@ -58,7 +58,7 @@ export function init_managed_exports(): void { } return await promise; } finally { - Module.runtimeKeepalivePop();// after await promise ! + if (MonoWasmThreads) Module.runtimeKeepalivePop();// after await promise ! Module.stackRestore(sp); } }; diff --git a/src/mono/wasm/runtime/scheduling.ts b/src/mono/wasm/runtime/scheduling.ts index 885b7f8b7493ea..1309cf8043683f 100644 --- a/src/mono/wasm/runtime/scheduling.ts +++ b/src/mono/wasm/runtime/scheduling.ts @@ -1,6 +1,8 @@ // 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 cwraps from "./cwraps"; import { Module, loaderHelpers } from "./globals"; @@ -49,8 +51,10 @@ export function mono_wasm_schedule_timer(shortestDueTimeMs: number): void { if (lastScheduledTimeoutId) { globalThis.clearTimeout(lastScheduledTimeoutId); lastScheduledTimeoutId = undefined; - // NOTE: Module.safeSetTimeout() does the runtimeKeepalivePush() but clearTimeout is asymmetric. - Module.runtimeKeepalivePop(); + // NOTE: Multi-threaded Module.safeSetTimeout() does the runtimeKeepalivePush() + // and non-Multi-threaded Module.safeSetTimeout does not runtimeKeepalivePush() + // but clearTimeout does not runtimeKeepalivePop() so we need to do it here in MT only. + if (MonoWasmThreads) Module.runtimeKeepalivePop(); } lastScheduledTimeoutId = Module.safeSetTimeout(mono_wasm_schedule_timer_tick, shortestDueTimeMs); } From dcb433fbbc411819cc187897f836d35c5bbc6812 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 12 Jun 2023 14:35:00 +0200 Subject: [PATCH 14/15] fix non MT --- src/mono/wasm/runtime/managed-exports.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mono/wasm/runtime/managed-exports.ts b/src/mono/wasm/runtime/managed-exports.ts index 9101523eda7a00..4fa83a32ea93cf 100644 --- a/src/mono/wasm/runtime/managed-exports.ts +++ b/src/mono/wasm/runtime/managed-exports.ts @@ -41,7 +41,7 @@ export function init_managed_exports(): void { runtimeHelpers.javaScriptExports.call_entry_point = async (entry_point: MonoMethod, program_args?: string[]): Promise => { const sp = Module.stackSave(); try { - if (MonoWasmThreads) Module.runtimeKeepalivePush(); + Module.runtimeKeepalivePush(); const args = alloc_stack_frame(4); const res = get_arg(args, 1); const arg1 = get_arg(args, 2); @@ -58,7 +58,7 @@ export function init_managed_exports(): void { } return await promise; } finally { - if (MonoWasmThreads) Module.runtimeKeepalivePop();// after await promise ! + Module.runtimeKeepalivePop();// after await promise ! Module.stackRestore(sp); } }; From f93ad51167e2f3b29e9476e2683a241eca2c7ab5 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 12 Jun 2023 17:52:25 +0200 Subject: [PATCH 15/15] revert logging change, fix top level promise of void main --- src/mono/wasm/runtime/loader/logging.ts | 41 +++++++++++------------- src/mono/wasm/runtime/managed-exports.ts | 2 +- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/mono/wasm/runtime/loader/logging.ts b/src/mono/wasm/runtime/loader/logging.ts index f148653051f23d..72a62f97487fb5 100644 --- a/src/mono/wasm/runtime/loader/logging.ts +++ b/src/mono/wasm/runtime/loader/logging.ts @@ -81,9 +81,25 @@ export function setup_proxy_console(id: string, console: Console, origin: string } const methods = ["debug", "trace", "warn", "info", "error"]; + for (const m of methods) { + if (typeof (anyConsole[m]) !== "function") { + anyConsole[m] = proxyConsoleMethod(`console.${m}: `, console.log, false); + } + } const consoleUrl = `${origin}/console`.replace("https://", "wss://").replace("http://", "ws://"); + consoleWebSocket = new WebSocket(consoleUrl); + consoleWebSocket.addEventListener("open", () => { + originalConsole.log(`browser: [${id}] Console websocket connected.`); + }); + consoleWebSocket.addEventListener("error", (event) => { + originalConsole.error(`[${id}] websocket error: ${event}`, event); + }); + consoleWebSocket.addEventListener("close", (event) => { + originalConsole.error(`[${id}] websocket closed: ${event}`, event); + }); + const send = (msg: string) => { if (consoleWebSocket.readyState === WebSocket.OPEN) { consoleWebSocket.send(msg); @@ -93,27 +109,6 @@ export function setup_proxy_console(id: string, console: Console, origin: string } }; - try { - for (const m of methods) { - if (typeof (anyConsole[m]) !== "function") { - anyConsole[m] = proxyConsoleMethod(`console.${m}: `, console.log, false); - } - } - - consoleWebSocket = new WebSocket(consoleUrl); - consoleWebSocket.addEventListener("open", () => { - for (const m of ["log", ...methods]) - anyConsole[m] = proxyConsoleMethod(`console.${m}`, send, true); - - originalConsole.log(`browser: [${id}] Console websocket connected.`); - }); - consoleWebSocket.addEventListener("error", (event) => { - originalConsole.error(`[${id}] websocket error: ${event}`, event); - }); - consoleWebSocket.addEventListener("close", (event) => { - originalConsole.error(`[${id}] websocket closed: ${event}`, event); - }); - } catch (e) { - originalConsole.error(`[${id}] setup_proxy_console failed: ${e}`, e); - } + for (const m of ["log", ...methods]) + anyConsole[m] = proxyConsoleMethod(`console.${m}`, send, true); } diff --git a/src/mono/wasm/runtime/managed-exports.ts b/src/mono/wasm/runtime/managed-exports.ts index 4fa83a32ea93cf..6bd65eb2ec0254 100644 --- a/src/mono/wasm/runtime/managed-exports.ts +++ b/src/mono/wasm/runtime/managed-exports.ts @@ -53,7 +53,7 @@ export function init_managed_exports(): void { marshal_array_to_cs_impl(arg2, program_args, MarshalerType.String); invoke_method_and_handle_exception(call_entry_point, args); let promise = marshal_task_to_js(res, undefined, marshal_int32_to_js); - if (!promise) { + if (promise === null || promise === undefined) { promise = Promise.resolve(0); } return await promise;