From 3a8eaea4b3b4a61ad14eafb1e27adc508e5395e8 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 25 Mar 2024 12:28:01 +0100 Subject: [PATCH 1/5] rebase, less --- .../ref/System.Private.CoreLib.ExtraApis.cs | 2 + .../ref/System.Private.CoreLib.ExtraApis.txt | 1 + .../src/System/Threading/Thread.cs | 22 +++++- .../JavaScript/Interop/JavaScriptExports.cs | 71 ++++++++++++------- .../JavaScript/JSFunctionBinding.cs | 26 ++----- .../JavaScript/JSHostImplementation.Types.cs | 36 ++-------- .../JavaScript/JSProxyContext.cs | 6 +- .../JavaScript/JSSynchronizationContext.cs | 13 +++- .../JavaScript/WebWorkerTest.Http.cs | 2 +- .../JavaScript/WebWorkerTest.cs | 53 +++++++++----- .../JavaScript/WebWorkerTestBase.cs | 60 ++++++++++------ .../JavaScript/WebWorkerTestHelper.cs | 2 +- .../CompatibilitySuppressions.Threading.xml | 4 ++ src/mono/browser/runtime/corebindings.c | 2 + src/mono/browser/runtime/exports-binding.ts | 4 +- src/mono/browser/runtime/interp-pgo.ts | 2 - src/mono/browser/runtime/loader/config.ts | 37 +--------- src/mono/browser/runtime/managed-exports.ts | 36 +++++----- src/mono/browser/runtime/multi-threading.md | 52 -------------- src/mono/browser/runtime/pthreads/index.ts | 8 +++ src/mono/browser/runtime/startup.ts | 20 ++---- src/mono/browser/runtime/types/internal.ts | 63 ++++++++-------- src/mono/browser/test-main.js | 3 +- .../sample/wasm/browser-threads/Program.cs | 7 +- src/mono/sample/wasm/browser-threads/main.js | 3 + 25 files changed, 249 insertions(+), 286 deletions(-) delete mode 100644 src/mono/browser/runtime/multi-threading.md diff --git a/src/libraries/System.Private.CoreLib/ref/System.Private.CoreLib.ExtraApis.cs b/src/libraries/System.Private.CoreLib/ref/System.Private.CoreLib.ExtraApis.cs index b16548c7b4c37..45a5b9f037269 100644 --- a/src/libraries/System.Private.CoreLib/ref/System.Private.CoreLib.ExtraApis.cs +++ b/src/libraries/System.Private.CoreLib/ref/System.Private.CoreLib.ExtraApis.cs @@ -45,6 +45,8 @@ public partial class Thread { [ThreadStatic] public static bool ThrowOnBlockingWaitOnJSInteropThread; + [ThreadStatic] + public static bool WarnOnBlockingWaitOnJSInteropThread; public static void AssureBlockingPossible() { throw null; } public static void ForceBlockingWait(Action action, object? state) { throw null; } diff --git a/src/libraries/System.Private.CoreLib/ref/System.Private.CoreLib.ExtraApis.txt b/src/libraries/System.Private.CoreLib/ref/System.Private.CoreLib.ExtraApis.txt index 3b80cb0de6753..2a6434973ff1e 100644 --- a/src/libraries/System.Private.CoreLib/ref/System.Private.CoreLib.ExtraApis.txt +++ b/src/libraries/System.Private.CoreLib/ref/System.Private.CoreLib.ExtraApis.txt @@ -7,4 +7,5 @@ T:System.Diagnostics.DebugProvider M:System.Diagnostics.Debug.SetProvider(System.Diagnostics.DebugProvider) M:System.Threading.Thread.AssureBlockingPossible F:System.Threading.Thread.ThrowOnBlockingWaitOnJSInteropThread +F:System.Threading.Thread.WarnOnBlockingWaitOnJSInteropThread F:System.Threading.Thread.ForceBlockingWait diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs index 9d3fd7a0466d7..3ef77076a0198 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs @@ -729,26 +729,46 @@ public static int GetCurrentProcessorId() [ThreadStatic] public static bool ThrowOnBlockingWaitOnJSInteropThread; - public static void AssureBlockingPossible() + [ThreadStatic] + public static bool WarnOnBlockingWaitOnJSInteropThread; + +#pragma warning disable CS3001 + [MethodImplAttribute(MethodImplOptions.InternalCall)] + private static extern unsafe void WarnAboutBlockingWait(char* stack, int length); + + public static unsafe void AssureBlockingPossible() { if (ThrowOnBlockingWaitOnJSInteropThread) { throw new PlatformNotSupportedException(SR.WasmThreads_BlockingWaitNotSupportedOnJSInterop); } + else if (WarnOnBlockingWaitOnJSInteropThread) + { + var st = $"Blocking the thread with JS interop is dangerous and could lead to deadlock. ManagedThreadId: {Environment.CurrentManagedThreadId}\n{Environment.StackTrace}"; + fixed (char* stack = st) + { + WarnAboutBlockingWait(stack, st.Length); + } + } } +#pragma warning restore CS3001 + public static void ForceBlockingWait(Action action, object? state = null) { var flag = ThrowOnBlockingWaitOnJSInteropThread; + var wflag = WarnOnBlockingWaitOnJSInteropThread; try { ThrowOnBlockingWaitOnJSInteropThread = false; + WarnOnBlockingWaitOnJSInteropThread = false; action(state); } finally { ThrowOnBlockingWaitOnJSInteropThread = flag; + WarnOnBlockingWaitOnJSInteropThread = wflag; } } #endif diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs index 6faf786f3bd53..c7bb4a81d3bd5 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs @@ -123,18 +123,25 @@ public static void CallDelegate(JSMarshalerArgument* arguments_buffer) // arg_2 set by JS caller when there are arguments // arg_3 set by JS caller when there are arguments // arg_4 set by JS caller when there are arguments +#if !FEATURE_WASM_MANAGED_THREADS try { -#if FEATURE_WASM_MANAGED_THREADS - // when we arrive here, we are on the thread which owns the proxies - // if we need to dispatch the call to another thread in the future - // we may need to consider how to solve blocking of the synchronous call - // see also https://github.com/dotnet/runtime/issues/76958#issuecomment-1921418290 - arg_exc.AssertCurrentThreadContext(); +#else + // when we arrive here, we are on the thread which owns the proxies + var ctx = arg_exc.AssertCurrentThreadContext(); - if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.AllowBlockingWaitInAsyncCode) + try + { + if (ctx.IsMainThread) { - Thread.ThrowOnBlockingWaitOnJSInteropThread = true; + if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.ThrowWhenBlockingWait) + { + Thread.ThrowOnBlockingWaitOnJSInteropThread = true; + } + else if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.WarnWhenBlockingWait) + { + Thread.WarnOnBlockingWaitOnJSInteropThread = true; + } } #endif @@ -156,9 +163,16 @@ public static void CallDelegate(JSMarshalerArgument* arguments_buffer) #if FEATURE_WASM_MANAGED_THREADS finally { - if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.AllowBlockingWaitInAsyncCode) + if (ctx.IsMainThread) { - Thread.ThrowOnBlockingWaitOnJSInteropThread = false; + if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.ThrowWhenBlockingWait) + { + Thread.ThrowOnBlockingWaitOnJSInteropThread = false; + } + else if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.WarnWhenBlockingWait) + { + Thread.WarnOnBlockingWaitOnJSInteropThread = false; + } } } #endif @@ -189,12 +203,9 @@ public static void CompleteTask(JSMarshalerArgument* arguments_buffer) } } - if (holder.CallbackReady != null) - { -#pragma warning disable CA1416 // Validate platform compatibility - Thread.ForceBlockingWait(static (callbackReady) => ((ManualResetEventSlim)callbackReady!).Wait(), holder.CallbackReady); -#pragma warning restore CA1416 // Validate platform compatibility - } + // this is always running on I/O thread, so it will not throw PNSE + // it's also OK to block here, because we know we will only block shortly, as this is just race with the other thread. + holder.CallbackReady?.Wait(); lock (ctx) { @@ -247,21 +258,17 @@ public static void GetManagedStackTrace(JSMarshalerArgument* arguments_buffer) // this is here temporarily, until JSWebWorker becomes public API [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, "System.Runtime.InteropServices.JavaScript.JSWebWorker", "System.Runtime.InteropServices.JavaScript")] - // the marshaled signature is: GCHandle InstallMainSynchronizationContext(nint jsNativeTID, JSThreadBlockingMode jsThreadBlockingMode, JSThreadInteropMode jsThreadInteropMode, MainThreadingMode mainThreadingMode) + // the marshaled signature is: GCHandle InstallMainSynchronizationContext(nint jsNativeTID, JSThreadBlockingMode jsThreadBlockingMode) public static void InstallMainSynchronizationContext(JSMarshalerArgument* arguments_buffer) { ref JSMarshalerArgument arg_exc = ref arguments_buffer[0]; // initialized by caller in alloc_stack_frame() ref JSMarshalerArgument arg_res = ref arguments_buffer[1];// initialized and set by caller ref JSMarshalerArgument arg_1 = ref arguments_buffer[2];// initialized and set by caller ref JSMarshalerArgument arg_2 = ref arguments_buffer[3];// initialized and set by caller - ref JSMarshalerArgument arg_3 = ref arguments_buffer[4];// initialized and set by caller - ref JSMarshalerArgument arg_4 = ref arguments_buffer[5];// initialized and set by caller try { JSProxyContext.ThreadBlockingMode = (JSHostImplementation.JSThreadBlockingMode)arg_2.slot.Int32Value; - JSProxyContext.ThreadInteropMode = (JSHostImplementation.JSThreadInteropMode)arg_3.slot.Int32Value; - JSProxyContext.MainThreadingMode = (JSHostImplementation.MainThreadingMode)arg_4.slot.Int32Value; var jsSynchronizationContext = JSSynchronizationContext.InstallWebWorkerInterop(true, CancellationToken.None); jsSynchronizationContext.ProxyContext.JSNativeTID = arg_1.slot.IntPtrValue; arg_res.slot.GCHandle = jsSynchronizationContext.ProxyContext.ContextHandle; @@ -283,9 +290,16 @@ public static void BeforeSyncJSExport(JSMarshalerArgument* arguments_buffer) { var ctx = arg_exc.AssertCurrentThreadContext(); ctx.IsPendingSynchronousCall = true; - if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.AllowBlockingWaitInAsyncCode) + if (ctx.IsMainThread) { - Thread.ThrowOnBlockingWaitOnJSInteropThread = true; + if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.ThrowWhenBlockingWait) + { + Thread.ThrowOnBlockingWaitOnJSInteropThread = true; + } + else if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.WarnWhenBlockingWait) + { + Thread.WarnOnBlockingWaitOnJSInteropThread = true; + } } } catch (Exception ex) @@ -305,9 +319,16 @@ public static void AfterSyncJSExport(JSMarshalerArgument* arguments_buffer) { var ctx = arg_exc.AssertCurrentThreadContext(); ctx.IsPendingSynchronousCall = false; - if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.AllowBlockingWaitInAsyncCode) + if (ctx.IsMainThread) { - Thread.ThrowOnBlockingWaitOnJSInteropThread = false; + if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.ThrowWhenBlockingWait) + { + Thread.ThrowOnBlockingWaitOnJSInteropThread = false; + } + else if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.WarnWhenBlockingWait) + { + Thread.WarnOnBlockingWaitOnJSInteropThread = false; + } } } catch (Exception ex) diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSFunctionBinding.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSFunctionBinding.cs index 550956cce3309..c4963b52d04a3 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSFunctionBinding.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSFunctionBinding.cs @@ -230,11 +230,7 @@ internal static unsafe void InvokeJSFunction(JSObject jsFunction, Span arguments) { #if FEATURE_WASM_MANAGED_THREADS - if (JSProxyContext.ThreadInteropMode == JSHostImplementation.JSThreadInteropMode.NoSyncJSInterop) - { - throw new PlatformNotSupportedException("Cannot call synchronous JS functions."); - } - else if (jsFunction.ProxyContext.IsPendingSynchronousCall) + if (jsFunction.ProxyContext.IsPendingSynchronousCall && jsFunction.ProxyContext.IsMainThread) { throw new PlatformNotSupportedException("Cannot call synchronous JS function from inside a synchronous call to a C# method."); } @@ -260,11 +256,7 @@ internal static unsafe void InvokeJSFunctionCurrent(JSObject jsFunction, Span arguments) { #if FEATURE_WASM_MANAGED_THREADS - if (JSProxyContext.ThreadInteropMode == JSHostImplementation.JSThreadInteropMode.NoSyncJSInterop) - { - throw new PlatformNotSupportedException("Cannot call synchronous JS functions."); - } - else if (jsFunction.ProxyContext.IsPendingSynchronousCall) + if (jsFunction.ProxyContext.IsPendingSynchronousCall && jsFunction.ProxyContext.IsMainThread) { throw new PlatformNotSupportedException("Cannot call synchronous JS function from inside a synchronous call to a C# method."); } @@ -274,10 +266,8 @@ internal static unsafe void DispatchJSFunctionSync(JSObject jsFunction, Span(async () => { CancellationTokenSource cts = new CancellationTokenSource(); var promise = response.Content.ReadAsStringAsync(cts.Token); - Console.WriteLine("HttpClient_CancelInDifferentThread: ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId); + WebWorkerTestHelper.Log("HttpClient_CancelInDifferentThread: ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId); cts.Cancel(); var res = await promise; throw new Exception("This should be unreachable: " + res); diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs index c67bac997294b..0a2ae44142dc3 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs @@ -11,15 +11,9 @@ namespace System.Runtime.InteropServices.JavaScript.Tests // TODO test: // JSExport 2x - // JSExport async - // lock - // thread allocation, many threads // ProxyContext flow, child thread, child task // use JSObject after JSWebWorker finished, especially HTTP - // WS on JSWebWorker - // HTTP continue on TP // event pipe - // FS // JS setTimeout till after JSWebWorker close // synchronous .Wait for JS setTimeout on the same thread -> deadlock problem **7)** @@ -159,7 +153,7 @@ public async Task JSSynchronizationContext_Send_Post_Items_Cancellation() } catch (Exception ex) { - Console.WriteLine("Unexpected exception " + ex); + WebWorkerTestHelper.Log("Unexpected exception " + ex); postReady.SetException(ex); return Task.FromException(ex); } @@ -344,7 +338,7 @@ public async Task ManagedConsole(Executor executor) using var cts = CreateTestCaseTimeoutSource(); await executor.Execute(() => { - Console.WriteLine("C# Hello from ManagedThreadId: " + Environment.CurrentManagedThreadId); + WebWorkerTestHelper.Log("C# Hello from ManagedThreadId: " + Environment.CurrentManagedThreadId); Console.Clear(); return Task.CompletedTask; }, cts.Token); @@ -392,7 +386,7 @@ public async Task ThreadingTimer(Executor executor) await executor.Execute(async () => { TaskCompletionSource tcs = new TaskCompletionSource(); - Console.WriteLine("ThreadingTimer: Start Time: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + " ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId); + WebWorkerTestHelper.Log("ThreadingTimer: Start Time: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + " ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId); using var timer = new Timer(_ => { @@ -405,7 +399,7 @@ await executor.Execute(async () => await tcs.Task; }, cts.Token); - Console.WriteLine("ThreadingTimer: End Time: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + " ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId); + WebWorkerTestHelper.Log("ThreadingTimer: End Time: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + " ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId); Assert.True(hit); } @@ -496,7 +490,7 @@ await executor.Execute(async () => } [Theory, MemberData(nameof(GetTargetThreadsAndBlockingCalls))] - public async Task WaitDoesNotAssertInAsyncCode(Executor executor, NamedCall method) + public async Task WaitInAsyncAssertsOnlyOnJSWebWorker(Executor executor, NamedCall method) { using var cts = CreateTestCaseTimeoutSource(); await executor.Execute(async () => @@ -513,7 +507,15 @@ await executor.Execute(async () => exception = ex; } - Assert.Null(exception); + if (method.IsBlocking && executor.Type == ExecutorType.JSWebWorker) + { + Assert.NotNull(exception); + Assert.IsType(exception); + } + else + { + Assert.Null(exception); + } }, cts.Token); } @@ -527,7 +529,8 @@ await executor.Execute(async () => Exception? exception = null; // the callback will hit Main or JSWebWorker, not the original executor thread - await WebWorkerTestHelper.CallMeBackSync(() => { + await WebWorkerTestHelper.CallMeBackSync(() => + { // when we are inside of synchronous callback, all blocking .Wait is forbidden try { @@ -539,9 +542,15 @@ await WebWorkerTestHelper.CallMeBackSync(() => { } }); - Console.WriteLine("WaitAssertsOnJSInteropThreads: ExecuterType: " + executor.Type + " ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId); - Assert.NotNull(exception); - Assert.IsType(exception); + if (method.IsBlocking) + { + Assert.NotNull(exception); + Assert.IsType(exception); + } + else + { + Assert.Null(exception); + } }, cts.Token); } @@ -558,9 +567,15 @@ await executor.Execute(async () => // the callback will hit Main or JSWebWorker, not the original executor thread await WebWorkerTestHelper.CallExportBackSync(nameof(WebWorkerTestHelper.CallCurrentCallback)); - Console.WriteLine("WaitAssertsOnJSInteropThreads: ExecuterType: " + executor.Type + " ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId); - Assert.NotNull(WebWorkerTestHelper.LastException); - Assert.IsType(WebWorkerTestHelper.LastException); + if (method.IsBlocking) + { + Assert.NotNull(WebWorkerTestHelper.LastException); + Assert.IsType(WebWorkerTestHelper.LastException); + } + else + { + Assert.Null(WebWorkerTestHelper.LastException); + } }, cts.Token); } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestBase.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestBase.cs index 1df4c61e6bcbc..ca641c8da97d2 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestBase.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestBase.cs @@ -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. +using System.IO; using System.Threading.Tasks; using System.Threading; using Xunit; @@ -35,7 +36,7 @@ protected CancellationTokenSource CreateTestCaseTimeoutSource([CallerMemberName] cts.Token.Register(() => { var end = DateTime.Now; - Console.WriteLine($"Unexpected test case {memberName} timeout after {end - start} ManagedThreadId:{Environment.CurrentManagedThreadId}"); + WebWorkerTestHelper.Log($"Unexpected test case {memberName} timeout after {end - start} ManagedThreadId:{Environment.CurrentManagedThreadId}"); }); return cts; } @@ -90,7 +91,7 @@ async Task ActionsInDifferentThreads1() } catch (Exception ex) { - Console.WriteLine("ActionsInDifferentThreads1 failed\n" + ex); + WebWorkerTestHelper.Log("ActionsInDifferentThreads1 failed\n" + ex); job1ReadyTCS.SetResult(default); e1Failed = true; throw; @@ -137,36 +138,53 @@ async Task ActionsInDifferentThreads2() } if (!e1Done || !e2Done) { - Console.WriteLine("ActionsInDifferentThreads canceling because of unexpected fail: \n" + ex); + WebWorkerTestHelper.Log("ActionsInDifferentThreads canceling because of unexpected fail: \n" + ex); cts.Cancel(); } else { - Console.WriteLine("ActionsInDifferentThreads failed with: \n" + ex); + WebWorkerTestHelper.Log("ActionsInDifferentThreads failed with: \n" + ex); } throw; } } + static void LocalCtsIgnoringCall(Action action) + { + var cts = new CancellationTokenSource(8); + try + { + action(cts.Token); + } + catch (OperationCanceledException exception) + { + if (exception.CancellationToken != cts.Token) + { + throw; + } + /* ignore the local one */ + } + } + public static IEnumerable BlockingCalls = new List { - new NamedCall { Name = "Task.Wait", Call = delegate (CancellationToken ct) { Task.Delay(10, ct).Wait(ct); }}, - new NamedCall { Name = "Task.WaitAll", Call = delegate (CancellationToken ct) { Task.WaitAll(Task.Delay(10, ct)); }}, - new NamedCall { Name = "Task.WaitAny", Call = delegate (CancellationToken ct) { Task.WaitAny(Task.Delay(10, ct)); }}, - new NamedCall { Name = "ManualResetEventSlim.Wait", Call = delegate (CancellationToken ct) { - using var mr = new ManualResetEventSlim(false); - using var cts = new CancellationTokenSource(8); - try { - mr.Wait(cts.Token); - } catch (OperationCanceledException) { /* ignore */ } - }}, - new NamedCall { Name = "SemaphoreSlim.Wait", Call = delegate (CancellationToken ct) { - using var sem = new SemaphoreSlim(2); - var cts = new CancellationTokenSource(8); - try { - sem.Wait(cts.Token); - } catch (OperationCanceledException) { /* ignore */ } - }}, + // things that should NOT throw PNSE + new NamedCall { IsBlocking = false, Name = "Console.WriteLine", Call = delegate (CancellationToken ct) { Console.WriteLine("Blocking"); }}, + new NamedCall { IsBlocking = false, Name = "Directory.GetCurrentDirectory", Call = delegate (CancellationToken ct) { Directory.GetCurrentDirectory(); }}, + new NamedCall { IsBlocking = false, Name = "CancellationTokenSource.ctor", Call = delegate (CancellationToken ct) { + using var cts = new CancellationTokenSource(8); + }}, + new NamedCall { IsBlocking = false, Name = "Task.Delay", Call = delegate (CancellationToken ct) { + Task.Delay(30, ct); + }}, + new NamedCall { IsBlocking = false, Name = "new Timer", Call = delegate (CancellationToken ct) { + new Timer((_) => { }, null, 1, -1); + }}, + + // things which should throw PNSE on sync JSExport and JSWebWorker + new NamedCall { IsBlocking = true, Name = "Task.Wait", Call = delegate (CancellationToken ct) { Task.Delay(30, ct).Wait(ct); }}, + new NamedCall { IsBlocking = true, Name = "Task.WaitAll", Call = delegate (CancellationToken ct) { Task.WaitAll(Task.Delay(30, ct)); }}, + new NamedCall { IsBlocking = true, Name = "Task.WaitAny", Call = delegate (CancellationToken ct) { Task.WaitAny(Task.Delay(30, ct)); }}, }; public static IEnumerable GetTargetThreadsAndBlockingCalls() diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.cs index 9a1856780d5d5..fa83846f14929 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.cs @@ -385,10 +385,10 @@ public static Task RunOnTargetAsync(SynchronizationContext ctx, Func job, public class NamedCall { public string Name { get; set; } + public bool IsBlocking { get; set; } public delegate void Method(CancellationToken ct); public Method Call { get; set; } override public string ToString() => Name; } - } diff --git a/src/libraries/System.Threading.Thread/src/CompatibilitySuppressions.Threading.xml b/src/libraries/System.Threading.Thread/src/CompatibilitySuppressions.Threading.xml index 5fc41e30ed441..8eb2f78e6a706 100644 --- a/src/libraries/System.Threading.Thread/src/CompatibilitySuppressions.Threading.xml +++ b/src/libraries/System.Threading.Thread/src/CompatibilitySuppressions.Threading.xml @@ -20,6 +20,10 @@ CP0002 F:System.Threading.Thread.ThrowOnBlockingWaitOnJSInteropThread + + CP0002 + F:System.Threading.Thread.WarnOnBlockingWaitOnJSInteropThread + CP0002 M:System.Threading.Thread.AssureBlockingPossible diff --git a/src/mono/browser/runtime/corebindings.c b/src/mono/browser/runtime/corebindings.c index b2a271b4e91eb..7e4db51b5568c 100644 --- a/src/mono/browser/runtime/corebindings.c +++ b/src/mono/browser/runtime/corebindings.c @@ -51,6 +51,7 @@ void mono_wasm_invoke_js_function_send (pthread_t target_tid, int function_js_ha extern void mono_threads_wasm_async_run_in_target_thread_vi (pthread_t target_thread, void (*func) (gpointer), gpointer user_data1); extern void mono_threads_wasm_async_run_in_target_thread_vii (pthread_t target_thread, void (*func) (gpointer, gpointer), gpointer user_data1, gpointer user_data2); extern void mono_threads_wasm_sync_run_in_target_thread_vii (pthread_t target_thread, void (*func) (gpointer, gpointer), gpointer user_data1, gpointer args); +extern void mono_wasm_warn_about_blocking_wait (void* ptr, int32_t length); #else extern void mono_wasm_bind_js_import (void *signature, int *is_exception, MonoObject **result); extern void mono_wasm_invoke_jsimport_ST (int function_handle, void *args); @@ -85,6 +86,7 @@ void bindings_initialize_internals (void) mono_add_internal_call ("Interop/Runtime::InvokeJSImportAsyncPost", mono_wasm_invoke_jsimport_async_post); mono_add_internal_call ("Interop/Runtime::InvokeJSFunctionSend", mono_wasm_invoke_js_function_send); mono_add_internal_call ("Interop/Runtime::CancelPromisePost", mono_wasm_cancel_promise_post); + mono_add_internal_call ("System.Threading.Thread::WarnAboutBlockingWait", mono_wasm_warn_about_blocking_wait); #else mono_add_internal_call ("Interop/Runtime::BindJSImport", mono_wasm_bind_js_import); mono_add_internal_call ("Interop/Runtime::InvokeJSImportST", mono_wasm_invoke_jsimport_ST); diff --git a/src/mono/browser/runtime/exports-binding.ts b/src/mono/browser/runtime/exports-binding.ts index bb88ac3b65fbb..567666a96b84c 100644 --- a/src/mono/browser/runtime/exports-binding.ts +++ b/src/mono/browser/runtime/exports-binding.ts @@ -29,11 +29,10 @@ import { mono_wasm_cancel_promise } from "./cancelable-promise"; import { mono_wasm_start_deputy_thread_async, mono_wasm_pthread_on_pthread_attached, mono_wasm_pthread_on_pthread_unregistered, - mono_wasm_pthread_on_pthread_registered, mono_wasm_pthread_set_name, mono_wasm_install_js_worker_interop, mono_wasm_uninstall_js_worker_interop, mono_wasm_start_io_thread_async + mono_wasm_pthread_on_pthread_registered, mono_wasm_pthread_set_name, mono_wasm_install_js_worker_interop, mono_wasm_uninstall_js_worker_interop, mono_wasm_start_io_thread_async, mono_wasm_warn_about_blocking_wait } from "./pthreads"; import { mono_wasm_dump_threads } from "./pthreads/ui-thread"; - // the JS methods would be visible to EMCC linker and become imports of the WASM module export const mono_wasm_threads_imports = !WasmEnableThreads ? [] : [ @@ -56,6 +55,7 @@ export const mono_wasm_threads_imports = !WasmEnableThreads ? [] : [ mono_wasm_install_js_worker_interop, mono_wasm_uninstall_js_worker_interop, mono_wasm_invoke_jsimport_MT, + mono_wasm_warn_about_blocking_wait, ]; export const mono_wasm_imports = [ diff --git a/src/mono/browser/runtime/interp-pgo.ts b/src/mono/browser/runtime/interp-pgo.ts index c4214b4fe5d6a..79678489df6e2 100644 --- a/src/mono/browser/runtime/interp-pgo.ts +++ b/src/mono/browser/runtime/interp-pgo.ts @@ -206,9 +206,7 @@ export async function getCacheKey(prefix: string): Promise { delete inputs.enableDownloadRetry; delete inputs.extensions; delete inputs.runtimeId; - delete inputs.mainThreadingMode; delete inputs.jsThreadBlockingMode; - delete inputs.jsThreadInteropMode; inputs.GitHash = loaderHelpers.gitHash; inputs.ProductVersion = ProductVersion; diff --git a/src/mono/browser/runtime/loader/config.ts b/src/mono/browser/runtime/loader/config.ts index 07b7750a2a2c6..5ba2a4476df01 100644 --- a/src/mono/browser/runtime/loader/config.ts +++ b/src/mono/browser/runtime/loader/config.ts @@ -4,7 +4,7 @@ import BuildConfiguration from "consts:configuration"; import WasmEnableThreads from "consts:wasmEnableThreads"; -import { MainThreadingMode, type DotnetModuleInternal, type MonoConfigInternal, JSThreadBlockingMode, JSThreadInteropMode } from "../types/internal"; +import { type DotnetModuleInternal, type MonoConfigInternal, JSThreadBlockingMode } from "../types/internal"; import type { DotnetModuleConfig, MonoConfig, ResourceGroups, ResourceList } from "../types"; import { exportedRuntimeAPI, loaderHelpers, runtimeHelpers } from "./globals"; import { mono_log_error, mono_log_debug } from "./logging"; @@ -12,7 +12,6 @@ import { importLibraryInitializers, invokeLibraryInitializers } from "./libraryI import { mono_exit } from "./exit"; import { makeURLAbsoluteWithApplicationBase } from "./polyfills"; import { appendUniqueQuery } from "./assets"; -import { mono_log_warn } from "./logging"; export function deep_merge_config(target: MonoConfigInternal, source: MonoConfigInternal): MonoConfigInternal { // no need to merge the same object @@ -198,40 +197,8 @@ export function normalizeConfig() { if (!Number.isInteger(config.finalizerThreadStartDelayMs)) { config.finalizerThreadStartDelayMs = 200; } - if (config.mainThreadingMode == undefined) { - config.mainThreadingMode = MainThreadingMode.DeputyAndIOThreads; - } if (config.jsThreadBlockingMode == undefined) { - config.jsThreadBlockingMode = JSThreadBlockingMode.AllowBlockingWaitInAsyncCode; - } - if (config.jsThreadInteropMode == undefined) { - config.jsThreadInteropMode = JSThreadInteropMode.SimpleSynchronousJSInterop; - } - let validModes = false; - if (config.mainThreadingMode == MainThreadingMode.DeputyThread - && config.jsThreadBlockingMode == JSThreadBlockingMode.NoBlockingWait - && config.jsThreadInteropMode == JSThreadInteropMode.SimpleSynchronousJSInterop - ) { - validModes = true; - } - else if (config.mainThreadingMode == MainThreadingMode.DeputyAndIOThreads - && config.jsThreadBlockingMode == JSThreadBlockingMode.AllowBlockingWaitInAsyncCode - && config.jsThreadInteropMode == JSThreadInteropMode.SimpleSynchronousJSInterop - ) { - validModes = true; - } - else if (config.mainThreadingMode == MainThreadingMode.DeputyThread - && config.jsThreadBlockingMode == JSThreadBlockingMode.AllowBlockingWait - && config.jsThreadInteropMode == JSThreadInteropMode.SimpleSynchronousJSInterop - ) { - validModes = true; - } - if (!validModes) { - mono_log_warn("Unsupported threading configuration", { - mainThreadingMode: config.mainThreadingMode, - jsThreadBlockingMode: config.jsThreadBlockingMode, - jsThreadInteropMode: config.jsThreadInteropMode - }); + config.jsThreadBlockingMode = JSThreadBlockingMode.PreventSynchronousJSExport; } } diff --git a/src/mono/browser/runtime/managed-exports.ts b/src/mono/browser/runtime/managed-exports.ts index df99745d6af37..1b9be70d1b203 100644 --- a/src/mono/browser/runtime/managed-exports.ts +++ b/src/mono/browser/runtime/managed-exports.ts @@ -3,7 +3,7 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; -import { GCHandle, GCHandleNull, JSMarshalerArguments, JSThreadInteropMode, MarshalerToCs, MarshalerToJs, MarshalerType, MonoMethod, PThreadPtr } from "./types/internal"; +import { GCHandle, GCHandleNull, JSMarshalerArguments, JSThreadBlockingMode, MarshalerToCs, MarshalerToJs, MarshalerType, MonoMethod, PThreadPtr } from "./types/internal"; import cwraps, { threads_c_functions as twraps } from "./cwraps"; import { runtimeHelpers, Module, loaderHelpers, mono_assert } from "./globals"; import { JavaScriptMarshalerArgSize, alloc_stack_frame, get_arg, get_arg_gc_handle, is_args_exception, set_arg_i32, set_arg_intptr, set_arg_type, set_gc_handle, set_receiver_should_free } from "./marshal"; @@ -165,11 +165,13 @@ export function complete_task(holder_gc_handle: GCHandle, error?: any, data?: an export function call_delegate(callback_gc_handle: GCHandle, arg1_js: any, arg2_js: any, arg3_js: any, res_converter?: MarshalerToJs, arg1_converter?: MarshalerToCs, arg2_converter?: MarshalerToCs, arg3_converter?: MarshalerToCs) { loaderHelpers.assert_runtime_running(); if (WasmEnableThreads) { - if (runtimeHelpers.config.jsThreadInteropMode == JSThreadInteropMode.NoSyncJSInterop) { - throw new Error("Cannot call synchronous C# methods."); - } - else if (runtimeHelpers.isPendingSynchronousCall) { - throw new Error("Cannot call synchronous C# method from inside a synchronous call to a JS method."); + if (monoThreadInfo.isUI) { + if (runtimeHelpers.config.jsThreadBlockingMode == JSThreadBlockingMode.PreventSynchronousJSExport) { + throw new Error("Cannot call synchronous C# methods."); + } + else if (runtimeHelpers.isPendingSynchronousCall) { + throw new Error("Cannot call synchronous C# method from inside a synchronous call to a JS method."); + } } } const sp = Module.stackSave(); @@ -226,26 +228,22 @@ export function get_managed_stack_trace(exception_gc_handle: GCHandle) { } } -// GCHandle InstallMainSynchronizationContext(nint jsNativeTID, JSThreadBlockingMode jsThreadBlockingMode, JSThreadInteropMode jsThreadInteropMode, MainThreadingMode mainThreadingMode) -export function install_main_synchronization_context(jsThreadBlockingMode: number, jsThreadInteropMode: number, mainThreadingMode: number): GCHandle { +// GCHandle InstallMainSynchronizationContext(nint jsNativeTID, JSThreadBlockingMode jsThreadBlockingMode) +export function install_main_synchronization_context(jsThreadBlockingMode: number): GCHandle { if (!WasmEnableThreads) return GCHandleNull; assert_c_interop(); try { // this block is like alloc_stack_frame() but without set_args_context() - const bytes = JavaScriptMarshalerArgSize * 6; + const bytes = JavaScriptMarshalerArgSize * 4; const args = Module.stackAlloc(bytes) as any; _zero_region(args, bytes); const res = get_arg(args, 1); const arg1 = get_arg(args, 2); const arg2 = get_arg(args, 3); - const arg3 = get_arg(args, 4); - const arg4 = get_arg(args, 5); set_arg_intptr(arg1, mono_wasm_main_thread_ptr() as any); set_arg_i32(arg2, jsThreadBlockingMode); - set_arg_i32(arg3, jsThreadInteropMode); - set_arg_i32(arg4, mainThreadingMode); // this block is like invoke_sync_jsexport() but without assert_js_interop() cwraps.mono_wasm_invoke_jsexport(managedExports.InstallMainSynchronizationContext!, args); @@ -282,11 +280,13 @@ export function invoke_sync_jsexport(method: MonoMethod, args: JSMarshalerArgume if (!WasmEnableThreads) { cwraps.mono_wasm_invoke_jsexport(method, args as any); } else { - if (runtimeHelpers.config.jsThreadInteropMode == JSThreadInteropMode.NoSyncJSInterop) { - throw new Error("Cannot call synchronous C# methods."); - } - else if (runtimeHelpers.isPendingSynchronousCall) { - throw new Error("Cannot call synchronous C# method from inside a synchronous call to a JS method."); + if (monoThreadInfo.isUI) { + if (runtimeHelpers.config.jsThreadBlockingMode == JSThreadBlockingMode.PreventSynchronousJSExport) { + throw new Error("Cannot call synchronous C# methods."); + } + else if (runtimeHelpers.isPendingSynchronousCall) { + throw new Error("Cannot call synchronous C# method from inside a synchronous call to a JS method."); + } } if (runtimeHelpers.isManagedRunningOnCurrentThread) { twraps.mono_wasm_invoke_jsexport_sync(method, args as any); diff --git a/src/mono/browser/runtime/multi-threading.md b/src/mono/browser/runtime/multi-threading.md deleted file mode 100644 index e4b3985923d50..0000000000000 --- a/src/mono/browser/runtime/multi-threading.md +++ /dev/null @@ -1,52 +0,0 @@ -# Multi-threading with JavaScript interop - -## Meaningful configurations are: - - * Single-threaded mode as you know it since .Net 6 - - default, safe, tested, supported - - from .Net 8 it could be easily started also as a web worker, but you need your own messaging between main and worker - * `MainThreadingMode.DeputyThread` + `JSThreadBlockingMode.NoBlockingWait` + `JSThreadInteropMode.SimpleSynchronousJSInterop` - + **default threading**, safe, tested, supported - + blocking `.Wait` is allowed on thread pool and new threads - - blocking `.Wait` throws `PlatformNotSupportedException` on `JSWebWorker` and main thread - - DOM events like `onClick` need to be asynchronous, if the handler needs use synchronous `[JSImport]` - - synchronous calls to `[JSImport]`/`[JSExport]` can't synchronously call back - - * `MainThreadingMode.DeputyAndIOThreads` + `JSThreadBlockingMode.AllowBlockingWaitInAsyncCode` + `JSThreadInteropMode.SimpleSynchronousJSInterop` - + **default threading**, safe, tested, supported - + blocking `.Wait` is allowed on thread pool and new threads - - blocking `.Wait` throws `PlatformNotSupportedException` on `JSWebWorker` and main thread only when they are called from JS via synchronous `JSExport` - - DOM events like `onClick` need to be asynchronous, if the handler needs use synchronous `[JSImport]` - - synchronous calls to `[JSImport]`/`[JSExport]` can't synchronously call back - - * `MainThreadingMode.DeputyThread` + `JSThreadBlockingMode.AllowBlockingWait` + `JSThreadInteropMode.SimpleSynchronousJSInterop` - + pragmatic for legacy codebase, which contains blocking code and can't be fully executed on thread pool or new threads - - ** could cause deadlocks !!!** - - Use your own judgment before you opt in. - - blocking .Wait is allowed on all threads! - - blocking .Wait on pending JS `Task`/`Promise` (like HTTP/WS requests) could cause deadlocks! - - reason is that blocked thread can't process the browser event loop - - so it can't resolve the promises - - even when it's longer `Promise`/`Task` chain - - DOM events like `onClick` need to be asynchronous, if the handler needs use synchronous `[JSImport]` - - synchronous calls to `[JSImport]`/`[JSExport]` can't synchronously call back - -## Unsupported combinations are: - * `MainThreadingMode.DeputyThread` + `JSThreadBlockingMode.NoBlockingWait` + `JSThreadInteropMode.NoSyncJSInterop` - + very safe - - HTTP/WS requests are not possible because it currently uses synchronous JS interop - - Blazor doesn't work because it currently uses synchronous JS interop - * `MainThreadingMode.UIThread` - - not recommended, not tested, not supported! - - can deadlock on creating new threads - - can deadlock on blocking `.Wait` for a pending JS `Promise`/`Task`, including HTTP/WS requests - - .Wait is spin-waiting - it blocks debugger, network, UI rendering, ... - + JS interop to UI is faster, synchronous and re-entrant - -### There could be more JSThreadInteropModes: - - allow re-entrant synchronous JS interop on `JSWebWorker`. - - This is possible because managed code is running on same thread as JS. - - But it's nuanced to debug it, when things go wrong. - - allow re-entrant synchronous JS interop also on deputy thread. - - This is not possible for deputy, because it would deadlock on call back to different thread. - - The thread receiving the callback is still blocked waiting for the first synchronous call to finish. diff --git a/src/mono/browser/runtime/pthreads/index.ts b/src/mono/browser/runtime/pthreads/index.ts index 0a5911605282d..57de39e73792e 100644 --- a/src/mono/browser/runtime/pthreads/index.ts +++ b/src/mono/browser/runtime/pthreads/index.ts @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +import { mono_log_warn } from "../logging"; +import { utf16ToString } from "../strings"; + export { mono_wasm_main_thread_ptr, mono_wasm_install_js_worker_interop, mono_wasm_uninstall_js_worker_interop, mono_wasm_pthread_ptr, update_thread_info, isMonoThreadMessage, monoThreadInfo, @@ -18,3 +21,8 @@ export { export { mono_wasm_start_deputy_thread_async } from "./deputy-thread"; export { mono_wasm_start_io_thread_async } from "./io-thread"; + +export function mono_wasm_warn_about_blocking_wait(ptr: number, length: number) { + const warning = utf16ToString(ptr, ptr + (length * 2)); + mono_log_warn(warning); +} \ No newline at end of file diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts index 1ae17e43edf19..8580e5aca4b4b 100644 --- a/src/mono/browser/runtime/startup.ts +++ b/src/mono/browser/runtime/startup.ts @@ -4,7 +4,7 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; import BuildConfiguration from "consts:configuration"; -import { DotnetModuleInternal, CharPtrNull, MainThreadingMode } from "./types/internal"; +import { DotnetModuleInternal, CharPtrNull } from "./types/internal"; import { exportedRuntimeAPI, INTERNAL, loaderHelpers, Module, runtimeHelpers, createPromiseController, mono_assert } from "./globals"; import cwraps, { init_c_exports, threads_c_functions as tcwraps } from "./cwraps"; import { mono_wasm_raise_debug_event, mono_wasm_runtime_ready } from "./debug"; @@ -274,19 +274,14 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { mono_log_info("UI thread is alive!"); }, 3000); - if (WasmEnableThreads && - (runtimeHelpers.config.mainThreadingMode == MainThreadingMode.DeputyThread - || runtimeHelpers.config.mainThreadingMode == MainThreadingMode.DeputyAndIOThreads)) { + if (WasmEnableThreads) { // this will create thread and call start_runtime() on it runtimeHelpers.monoThreadInfo = monoThreadInfo; runtimeHelpers.isManagedRunningOnCurrentThread = false; update_thread_info(); runtimeHelpers.managedThreadTID = tcwraps.mono_wasm_create_deputy_thread(); runtimeHelpers.proxyGCHandle = await runtimeHelpers.afterMonoStarted.promise; - - if (WasmEnableThreads && runtimeHelpers.config.mainThreadingMode == MainThreadingMode.DeputyAndIOThreads) { - runtimeHelpers.ioThreadTID = tcwraps.mono_wasm_create_io_thread(); - } + runtimeHelpers.ioThreadTID = tcwraps.mono_wasm_create_io_thread(); // TODO make UI thread not managed tcwraps.mono_wasm_register_ui_thread(); @@ -301,9 +296,7 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { await start_runtime(); } - if (WasmEnableThreads && runtimeHelpers.config.mainThreadingMode == MainThreadingMode.DeputyAndIOThreads) { - await runtimeHelpers.afterIOStarted.promise; - } + await runtimeHelpers.afterIOStarted.promise; runtimeList.registerRuntime(exportedRuntimeAPI); @@ -541,10 +534,7 @@ export async function start_runtime() { monoThreadInfo.isRegistered = true; runtimeHelpers.currentThreadTID = monoThreadInfo.pthreadId = runtimeHelpers.managedThreadTID = mono_wasm_pthread_ptr(); update_thread_info(); - runtimeHelpers.proxyGCHandle = install_main_synchronization_context( - runtimeHelpers.config.jsThreadBlockingMode!, - runtimeHelpers.config.jsThreadInteropMode!, - runtimeHelpers.config.mainThreadingMode!); + runtimeHelpers.proxyGCHandle = install_main_synchronization_context(runtimeHelpers.config.jsThreadBlockingMode!); runtimeHelpers.isManagedRunningOnCurrentThread = true; // start finalizer thread, lazy diff --git a/src/mono/browser/runtime/types/internal.ts b/src/mono/browser/runtime/types/internal.ts index 16967d26b9d29..22eaebf7545c1 100644 --- a/src/mono/browser/runtime/types/internal.ts +++ b/src/mono/browser/runtime/types/internal.ts @@ -95,9 +95,7 @@ export type MonoConfigInternal = MonoConfig & { GitHash?: string, ProductVersion?: string, - mainThreadingMode?: MainThreadingMode, jsThreadBlockingMode?: JSThreadBlockingMode, - jsThreadInteropMode?: JSThreadInteropMode, }; export type RunArguments = { @@ -569,36 +567,37 @@ export interface MonoThreadMessage { cmd: string; } -// keep in sync with JSHostImplementation.Types.cs -export const enum MainThreadingMode { - // Running the managed main thread on UI thread. - // Managed GC and similar scenarios could be blocking the UI. - // Easy to deadlock. Not recommended for production. - UIThread = 0, - // Running the managed main thread on dedicated WebWorker. Marshaling all JavaScript calls to and from the main thread. - DeputyThread = 1, - // TODO comment - DeputyAndIOThreads = 2, -} - // keep in sync with JSHostImplementation.Types.cs export const enum JSThreadBlockingMode { - // throw PlatformNotSupportedException if blocking .Wait is called on threads with JS interop, like JSWebWorker and Main thread. - // Avoids deadlocks (typically with pending JS promises on the same thread) by throwing exceptions. - NoBlockingWait = 0, - // TODO comment - AllowBlockingWaitInAsyncCode = 1, - // allow .Wait on all threads. - // Could cause deadlocks with blocking .Wait on a pending JS Task/Promise on the same thread or similar Task/Promise chain. - AllowBlockingWait = 100, -} - -// keep in sync with JSHostImplementation.Types.cs -export const enum JSThreadInteropMode { - // throw PlatformNotSupportedException if synchronous JSImport/JSExport is called on threads with JS interop, like JSWebWorker and Main thread. - // calling synchronous JSImport on thread pool or new threads is allowed. - NoSyncJSInterop = 0, - // allow non-re-entrant synchronous blocking calls to and from JS on JSWebWorker on threads with JS interop, like JSWebWorker and Main thread. - // calling synchronous JSImport on thread pool or new threads is allowed. - SimpleSynchronousJSInterop = 1, + /** + * Prevents synchronous JSExport from being called from JavaScript code in UI thread. + * On JSWebWorker synchronous JSExport always works. + * On JSWebWorker blocking .Wait always warns. + * This is the default mode. + */ + PreventSynchronousJSExport = 0, + /** + * Allows synchronous JSExport to be called from JavaScript code also in UI thread. + * Inside of that call blocking .Wait throws PNSE. + * Inside of that call nested call back to synchronous JSImport throws PNSE (because it would deadlock otherwise in 100% cases). + * On JSWebWorker synchronous JSExport always works. + * On JSWebWorker blocking .Wait always throws PNSE. + */ + ThrowWhenBlockingWait = 1, + /** + * Allows synchronous JSExport to be called from JavaScript code also in UI thread. + * Inside of that call blocking .Wait warns. + * Inside of that call nested call back to synchronous JSImport throws PNSE (because it would deadlock otherwise in 100% cases). + * On JSWebWorker synchronous JSExport always works. + * On JSWebWorker blocking .Wait always warns. + */ + WarnWhenBlockingWait = 2, + /** + * Allows synchronous JSExport to be called from JavaScript code, and allows managed code to use blocking .Wait + * .Wait on Promise/Task chains could lead to deadlock because JS event loop is not processed and it can't resolve JS promises. + * This mode is dangerous and not supported. + * Allows synchronous JSExport to be called from JavaScript code also in Main thread. + * Inside of that call nested call back to synchronous JSImport throws PNSE (because it would deadlock otherwise in 100% cases). + */ + DangerousAllowBlockingWait = 100, } \ No newline at end of file diff --git a/src/mono/browser/test-main.js b/src/mono/browser/test-main.js index 3aacd8e2c67d6..c7403640ce51c 100644 --- a/src/mono/browser/test-main.js +++ b/src/mono/browser/test-main.js @@ -252,7 +252,8 @@ function configureRuntime(dotnet, runArgs) { .withInteropCleanupOnExit() .withDumpThreadsOnNonZeroExit() .withConfig({ - loadAllSatelliteResources: true + loadAllSatelliteResources: true, + jsThreadBlockingMode: 1, /* ThrowWhenBlockingWait */ }); if (ENVIRONMENT_IS_NODE) { diff --git a/src/mono/sample/wasm/browser-threads/Program.cs b/src/mono/sample/wasm/browser-threads/Program.cs index 331f8e35de3cc..3eaba624a0f36 100644 --- a/src/mono/sample/wasm/browser-threads/Program.cs +++ b/src/mono/sample/wasm/browser-threads/Program.cs @@ -38,9 +38,14 @@ public static async Task Main(string[] args) public static void Progress2() { // both calls here are sync POSIX calls dispatched to UI thread, which is already blocked because this is synchronous method on deputy thread - // in should not deadlock anyway, see also invoke_later_when_on_ui_thread_sync and emscripten_yield + // it should not deadlock anyway, see also invoke_later_when_on_ui_thread_sync and emscripten_yield var cwd = Directory.GetCurrentDirectory(); Console.WriteLine("Progress! "+ cwd); + + // below is blocking call, which means that UI will spin-lock little longer + // it will warn about blocking wait because of jsThreadBlockingMode: 2 + // but it will not deadlock because underlying task chain is not JS promise + Task.Delay(10).Wait(); } [JSExport] diff --git a/src/mono/sample/wasm/browser-threads/main.js b/src/mono/sample/wasm/browser-threads/main.js index 8da1e4fb608e6..1e9d060f41b49 100644 --- a/src/mono/sample/wasm/browser-threads/main.js +++ b/src/mono/sample/wasm/browser-threads/main.js @@ -17,6 +17,9 @@ try { .withElementOnExit() .withExitCodeLogging() .withExitOnUnhandledError() + .withConfig({ + jsThreadBlockingMode: 2, /* WarnWhenBlockingWait */ + }) .create(); setModuleImports("main.js", { From 49c98c3ac96c07cf1898d38c641ed58754a37f9f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 25 Mar 2024 16:53:15 +0100 Subject: [PATCH 2/5] fix --- src/mono/browser/runtime/startup.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts index 8580e5aca4b4b..9fdc426854ac3 100644 --- a/src/mono/browser/runtime/startup.ts +++ b/src/mono/browser/runtime/startup.ts @@ -296,7 +296,9 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { await start_runtime(); } - await runtimeHelpers.afterIOStarted.promise; + if (WasmEnableThreads) { + await runtimeHelpers.afterIOStarted.promise; + } runtimeList.registerRuntime(exportedRuntimeAPI); From ff0662594c96d1f5b99b65dc699dbb1f5e31d8d8 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 26 Mar 2024 14:19:51 +0100 Subject: [PATCH 3/5] more asserts and tests --- .../System/Threading/LowLevelLifoSemaphore.cs | 4 ++++ .../System/Threading/ManualResetEventSlim.cs | 4 ++++ .../src/System/Threading/WaitHandle.cs | 4 ++++ .../JavaScript/WebWorkerTestBase.cs | 21 +++++++++++++++++++ 4 files changed, 33 insertions(+) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.cs index 7f7bddf24737b..39233c87c15c9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.cs @@ -41,6 +41,10 @@ public bool Wait(int timeoutMs, bool spinWait) { Debug.Assert(timeoutMs >= -1); +#if FEATURE_WASM_MANAGED_THREADS + Thread.AssureBlockingPossible(); +#endif + int spinCount = spinWait ? _spinCount : 0; // Try to acquire the semaphore or diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs index a385543f9174a..516fb42bf0a52 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs @@ -485,6 +485,10 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken) ArgumentOutOfRangeException.ThrowIfLessThan(millisecondsTimeout, -1); +#if FEATURE_WASM_MANAGED_THREADS + Thread.AssureBlockingPossible(); +#endif + if (!IsSet) { if (millisecondsTimeout == 0) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.cs index 21920bc39b754..d215a82cd3234 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.cs @@ -117,6 +117,10 @@ internal bool WaitOneNoCheck( SafeWaitHandle? waitHandle = _waitHandle; ObjectDisposedException.ThrowIf(waitHandle is null, this); +#if FEATURE_WASM_MANAGED_THREADS + Thread.AssureBlockingPossible(); +#endif + bool success = false; try { diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestBase.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestBase.cs index ca641c8da97d2..77aef0857a8d2 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestBase.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestBase.cs @@ -185,6 +185,27 @@ static void LocalCtsIgnoringCall(Action action) new NamedCall { IsBlocking = true, Name = "Task.Wait", Call = delegate (CancellationToken ct) { Task.Delay(30, ct).Wait(ct); }}, new NamedCall { IsBlocking = true, Name = "Task.WaitAll", Call = delegate (CancellationToken ct) { Task.WaitAll(Task.Delay(30, ct)); }}, new NamedCall { IsBlocking = true, Name = "Task.WaitAny", Call = delegate (CancellationToken ct) { Task.WaitAny(Task.Delay(30, ct)); }}, + new NamedCall { IsBlocking = true, Name = "ManualResetEventSlim.Wait", Call = delegate (CancellationToken ct) { + using var mr = new ManualResetEventSlim(false); + LocalCtsIgnoringCall(mr.Wait); + }}, + new NamedCall { IsBlocking = true, Name = "SemaphoreSlim.Wait", Call = delegate (CancellationToken ct) { + using var sem = new SemaphoreSlim(2); + LocalCtsIgnoringCall(sem.Wait); + }}, + new NamedCall { IsBlocking = true, Name = "Mutex.WaitOne", Call = delegate (CancellationToken ct) { + using var mr = new ManualResetEventSlim(false); + var mutex = new Mutex(); + var thread = new Thread(() => { + mutex.WaitOne(); + mr.Set(); + Thread.Sleep(50); + mutex.ReleaseMutex(); + }); + thread.Start(); + Thread.ForceBlockingWait(static (b) => ((ManualResetEventSlim)b).Wait(), mr); + mutex.WaitOne(); + }}, }; public static IEnumerable GetTargetThreadsAndBlockingCalls() From 16b2a5433349d55f9dcdfc760948b4a5253d2d9e Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 28 Mar 2024 09:02:00 +0100 Subject: [PATCH 4/5] whitespace --- src/mono/browser/runtime/pthreads/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mono/browser/runtime/pthreads/index.ts b/src/mono/browser/runtime/pthreads/index.ts index 57de39e73792e..f68df83120944 100644 --- a/src/mono/browser/runtime/pthreads/index.ts +++ b/src/mono/browser/runtime/pthreads/index.ts @@ -22,7 +22,7 @@ export { export { mono_wasm_start_deputy_thread_async } from "./deputy-thread"; export { mono_wasm_start_io_thread_async } from "./io-thread"; -export function mono_wasm_warn_about_blocking_wait(ptr: number, length: number) { +export function mono_wasm_warn_about_blocking_wait (ptr: number, length: number) { const warning = utf16ToString(ptr, ptr + (length * 2)); mono_log_warn(warning); -} \ No newline at end of file +} From 5575040195ba3e5aac1455d4d1d8924d6536243c Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 3 Apr 2024 15:21:46 +0200 Subject: [PATCH 5/5] feedback --- src/mono/browser/runtime/managed-exports.ts | 21 +++++++++++++++++-- src/mono/browser/runtime/types/internal.ts | 8 +++---- src/mono/browser/test-main.js | 2 +- .../sample/wasm/browser-threads/Program.cs | 2 +- src/mono/sample/wasm/browser-threads/main.js | 2 +- 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/mono/browser/runtime/managed-exports.ts b/src/mono/browser/runtime/managed-exports.ts index ca4bc12ebf374..065136faaba47 100644 --- a/src/mono/browser/runtime/managed-exports.ts +++ b/src/mono/browser/runtime/managed-exports.ts @@ -228,7 +228,7 @@ export function get_managed_stack_trace (exception_gc_handle: GCHandle) { } // GCHandle InstallMainSynchronizationContext(nint jsNativeTID, JSThreadBlockingMode jsThreadBlockingMode) -export function install_main_synchronization_context (jsThreadBlockingMode: number): GCHandle { +export function install_main_synchronization_context (jsThreadBlockingMode: JSThreadBlockingMode): GCHandle { if (!WasmEnableThreads) return GCHandleNull; assert_c_interop(); @@ -242,7 +242,24 @@ export function install_main_synchronization_context (jsThreadBlockingMode: numb const arg1 = get_arg(args, 2); const arg2 = get_arg(args, 3); set_arg_intptr(arg1, mono_wasm_main_thread_ptr() as any); - set_arg_i32(arg2, jsThreadBlockingMode); + + // sync with JSHostImplementation.Types.cs + switch (jsThreadBlockingMode) { + case JSThreadBlockingMode.PreventSynchronousJSExport: + set_arg_i32(arg2, 0); + break; + case JSThreadBlockingMode.ThrowWhenBlockingWait: + set_arg_i32(arg2, 1); + break; + case JSThreadBlockingMode.WarnWhenBlockingWait: + set_arg_i32(arg2, 2); + break; + case JSThreadBlockingMode.DangerousAllowBlockingWait: + set_arg_i32(arg2, 100); + break; + default: + throw new Error("Invalid jsThreadBlockingMode"); + } // this block is like invoke_sync_jsexport() but without assert_js_interop() cwraps.mono_wasm_invoke_jsexport(managedExports.InstallMainSynchronizationContext!, args); diff --git a/src/mono/browser/runtime/types/internal.ts b/src/mono/browser/runtime/types/internal.ts index e364f34ba09c9..c9477ebae7b28 100644 --- a/src/mono/browser/runtime/types/internal.ts +++ b/src/mono/browser/runtime/types/internal.ts @@ -575,7 +575,7 @@ export const enum JSThreadBlockingMode { * On JSWebWorker blocking .Wait always warns. * This is the default mode. */ - PreventSynchronousJSExport = 0, + PreventSynchronousJSExport = "PreventSynchronousJSExport", /** * Allows synchronous JSExport to be called from JavaScript code also in UI thread. * Inside of that call blocking .Wait throws PNSE. @@ -583,7 +583,7 @@ export const enum JSThreadBlockingMode { * On JSWebWorker synchronous JSExport always works. * On JSWebWorker blocking .Wait always throws PNSE. */ - ThrowWhenBlockingWait = 1, + ThrowWhenBlockingWait = "ThrowWhenBlockingWait", /** * Allows synchronous JSExport to be called from JavaScript code also in UI thread. * Inside of that call blocking .Wait warns. @@ -591,7 +591,7 @@ export const enum JSThreadBlockingMode { * On JSWebWorker synchronous JSExport always works. * On JSWebWorker blocking .Wait always warns. */ - WarnWhenBlockingWait = 2, + WarnWhenBlockingWait = "WarnWhenBlockingWait", /** * Allows synchronous JSExport to be called from JavaScript code, and allows managed code to use blocking .Wait * .Wait on Promise/Task chains could lead to deadlock because JS event loop is not processed and it can't resolve JS promises. @@ -599,5 +599,5 @@ export const enum JSThreadBlockingMode { * Allows synchronous JSExport to be called from JavaScript code also in Main thread. * Inside of that call nested call back to synchronous JSImport throws PNSE (because it would deadlock otherwise in 100% cases). */ - DangerousAllowBlockingWait = 100, + DangerousAllowBlockingWait = "DangerousAllowBlockingWait", } diff --git a/src/mono/browser/test-main.js b/src/mono/browser/test-main.js index c7403640ce51c..1feb21ef2f796 100644 --- a/src/mono/browser/test-main.js +++ b/src/mono/browser/test-main.js @@ -253,7 +253,7 @@ function configureRuntime(dotnet, runArgs) { .withDumpThreadsOnNonZeroExit() .withConfig({ loadAllSatelliteResources: true, - jsThreadBlockingMode: 1, /* ThrowWhenBlockingWait */ + jsThreadBlockingMode: "ThrowWhenBlockingWait", }); if (ENVIRONMENT_IS_NODE) { diff --git a/src/mono/sample/wasm/browser-threads/Program.cs b/src/mono/sample/wasm/browser-threads/Program.cs index 3eaba624a0f36..8783ace18be7b 100644 --- a/src/mono/sample/wasm/browser-threads/Program.cs +++ b/src/mono/sample/wasm/browser-threads/Program.cs @@ -43,7 +43,7 @@ public static void Progress2() Console.WriteLine("Progress! "+ cwd); // below is blocking call, which means that UI will spin-lock little longer - // it will warn about blocking wait because of jsThreadBlockingMode: 2 + // it will warn about blocking wait because of jsThreadBlockingMode: "WarnWhenBlockingWait" // but it will not deadlock because underlying task chain is not JS promise Task.Delay(10).Wait(); } diff --git a/src/mono/sample/wasm/browser-threads/main.js b/src/mono/sample/wasm/browser-threads/main.js index 1e9d060f41b49..ea97a5ce200c8 100644 --- a/src/mono/sample/wasm/browser-threads/main.js +++ b/src/mono/sample/wasm/browser-threads/main.js @@ -18,7 +18,7 @@ try { .withExitCodeLogging() .withExitOnUnhandledError() .withConfig({ - jsThreadBlockingMode: 2, /* WarnWhenBlockingWait */ + jsThreadBlockingMode: "WarnWhenBlockingWait", }) .create();