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..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 @@ -24,7 +24,7 @@ public static Dictionary> ThreadCsOwnedObjects { get { - s_csOwnedObjects ??= new (); + s_csOwnedObjects ??= new(); return s_csOwnedObjects; } } @@ -197,5 +197,71 @@ 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; + + // 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) + { + if (external_eventloop_Field == null) + { + external_eventloop_Field = typeof(Thread).GetField("external_eventloop", BindingFlags.NonPublic | BindingFlags.Instance)!; + } + 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() + { + 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..9b95c18678687d --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/WebWorker.cs @@ -0,0 +1,228 @@ +// 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. + /// 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, CancellationToken cancellationToken) + { + var parentContext = SynchronizationContext.Current ?? new SynchronizationContext(); + var tcs = new TaskCompletionSource(); + var capturedContext = SynchronizationContext.Current; + var t = new Thread(() => + { + try + { + if (cancellationToken.IsCancellationRequested) + { + PostWhenCancellation(parentContext, tcs); + return; + } + + 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 => + { + PostWhenDone(parentContext, tcs, res); + JSHostImplementation.UninstallWebWorkerInterop(); + }, childScheduler); + } + catch (Exception e) + { + Environment.FailFast("WebWorker.RunAsync failed", e); + } + + }); + JSHostImplementation.SetHasExternalEventLoop(t); + t.Start(); + return tcs.Task; + } + + public static Task RunAsyncVoid(Func body, CancellationToken cancellationToken) + { + var parentContext = SynchronizationContext.Current ?? new SynchronizationContext(); + var tcs = new TaskCompletionSource(); + var capturedContext = SynchronizationContext.Current; + var t = new Thread(() => + { + try + { + if (cancellationToken.IsCancellationRequested) + { + PostWhenCancellation(parentContext, tcs); + return; + } + + 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 => + { + PostWhenDone(parentContext, tcs, res); + JSHostImplementation.UninstallWebWorkerInterop(); + }, childScheduler); + } + catch (Exception e) + { + Environment.FailFast("WebWorker.RunAsync failed", e); + } + + }); + JSHostImplementation.SetHasExternalEventLoop(t); + t.Start(); + return tcs.Task; + } + + public static Task Run(Action body, CancellationToken cancellationToken) + { + var parentContext = SynchronizationContext.Current ?? new SynchronizationContext(); + var tcs = new TaskCompletionSource(); + var capturedContext = SynchronizationContext.Current; + var t = new Thread(() => + { + try + { + if (cancellationToken.IsCancellationRequested) + { + PostWhenCancellation(parentContext, tcs); + return; + } + + JSHostImplementation.InstallWebWorkerInterop(false); + try + { + body(); + PostWhenDone(parentContext, tcs); + } + catch (Exception ex) + { + PostWhenException(parentContext, tcs, ex); + } + JSHostImplementation.UninstallWebWorkerInterop(); + } + catch (Exception e) + { + tcs.SetException(e); + } + + }); + JSHostImplementation.SetHasExternalEventLoop(t); + 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 + + } +} + +#endif 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/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs index d5b1918608b77f..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 @@ -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 */ 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 7cde95a9dc0b1a..b4c620ac1852ec 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 @@ -1068,7 +1069,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. */ } @@ -1273,7 +1274,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..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,6 +24,7 @@ #include #endif + #define round_down(addr, val) ((void*)((addr) & ~((val) - 1))) EMSCRIPTEN_KEEPALIVE @@ -305,6 +307,7 @@ gboolean mono_thread_platform_external_eventloop_keepalive_check (void) { #if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) + MONO_REQ_GC_SAFE_MODE; /* 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 +405,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 +476,8 @@ 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); +extern void mono_wasm_pthread_on_pthread_detached (MonoNativeThreadId pthread_id); #endif void @@ -484,6 +497,21 @@ 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 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/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..b1036898f24668 --- /dev/null +++ b/src/mono/sample/wasm/browser-threads-minimal/WebWorker.cs @@ -0,0 +1,76 @@ +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")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060:UnrecognizedReflectionPattern", Justification = "work in progress")] + public static Task RunAsync(Func> body, CancellationToken cancellationToken) + { + if(runAsyncMethod == null) + { + var webWorker = typeof(JSObject).Assembly.GetType("System.Runtime.InteropServices.JavaScript.WebWorker"); + runAsyncMethod = webWorker.GetMethod("RunAsync", BindingFlags.Public|BindingFlags.Static); + } + + var genericRunAsyncMethod = runAsyncMethod.MakeGenericMethod(typeof(T)); + 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, 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, 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, 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, cancellationToken }); + } + + public static Task RunAsync(Action body) + { + return RunAsync(body, CancellationToken.None); + } + } +} \ 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..1792f3d4582682 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: 6, + }) .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 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/es6/dotnet.es6.lib.js b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js index 3f5fcabf626565..66477cc88d8deb 100644 --- a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js +++ b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js @@ -118,14 +118,18 @@ let linked_functions = [ #if USE_PTHREADS linked_functions = [...linked_functions, - /// mono-threads-wasm.c + // 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 "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..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"; @@ -20,27 +20,34 @@ 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 --- 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 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 cbd6f10606d06c..259867305fe235 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 } 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 = { @@ -91,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; @@ -244,9 +255,9 @@ type BindingClosure = { } export function invoke_method_and_handle_exception(method: MonoMethod, args: JSMarshalerArguments): void { + 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)) { @@ -290,7 +301,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..98ca18a31d8fb4 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 = { @@ -88,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; @@ -383,3 +395,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/managed-exports.ts b/src/mono/wasm/runtime/managed-exports.ts index 0dbe70b4106084..6bd65eb2ec0254 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); - if (!promise) { - return Promise.resolve(0); + let promise = marshal_task_to_js(res, undefined, marshal_int32_to_js); + if (promise === null || promise === undefined) { + promise = Promise.resolve(0); } - return promise; + return await promise; } finally { + Module.runtimeKeepalivePop();// after await promise ! Module.stackRestore(sp); } }; 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 1ac0ea7fe47767..1400f7f84ed263 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 } 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"; import { monoStringToStringUnsafe } from "./strings"; 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 9c85a54926c9af..0a6e4d41903eb7 100644 --- a/src/mono/wasm/runtime/net6-legacy/strings.ts +++ b/src/mono/wasm/runtime/net6-legacy/strings.ts @@ -1,12 +1,12 @@ // 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, monoStringToString, mono_wasm_empty_string, stringToInternedMonoStringRoot, stringToMonoStringRoot } from "../strings"; import { MonoString, MonoStringNull, is_nullish } from "../types/internal"; let mono_wasm_string_root: any; +import { assert_legacy_interop } from "./method-binding"; /** * @deprecated Not GC or thread safe @@ -38,11 +38,11 @@ export function stringToMonoStringIntern(string: string): string { root.release(); } } - /* @deprecated not GC safe, use monoStringToString */ export function monoStringToStringUnsafe(mono_string: MonoString): string | null { if (mono_string === MonoStringNull) return null; + assert_legacy_interop(); if (!mono_wasm_string_root) mono_wasm_string_root = mono_wasm_new_root(); diff --git a/src/mono/wasm/runtime/pthreads/shared/index.ts b/src/mono/wasm/runtime/pthreads/shared/index.ts index 6fa9789bd68654..6ab053516ad007 100644 --- a/src/mono/wasm/runtime/pthreads/shared/index.ts +++ b/src/mono/wasm/runtime/pthreads/shared/index.ts @@ -2,10 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. import MonoWasmThreads from "consts:monoWasmThreads"; +import BuildConfiguration from "consts:configuration"; -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"; +import { pthread_self } from "../worker"; export interface PThreadInfo { readonly pthreadId: pthreadPtr; @@ -131,23 +136,51 @@ export function isMonoWorkerMessagePreload(message: MonoWorkerMessage 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"; @@ -18,6 +19,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,15 +80,24 @@ 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_log_debug("attaching pthread to runtime 0x" + pthread_id.toString(16)); + mono_set_thread_id("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/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 b0ce9c0fec4c56..1309cf8043683f 100644 --- a/src/mono/wasm/runtime/scheduling.ts +++ b/src/mono/wasm/runtime/scheduling.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 cwraps from "./cwraps"; -import { loaderHelpers } from "./globals"; +import { Module, loaderHelpers } from "./globals"; let spread_timers_maximum = 0; let pump_count = 0; @@ -26,6 +28,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(); @@ -40,7 +43,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; @@ -48,10 +51,15 @@ export function mono_wasm_schedule_timer(shortestDueTimeMs: number): void { if (lastScheduledTimeoutId) { globalThis.clearTimeout(lastScheduledTimeoutId); lastScheduledTimeoutId = undefined; + // 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 = 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(); } 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 5816f9a91bd1be..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 @@ -416,6 +417,10 @@ 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; + maybeExit(): 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..7bc98cdff5152c 100644 --- a/src/mono/wasm/wasm.proj +++ b/src/mono/wasm/wasm.proj @@ -199,6 +199,10 @@ + + + + @@ -260,6 +264,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;