[browser][MT] GC, threadpool and some JS interop improvements (#86759)
authorPavel Savara <pavel.savara@gmail.com>
Mon, 12 Jun 2023 19:43:16 +0000 (21:43 +0200)
committerGitHub <noreply@github.com>
Mon, 12 Jun 2023 19:43:16 +0000 (21:43 +0200)
45 files changed:
eng/Subsets.props
src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs
src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj
src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs
src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs
src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSSynchronizationContext.cs
src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/WebWorker.cs [new file with mode: 0644]
src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.Legacy.UnitTests/timers.mjs
src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs
src/mono/System.Private.CoreLib/src/System/Threading/ThreadPool.Browser.Mono.cs
src/mono/mono/metadata/threads.c
src/mono/mono/mini/mini-wasm.c
src/mono/mono/utils/lifo-semaphore.c
src/mono/mono/utils/lifo-semaphore.h
src/mono/mono/utils/mono-threads-wasm.c
src/mono/mono/utils/mono-threads-wasm.h
src/mono/mono/utils/mono-threads.c
src/mono/mono/utils/mono-threads.h
src/mono/sample/wasm/browser-threads-minimal/Program.cs
src/mono/sample/wasm/browser-threads-minimal/WebWorker.cs [new file with mode: 0644]
src/mono/sample/wasm/browser-threads-minimal/main.js
src/mono/wasm/runtime/corebindings.c
src/mono/wasm/runtime/dotnet.d.ts
src/mono/wasm/runtime/driver.c
src/mono/wasm/runtime/es6/dotnet.es6.lib.js
src/mono/wasm/runtime/exports-linker.ts
src/mono/wasm/runtime/gc-handles.ts
src/mono/wasm/runtime/invoke-cs.ts
src/mono/wasm/runtime/invoke-js.ts
src/mono/wasm/runtime/logging.ts
src/mono/wasm/runtime/managed-exports.ts
src/mono/wasm/runtime/net6-legacy/cs-to-js.ts
src/mono/wasm/runtime/net6-legacy/js-to-cs.ts
src/mono/wasm/runtime/net6-legacy/method-binding.ts
src/mono/wasm/runtime/net6-legacy/method-calls.ts
src/mono/wasm/runtime/net6-legacy/strings.ts
src/mono/wasm/runtime/pthreads/shared/index.ts
src/mono/wasm/runtime/pthreads/worker/index.ts
src/mono/wasm/runtime/run.ts
src/mono/wasm/runtime/scheduling.ts
src/mono/wasm/runtime/startup.ts
src/mono/wasm/runtime/types/internal.ts
src/mono/wasm/threads.md
src/mono/wasm/wasm.proj
src/native/libs/System.Native/pal_time.c

index 211dc6a..daa926e 100644 (file)
   </ItemGroup>
 
   <ItemGroup Condition="$(_subset.Contains('+mono.wasmruntime+'))">
+    <ProjectToBuild Include="$(LibrariesProjectRoot)\System.Runtime.InteropServices.JavaScript\src\System.Runtime.InteropServices.JavaScript.csproj" Category="mono" />
     <ProjectToBuild Include="$(MonoProjectRoot)wasm\wasm.proj" Category="mono" />
   </ItemGroup>
 
index 202afdf..edf075a 100644 (file)
@@ -28,6 +28,13 @@ internal static partial class Interop
         [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)]
index a77e9bf..43251dd 100644 (file)
@@ -58,8 +58,6 @@
     <Compile Include="System\Runtime\InteropServices\JavaScript\Marshaling\JSMarshalerArgument.JSObject.cs" />
     <Compile Include="System\Runtime\InteropServices\JavaScript\Marshaling\JSMarshalerArgument.String.cs" />
     <Compile Include="System\Runtime\InteropServices\JavaScript\Marshaling\JSMarshalerArgument.Exception.cs" />
-
-    <Compile Include="System\Runtime\InteropServices\JavaScript\JSSynchronizationContext.cs" />
   </ItemGroup>
 
   <!-- only include legacy interop when WasmEnableLegacyJsInterop is enabled -->
     <Compile Include="System\Runtime\InteropServices\JavaScript\Legacy\LegacyHostImplementation.cs" />
   </ItemGroup>
 
+  <!-- only include threads support when FeatureWasmThreads is enabled -->
+  <ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'browser' and '$(FeatureWasmThreads)' == 'true'">
+    <Compile Include="System\Runtime\InteropServices\JavaScript\WebWorker.cs" />
+    <Compile Include="System\Runtime\InteropServices\JavaScript\JSSynchronizationContext.cs" />
+  </ItemGroup>
+
   <ItemGroup>
     <Reference Include="System.Collections" />
     <Reference Include="System.Memory" />
index 9a5ec0d..9a0c75d 100644 (file)
@@ -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 @@ namespace System.Runtime.InteropServices.JavaScript
 
         // 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)
             {
index a333310..e844a5d 100644 (file)
@@ -24,7 +24,7 @@ namespace System.Runtime.InteropServices.JavaScript
         {
             get
             {
-                s_csOwnedObjects ??= new ();
+                s_csOwnedObjects ??= new();
                 return s_csOwnedObjects;
             }
         }
@@ -197,5 +197,71 @@ namespace System.Runtime.InteropServices.JavaScript
             }
             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
+
     }
 }
index 6c8a00b..c657eba 100644 (file)
@@ -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<System.Runtime.InteropServices.JavaScript.JSSynchronizationContext.WorkItem>;
+using WorkItemQueueType = System.Threading.Channels.Channel<System.Runtime.InteropServices.JavaScript.JSSynchronizationContext.WorkItem>;
 
 namespace System.Runtime.InteropServices.JavaScript
 {
     /// <summary>
     /// 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
     /// </summary>
     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 @@ namespace System.Runtime.InteropServices.JavaScript
             }
         }
 
-        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<WorkItem>(
                     new UnboundedChannelOptions { SingleWriter = false, SingleReader = true, AllowSynchronousContinuations = true }
                 )
@@ -51,20 +53,23 @@ namespace System.Runtime.InteropServices.JavaScript
         {
         }
 
-        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 @@ namespace System.Runtime.InteropServices.JavaScript
         {
             // 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]<void>)&BackgroundJobHandler);
+            TargetThreadScheduleBackgroundJob(TargetThreadId, (void*)(delegate* unmanaged[Cdecl]<void>)&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 @@ namespace System.Runtime.InteropServices.JavaScript
 
         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 @@ namespace System.Runtime.InteropServices.JavaScript
             }
         }
 
-        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 @@ namespace System.Runtime.InteropServices.JavaScript
             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 (file)
index 0000000..9b95c18
--- /dev/null
@@ -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
+{
+    /// <summary>
+    /// 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.
+    /// </summary>
+    internal static class WebWorker
+    {
+        public static Task<T> RunAsync<T>(Func<Task<T>> body, CancellationToken cancellationToken)
+        {
+            var parentContext = SynchronizationContext.Current ?? new SynchronizationContext();
+            var tcs = new TaskCompletionSource<T>();
+            var capturedContext = SynchronizationContext.Current;
+            var t = new Thread(() =>
+            {
+                try
+                {
+                    if (cancellationToken.IsCancellationRequested)
+                    {
+                        PostWhenCancellation(parentContext, tcs);
+                        return;
+                    }
+
+                    JSHostImplementation.InstallWebWorkerInterop(true);
+                    var childScheduler = TaskScheduler.FromCurrentSynchronizationContext();
+                    Task<T> 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<Task> 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<T>(SynchronizationContext ctx, TaskCompletionSource<T> 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<T>(SynchronizationContext ctx, TaskCompletionSource<T> tcs, Task<T> 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
index f080484..c2895c3 100644 (file)
@@ -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;
 }
index d5b1918..74f37e6 100644 (file)
@@ -29,7 +29,6 @@ namespace System.Threading
         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 */
index 96ca4d4..2ad06cd 100644 (file)
@@ -13,9 +13,10 @@ using Microsoft.Win32.SafeHandles;
 
 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()
index 7cde95a..b4c620a 100644 (file)
@@ -46,6 +46,7 @@
 #include <mono/utils/mono-error-internals.h>
 #include <mono/utils/os-event.h>
 #include <mono/utils/mono-threads-debug.h>
+#include <mono/utils/mono-threads-wasm.h>
 #include <mono/utils/unlocked.h>
 #include <mono/utils/ftnptr.h>
 #include <mono/metadata/w32handle.h>
@@ -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;
                }
        }
index 8aa7934..cab202a 100644 (file)
@@ -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 */
 }
index 51117ab..dce67c4 100644 (file)
@@ -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 */
index a97a560..1a91a6f 100644 (file)
@@ -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,
index dd49bf4..daf5967 100644 (file)
@@ -10,6 +10,7 @@
 #include <mono/utils/mono-mmap.h>
 #include <mono/utils/mono-threads-api.h>
 #include <mono/utils/mono-threads-debug.h>
+#include <mono/utils/checked-build.h>
 
 #include <glib.h>
 
@@ -23,6 +24,7 @@
 #include <emscripten/threading.h>
 #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
index 95f8e63..38b6311 100644 (file)
@@ -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__ */
index b364d94..e453030 100644 (file)
@@ -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.*/
index 80d6f19..0d3d802 100644 (file)
@@ -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
index 0e9e558..bd2646a 100644 (file)
@@ -5,6 +5,7 @@ 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;
 
@@ -18,6 +19,41 @@ namespace Sample
             return 0;
         }
 
+        [JSImport("globalThis.setTimeout")]
+        static partial void GlobalThisSetTimeout([JSMarshalAs<JSType.Function>] Action cb, int timeoutMs);
+
+        [JSImport("globalThis.fetch")]
+        private static partial Task<JSObject> 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<string> 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 @@ namespace Sample
                 throw new Exception("Child thread ran on same thread as parent");
         }
 
-        [JSImport("globalThis.setTimeout")]
-        static partial void GlobalThisSetTimeout([JSMarshalAs<JSType.Function>] Action cb, int timeoutMs);
+        static bool _timerDone = false;
 
-        [JSImport("globalThis.fetch")]
-        private static partial Task<JSObject> 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<int[]>();
+                    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<string> 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<string> 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 @@ namespace Sample
                 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 @@ namespace Sample
             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 (file)
index 0000000..b103689
--- /dev/null
@@ -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<T> RunAsync<T>(Func<Task<T>> 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<T>)genericRunAsyncMethod.Invoke(null, new object[] { body, cancellationToken });
+        }
+
+        public static Task<T> RunAsync<T>(Func<Task<T>> 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<Task> 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<Task> 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
index b90abdb..1792f3d 100644 (file)
@@ -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));
+}
+
index a6d0f39..0dd7f9f 100644 (file)
@@ -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);
index 8a0dfc2..7e055b7 100644 (file)
@@ -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";
index 01e0e62..6f2bee8 100644 (file)
@@ -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;
index 3f5fcab..66477cc 100644 (file)
@@ -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) {
index b56fe36..857c620 100644 (file)
@@ -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 : {
index a4e8117..b5b6382 100644 (file)
@@ -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
index cbd6f10..2598673 100644 (file)
@@ -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<MonoString>(fully_qualified_name), resultRoot = mono_wasm_new_external_root<MonoObject>(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;
+            }
         }
 
         (<any>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<MonoString>();
     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<any> {
-    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();
index e1caf24..98ca18a 100644 (file)
@@ -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[] = <any>[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<MonoString>(function_name),
         module_name_root = mono_wasm_new_external_root<MonoString>(module_name),
         resultRoot = mono_wasm_new_external_root<MonoObject>(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;
+            }
         }
 
         (<any>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.");
+    }
+}
index 305fe6a..ad1da87 100644 (file)
@@ -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) {
index 0dbe70b..6bd65eb 100644 (file)
@@ -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<number> => {
         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);
         }
     };
index 1ac0ea7..1400f7f 100644 (file)
@@ -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");
index 83df9bd..9e9f5d9 100644 (file)
@@ -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<MonoObject>): void {
     switch (true) {
index bd1ded5..5764e9a 100644 (file)
@@ -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<MonoObject>): 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
index e69883b..753c585 100644 (file)
@@ -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<string, Function> = 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 = [[]];
index 9c85a54..0a6e4d4 100644 (file)
@@ -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();
 
index 6fa9789..6ab0535 100644 (file)
@@ -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<TPort>(message: MonoWorkerMessage<TPo
     return false;
 }
 
-let synchronization_context_installed = false;
-export function install_synchronization_context(): void {
-    if (MonoWasmThreads && !synchronization_context_installed) {
-        runtimeHelpers.javaScriptExports.install_synchronization_context();
-        synchronization_context_installed = true;
+let worker_js_synchronization_context_installed = false;
+
+export function mono_wasm_install_js_worker_interop(install_js_synchronization_context: number): void {
+    if (!MonoWasmThreads) return;
+    bindings_init();
+    if (install_js_synchronization_context && !worker_js_synchronization_context_installed) {
+        worker_js_synchronization_context_installed = true;
+        mono_log_debug("Installed JSSynchronizationContext");
+    }
+    if (install_js_synchronization_context) {
+        Module.runtimeKeepalivePush();
     }
+
+    set_thread_info(pthread_self ? pthread_self.pthreadId : 0, true, true, !!install_js_synchronization_context);
 }
 
-export function assert_synchronization_context(): void {
-    if (MonoWasmThreads) {
-        // TODO mono_assert(synchronization_context_installed, "Synchronization context not installed on the current worker. Please use dedicated worker for working with JavaScript interop.");
+export function mono_wasm_uninstall_js_worker_interop(uninstall_js_synchronization_context: number): void {
+    if (!MonoWasmThreads) return;
+    mono_assert(runtimeHelpers.mono_wasm_bindings_is_ready, "JS interop is not installed on this worker.");
+    mono_assert(!uninstall_js_synchronization_context || worker_js_synchronization_context_installed, "JSSynchronizationContext is not installed on this worker.");
+
+    forceDisposeProxies(false);
+    if (uninstall_js_synchronization_context) {
+        Module.runtimeKeepalivePop();
     }
+
+    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_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");
     }
 }
 
+// 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" && !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;
+        }
+    }
+}
index 2d44436..9ff8394 100644 (file)
@@ -4,8 +4,9 @@
 /// <reference lib="webworker" />
 
 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.
index 5839b45..3c8e46d 100644 (file)
@@ -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);
index b0ce9c0..1309cf8 100644 (file)
@@ -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();
 }
index bbb7816..e9eee44 100644 (file)
@@ -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 = <any>Module._malloc(4);
index 5816f9a..f2dba23 100644 (file)
@@ -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<void>;
+    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.
index 697c8c2..bf75f46 100644 (file)
@@ -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.
index 771208e..7bc98cd 100644 (file)
       <EmccExportedRuntimeMethod Include="removeRunDependency" />
       <EmccExportedRuntimeMethod Include="addRunDependency" />
       <EmccExportedRuntimeMethod Include="addFunction" />
+      <EmccExportedRuntimeMethod Include="safeSetTimeout" />
+      <EmccExportedRuntimeMethod Include="runtimeKeepalivePush" />
+      <EmccExportedRuntimeMethod Include="runtimeKeepalivePop" />
+      <EmccExportedRuntimeMethod Include="maybeExit" />
 
       <EmccExportedFunction Include="_free" />
       <EmccExportedFunction Include="_htons" />
     <ItemGroup Condition="'$(MonoWasmThreads)' == 'true'">
       <EmccExportedFunction Include="_emscripten_main_runtime_thread_id"  />
     </ItemGroup>
+    <ItemGroup Condition="'$(MonoWasmThreads)' == 'true'">
+      <EmccExportedFunction Include="_emscripten_main_runtime_thread_id"  />
+    </ItemGroup>
     <PropertyGroup>
       <_EmccExportedLibraryFunction>&quot;[@(EmccExportedLibraryFunction -> '%27%(Identity)%27', ',')]&quot;</_EmccExportedLibraryFunction>
       <_EmccExportedRuntimeMethods>&quot;[@(EmccExportedRuntimeMethod -> '%27%(Identity)%27', ',')]&quot;</_EmccExportedRuntimeMethods>
index 51b463c..a249fe6 100644 (file)
@@ -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;