diff --git a/src/libraries/Common/tests/WasmTestRunner/WasmTestRunner.cs b/src/libraries/Common/tests/WasmTestRunner/WasmTestRunner.cs index 9fb129d825d742..f89d3a3adf55a3 100644 --- a/src/libraries/Common/tests/WasmTestRunner/WasmTestRunner.cs +++ b/src/libraries/Common/tests/WasmTestRunner/WasmTestRunner.cs @@ -24,6 +24,7 @@ public static async Task Main(string[] args) var includedClasses = new List(); var includedMethods = new List(); var backgroundExec = false; + var untilFailed = false; for (int i = 1; i < args.Length; i++) { @@ -53,6 +54,9 @@ public static async Task Main(string[] args) case "-backgroundExec": backgroundExec = true; break; + case "-untilFailed": + untilFailed = true; + break; default: throw new ArgumentException($"Invalid argument '{option}'."); } @@ -72,10 +76,21 @@ public static async Task Main(string[] args) { await Task.Yield(); } - if (backgroundExec) + + var res = 0; + do { - return await Task.Run(() => runner.Run()); + if (backgroundExec) + { + res = await Task.Run(() => runner.Run()); + } + else + { + res = await runner.Run(); + } } - return await runner.Run(); + while(res == 0 && untilFailed); + + return res; } } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSWebWorker.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSWebWorker.cs index a68d7d11969357..ddbf5b8d619e57 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSWebWorker.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSWebWorker.cs @@ -194,6 +194,7 @@ private void Dispose(bool disposing) { _cancellationRegistration?.Dispose(); _cancellationRegistration = null; + _thread?.Join(50); } if (_jsSynchronizationContext != null) diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj index 90b2454d6e375f..a4e88845ec0808 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj @@ -13,19 +13,15 @@ true - $(WasmXHarnessMonoArgs) --setenv=XHARNESS_LOG_TEST_START=true + $(WasmXHarnessMonoArgs) --setenv=XHARNESS_LOG_TEST_START=true --no-memory-snapshot false - $(WasmXHarnessMonoArgs) --setenv=XHARNESS_LOG_TEST_START=true - - - @@ -33,13 +29,23 @@ - + + + + + + - + + + + + - + + diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/SecondRuntimeTest.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/SecondRuntimeTest.cs index 7985ec95f116fb..af17b2881deeca 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/SecondRuntimeTest.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/SecondRuntimeTest.cs @@ -14,10 +14,11 @@ public partial class SecondRuntimeTest [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBrowserDomSupportedOrNodeJS))] public static async Task RunSecondRuntimeAndTestStaticState() { - await JSHost.ImportAsync("SecondRuntimeTest", "../SecondRuntimeTest.js"); + var runId = Guid.NewGuid().ToString(); + await JSHost.ImportAsync("SecondRuntimeTest", "../SecondRuntimeTest.js?run=" + runId); Interop.State = 42; - var state2 = await Interop.RunSecondRuntimeAndTestStaticState(); + var state2 = await Interop.RunSecondRuntimeAndTestStaticState(runId); Assert.Equal(44, Interop.State); Assert.Equal(3, state2); } @@ -31,7 +32,7 @@ public static partial class Interop [JSImport("runSecondRuntimeAndTestStaticState", "SecondRuntimeTest")] [return: JSMarshalAs>] - internal static partial Task RunSecondRuntimeAndTestStaticState(); + internal static partial Task RunSecondRuntimeAndTestStaticState(string guid); } } } \ No newline at end of file diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/SecondRuntimeTest.js b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/SecondRuntimeTest.js index a4c018258f9b21..1c5ad1f1cf6d4f 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/SecondRuntimeTest.js +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/SecondRuntimeTest.js @@ -1,5 +1,5 @@ -export async function runSecondRuntimeAndTestStaticState() { - const { dotnet: dotnet2 } = await import('./_framework/dotnet.js?instance=2'); +export async function runSecondRuntimeAndTestStaticState(guid) { + const { dotnet: dotnet2 } = await import('./_framework/dotnet.js?instance=2-' + guid); const runtime2 = await dotnet2 .withStartupMemoryCache(false) .withConfig({ diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.Http.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.Http.cs new file mode 100644 index 00000000000000..8939cca759b19d --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.Http.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using System.Threading; +using System.Net.Http; +using Xunit; +using System.IO; +using System.Text; + +namespace System.Runtime.InteropServices.JavaScript.Tests +{ + public class WebWorkerHttpTest : WebWorkerTestBase + { + #region HTTP + + [Theory, MemberData(nameof(GetTargetThreads))] + public async Task HttpClient_ContentInSameThread(Executor executor) + { + using var cts = CreateTestCaseTimeoutSource(); + var uri = WebWorkerTestHelper.GetOriginUrl() + "/test.json"; + + await executor.Execute(async () => + { + using var client = new HttpClient(); + using var response = await client.GetAsync(uri); + response.EnsureSuccessStatusCode(); + var body = await response.Content.ReadAsStringAsync(); + Assert.Contains("hello", body); + Assert.Contains("world", body); + }, cts.Token); + } + + private static HttpRequestOptionsKey WebAssemblyEnableStreamingRequestKey = new("WebAssemblyEnableStreamingRequest"); + private static HttpRequestOptionsKey WebAssemblyEnableStreamingResponseKey = new("WebAssemblyEnableStreamingResponse"); + private static string HelloJson = "{'hello':'world'}".Replace('\'', '"'); + private static string EchoStart = "{\"Method\":\"POST\",\"Url\":\"/Echo.ashx"; + + private async Task HttpClient_ActionInDifferentThread(string url, Executor executor1, Executor executor2, Func e2Job) + { + using var cts = CreateTestCaseTimeoutSource(); + + var e1Job = async (Task e2done, TaskCompletionSource e1State) => + { + using var ms = new MemoryStream(); + await ms.WriteAsync(Encoding.UTF8.GetBytes(HelloJson)); + + using var req = new HttpRequestMessage(HttpMethod.Post, url); + req.Options.Set(WebAssemblyEnableStreamingResponseKey, true); + req.Content = new StreamContent(ms); + using var client = new HttpClient(); + var pr = client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead); + using var response = await pr; + + // share the state with the E2 continuation + e1State.SetResult(response); + + await e2done; + }; + await ActionsInDifferentThreads(executor1, executor2, e1Job, e2Job, cts); + } + + [Theory, MemberData(nameof(GetTargetThreads2x))] + public async Task HttpClient_ContentInDifferentThread(Executor executor1, Executor executor2) + { + var url = WebWorkerTestHelper.LocalHttpEcho + "?guid=" + Guid.NewGuid(); + await HttpClient_ActionInDifferentThread(url, executor1, executor2, async (HttpResponseMessage response) => + { + response.EnsureSuccessStatusCode(); + var body = await response.Content.ReadAsStringAsync(); + Assert.StartsWith(EchoStart, body); + }); + } + + [Theory, MemberData(nameof(GetTargetThreads2x))] + public async Task HttpClient_CancelInDifferentThread(Executor executor1, Executor executor2) + { + var url = WebWorkerTestHelper.LocalHttpEcho + "?delay10sec=true&guid=" + Guid.NewGuid(); + await HttpClient_ActionInDifferentThread(url, executor1, executor2, async (HttpResponseMessage response) => + { + await Assert.ThrowsAsync(async () => + { + CancellationTokenSource cts = new CancellationTokenSource(); + var promise = response.Content.ReadAsStringAsync(cts.Token); + cts.Cancel(); + await promise; + }); + }); + } + + #endregion + } +} diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.WebSocket.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.WebSocket.cs new file mode 100644 index 00000000000000..10153ee995ecc8 --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.WebSocket.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using System.Threading; +using Xunit; +using System.Net.WebSockets; +using System.Text; + +namespace System.Runtime.InteropServices.JavaScript.Tests +{ + public class WebWorkerWebSocketTest : WebWorkerTestBase + { + #region WebSocket + + [Theory, MemberData(nameof(GetTargetThreads))] + public async Task WebSocketClient_ContentInSameThread(Executor executor) + { + using var cts = CreateTestCaseTimeoutSource(); + + var uri = new Uri(WebWorkerTestHelper.LocalWsEcho + "?guid=" + Guid.NewGuid()); + var message = "hello"; + var send = Encoding.UTF8.GetBytes(message); + var receive = new byte[100]; + + await executor.Execute(async () => + { + using var client = new ClientWebSocket(); + await client.ConnectAsync(uri, CancellationToken.None); + await client.SendAsync(send, WebSocketMessageType.Text, true, CancellationToken.None); + + var res = await client.ReceiveAsync(receive, CancellationToken.None); + Assert.Equal(WebSocketMessageType.Text, res.MessageType); + Assert.True(res.EndOfMessage); + Assert.Equal(send.Length, res.Count); + Assert.Equal(message, Encoding.UTF8.GetString(receive, 0, res.Count)); + }, cts.Token); + } + + + [Theory, MemberData(nameof(GetTargetThreads2x))] + public async Task WebSocketClient_ResponseCloseInDifferentThread(Executor executor1, Executor executor2) + { + using var cts = CreateTestCaseTimeoutSource(); + + var uri = new Uri(WebWorkerTestHelper.LocalWsEcho + "?guid=" + Guid.NewGuid()); + var message = "hello"; + var send = Encoding.UTF8.GetBytes(message); + var receive = new byte[100]; + + var e1Job = async (Task e2done, TaskCompletionSource e1State) => + { + using var client = new ClientWebSocket(); + await client.ConnectAsync(uri, CancellationToken.None); + await client.SendAsync(send, WebSocketMessageType.Text, true, CancellationToken.None); + + // share the state with the E2 continuation + e1State.SetResult(client); + await e2done; + }; + + var e2Job = async (ClientWebSocket client) => + { + var res = await client.ReceiveAsync(receive, CancellationToken.None); + Assert.Equal(WebSocketMessageType.Text, res.MessageType); + Assert.True(res.EndOfMessage); + Assert.Equal(send.Length, res.Count); + Assert.Equal(message, Encoding.UTF8.GetString(receive, 0, res.Count)); + + await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", CancellationToken.None); + }; + + await ActionsInDifferentThreads(executor1, executor2, e1Job, e2Job, cts); + } + + [Theory, MemberData(nameof(GetTargetThreads2x))] + public async Task WebSocketClient_CancelInDifferentThread(Executor executor1, Executor executor2) + { + using var cts = CreateTestCaseTimeoutSource(); + + var uri = new Uri(WebWorkerTestHelper.LocalWsEcho + "?guid=" + Guid.NewGuid()); + var message = ".delay5sec"; // this will make the loopback server slower + var send = Encoding.UTF8.GetBytes(message); + var receive = new byte[100]; + + var e1Job = async (Task e2done, TaskCompletionSource e1State) => + { + using var client = new ClientWebSocket(); + await client.ConnectAsync(uri, CancellationToken.None); + await client.SendAsync(send, WebSocketMessageType.Text, true, CancellationToken.None); + + // share the state with the E2 continuation + e1State.SetResult(client); + await e2done; + }; + + var e2Job = async (ClientWebSocket client) => + { + CancellationTokenSource cts2 = new CancellationTokenSource(); + var resTask = client.ReceiveAsync(receive, cts2.Token); + cts2.Cancel(); + var ex = await Assert.ThrowsAnyAsync(() => resTask); + Assert.Equal(cts2.Token, ex.CancellationToken); + }; + + await ActionsInDifferentThreads(executor1, executor2, e1Job, e2Job, cts); + } + + #endregion + } +} 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 c0c03addd6bb87..63dc4460754fa7 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 @@ -1,19 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Threading.Tasks; using System.Threading; -using System.Net.Http; using Xunit; -using System.IO; -using System.Collections.Generic; -using System.Net.WebSockets; -using System.Text; -using System.Linq; namespace System.Runtime.InteropServices.JavaScript.Tests { + // TODO test: // JSExport 2x // JSExport async @@ -28,54 +22,10 @@ namespace System.Runtime.InteropServices.JavaScript.Tests // JS setTimeout till after JSWebWorker close // synchronous .Wait for JS setTimeout on the same thread -> deadlock problem **7)** - public class WebWorkerTest : IAsyncLifetime + public class WebWorkerTest : WebWorkerTestBase { - const int TimeoutMilliseconds = 5000; - - public static bool _isWarmupDone; - - public async Task InitializeAsync() - { - if (_isWarmupDone) - { - return; - } - await Task.Delay(500); - _isWarmupDone = true; - } - - public Task DisposeAsync() => Task.CompletedTask; - #region Executors - private CancellationTokenSource CreateTestCaseTimeoutSource() - { - var cts = new CancellationTokenSource(TimeoutMilliseconds); - cts.Token.Register(() => - { - Console.WriteLine($"Unexpected test case timeout at {DateTime.Now.ToString("u")} ManagedThreadId:{Environment.CurrentManagedThreadId}"); - }); - return cts; - } - - public static IEnumerable GetTargetThreads() - { - return Enum.GetValues().Select(type => new object[] { new Executor(type) }); - } - - public static IEnumerable GetSpecificTargetThreads() - { - yield return new object[] { new Executor(ExecutorType.JSWebWorker), new Executor(ExecutorType.Main) }; - yield break; - } - - public static IEnumerable GetTargetThreads2x() - { - return Enum.GetValues().SelectMany( - type1 => Enum.GetValues().Select( - type2 => new object[] { new Executor(type1), new Executor(type2) })); - } - [Theory, MemberData(nameof(GetTargetThreads))] public async Task Executor_Cancellation(Executor executor) { @@ -247,11 +197,11 @@ public async Task JSSynchronizationContext_Send_Post_To_Canceled() Assert.False(shouldNotHitPost); } + [ActiveIssue("https://github.com/dotnet/runtime/issues/96628#issuecomment-1907602744")] [Fact] + // this will say something like `JSSynchronizationContext is still installed on worker 0x4ff0030.` in the console during shutdown. public async Task JSWebWorker_Abandon_Running() { - var cts = new CancellationTokenSource(); - TaskCompletionSource never = new TaskCompletionSource(); TaskCompletionSource ready = new TaskCompletionSource(); @@ -261,7 +211,7 @@ public async Task JSWebWorker_Abandon_Running() { ready.SetResult(); return never.Task; - }, cts.Token); + }, CancellationToken.None); #pragma warning restore CS4014 await ready.Task; @@ -273,10 +223,9 @@ public async Task JSWebWorker_Abandon_Running() } [Fact] + // this will say something like `JSSynchronizationContext is still installed on worker 0x4ff0030.` in the console during shutdown. public async Task JSWebWorker_Abandon_Running_JS() { - var cts = new CancellationTokenSource(); - TaskCompletionSource ready = new TaskCompletionSource(); #pragma warning disable CS4014 @@ -287,7 +236,7 @@ public async Task JSWebWorker_Abandon_Running_JS() var never = WebWorkerTestHelper.JSDelay(int.MaxValue); ready.SetResult(); await never; - }, cts.Token); + }, CancellationToken.None); #pragma warning restore CS4014 await ready.Task; @@ -301,7 +250,7 @@ public async Task JSWebWorker_Abandon_Running_JS() [Theory, MemberData(nameof(GetTargetThreads))] public async Task Executor_Propagates(Executor executor) { - var cts = CreateTestCaseTimeoutSource(); + using var cts = CreateTestCaseTimeoutSource(); bool hit = false; var failedTask = executor.Execute(() => { @@ -314,6 +263,25 @@ public async Task Executor_Propagates(Executor executor) Assert.Equal("Test", ex.Message); } + [Theory, MemberData(nameof(GetTargetThreads))] + public async Task Executor_Propagates_After_Delay(Executor executor) + { + using var cts = CreateTestCaseTimeoutSource(); + bool hit = false; + var failedTask = executor.Execute(async () => + { + await executor.StickyAwait(WebWorkerTestHelper.CreateDelay(), cts.Token); + await WebWorkerTestHelper.JSDelay(10); + + hit = true; + throw new InvalidOperationException("Test"); + }, cts.Token); + + var ex = await Assert.ThrowsAsync(async () => await failedTask); + Assert.True(hit); + Assert.Equal("Test", ex.Message); + } + #endregion #region Console, Yield, Delay, Timer @@ -321,7 +289,7 @@ public async Task Executor_Propagates(Executor executor) [Theory, MemberData(nameof(GetTargetThreads))] public async Task ManagedConsole(Executor executor) { - var cts = CreateTestCaseTimeoutSource(); + using var cts = CreateTestCaseTimeoutSource(); await executor.Execute(() => { Console.WriteLine("C# Hello from ManagedThreadId: " + Environment.CurrentManagedThreadId); @@ -332,7 +300,7 @@ await executor.Execute(() => [Theory, MemberData(nameof(GetTargetThreads))] public async Task JSConsole(Executor executor) { - var cts = CreateTestCaseTimeoutSource(); + using var cts = CreateTestCaseTimeoutSource(); await executor.Execute(() => { WebWorkerTestHelper.Log("JS Hello from ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId); @@ -343,7 +311,7 @@ await executor.Execute(() => [Theory, MemberData(nameof(GetTargetThreads))] public async Task NativeThreadId(Executor executor) { - var cts = CreateTestCaseTimeoutSource(); + using var cts = CreateTestCaseTimeoutSource(); await executor.Execute(async () => { await executor.StickyAwait(WebWorkerTestHelper.InitializeAsync(), cts.Token); @@ -367,7 +335,7 @@ await executor.Execute(async () => public async Task ThreadingTimer(Executor executor) { var hit = false; - var cts = CreateTestCaseTimeoutSource(); + using var cts = CreateTestCaseTimeoutSource(); await executor.Execute(async () => { TaskCompletionSource tcs = new TaskCompletionSource(); @@ -389,7 +357,7 @@ await executor.Execute(async () => [Theory, MemberData(nameof(GetTargetThreads))] public async Task JSDelay_ContinueWith(Executor executor) { - var cts = CreateTestCaseTimeoutSource(); + using var cts = CreateTestCaseTimeoutSource(); await executor.Execute(async () => { await executor.StickyAwait(WebWorkerTestHelper.CreateDelay(), cts.Token); @@ -405,7 +373,7 @@ await WebWorkerTestHelper.JSDelay(10).ContinueWith(_ => [Theory, MemberData(nameof(GetTargetThreads))] public async Task JSDelay_ConfigureAwait_True(Executor executor) { - var cts = CreateTestCaseTimeoutSource(); + using var cts = CreateTestCaseTimeoutSource(); await executor.Execute(async () => { await executor.StickyAwait(WebWorkerTestHelper.CreateDelay(), cts.Token); @@ -420,7 +388,7 @@ await executor.Execute(async () => public async Task ManagedDelay_ContinueWith(Executor executor) { var hit = false; - var cts = CreateTestCaseTimeoutSource(); + using var cts = CreateTestCaseTimeoutSource(); await executor.Execute(async () => { await Task.Delay(10, cts.Token).ContinueWith(_ => @@ -434,7 +402,7 @@ await Task.Delay(10, cts.Token).ContinueWith(_ => [Theory, MemberData(nameof(GetTargetThreads))] public async Task ManagedDelay_ConfigureAwait_True(Executor executor) { - var cts = CreateTestCaseTimeoutSource(); + using var cts = CreateTestCaseTimeoutSource(); await executor.Execute(async () => { await Task.Delay(10, cts.Token).ConfigureAwait(true); @@ -446,7 +414,7 @@ await executor.Execute(async () => [Theory, MemberData(nameof(GetTargetThreads))] public async Task ManagedYield(Executor executor) { - var cts = CreateTestCaseTimeoutSource(); + using var cts = CreateTestCaseTimeoutSource(); await executor.Execute(async () => { await Task.Yield(); @@ -459,46 +427,10 @@ await executor.Execute(async () => #region Thread Affinity - private async Task ActionsInDifferentThreads(Executor executor1, Executor executor2, Func, Task> e1Job, Func e2Job, CancellationTokenSource cts) - { - TaskCompletionSource readyTCS = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - TaskCompletionSource doneTCS = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - var e1 = executor1.Execute(async () => - { - await e1Job(doneTCS.Task, readyTCS); - if (!readyTCS.Task.IsCompleted) - { - readyTCS.SetResult(default); - } - await doneTCS.Task; - }, cts.Token); - - var r1 = await readyTCS.Task.ConfigureAwait(true); - - var e2 = executor2.Execute(async () => - { - await e2Job(r1); - - }, cts.Token); - - try - { - await e2; - doneTCS.SetResult(); - await e1; - } - catch (Exception) - { - cts.Cancel(); - throw; - } - } - [Theory, MemberData(nameof(GetTargetThreads2x))] public async Task JSObject_CapturesAffinity(Executor executor1, Executor executor2) { - var cts = CreateTestCaseTimeoutSource(); + using var cts = CreateTestCaseTimeoutSource(); var e1Job = async (Task e2done, TaskCompletionSource e1State) => { @@ -528,178 +460,5 @@ public async Task JSObject_CapturesAffinity(Executor executor1, Executor executo #endregion - #region WebSocket - - [Theory, MemberData(nameof(GetTargetThreads))] - public async Task WebSocketClient_ContentInSameThread(Executor executor) - { - var cts = CreateTestCaseTimeoutSource(); - - var uri = new Uri(WebWorkerTestHelper.LocalWsEcho + "?guid=" + Guid.NewGuid()); - var message = "hello"; - var send = Encoding.UTF8.GetBytes(message); - var receive = new byte[100]; - - await executor.Execute(async () => - { - using var client = new ClientWebSocket(); - await client.ConnectAsync(uri, CancellationToken.None); - await client.SendAsync(send, WebSocketMessageType.Text, true, CancellationToken.None); - - var res = await client.ReceiveAsync(receive, CancellationToken.None); - Assert.Equal(WebSocketMessageType.Text, res.MessageType); - Assert.True(res.EndOfMessage); - Assert.Equal(send.Length, res.Count); - Assert.Equal(message, Encoding.UTF8.GetString(receive, 0, res.Count)); - }, cts.Token); - } - - - [Theory, MemberData(nameof(GetTargetThreads2x))] - public Task WebSocketClient_ResponseCloseInDifferentThread(Executor executor1, Executor executor2) - { - var cts = CreateTestCaseTimeoutSource(); - - var uri = new Uri(WebWorkerTestHelper.LocalWsEcho + "?guid=" + Guid.NewGuid()); - var message = "hello"; - var send = Encoding.UTF8.GetBytes(message); - var receive = new byte[100]; - - var e1Job = async (Task e2done, TaskCompletionSource e1State) => - { - using var client = new ClientWebSocket(); - await client.ConnectAsync(uri, CancellationToken.None); - await client.SendAsync(send, WebSocketMessageType.Text, true, CancellationToken.None); - - // share the state with the E2 continuation - e1State.SetResult(client); - await e2done; - }; - - var e2Job = async (ClientWebSocket client) => - { - var res = await client.ReceiveAsync(receive, CancellationToken.None); - Assert.Equal(WebSocketMessageType.Text, res.MessageType); - Assert.True(res.EndOfMessage); - Assert.Equal(send.Length, res.Count); - Assert.Equal(message, Encoding.UTF8.GetString(receive, 0, res.Count)); - - await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", CancellationToken.None); - }; - - return ActionsInDifferentThreads(executor1, executor2, e1Job, e2Job, cts); - } - - [Theory, MemberData(nameof(GetTargetThreads2x))] - public Task WebSocketClient_CancelInDifferentThread(Executor executor1, Executor executor2) - { - var cts = new CancellationTokenSource(); - - var uri = new Uri(WebWorkerTestHelper.LocalWsEcho + "?guid=" + Guid.NewGuid()); - var message = ".delay5sec"; // this will make the loopback server slower - var send = Encoding.UTF8.GetBytes(message); - var receive = new byte[100]; - - var e1Job = async (Task e2done, TaskCompletionSource e1State) => - { - using var client = new ClientWebSocket(); - await client.ConnectAsync(uri, CancellationToken.None); - await client.SendAsync(send, WebSocketMessageType.Text, true, CancellationToken.None); - - // share the state with the E2 continuation - e1State.SetResult(client); - await e2done; - }; - - var e2Job = async (ClientWebSocket client) => - { - CancellationTokenSource cts2 = new CancellationTokenSource(); - var resTask = client.ReceiveAsync(receive, cts2.Token); - cts2.Cancel(); - var ex = await Assert.ThrowsAnyAsync(() => resTask); - Assert.Equal(cts2.Token, ex.CancellationToken); - }; - - return ActionsInDifferentThreads(executor1, executor2, e1Job, e2Job, cts); - } - - #endregion - - #region HTTP - - [Theory, MemberData(nameof(GetTargetThreads))] - public async Task HttpClient_ContentInSameThread(Executor executor) - { - var cts = CreateTestCaseTimeoutSource(); - var uri = WebWorkerTestHelper.GetOriginUrl() + "/_framework/blazor.boot.json"; - - await executor.Execute(async () => - { - using var client = new HttpClient(); - using var response = await client.GetAsync(uri); - response.EnsureSuccessStatusCode(); - var body = await response.Content.ReadAsStringAsync(); - Assert.StartsWith("{", body); - }, cts.Token); - } - - private static HttpRequestOptionsKey WebAssemblyEnableStreamingRequestKey = new("WebAssemblyEnableStreamingRequest"); - private static HttpRequestOptionsKey WebAssemblyEnableStreamingResponseKey = new("WebAssemblyEnableStreamingResponse"); - private static string HelloJson = "{'hello':'world'}".Replace('\'', '"'); - private static string EchoStart = "{\"Method\":\"POST\",\"Url\":\"/Echo.ashx"; - - private Task HttpClient_ActionInDifferentThread(string url, Executor executor1, Executor executor2, Func e2Job) - { - var cts = CreateTestCaseTimeoutSource(); - - var e1Job = async (Task e2done, TaskCompletionSource e1State) => - { - using var ms = new MemoryStream(); - await ms.WriteAsync(Encoding.UTF8.GetBytes(HelloJson)); - - using var req = new HttpRequestMessage(HttpMethod.Post, url); - req.Options.Set(WebAssemblyEnableStreamingResponseKey, true); - req.Content = new StreamContent(ms); - using var client = new HttpClient(); - var pr = client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead); - using var response = await pr; - - // share the state with the E2 continuation - e1State.SetResult(response); - - await e2done; - }; - return ActionsInDifferentThreads(executor1, executor2, e1Job, e2Job, cts); - } - - [Theory, MemberData(nameof(GetTargetThreads2x))] - public async Task HttpClient_ContentInDifferentThread(Executor executor1, Executor executor2) - { - var url = WebWorkerTestHelper.LocalHttpEcho + "?guid=" + Guid.NewGuid(); - await HttpClient_ActionInDifferentThread(url, executor1, executor2, async (HttpResponseMessage response) => - { - response.EnsureSuccessStatusCode(); - var body = await response.Content.ReadAsStringAsync(); - Assert.StartsWith(EchoStart, body); - }); - } - - [Theory, MemberData(nameof(GetTargetThreads2x))] - public async Task HttpClient_CancelInDifferentThread(Executor executor1, Executor executor2) - { - var url = WebWorkerTestHelper.LocalHttpEcho + "?delay10sec=true&guid=" + Guid.NewGuid(); - await HttpClient_ActionInDifferentThread(url, executor1, executor2, async (HttpResponseMessage response) => - { - await Assert.ThrowsAsync(async () => - { - CancellationTokenSource cts = new CancellationTokenSource(); - var promise = response.Content.ReadAsStringAsync(cts.Token); - cts.Cancel(); - await promise; - }); - }); - } - - #endregion } } 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 new file mode 100644 index 00000000000000..0ca424470d60e2 --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestBase.cs @@ -0,0 +1,140 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using System.Threading; +using Xunit; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace System.Runtime.InteropServices.JavaScript.Tests +{ + public class WebWorkerTestBase : IAsyncLifetime + { + const int TimeoutMilliseconds = 5000; + + public static bool _isWarmupDone; + + public async Task InitializeAsync() + { + if (_isWarmupDone) + { + return; + } + await Task.Delay(500); + _isWarmupDone = true; + } + + public Task DisposeAsync() => Task.CompletedTask; + + protected CancellationTokenSource CreateTestCaseTimeoutSource([CallerMemberName] string memberName = "") + { + var start = DateTime.Now; + var cts = new CancellationTokenSource(TimeoutMilliseconds); + cts.Token.Register(() => + { + var end = DateTime.Now; + Console.WriteLine($"Unexpected test case {memberName} timeout after {end - start} ManagedThreadId:{Environment.CurrentManagedThreadId}"); + }); + return cts; + } + + public static IEnumerable GetTargetThreads() + { + return Enum.GetValues().Select(type => new object[] { new Executor(type) }); + } + + public static IEnumerable GetSpecificTargetThreads() + { + yield return new object[] { new Executor(ExecutorType.JSWebWorker), new Executor(ExecutorType.Main) }; + yield break; + } + + public static IEnumerable GetTargetThreads2x() + { + return Enum.GetValues().SelectMany( + type1 => Enum.GetValues().Select( + type2 => new object[] { new Executor(type1), new Executor(type2) })); + } + + protected async Task ActionsInDifferentThreads(Executor executor1, Executor executor2, Func, Task> e1Job, Func e2Job, CancellationTokenSource cts) + { + TaskCompletionSource job1ReadyTCS = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + TaskCompletionSource job2DoneTCS = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var e1Done = false; + var e2Done = false; + var e1Failed = false; + Task e1; + Task e2; + T r1; + + async Task ActionsInDifferentThreads1() + { + try + { + await e1Job(job2DoneTCS.Task, job1ReadyTCS); + if (!job1ReadyTCS.Task.IsCompleted) + { + job1ReadyTCS.SetResult(default); + } + await job2DoneTCS.Task; + } + catch (Exception ex) + { + Console.WriteLine("ActionsInDifferentThreads1 failed\n" + ex); + job1ReadyTCS.SetResult(default); + e1Failed = true; + throw; + } + finally + { + e1Done = true; + } + } + + async Task ActionsInDifferentThreads2() + { + try + { + await e2Job(r1); + } + finally + { + e2Done = true; + } + } + + + e1 = executor1.Execute(ActionsInDifferentThreads1, cts.Token); + r1 = await job1ReadyTCS.Task.ConfigureAwait(true); + if (e1Failed || e1.IsFaulted) + { + await e1; + } + e2 = executor2.Execute(ActionsInDifferentThreads2, cts.Token); + + try + { + await e2; + job2DoneTCS.SetResult(); + await e1; + } + catch (Exception ex) + { + job2DoneTCS.TrySetException(ex); + if (ex is OperationCanceledException oce && cts.Token.IsCancellationRequested) + { + throw; + } + Console.WriteLine("ActionsInDifferentThreads failed with: \n" + ex); + if (!e1Done || !e2Done) + { + Console.WriteLine("ActionsInDifferentThreads canceling!"); + cts.Cancel(); + } + throw; + } + } + } +} 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 91fa40695e1be3..24cafa2f743a53 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 @@ -330,7 +330,14 @@ public static Task RunOnNewThread(Func job, CancellationToken cancellation } catch (Exception ex) { - tcs.TrySetException(ex); + if(ex is AggregateException agg) + { + tcs.TrySetException(agg.InnerException); + } + else + { + tcs.TrySetException(ex); + } } finally { @@ -338,6 +345,7 @@ public static Task RunOnNewThread(Func job, CancellationToken cancellation } }); thread.Start(); + tcs.Task.ContinueWith((t) => { thread.Join(); }, cancellationToken, TaskContinuationOptions.RunContinuationsAsynchronously, TaskScheduler.Default); return tcs.Task; } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.mjs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.mjs index ee4aa200942819..558fb181b47d67 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.mjs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.mjs @@ -1,13 +1,33 @@ let dllExports; -const runtime = getDotnetRuntime(0); - let jsState = {}; +let runtime; + export async function setup() { - dllExports = await runtime.getAssemblyExports("System.Runtime.InteropServices.JavaScript.Tests.dll"); - jsState.id = getRndInteger(0, 1000); - jsState.tid = getTid(); + try { + if (!getDotnetRuntime) { + throw new Error("getDotnetRuntime is null or undefined"); + } + if (!runtime) { + runtime = getDotnetRuntime(0); + } + if (!runtime) { + console.error(getDotnetRuntime); + throw new Error("runtime is null or undefined"); + } + dllExports = await runtime.getAssemblyExports("System.Runtime.InteropServices.JavaScript.Tests.dll"); + if (!dllExports) { + throw new Error("dllExports is null or undefined"); + } + jsState.id = getRndInteger(0, 1000); + jsState.tid = getTid(); + } + catch (e) { + console.error("MONO_WASM: WebWorkerTestHelper.setup failed: " + JSON.stringify(globalThis.monoThreadInfo, null, 2)); + console.error("MONO_WASM: WebWorkerTestHelper.setup failed: " + e.toString()); + throw e; + } } export function getState() { @@ -15,12 +35,21 @@ export function getState() { } export function validateState(state) { - const isvalid = state.tid === jsState.tid && state.id === jsState.id; - if (!isvalid) { - console.log("Expected: ", JSON.stringify(jsState)); - console.log("Actual: ", JSON.stringify(state)); + try { + if (!state) { + throw new Error("state is null or undefined"); + } + const isvalid = state.tid === jsState.tid && state.id === jsState.id; + if (!isvalid) { + console.log("Expected: ", JSON.stringify(jsState)); + console.log("Actual: ", JSON.stringify(state)); + } + return isvalid; + } + catch (e) { + console.error("MONO_WASM: WebWorkerTestHelper.validateState failed: " + e.toString()); + throw e; } - return isvalid; } export async function promiseState() { diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/test.json b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/test.json new file mode 100644 index 00000000000000..9c3916c2a43ae9 --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/test.json @@ -0,0 +1,3 @@ +{ + "hello":"world" +} \ No newline at end of file diff --git a/src/libraries/tests.proj b/src/libraries/tests.proj index ec3fc45ea75028..b2dd78adc39037 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -607,9 +607,7 @@ - diff --git a/src/mono/browser/runtime/exports-binding.ts b/src/mono/browser/runtime/exports-binding.ts index 2f69b78b95fecf..8a363e8652db07 100644 --- a/src/mono/browser/runtime/exports-binding.ts +++ b/src/mono/browser/runtime/exports-binding.ts @@ -12,7 +12,7 @@ import { mono_interp_jit_wasm_entry_trampoline, mono_interp_record_interp_entry import { mono_interp_jit_wasm_jit_call_trampoline, mono_interp_invoke_wasm_jit_call_trampoline, mono_interp_flush_jitcall_queue } from "./jiterpreter-jit-call"; import { mono_wasm_resolve_or_reject_promise } from "./marshal-to-js"; import { mono_wasm_eventloop_has_unsettled_interop_promises } from "./pthreads/shared/eventloop"; -import { mono_wasm_pthread_on_pthread_attached, mono_wasm_pthread_on_pthread_detached } from "./pthreads/worker"; +import { mono_wasm_pthread_on_pthread_attached, mono_wasm_pthread_on_pthread_unregistered, mono_wasm_pthread_on_pthread_registered } 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"; @@ -33,8 +33,9 @@ import { mono_wasm_browser_entropy } from "./crypto"; export const mono_wasm_threads_imports = !MonoWasmThreads ? [] : [ // mono-threads-wasm.c + mono_wasm_pthread_on_pthread_registered, mono_wasm_pthread_on_pthread_attached, - mono_wasm_pthread_on_pthread_detached, + mono_wasm_pthread_on_pthread_unregistered, // threads.c mono_wasm_eventloop_has_unsettled_interop_promises, // diagnostics_server.c diff --git a/src/mono/browser/runtime/exports.ts b/src/mono/browser/runtime/exports.ts index 14032d5134a8d8..1cea8798bb4b40 100644 --- a/src/mono/browser/runtime/exports.ts +++ b/src/mono/browser/runtime/exports.ts @@ -5,7 +5,7 @@ import ProductVersion from "consts:productVersion"; import BuildConfiguration from "consts:configuration"; import type { RuntimeAPI } from "./types"; -import { Module, exportedRuntimeAPI, passEmscriptenInternals, runtimeHelpers, setRuntimeGlobals, } from "./globals"; +import { Module, exportedRuntimeAPI, loaderHelpers, passEmscriptenInternals, runtimeHelpers, setRuntimeGlobals, } from "./globals"; import { GlobalObjects } from "./types/internal"; import { configureEmscriptenStartup, configureRuntimeStartup, configureWorkerStartup } from "./startup"; @@ -19,6 +19,8 @@ import { instantiate_asset, instantiate_symbols_asset, instantiate_segmentation_ import { jiterpreter_dump_stats } from "./jiterpreter"; import { forceDisposeProxies } from "./gc-handles"; +export let runtimeList: RuntimeList; + function initializeExports(globalObjects: GlobalObjects): RuntimeAPI { const module = Module; const globals = globalObjects; @@ -47,15 +49,13 @@ function initializeExports(globalObjects: GlobalObjects): RuntimeAPI { }); // this code makes it possible to find dotnet runtime on a page via global namespace, even when there are multiple runtimes at the same time - let list: RuntimeList; if (!globalThisAny.getDotnetRuntime) { globalThisAny.getDotnetRuntime = (runtimeId: string) => globalThisAny.getDotnetRuntime.__list.getRuntime(runtimeId); - globalThisAny.getDotnetRuntime.__list = list = new RuntimeList(); + globalThisAny.getDotnetRuntime.__list = runtimeList = new RuntimeList(); } else { - list = globalThisAny.getDotnetRuntime.__list; + runtimeList = globalThisAny.getDotnetRuntime.__list; } - list.registerRuntime(exportedRuntimeAPI); return exportedRuntimeAPI; } @@ -64,8 +64,11 @@ class RuntimeList { private list: { [runtimeId: number]: WeakRef } = {}; public registerRuntime(api: RuntimeAPI): number { - api.runtimeId = Object.keys(this.list).length; + if (api.runtimeId === undefined) { + api.runtimeId = Object.keys(this.list).length; + } this.list[api.runtimeId] = create_weak_ref(api); + loaderHelpers.config.runtimeId = api.runtimeId; return api.runtimeId; } diff --git a/src/mono/browser/runtime/gc-handles.ts b/src/mono/browser/runtime/gc-handles.ts index d0c8c6094cba17..3e56b387ce8d5c 100644 --- a/src/mono/browser/runtime/gc-handles.ts +++ b/src/mono/browser/runtime/gc-handles.ts @@ -151,7 +151,9 @@ export function teardown_managed_proxy(owner: any, gc_handle: GCHandle, skipMana } } if (gc_handle !== GCHandleNull && _js_owned_object_table.delete(gc_handle) && !skipManaged) { - runtimeHelpers.javaScriptExports.release_js_owned_object_by_gc_handle(gc_handle); + if (loaderHelpers.is_runtime_running()) { + runtimeHelpers.javaScriptExports.release_js_owned_object_by_gc_handle(gc_handle); + } } if (is_gcv_handle(gc_handle)) { free_gcv_handle(gc_handle); diff --git a/src/mono/browser/runtime/globals.ts b/src/mono/browser/runtime/globals.ts index 3ccef85d631b12..702bb3fc94d8c1 100644 --- a/src/mono/browser/runtime/globals.ts +++ b/src/mono/browser/runtime/globals.ts @@ -104,5 +104,5 @@ export function mono_assert(condition: unknown, messageFactory: string | (() => ? messageFactory() : messageFactory); const error = new Error(message); - runtimeHelpers.abort(error); + runtimeHelpers.nativeAbort(error); } diff --git a/src/mono/browser/runtime/http.ts b/src/mono/browser/runtime/http.ts index bf29d8e0eaa142..8246547eb62023 100644 --- a/src/mono/browser/runtime/http.ts +++ b/src/mono/browser/runtime/http.ts @@ -62,22 +62,32 @@ export function http_wasm_create_controller(): HttpController { } export function http_wasm_abort_request(controller: HttpController): void { - if (controller.streamWriter) { - controller.streamWriter.abort(); + try { + if (controller.streamWriter) { + controller.streamWriter.abort(); + } + } + catch (err) { + // ignore } http_wasm_abort_response(controller); } export function http_wasm_abort_response(controller: HttpController): void { if (BuildConfiguration === "Debug") commonAsserts(controller); - controller.abortController.abort(); - if (controller.streamReader) { - controller.streamReader.cancel().catch((err) => { - if (err && err.name !== "AbortError") { - Module.err("Error in http_wasm_abort_response: " + err); - } - // otherwise, it's expected - }); + try { + if (controller.streamReader) { + controller.streamReader.cancel().catch((err) => { + if (err && err.name !== "AbortError") { + Module.err("Error in http_wasm_abort_response: " + err); + } + // otherwise, it's expected + }); + } + controller.abortController.abort(); + } + catch (err) { + // ignore } } diff --git a/src/mono/browser/runtime/interp-pgo.ts b/src/mono/browser/runtime/interp-pgo.ts index 0530552a59a4cc..fad17a1b8f7597 100644 --- a/src/mono/browser/runtime/interp-pgo.ts +++ b/src/mono/browser/runtime/interp-pgo.ts @@ -1,7 +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 { Module } from "./globals"; +import { Module, loaderHelpers } from "./globals"; import { getCacheKey, cleanupCache, getCacheEntry, storeCacheEntry } from "./snapshot"; import { mono_log_info, mono_log_error } from "./logging"; import { localHeapViewU8 } from "./memory"; @@ -9,24 +9,11 @@ import cwraps from "./cwraps"; export const tablePrefix = "https://dotnet.generated.invalid/interp_pgo"; -export async function getInterpPgoTable(): Promise { - const cacheKey = await getCacheKey(tablePrefix); - if (!cacheKey) - return undefined; - return await getCacheEntry(cacheKey); -} - -async function storeInterpPgoTable(memory: ArrayBuffer) { - const cacheKey = await getCacheKey(tablePrefix); - if (!cacheKey) +export async function interp_pgo_save_data() { + if (!loaderHelpers.is_runtime_running()) { + mono_log_info("Skipped saving interp_pgo table (already exited)"); return; - - await storeCacheEntry(cacheKey, memory, "application/octet-stream"); - - cleanupCache(tablePrefix, cacheKey); // no await -} - -export async function interp_pgo_save_data () { + } const cacheKey = await getCacheKey(tablePrefix); if (!cacheKey) { mono_log_error("Failed to save interp_pgo table (No cache key)"); @@ -52,17 +39,26 @@ export async function interp_pgo_save_data () { const u8 = localHeapViewU8(); const data = u8.slice(pData, pData + expectedSize); - await storeInterpPgoTable(data); + if (await storeCacheEntry(cacheKey, data, "application/octet-stream")) { + mono_log_info("Saved interp_pgo table to cache"); + } + + cleanupCache(tablePrefix, cacheKey); // no await - mono_log_info("Saved interp_pgo table to cache"); Module._free(pData); } catch (exc) { mono_log_error(`Failed to save interp_pgo table: ${exc}`); } } -export async function interp_pgo_load_data () { - const data = await getInterpPgoTable(); +export async function interp_pgo_load_data() { + const cacheKey = await getCacheKey(tablePrefix); + if (!cacheKey) { + mono_log_error("Failed to create cache key for interp_pgo table"); + return; + } + + const data = await getCacheEntry(cacheKey); if (!data) { mono_log_info("Failed to load interp_pgo table (No table found in cache)"); return; diff --git a/src/mono/browser/runtime/invoke-cs.ts b/src/mono/browser/runtime/invoke-cs.ts index dccdbe5be31317..2bdf952464318c 100644 --- a/src/mono/browser/runtime/invoke-cs.ts +++ b/src/mono/browser/runtime/invoke-cs.ts @@ -354,7 +354,7 @@ export function invoke_method_and_handle_exception(method: MonoMethod, args: JSM try { set_args_context(args); const fail = cwraps.mono_wasm_invoke_method_bound(method, args, fail_root.address); - if (fail) runtimeHelpers.abort("ERR24: Unexpected error: " + monoStringToString(fail_root)); + if (fail) runtimeHelpers.nativeAbort("ERR24: Unexpected error: " + monoStringToString(fail_root)); if (is_args_exception(args)) { const exc = get_arg(args, 0); throw marshal_exception_to_js(exc); @@ -370,7 +370,7 @@ export function invoke_method_raw(method: MonoMethod): void { const fail_root = mono_wasm_new_root(); try { const fail = cwraps.mono_wasm_invoke_method_raw(method, fail_root.address); - if (fail) runtimeHelpers.abort("ERR24: Unexpected error: " + monoStringToString(fail_root)); + if (fail) runtimeHelpers.nativeAbort("ERR24: Unexpected error: " + monoStringToString(fail_root)); } finally { fail_root.release(); diff --git a/src/mono/browser/runtime/loader/exit.ts b/src/mono/browser/runtime/loader/exit.ts index 3aaf8144a48e82..7cf42e05ec0903 100644 --- a/src/mono/browser/runtime/loader/exit.ts +++ b/src/mono/browser/runtime/loader/exit.ts @@ -126,10 +126,10 @@ export function mono_exit(exit_code: number, reason?: any): void { } function set_exit_code_and_quit_now(exit_code: number, reason?: any): void { - if (runtimeHelpers.runtimeReady && runtimeHelpers.mono_wasm_exit) { + if (runtimeHelpers.runtimeReady && runtimeHelpers.nativeExit) { runtimeHelpers.runtimeReady = false; try { - runtimeHelpers.mono_wasm_exit(exit_code); + runtimeHelpers.nativeExit(exit_code); } catch (err) { if (runtimeHelpers.ExitStatus && !(err instanceof runtimeHelpers.ExitStatus)) { diff --git a/src/mono/browser/runtime/loader/globals.ts b/src/mono/browser/runtime/loader/globals.ts index e5de83002c7a14..386624b119eb4d 100644 --- a/src/mono/browser/runtime/loader/globals.ts +++ b/src/mono/browser/runtime/loader/globals.ts @@ -72,14 +72,15 @@ export function setLoaderGlobals( Object.assign(globalObjects.module, { config: deep_merge_config(monoConfig, { environmentVariables: {} }), }); - Object.assign(runtimeHelpers, { + const rh: Partial = { mono_wasm_bindings_is_ready: false, javaScriptExports: {} as any, config: globalObjects.module.config, diagnosticTracing: false, - abort: (reason: any) => { throw reason; }, - }); - Object.assign(loaderHelpers, { + nativeAbort: (reason: any) => { throw reason; }, + nativeExit: (code: number) => { throw new Error("exit:" + code); } + }; + const lh: Partial = { gitHash, config: globalObjects.module.config, diagnosticTracing: false, @@ -124,8 +125,9 @@ export function setLoaderGlobals( // from wasm-feature-detect npm package exceptions, simd, - - } as Partial); + }; + Object.assign(runtimeHelpers, rh); + Object.assign(loaderHelpers, lh); } // this will abort the program if the condition is false @@ -137,5 +139,5 @@ export function mono_assert(condition: unknown, messageFactory: string | (() => ? messageFactory() : messageFactory); const error = new Error(message); - runtimeHelpers.abort(error); + runtimeHelpers.nativeAbort(error); } \ No newline at end of file diff --git a/src/mono/browser/runtime/loader/logging.ts b/src/mono/browser/runtime/loader/logging.ts index e8fba663b78109..7c94e88f6b0886 100644 --- a/src/mono/browser/runtime/loader/logging.ts +++ b/src/mono/browser/runtime/loader/logging.ts @@ -43,7 +43,8 @@ export function mono_log_error(msg: string, ...data: any) { } console.error(prefix + msg, ...data); } - +let tick = ""; +let last = new Date().valueOf(); function proxyConsoleMethod(prefix: string, func: any, asJson: boolean) { return function (...args: any[]) { try { @@ -60,19 +61,19 @@ function proxyConsoleMethod(prefix: string, func: any, asJson: boolean) { } if (typeof payload === "string") { - if (payload[0] == "[") { - const now = new Date().toISOString(); - if (MonoWasmThreads && ENVIRONMENT_IS_WORKER) { - payload = `[${threadNamePrefix}][${now}] ${payload}`; - } else { - payload = `[${now}] ${payload}`; - } - } else if (MonoWasmThreads && ENVIRONMENT_IS_WORKER) { - if (payload.indexOf("keeping the worker alive for asynchronous operation") !== -1) { + if (MonoWasmThreads) { + if (ENVIRONMENT_IS_WORKER && payload.indexOf("keeping the worker alive for asynchronous operation") !== -1) { // muting emscripten noise return; } - payload = `[${threadNamePrefix}] ${payload}`; + if (payload.indexOf("MONO_WASM: ") === 0 || payload.indexOf("[MONO]") === 0) { + const now = new Date(); + if (last !== now.valueOf()) { + tick = now.toISOString().substring(11, 23); + last = now.valueOf(); + } + payload = `[${threadNamePrefix} ${tick}] ${payload}`; + } } } diff --git a/src/mono/browser/runtime/loader/run.ts b/src/mono/browser/runtime/loader/run.ts index 7f3804917a26cf..40ea4cb19ada7b 100644 --- a/src/mono/browser/runtime/loader/run.ts +++ b/src/mono/browser/runtime/loader/run.ts @@ -4,7 +4,7 @@ import BuildConfiguration from "consts:configuration"; import type { MonoConfig, DotnetHostBuilder, DotnetModuleConfig, RuntimeAPI, LoadBootResourceCallback } from "../types"; -import type { MonoConfigInternal, EmscriptenModuleInternal, RuntimeModuleExportsInternal, NativeModuleExportsInternal, } from "../types/internal"; +import type { EmscriptenModuleInternal, RuntimeModuleExportsInternal, NativeModuleExportsInternal, } from "../types/internal"; import { ENVIRONMENT_IS_WEB, ENVIRONMENT_IS_WORKER, emscriptenModule, exportedRuntimeAPI, globalObjectsRoot, monoConfig, mono_assert } from "./globals"; import { deep_merge_config, deep_merge_module, mono_wasm_load_config } from "./config"; @@ -392,7 +392,7 @@ export class HostBuilder implements DotnetHostBuilder { } export async function createApi(): Promise { - if (ENVIRONMENT_IS_WEB && (loaderHelpers.config! as MonoConfigInternal).forwardConsoleLogsToWS && typeof globalThis.WebSocket != "undefined") { + if (ENVIRONMENT_IS_WEB && loaderHelpers.config.forwardConsoleLogsToWS && typeof globalThis.WebSocket != "undefined") { setup_proxy_console("main", globalThis.console, globalThis.location.origin); } mono_assert(emscriptenModule, "Null moduleConfig"); diff --git a/src/mono/browser/runtime/loader/worker.ts b/src/mono/browser/runtime/loader/worker.ts index 3bb042aed5e4fd..81a9cecad6a741 100644 --- a/src/mono/browser/runtime/loader/worker.ts +++ b/src/mono/browser/runtime/loader/worker.ts @@ -1,14 +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 { MonoConfigInternal, WorkerToMainMessageType, monoMessageSymbol } from "../types/internal"; import { MonoConfig } from "../types"; -import { MonoConfigInternal } from "../types/internal"; import { deep_merge_config, normalizeConfig } from "./config"; import { ENVIRONMENT_IS_WEB, loaderHelpers } from "./globals"; import { mono_log_debug } from "./logging"; -export const monoSymbol = "__mono_message_please_dont_collide__"; //Symbol("mono"); - export function setupPreloadChannelToMainThread() { const channel = new MessageChannel(); const workerPort = channel.port1; @@ -20,7 +18,13 @@ export function setupPreloadChannelToMainThread() { mainPort.close(); }, { once: true }); workerPort.start(); - self.postMessage(makePreloadMonoMessage(mainPort), [mainPort]); + // ask for config even before WASM is loaded + self.postMessage({ + [monoMessageSymbol]: { + monoCmd: WorkerToMainMessageType.preload, + port: mainPort + } + }, [mainPort]); } let workerMonoConfigReceived = false; @@ -43,16 +47,3 @@ function onMonoConfigReceived(config: MonoConfigInternal): void { } } -export function makePreloadMonoMessage(port: TPort): any { - return { - [monoSymbol]: { - monoCmd: WorkerMonoCommandType.preload, - port - } - }; -} - -const enum WorkerMonoCommandType { - channelCreated = "channel_created", - preload = "preload", -} diff --git a/src/mono/browser/runtime/logging.ts b/src/mono/browser/runtime/logging.ts index 83f61569f03ecb..f7b8b0fde9e967 100644 --- a/src/mono/browser/runtime/logging.ts +++ b/src/mono/browser/runtime/logging.ts @@ -9,7 +9,7 @@ import { CharPtr, VoidPtr } from "./types/emscripten"; let prefix = "MONO_WASM: "; export function mono_set_thread_name(threadName: string) { - prefix = `MONO_WASM [${threadName}]: `; + prefix = `[${threadName}] MONO_WASM: `; } export function mono_log_debug(msg: string, ...data: any) { diff --git a/src/mono/browser/runtime/marshal-to-cs.ts b/src/mono/browser/runtime/marshal-to-cs.ts index defc56cb0417a9..fa5e0a7d7056d2 100644 --- a/src/mono/browser/runtime/marshal-to-cs.ts +++ b/src/mono/browser/runtime/marshal-to-cs.ts @@ -349,14 +349,14 @@ function _marshal_task_to_cs(arg: JSMarshalerArgument, value: Promise, _?: if (MonoWasmThreads) { settleUnsettledPromise(); } - // we can unregister the GC handle on JS side - teardown_managed_proxy(holder, gc_handle, true); + // we can unregister the GC handle just on JS side + teardown_managed_proxy(holder, gc_handle, /*skipManaged: */ true); // order of operations with teardown_managed_proxy matters // so that managed user code running in the continuation could allocate the same GCHandle number and the local registry would be already ok with that runtimeHelpers.javaScriptExports.complete_task(gc_handle, null, data, res_converter || _marshal_cs_object_to_cs); } catch (ex) { - runtimeHelpers.abort(ex); + runtimeHelpers.nativeAbort(ex); } } @@ -370,13 +370,13 @@ function _marshal_task_to_cs(arg: JSMarshalerArgument, value: Promise, _?: if (MonoWasmThreads) { settleUnsettledPromise(); } - // we can unregister the GC handle on JS side - teardown_managed_proxy(holder, gc_handle, true); + // we can unregister the GC handle just on JS side + teardown_managed_proxy(holder, gc_handle, /*skipManaged: */ true); // order of operations with teardown_managed_proxy matters runtimeHelpers.javaScriptExports.complete_task(gc_handle, reason, null, undefined); } catch (ex) { - runtimeHelpers.abort(ex); + runtimeHelpers.nativeAbort(ex); } } diff --git a/src/mono/browser/runtime/marshal.ts b/src/mono/browser/runtime/marshal.ts index bf4c5145badd47..2a91aa58ddceb2 100644 --- a/src/mono/browser/runtime/marshal.ts +++ b/src/mono/browser/runtime/marshal.ts @@ -349,7 +349,11 @@ export class ManagedError extends Error implements IDisposable { if (this.managed_stack) { return this.managed_stack; } - if (loaderHelpers.is_runtime_running() && (!MonoWasmThreads || runtimeHelpers.proxy_context_gc_handle)) { + if (!loaderHelpers.is_runtime_running()) { + this.managed_stack = "... omitted managed stack trace.\n" + this.getSuperStack(); + return this.managed_stack; + } + if (!MonoWasmThreads || runtimeHelpers.proxy_context_gc_handle) { const gc_handle = (this)[js_owned_gc_handle_symbol]; if (gc_handle !== GCHandleNull) { const managed_stack = runtimeHelpers.javaScriptExports.get_managed_stack_trace(gc_handle); diff --git a/src/mono/browser/runtime/pthreads/browser/index.ts b/src/mono/browser/runtime/pthreads/browser/index.ts index 2ff00afe4eda9f..7e6a4f41f9402f 100644 --- a/src/mono/browser/runtime/pthreads/browser/index.ts +++ b/src/mono/browser/runtime/pthreads/browser/index.ts @@ -3,19 +3,16 @@ import MonoWasmThreads from "consts:monoWasmThreads"; -import { monoSymbol, makeMonoThreadMessageApplyMonoConfig, isMonoWorkerMessagePreload, MonoWorkerMessage, isMonoWorkerMessageEnabledInterop, isMonoWorkerMessageChannelCreated } from "../shared"; -import { pthreadPtr } from "../shared/types"; +import { MonoWorkerToMainMessage, pthreadPtr } from "../shared/types"; import { MonoThreadMessage } from "../shared"; -import { Internals, PThreadWorker } from "../shared/emscripten-internals"; -import { createPromiseController, runtimeHelpers } from "../../globals"; -import { PromiseAndController, PromiseController } from "../../types/internal"; -import { mono_log_debug } from "../../logging"; +import { PThreadWorker, allocateUnusedWorker, getRunningWorkers, getUnusedWorkerPool, getWorker, loadWasmModuleToWorker } from "../shared/emscripten-internals"; +import { createPromiseController, mono_assert, runtimeHelpers } from "../../globals"; +import { MainToWorkerMessageType, PromiseAndController, PromiseController, WorkerToMainMessageType, monoMessageSymbol } from "../../types/internal"; -const threads: Map = new Map(); +const threadPromises: Map[]> = new Map(); export interface Thread { readonly pthreadPtr: pthreadPtr; - readonly worker: Worker; readonly port: MessagePort; postMessageToWorker(message: T): void; } @@ -27,14 +24,13 @@ class ThreadImpl implements Thread { } } -const threadPromises: Map[]> = new Map(); /// wait until the thread with the given id has set up a message port to the runtime export function waitForThread(pthreadPtr: pthreadPtr): Promise { if (!MonoWasmThreads) return null as any; - - if (threads.has(pthreadPtr)) { - return Promise.resolve(threads.get(pthreadPtr)!); + const worker = getWorker(pthreadPtr); + if (worker?.thread) { + return Promise.resolve(worker?.thread); } const promiseAndController = createPromiseController(); const arr = threadPromises.get(pthreadPtr); @@ -46,72 +42,70 @@ export function waitForThread(pthreadPtr: pthreadPtr): Promise { return promiseAndController.promise; } -function resolvePromises(pthreadPtr: pthreadPtr, thread: Thread): void { +export function resolveThreadPromises(pthreadPtr: pthreadPtr, thread?: Thread): void { if (!MonoWasmThreads) return; const arr = threadPromises.get(pthreadPtr); if (arr !== undefined) { - arr.forEach((controller) => controller.resolve(thread)); + arr.forEach((controller) => { + if (thread) { + controller.resolve(thread); + } else { + controller.reject(); + } + }); threadPromises.delete(pthreadPtr); } } -function addThread(pthreadPtr: pthreadPtr, worker: Worker, port: MessagePort): Thread { - if (!MonoWasmThreads) return null as any; - const thread = new ThreadImpl(pthreadPtr, worker, port); - threads.set(pthreadPtr, thread); - return thread; -} - -function removeThread(pthreadPtr: pthreadPtr): void { - threads.delete(pthreadPtr); -} - -/// Given a thread id, return the thread object with the worker where the thread is running, and a message port. -export function getThread(pthreadPtr: pthreadPtr): Thread | undefined { - if (!MonoWasmThreads) return null as any; - const thread = threads.get(pthreadPtr); - if (thread === undefined) { - return undefined; - } - // validate that the worker is still running pthreadPtr - const worker = thread.worker; - if (Internals.getThreadId(worker) !== pthreadPtr) { - removeThread(pthreadPtr); - thread.port.close(); - return undefined; - } - return thread; -} - -/// Returns all the threads we know about -export const getThreadIds = (): IterableIterator => threads.keys(); - -function monoDedicatedChannelMessageFromWorkerToMain(event: MessageEvent, thread: Thread): void { - // TODO: add callbacks that will be called from here - mono_log_debug("got message from worker on the dedicated channel", event.data, thread); -} - // handler that runs in the main thread when a message is received from a pthread worker -function monoWorkerMessageHandler(worker: Worker, ev: MessageEvent): void { +function monoWorkerMessageHandler(worker: PThreadWorker, ev: MessageEvent): void { if (!MonoWasmThreads) return; - /// N.B. important to ignore messages we don't recognize - Emscripten uses the message event to send internal messages - const data = ev.data; - if (isMonoWorkerMessagePreload(data)) { - const port = data[monoSymbol].port; - port.postMessage(makeMonoThreadMessageApplyMonoConfig(runtimeHelpers.config)); + let pthreadId: pthreadPtr; + // this is emscripten message + if (ev.data.cmd === "killThread") { + pthreadId = ev.data["thread"]; + mono_assert(pthreadId == worker.info.pthreadId, "expected pthreadId to match"); + worker.info.isRunning = false; + worker.info.pthreadId = 0; + return; } - else if (isMonoWorkerMessageChannelCreated(data)) { - const port = data[monoSymbol].port; - const pthreadId = data[monoSymbol].threadId; - const thread = addThread(pthreadId, worker, port); - port.addEventListener("message", (ev) => monoDedicatedChannelMessageFromWorkerToMain(ev, thread)); - port.start(); - resolvePromises(pthreadId, thread); + + const message = ev.data[monoMessageSymbol] as MonoWorkerToMainMessage; + if (message === undefined) { + /// N.B. important to ignore messages we don't recognize - Emscripten uses the message event to send internal messages + return; } - else if (isMonoWorkerMessageEnabledInterop(data)) { - const pthreadId = data[monoSymbol].threadId; - const worker = Internals.getWorker(pthreadId) as PThreadWorker; - worker.interopInstalled = true; + + let port: MessagePort; + let thread: Thread; + pthreadId = message.info?.pthreadId ?? 0; + + switch (message.monoCmd) { + case WorkerToMainMessageType.preload: + // this one shot port from setupPreloadChannelToMainThread + port = message.port!; + port.postMessage({ + type: "pthread", + cmd: MainToWorkerMessageType.applyConfig, + config: JSON.stringify(runtimeHelpers.config) + }); + port.close(); + break; + case WorkerToMainMessageType.pthreadCreated: + port = message.port!; + thread = new ThreadImpl(pthreadId, worker, port); + worker.thread = thread; + worker.info.isRunning = true; + resolveThreadPromises(pthreadId, thread); + // fall through + case WorkerToMainMessageType.monoRegistered: + case WorkerToMainMessageType.monoAttached: + case WorkerToMainMessageType.enabledInterop: + case WorkerToMainMessageType.monoUnRegistered: + worker.info = Object.assign(worker.info!, message.info, {}); + break; + default: + throw new Error(`Unhandled message from worker: ${message.monoCmd}`); } } @@ -119,13 +113,14 @@ let pendingWorkerLoad: PromiseAndController | undefined; /// Called by Emscripten internals on the browser thread when a new pthread worker is created and added to the pthread worker pool. /// At this point the worker doesn't have any pthread assigned to it, yet. -export function onWorkerLoadInitiated(worker: Worker, loaded: Promise): void { +export function onWorkerLoadInitiated(worker: PThreadWorker, loaded: Promise): void { if (!MonoWasmThreads) return; worker.addEventListener("message", (ev) => monoWorkerMessageHandler(worker, ev)); if (pendingWorkerLoad == undefined) { pendingWorkerLoad = createPromiseController(); } loaded.then(() => { + worker.info.isLoaded = true; if (pendingWorkerLoad != undefined) { pendingWorkerLoad.promise_control.resolve(); pendingWorkerLoad = undefined; @@ -147,7 +142,7 @@ export function thread_available(): Promise { export function preAllocatePThreadWorkerPool(pthreadPoolSize: number): void { if (!MonoWasmThreads) return; for (let i = 0; i < pthreadPoolSize; i++) { - Internals.allocateUnusedWorker(); + allocateUnusedWorker(); } } @@ -159,9 +154,19 @@ export function preAllocatePThreadWorkerPool(pthreadPoolSize: number): void { export async function instantiateWasmPThreadWorkerPool(): Promise { if (!MonoWasmThreads) return null as any; // this is largely copied from emscripten's "receiveInstance" in "createWasm" in "src/preamble.js" - const workers = Internals.getUnusedWorkerPool(); + const workers = getUnusedWorkerPool(); if (workers.length > 0) { - const promises = workers.map(Internals.loadWasmModuleToWorker); + const promises = workers.map(loadWasmModuleToWorker); await Promise.all(promises); } } + +// when we create threads with browser event loop, it's not able to be joined by mono's thread join during shutdown and blocks process exit +export function cancelThreads() { + const workers: PThreadWorker[] = getRunningWorkers(); + for (const worker of workers) { + if (worker.info.isExternalEventLoop) { + worker.postMessage({ cmd: "cancel" }); + } + } +} \ No newline at end of file diff --git a/src/mono/browser/runtime/pthreads/shared/emscripten-internals.ts b/src/mono/browser/runtime/pthreads/shared/emscripten-internals.ts index 0341a16ffc0c67..5516f1a0f813db 100644 --- a/src/mono/browser/runtime/pthreads/shared/emscripten-internals.ts +++ b/src/mono/browser/runtime/pthreads/shared/emscripten-internals.ts @@ -2,8 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. import { Module } from "../../globals"; -import { pthreadPtr } from "./types"; -import MonoWasmThreads from "consts:monoWasmThreads"; +import { Thread } from "../browser"; +import { PThreadInfo, pthreadPtr } from "./types"; /** @module emscripten-internals accessors to the functions in the emscripten PThreads library, including * the low-level representations of {@linkcode pthreadPtr} thread info structs, etc. @@ -15,9 +15,10 @@ import MonoWasmThreads from "consts:monoWasmThreads"; // This is what we know about the Emscripten PThread library export interface PThreadLibrary { unusedWorkers: PThreadWorker[]; + runningWorkers: PThreadWorker[]; pthreads: PThreadInfoMap; allocateUnusedWorker: () => void; - loadWasmModuleToWorker: (worker: Worker) => Promise; + loadWasmModuleToWorker: (worker: PThreadWorker) => Promise; threadInitTLS: () => void, getNewWorker: () => PThreadWorker, returnWorkerToPool: (worker: PThreadWorker) => void, @@ -28,47 +29,37 @@ export interface PThreadLibrary { export interface PThreadWorker extends Worker { pthread_ptr: pthreadPtr; loaded: boolean; - interopInstalled: boolean; + // this info is updated via async messages from the worker, it could be stale + info: PThreadInfo; + thread?: Thread; } -interface PThreadObject { - worker: PThreadWorker; +interface PThreadInfoMap { + [key: pthreadPtr]: PThreadWorker; } -interface PThreadInfoMap { - [key: pthreadPtr]: PThreadObject | undefined; + +export function getWorker(pthreadPtr: pthreadPtr): PThreadWorker | undefined { + return getModulePThread().pthreads[pthreadPtr]; } +export function allocateUnusedWorker(): void { + /// See library_pthread.js in Emscripten. + /// This function allocates a new worker and adds it to the pool of workers. + /// It's called when the pool of workers is empty and a new thread is created. + getModulePThread().allocateUnusedWorker(); +} +export function getUnusedWorkerPool(): PThreadWorker[] { + return getModulePThread().unusedWorkers; +} +export function getRunningWorkers(): PThreadWorker[] { + return getModulePThread().runningWorkers; +} -function isRunningPThreadWorker(w: Worker): w is PThreadWorker { - return (w).pthread !== undefined; +export function loadWasmModuleToWorker(worker: PThreadWorker): Promise { + return getModulePThread().loadWasmModuleToWorker(worker); } -/// These utility functions dig into Emscripten internals -export const Internals = !MonoWasmThreads ? null as any : { - get modulePThread(): PThreadLibrary { - return (Module).PThread as PThreadLibrary; - }, - getWorker: (pthreadPtr: pthreadPtr): PThreadWorker | undefined => { - return Internals.modulePThread.pthreads[pthreadPtr]; - }, - getThreadId: (worker: Worker): pthreadPtr | undefined => { - /// See library_pthread.js in Emscripten. - /// They hang a "pthread" object from the worker if the worker is running a thread, and remove it when the thread stops by doing `pthread_exit` or when it's joined using `pthread_join`. - if (!isRunningPThreadWorker(worker)) - return undefined; - return worker.pthread_ptr; - }, - allocateUnusedWorker: (): void => { - /// See library_pthread.js in Emscripten. - /// This function allocates a new worker and adds it to the pool of workers. - /// It's called when the pool of workers is empty and a new thread is created. - Internals.modulePThread.allocateUnusedWorker(); - }, - getUnusedWorkerPool: (): Worker[] => { - return Internals.modulePThread.unusedWorkers; - }, - loadWasmModuleToWorker: (worker: Worker): Promise => { - return Internals.modulePThread.loadWasmModuleToWorker(worker); - } -}; +export function getModulePThread(): PThreadLibrary { + return (Module).PThread as PThreadLibrary; +} diff --git a/src/mono/browser/runtime/pthreads/shared/emscripten-replacements.ts b/src/mono/browser/runtime/pthreads/shared/emscripten-replacements.ts index 30e150df221339..48c5a0c5ce34ed 100644 --- a/src/mono/browser/runtime/pthreads/shared/emscripten-replacements.ts +++ b/src/mono/browser/runtime/pthreads/shared/emscripten-replacements.ts @@ -2,10 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. import MonoWasmThreads from "consts:monoWasmThreads"; +import BuildConfiguration from "consts:configuration"; -import { onWorkerLoadInitiated } from "../browser"; -import { afterThreadInitTLS } from "../worker"; -import { Internals, PThreadLibrary, PThreadWorker } from "./emscripten-internals"; +import { onWorkerLoadInitiated, resolveThreadPromises } from "../browser"; +import { mono_wasm_pthread_on_pthread_created } from "../worker"; +import { PThreadLibrary, PThreadWorker, getModulePThread, getRunningWorkers, getUnusedWorkerPool } from "./emscripten-internals"; import { loaderHelpers, mono_assert } from "../../globals"; import { mono_log_warn } from "../../logging"; @@ -21,7 +22,7 @@ export function replaceEmscriptenPThreadLibrary(modulePThread: PThreadLibrary): const originalThreadInitTLS = modulePThread.threadInitTLS; const originalReturnWorkerToPool = modulePThread.returnWorkerToPool; - modulePThread.loadWasmModuleToWorker = (worker: Worker): Promise => { + modulePThread.loadWasmModuleToWorker = (worker: PThreadWorker): Promise => { const afterLoaded = originalLoadWasmModuleToWorker(worker); afterLoaded.then(() => { availableThreadCount++; @@ -31,14 +32,21 @@ export function replaceEmscriptenPThreadLibrary(modulePThread: PThreadLibrary): }; modulePThread.threadInitTLS = (): void => { originalThreadInitTLS(); - afterThreadInitTLS(); + mono_wasm_pthread_on_pthread_created(); }; modulePThread.allocateUnusedWorker = allocateUnusedWorker; modulePThread.getNewWorker = () => getNewWorker(modulePThread); modulePThread.returnWorkerToPool = (worker: PThreadWorker) => { // when JS interop is installed on JSWebWorker // we can't reuse the worker, because user code could leave the worker JS globals in a dirty state - if (worker.interopInstalled) { + worker.info.isRunning = false; + resolveThreadPromises(worker.pthread_ptr, undefined); + worker.info.pthreadId = 0; + if (worker.thread?.port) { + worker.thread.port.close(); + } + worker.thread = undefined; + if (worker.info && worker.info.isDirtyBecauseOfInterop) { // we are on UI thread, invoke the handler directly to destroy the dirty worker worker.onmessage!(new MessageEvent("message", { data: { @@ -51,6 +59,10 @@ export function replaceEmscriptenPThreadLibrary(modulePThread: PThreadLibrary): originalReturnWorkerToPool(worker); } }; + if (BuildConfiguration === "Debug") { + (globalThis as any).dumpThreads = dumpThreads; + (globalThis as any).getModulePThread = getModulePThread; + } } let availableThreadCount = 0; @@ -62,7 +74,7 @@ function getNewWorker(modulePThread: PThreadLibrary): PThreadWorker { if (!MonoWasmThreads) return null as any; if (modulePThread.unusedWorkers.length == 0) { - mono_log_warn("Failed to find unused WebWorker, this may deadlock. Please increase the pthreadPoolSize."); + mono_log_warn(`Failed to find unused WebWorker, this may deadlock. Please increase the pthreadPoolSize. Running threads ${modulePThread.runningWorkers.length}. Loading workers: ${modulePThread.unusedWorkers.length}`); const worker = allocateUnusedWorker(); modulePThread.loadWasmModuleToWorker(worker); availableThreadCount--; @@ -83,7 +95,7 @@ function getNewWorker(modulePThread: PThreadLibrary): PThreadWorker { return worker; } } - mono_log_warn("Failed to find loaded WebWorker, this may deadlock. Please increase the pthreadPoolSize."); + mono_log_warn(`Failed to find loaded WebWorker, this may deadlock. Please increase the pthreadPoolSize. Running threads ${modulePThread.runningWorkers.length}. Loading workers: ${modulePThread.unusedWorkers.length}`); availableThreadCount--; // negative value return modulePThread.unusedWorkers.pop()!; } @@ -96,8 +108,32 @@ function allocateUnusedWorker(): PThreadWorker { const uri = asset.resolvedUrl; mono_assert(uri !== undefined, "could not resolve the uri for the js-module-threads asset"); const worker = new Worker(uri) as PThreadWorker; - Internals.getUnusedWorkerPool().push(worker); + getUnusedWorkerPool().push(worker); worker.loaded = false; - worker.interopInstalled = false; + worker.info = { + pthreadId: 0, + reuseCount: 0, + updateCount: 0, + threadName: "", + }; return worker; } + + +export function dumpThreads(): void { + if (!MonoWasmThreads) return; + // eslint-disable-next-line no-console + console.log("Running workers:"); + getRunningWorkers().forEach((worker) => { + // eslint-disable-next-line no-console + console.log(`${worker.info.threadName}: isRunning:${worker.info.isRunning} isAttached:${worker.info.isAttached} isExternalEventLoop:${worker.info.isExternalEventLoop} ${JSON.stringify(worker.info)}`); + }); + + // eslint-disable-next-line no-console + console.log("Unused workers:"); + getUnusedWorkerPool().forEach((worker) => { + // eslint-disable-next-line no-console + console.log(`${worker.info.threadName}: isRunning:${worker.info.isRunning} isAttached:${worker.info.isAttached} isExternalEventLoop:${worker.info.isExternalEventLoop} ${JSON.stringify(worker.info)}`); + }); + +} diff --git a/src/mono/browser/runtime/pthreads/shared/index.ts b/src/mono/browser/runtime/pthreads/shared/index.ts index 0024e09f1e4c89..824a50fcbc66bd 100644 --- a/src/mono/browser/runtime/pthreads/shared/index.ts +++ b/src/mono/browser/runtime/pthreads/shared/index.ts @@ -4,32 +4,13 @@ import MonoWasmThreads from "consts:monoWasmThreads"; import BuildConfiguration from "consts:configuration"; -import { ENVIRONMENT_IS_PTHREAD, Module, mono_assert, runtimeHelpers } from "../../globals"; -import { MonoConfig } from "../../types"; -import { pthreadPtr } from "./types"; -import { mono_log_debug } from "../../logging"; +import { ENVIRONMENT_IS_PTHREAD, Module, loaderHelpers, mono_assert, runtimeHelpers } from "../../globals"; +import { mono_log_debug, mono_set_thread_name } from "../../logging"; import { bindings_init } from "../../startup"; import { forceDisposeProxies } from "../../gc-handles"; -import { pthread_self } from "../worker"; -import { GCHandle, GCHandleNull } from "../../types/internal"; - -export interface PThreadInfo { - readonly pthreadId: pthreadPtr; - readonly isBrowserThread: boolean; -} - -export const MainThread: PThreadInfo = { - get pthreadId(): pthreadPtr { - return mono_wasm_main_thread_ptr(); - }, - isBrowserThread: true -}; - -const enum WorkerMonoCommandType { - enabledInterop = "notify_enabled_interop", - channelCreated = "channel_created", - preload = "preload", -} +import { GCHandle, GCHandleNull, WorkerToMainMessageType, monoMessageSymbol } from "../../types/internal"; +import { MonoWorkerToMainMessage } from "./types"; +import { monoThreadInfo } from "../worker"; /// Messages sent on the dedicated mono channel between a pthread and the browser thread @@ -49,116 +30,6 @@ export function isMonoThreadMessage(x: unknown): x is MonoThreadMessage { return typeof (xmsg.type) === "string" && typeof (xmsg.cmd) === "string"; } -// message from the main thread to the pthread worker that passes the MonoConfig to the worker -export interface MonoThreadMessageApplyMonoConfig extends MonoThreadMessage { - type: "pthread"; - cmd: "apply_mono_config"; - config: string; -} - -export function makeMonoThreadMessageApplyMonoConfig(config: MonoConfig): MonoThreadMessageApplyMonoConfig { - return { - type: "pthread", - cmd: "apply_mono_config", - config: JSON.stringify(config) - }; -} - -/// Messages sent using the worker object's postMessage() method /// - -/// a symbol that we use as a key on messages on the global worker-to-main channel to identify our own messages -/// we can't use an actual JS Symbol because those don't transfer between workers. -export const monoSymbol = "__mono_message_please_dont_collide__"; //Symbol("mono"); - -/// Messages sent from the main thread using Worker.postMessage or from the worker using DedicatedWorkerGlobalScope.postMessage -/// should use this interface. The message event is also used by emscripten internals (and possibly by 3rd party libraries targeting Emscripten). -/// We should just use this to establish a dedicated MessagePort for Mono's uses. -export interface MonoWorkerMessage { - [monoSymbol]: { - monoCmd: WorkerMonoCommandType; - }; -} -export type MonoWorkerMessagePort = MonoWorkerMessage & { - [monoSymbol]: { - port: MessagePort; - }; -} - -/// The message sent early during pthread creation to set up a dedicated MessagePort for Mono between the main thread and the pthread. -export interface MonoWorkerMessageChannelCreated extends MonoWorkerMessage { - [monoSymbol]: { - monoCmd: WorkerMonoCommandType.channelCreated; - threadId: pthreadPtr; - port: MessagePort; - }; -} - -export interface MonoWorkerMessageEnabledInterop extends MonoWorkerMessage { - [monoSymbol]: { - monoCmd: WorkerMonoCommandType.enabledInterop; - threadId: pthreadPtr; - }; -} - -export interface MonoWorkerMessagePreload extends MonoWorkerMessagePort { - [monoSymbol]: { - monoCmd: WorkerMonoCommandType.preload; - port: MessagePort; - }; -} - -export function makeChannelCreatedMonoMessage(threadId: pthreadPtr, port: MessagePort): MonoWorkerMessageChannelCreated { - return { - [monoSymbol]: { - monoCmd: WorkerMonoCommandType.channelCreated, - threadId: threadId, - port - } - }; -} -export function makeEnabledInteropMonoMessage(threadId: pthreadPtr): MonoWorkerMessageEnabledInterop { - return { - [monoSymbol]: { - monoCmd: WorkerMonoCommandType.enabledInterop, - threadId: threadId, - } - }; -} - -export function isMonoWorkerMessage(message: unknown): message is MonoWorkerMessage { - return message !== undefined && typeof message === "object" && message !== null && monoSymbol in message; -} - -export function isMonoWorkerMessageChannelCreated(message: MonoWorkerMessage): message is MonoWorkerMessageChannelCreated { - if (isMonoWorkerMessage(message)) { - const monoMessage = message[monoSymbol]; - if (monoMessage.monoCmd === WorkerMonoCommandType.channelCreated) { - return true; - } - } - return false; -} - -export function isMonoWorkerMessageEnabledInterop(message: MonoWorkerMessage): message is MonoWorkerMessageEnabledInterop { - if (isMonoWorkerMessage(message)) { - const monoMessage = message[monoSymbol]; - if (monoMessage.monoCmd === WorkerMonoCommandType.enabledInterop) { - return true; - } - } - return false; -} - -export function isMonoWorkerMessagePreload(message: MonoWorkerMessage): message is MonoWorkerMessagePreload { - if (isMonoWorkerMessage(message)) { - const monoMessage = message[monoSymbol]; - if (monoMessage.monoCmd === WorkerMonoCommandType.preload) { - return true; - } - } - return false; -} - export function mono_wasm_install_js_worker_interop(context_gc_handle: GCHandle): void { if (!MonoWasmThreads) return; bindings_init(); @@ -167,11 +38,14 @@ export function mono_wasm_install_js_worker_interop(context_gc_handle: GCHandle) mono_log_debug("Installed JSSynchronizationContext"); } Module.runtimeKeepalivePush(); + monoThreadInfo.isDirtyBecauseOfInterop = true; + update_thread_info(); if (ENVIRONMENT_IS_PTHREAD) { - self.postMessage(makeEnabledInteropMonoMessage(pthread_self.pthreadId), []); + postMessageToMain({ + monoCmd: WorkerToMainMessageType.enabledInterop, + info: monoThreadInfo, + }); } - - set_thread_info(pthread_self ? pthread_self.pthreadId : 0, true, true, true); } export function mono_wasm_uninstall_js_worker_interop(): void { @@ -184,14 +58,21 @@ export function mono_wasm_uninstall_js_worker_interop(): void { runtimeHelpers.proxy_context_gc_handle = GCHandleNull; runtimeHelpers.mono_wasm_bindings_is_ready = false; - set_thread_info(pthread_self ? pthread_self.pthreadId : 0, true, false, false); + update_thread_info(); } // 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 { +export function update_thread_info(): void { + loaderHelpers.mono_set_thread_name(monoThreadInfo.threadName!); + if (!loaderHelpers.config.forwardConsoleLogsToWS) { + mono_set_thread_name(monoThreadInfo.threadName!); + } + + (globalThis as any).monoThreadInfo = monoThreadInfo; if (MonoWasmThreads && BuildConfiguration === "Debug" && !runtimeHelpers.cspPolicy) { + monoThreadInfo.updateCount++; 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}" );`); + (globalThis as any).monoThreadInfoFn = new Function(`//# sourceURL=https://${monoThreadInfo.updateCount}WorkerInfo${monoThreadInfo.isAttached ? monoThreadInfo.threadName : ""}/\r\nconsole.log("${JSON.stringify(monoThreadInfo)}");`); } catch (ex) { runtimeHelpers.cspPolicy = true; @@ -208,3 +89,9 @@ export function mono_wasm_main_thread_ptr(): number { if (!MonoWasmThreads) return 0; return (Module)["_emscripten_main_runtime_thread_id"](); } + +export function postMessageToMain(message: MonoWorkerToMainMessage, transfer?: Transferable[]) { + self.postMessage({ + [monoMessageSymbol]: message + }, transfer ? transfer : []); +} \ No newline at end of file diff --git a/src/mono/browser/runtime/pthreads/shared/types.ts b/src/mono/browser/runtime/pthreads/shared/types.ts index 0378fbedde30c8..ab7e3b1a238bee 100644 --- a/src/mono/browser/runtime/pthreads/shared/types.ts +++ b/src/mono/browser/runtime/pthreads/shared/types.ts @@ -1,5 +1,41 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +import type { WorkerToMainMessageType } from "../../types/internal"; + /// pthread_t in C export type pthreadPtr = number; + +export interface PThreadInfo { + pthreadId: pthreadPtr; + + reuseCount: number, + updateCount: number, + + name?: string, + threadName: string, + + isLoaded?: boolean, + isRegistered?: boolean, + isRunning?: boolean, + isAttached?: boolean, + isExternalEventLoop?: boolean, + isBrowserThread?: boolean; + isBackground?: boolean, + isDebugger?: boolean, + isThreadPool?: boolean, + isTimer?: boolean, + isLongRunning?: boolean, + isThreadPoolGate?: boolean, + isFinalizer?: boolean, + isDirtyBecauseOfInterop?: boolean, +} + +/// Messages sent from the main thread using Worker.postMessage or from the worker using DedicatedWorkerGlobalScope.postMessage +/// should use this interface. The message event is also used by emscripten internals (and possibly by 3rd party libraries targeting Emscripten). +/// We should just use this to establish a dedicated MessagePort for Mono's uses. +export interface MonoWorkerToMainMessage { + monoCmd: WorkerToMainMessageType; + info: PThreadInfo; + port?: MessagePort; +} diff --git a/src/mono/browser/runtime/pthreads/worker/events.ts b/src/mono/browser/runtime/pthreads/worker/events.ts index 692b2fd0a191ab..6917a2cb8f4aba 100644 --- a/src/mono/browser/runtime/pthreads/worker/events.ts +++ b/src/mono/browser/runtime/pthreads/worker/events.ts @@ -2,17 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import MonoWasmThreads from "consts:monoWasmThreads"; -import type { pthreadPtr } from "../shared/types"; -import type { PThreadInfo, MonoThreadMessage } from "../shared"; - -/// Identification of the current thread executing on a worker -export interface PThreadSelf extends PThreadInfo { - readonly pthreadId: pthreadPtr; - readonly portToBrowser: MessagePort; - readonly isBrowserThread: boolean; - postMessageToBrowser: (message: T, transfer?: Transferable[]) => void; - addEventListenerFromBrowser: (listener: (event: MessageEvent) => void) => void; -} +import { PThreadSelf } from "./index"; export const dotnetPthreadCreated = "dotnet:pthread:created" as const; export const dotnetPthreadAttached = "dotnet:pthread:attached" as const; diff --git a/src/mono/browser/runtime/pthreads/worker/index.ts b/src/mono/browser/runtime/pthreads/worker/index.ts index 468c703bf65e95..ee0377a0d9f523 100644 --- a/src/mono/browser/runtime/pthreads/worker/index.ts +++ b/src/mono/browser/runtime/pthreads/worker/index.ts @@ -5,21 +5,21 @@ import MonoWasmThreads from "consts:monoWasmThreads"; -import { ENVIRONMENT_IS_PTHREAD, mono_assert, loaderHelpers } from "../../globals"; -import { makeChannelCreatedMonoMessage, mono_wasm_pthread_ptr, set_thread_info } from "../shared"; -import type { pthreadPtr } from "../shared/types"; -import { is_nullish } from "../../types/internal"; -import type { MonoThreadMessage } from "../shared"; +import { ENVIRONMENT_IS_PTHREAD, mono_assert } from "../../globals"; +import { mono_wasm_pthread_ptr, postMessageToMain, update_thread_info } from "../shared"; +import { PThreadInfo } from "../shared/types"; +import { WorkerToMainMessageType, is_nullish } from "../../types/internal"; +import { MonoThreadMessage } from "../shared"; import { - PThreadSelf, makeWorkerThreadEvent, dotnetPthreadCreated, dotnetPthreadAttached, WorkerThreadEventTarget } from "./events"; import { postRunWorker, preRunWorker } from "../../startup"; -import { mono_log_debug, mono_set_thread_name } from "../../logging"; -import { jiterpreter_allocate_tables } from "../../jiterpreter-support"; +import { mono_log_debug } from "../../logging"; +import { CharPtr } from "../../types/emscripten"; +import { utf8ToString } from "../../strings"; // re-export some of the events types export { @@ -30,9 +30,18 @@ export { WorkerThreadEventTarget, } from "./events"; +/// Identification of the current thread executing on a worker +export interface PThreadSelf { + info: PThreadInfo; + portToBrowser: MessagePort; + postMessageToBrowser: (message: T, transfer?: Transferable[]) => void; + addEventListenerFromBrowser: (listener: (event: MessageEvent) => void) => void; +} + class WorkerSelf implements PThreadSelf { - readonly isBrowserThread = false; - constructor(readonly pthreadId: pthreadPtr, readonly portToBrowser: MessagePort) { } + constructor(public info: PThreadInfo, public portToBrowser: MessagePort) { + } + postMessageToBrowser(message: MonoThreadMessage, transfer?: Transferable[]) { if (transfer) { this.portToBrowser.postMessage(message, transfer); @@ -48,6 +57,12 @@ class WorkerSelf implements PThreadSelf { // we are lying that this is never null, but afterThreadInit should be the first time we get to run any code // in the worker, so this becomes non-null very early. export let pthread_self: PThreadSelf = null as any as PThreadSelf; +export const monoThreadInfo: PThreadInfo = { + pthreadId: 0, + reuseCount: 0, + updateCount: 0, + threadName: "", +}; /// This is the "public internal" API for runtime subsystems that wish to be notified about /// pthreads that are running on the current worker. @@ -69,53 +84,98 @@ function monoDedicatedChannelMessageFromMainToWorker(event: MessageEvent } -function setupChannelToMainThread(pthread_ptr: pthreadPtr): PThreadSelf { - if (!MonoWasmThreads) return null as any; +/// Called by emscripten when a pthread is setup to run on a worker. Can be called multiple times +/// for the same webworker, since emscripten can reuse workers. +/// This is an implementation detail, that shouldn't be used directly. +export function mono_wasm_pthread_on_pthread_created(): void { + if (!MonoWasmThreads) return; + + const pthread_id = mono_wasm_pthread_ptr(); + mono_assert(!is_nullish(pthread_id), "pthread_self() returned null"); + monoThreadInfo.pthreadId = pthread_id; + monoThreadInfo.reuseCount++; + monoThreadInfo.updateCount++; + monoThreadInfo.threadName = `0x${pthread_id.toString(16).padStart(8, "0")}`; + update_thread_info(); + + // don't do this callback for the main thread + if (!ENVIRONMENT_IS_PTHREAD) return; + + currentWorkerThreadEvents.dispatchEvent(makeWorkerThreadEvent(dotnetPthreadCreated, pthread_self)); + const channel = new MessageChannel(); const workerPort = channel.port1; const mainPort = channel.port2; workerPort.addEventListener("message", monoDedicatedChannelMessageFromMainToWorker); workerPort.start(); - pthread_self = new WorkerSelf(pthread_ptr, workerPort); - self.postMessage(makeChannelCreatedMonoMessage(pthread_ptr, mainPort), [mainPort]); - return pthread_self; + + // this could be replacement + if (pthread_self && pthread_self.portToBrowser) { + pthread_self.portToBrowser.close(); + } + + pthread_self = new WorkerSelf(monoThreadInfo, workerPort); + postMessageToMain({ + monoCmd: WorkerToMainMessageType.pthreadCreated, + info: monoThreadInfo, + port: mainPort, + }, [mainPort]); } +/// Called in the worker thread (not main thread) from mono when a pthread becomes registered to the mono runtime. +export function mono_wasm_pthread_on_pthread_registered(pthread_id: number): void { + if (!MonoWasmThreads) return; + mono_assert(monoThreadInfo !== null && monoThreadInfo.pthreadId == pthread_id, "expected monoThreadInfo to be set already when registering"); + postMessageToMain({ + monoCmd: WorkerToMainMessageType.monoRegistered, + info: monoThreadInfo, + }); + preRunWorker(); +} -/// This is an implementation detail function. /// 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 { +export function mono_wasm_pthread_on_pthread_attached(pthread_id: number, thread_name: CharPtr, background_thread: number, threadpool_thread: number, external_eventloop: number, debugger_thread: number): void { if (!MonoWasmThreads) return; - mono_assert(pthread_self !== null && pthread_self.pthreadId == pthread_id, "expected pthread_self to be set already when attaching"); - const threadName = `0x${pthread_id.toString(16)}-worker`; - mono_set_thread_name(threadName); - loaderHelpers.mono_set_thread_name(threadName); - preRunWorker(); - set_thread_info(pthread_id, true, false, false); - jiterpreter_allocate_tables(); + mono_assert(monoThreadInfo !== null && monoThreadInfo.pthreadId == pthread_id, "expected monoThreadInfo to be set already when attaching"); + + const name = monoThreadInfo.name = utf8ToString(thread_name); + monoThreadInfo.isAttached = true; + monoThreadInfo.isThreadPool = threadpool_thread !== 0; + monoThreadInfo.isExternalEventLoop = external_eventloop !== 0; + monoThreadInfo.isBackground = background_thread !== 0; + monoThreadInfo.isDebugger = debugger_thread !== 0; + + // FIXME: this is a hack to get constant length thread names + monoThreadInfo.isTimer = name == ".NET Timer"; + monoThreadInfo.isLongRunning = name == ".NET Long Running Task"; + monoThreadInfo.isThreadPoolGate = name == ".NET TP Gate"; + const threadType = monoThreadInfo.isTimer ? "timr" + : monoThreadInfo.isLongRunning ? "long" + : monoThreadInfo.isThreadPoolGate ? "gate" + : monoThreadInfo.isDebugger ? "dbgr" + : monoThreadInfo.isThreadPool ? "pool" + : monoThreadInfo.isExternalEventLoop ? "jsww" + : monoThreadInfo.isBackground ? "back" + : "norm"; + monoThreadInfo.threadName = `0x${pthread_id.toString(16).padStart(8, "0")}-${threadType}`; + update_thread_info(); currentWorkerThreadEvents.dispatchEvent(makeWorkerThreadEvent(dotnetPthreadAttached, pthread_self)); + postMessageToMain({ + monoCmd: WorkerToMainMessageType.monoAttached, + info: monoThreadInfo, + }); } /// 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 { +export function mono_wasm_pthread_on_pthread_unregistered(pthread_id: number): void { if (!MonoWasmThreads) return; + mono_assert(pthread_id === monoThreadInfo.pthreadId, "expected pthread_id to match when un-registering"); postRunWorker(); - set_thread_info(pthread_id, false, false, false); - const threadName = `0x${pthread_id.toString(16)}-worker-detached`; - mono_set_thread_name(threadName); - loaderHelpers.mono_set_thread_name(threadName); -} - -/// 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. -export function afterThreadInitTLS(): void { - if (!MonoWasmThreads) return; - // don't do this callback for the main thread - if (ENVIRONMENT_IS_PTHREAD) { - const pthread_ptr = mono_wasm_pthread_ptr(); - mono_assert(!is_nullish(pthread_ptr), "pthread_self() returned null"); - const pthread_self = setupChannelToMainThread(pthread_ptr); - currentWorkerThreadEvents.dispatchEvent(makeWorkerThreadEvent(dotnetPthreadCreated, pthread_self)); - } + monoThreadInfo.isAttached = false; + monoThreadInfo.threadName = monoThreadInfo.threadName + "=>detached"; + update_thread_info(); + postMessageToMain({ + monoCmd: WorkerToMainMessageType.monoUnRegistered, + info: monoThreadInfo, + }); } diff --git a/src/mono/browser/runtime/run.ts b/src/mono/browser/runtime/run.ts index 4fda22cc2e713a..8443fc647749d3 100644 --- a/src/mono/browser/runtime/run.ts +++ b/src/mono/browser/runtime/run.ts @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +import MonoWasmThreads from "consts:monoWasmThreads"; + import { ENVIRONMENT_IS_NODE, loaderHelpers, runtimeHelpers } from "./globals"; import { mono_wasm_wait_for_debugger } from "./debug"; import { mono_wasm_set_main_args } from "./startup"; @@ -8,6 +10,7 @@ import cwraps from "./cwraps"; import { mono_log_info } from "./logging"; import { assert_js_interop } from "./invoke-js"; import { assembly_load } from "./invoke-cs"; +import { cancelThreads } from "./pthreads/browser"; /** * Possible signatures are described here https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/main-command-line @@ -81,3 +84,17 @@ export function find_entry_point(assembly: string) { return method; } +export function nativeExit(code: number) { + if (MonoWasmThreads) { + cancelThreads(); + } + cwraps.mono_wasm_exit(code); +} + +export function nativeAbort(reason: any) { + loaderHelpers.exitReason = reason; + if (!loaderHelpers.is_exited()) { + cwraps.mono_wasm_abort(); + } + throw reason; +} \ No newline at end of file diff --git a/src/mono/browser/runtime/snapshot.ts b/src/mono/browser/runtime/snapshot.ts index 7375c3399352f4..1806c748bc28f2 100644 --- a/src/mono/browser/runtime/snapshot.ts +++ b/src/mono/browser/runtime/snapshot.ts @@ -3,21 +3,24 @@ import ProductVersion from "consts:productVersion"; import MonoWasmThreads from "consts:monoWasmThreads"; -import { ENVIRONMENT_IS_WEB, loaderHelpers, runtimeHelpers } from "./globals"; +import { ENVIRONMENT_IS_WEB, ENVIRONMENT_IS_WORKER, loaderHelpers, runtimeHelpers } from "./globals"; import { mono_log_warn } from "./logging"; +import { MonoConfigInternal } from "./types/internal"; export const memoryPrefix = "https://dotnet.generated.invalid/wasm-memory"; // adapted from Blazor's WebAssemblyResourceLoader.ts export async function openCache(): Promise { - // caches will be undefined if we're running on an insecure origin (secure means https or localhost) - if (typeof globalThis.caches === "undefined") { - return null; - } - // cache integrity is compromised if the first request has been served over http (except localhost) // in this case, we want to disable caching and integrity validation if (ENVIRONMENT_IS_WEB && globalThis.window.isSecureContext === false) { + mono_log_warn("Failed to open the cache, running on an insecure origin"); + return null; + } + + // caches will be undefined if we're running on an insecure origin (secure means https or localhost) + if (typeof globalThis.caches === "undefined") { + mono_log_warn("Failed to open the cache, probably running on an insecure origin"); return null; } @@ -46,6 +49,9 @@ export async function openCache(): Promise { export async function checkMemorySnapshotSize(): Promise { try { + if (!ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { + return; + } if (!runtimeHelpers.config.startupMemoryCache) { // we could start downloading DLLs because snapshot is disabled return; @@ -110,11 +116,11 @@ export async function getCacheEntry(cacheKey: string): Promise { try { const cache = await openCache(); if (!cache) { - return; + return false; } const copy = MonoWasmThreads // storing SHaredArrayBuffer in the cache is not working @@ -129,9 +135,11 @@ export async function storeCacheEntry(cacheKey: string, memory: ArrayBuffer, mim }); await cache.put(cacheKey, responseToCache); + + return true; } catch (ex) { mono_log_warn("Failed to store entry to the cache: " + cacheKey, ex); - return; + return false; } } @@ -167,10 +175,10 @@ export async function getCacheKey(prefix: string): Promise { if (!runtimeHelpers.subtle) { return null; } - const inputs = Object.assign({}, runtimeHelpers.config) as any; + const inputs = Object.assign({}, runtimeHelpers.config) as MonoConfigInternal; // Now we remove assets collection from the hash. - inputs.resourcesHash = inputs.resources.hash; + inputs.resourcesHash = inputs.resources!.hash; delete inputs.assets; delete inputs.resources; // some things are calculated at runtime, so we need to add them to the hash @@ -192,6 +200,7 @@ export async function getCacheKey(prefix: string): Promise { delete inputs.enableDownloadRetry; delete inputs.exitAfterSnapshot; delete inputs.extensions; + delete inputs.runtimeId; inputs.GitHash = loaderHelpers.gitHash; inputs.ProductVersion = ProductVersion; diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts index 55088f67fe72a0..825f93b033ea37 100644 --- a/src/mono/browser/runtime/startup.ts +++ b/src/mono/browser/runtime/startup.ts @@ -22,15 +22,17 @@ import { replace_linker_placeholders } from "./exports-binding"; import { endMeasure, MeasuredBlock, startMeasure } from "./profiler"; import { checkMemorySnapshotSize, getMemorySnapshot, storeMemorySnapshot } from "./snapshot"; import { interp_pgo_load_data, interp_pgo_save_data } from "./interp-pgo"; -import { mono_log_debug, mono_log_error, mono_log_warn, mono_set_thread_name } from "./logging"; +import { mono_log_debug, mono_log_error, mono_log_warn } from "./logging"; // threads import { preAllocatePThreadWorkerPool, instantiateWasmPThreadWorkerPool } from "./pthreads/browser"; import { currentWorkerThreadEvents, dotnetPthreadCreated, initWorkerThreadEvents } from "./pthreads/worker"; -import { mono_wasm_main_thread_ptr } from "./pthreads/shared"; +import { mono_wasm_main_thread_ptr, mono_wasm_pthread_ptr } from "./pthreads/shared"; import { jiterpreter_allocate_tables } from "./jiterpreter-support"; import { localHeapViewU8 } from "./memory"; import { assertNoProxies } from "./gc-handles"; +import { runtimeList } from "./exports"; +import { nativeAbort, nativeExit } from "./run"; export async function configureRuntimeStartup(): Promise { await init_polyfills_async(); @@ -175,6 +177,8 @@ async function preInitWorkerAsync() { await ensureUsedWasmFeatures(); await init_polyfills_async(); runtimeHelpers.afterPreInit.promise_control.resolve(); + exportedRuntimeAPI.runtimeId = loaderHelpers.config.runtimeId!; + runtimeList.registerRuntime(exportedRuntimeAPI); endMeasure(mark, MeasuredBlock.preInitWorker); } catch (err) { mono_log_error("user preInitWorker() failed", err); @@ -183,9 +187,11 @@ async function preInitWorkerAsync() { } } +// runs for each re-attached worker export function preRunWorker() { - // signal next stage + jiterpreter_allocate_tables(); // this will return quickly if already allocated runtimeHelpers.runtimeReady = true; + // signal next stage runtimeHelpers.afterPreRun.promise_control.resolve(); } @@ -216,14 +222,8 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { await runtimeHelpers.afterPreRun.promise; mono_log_debug("onRuntimeInitialized"); - runtimeHelpers.mono_wasm_exit = cwraps.mono_wasm_exit; - runtimeHelpers.abort = (reason: any) => { - loaderHelpers.exitReason = reason; - if (!loaderHelpers.is_exited()) { - cwraps.mono_wasm_abort(); - } - throw reason; - }; + runtimeHelpers.nativeExit = nativeExit; + runtimeHelpers.nativeAbort = nativeAbort; const mark = startMeasure(); // signal this stage, this will allow pending assets to allocate memory @@ -257,7 +257,6 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { if (!ENVIRONMENT_IS_WORKER) { Module.runtimeKeepalivePush(); } - runtimeHelpers.runtimeReady = true; if (runtimeHelpers.config.virtualWorkingDirectory) { const FS = Module.FS; @@ -276,12 +275,14 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { bindings_init(); jiterpreter_allocate_tables(); - runtimeHelpers.runtimeReady = true; if (ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER) { Module.runtimeKeepalivePush(); } + runtimeHelpers.runtimeReady = true; + runtimeList.registerRuntime(exportedRuntimeAPI); + if (MonoWasmThreads) { runtimeHelpers.javaScriptExports.install_main_synchronization_context(); } @@ -343,8 +344,15 @@ async function postRunAsync(userpostRun: (() => void)[]) { runtimeHelpers.afterPostRun.promise_control.resolve(); } +// runs for each re-detached worker export function postRunWorker() { - assertNoProxies(); + if (runtimeHelpers.proxy_context_gc_handle) { + const pthread_ptr = mono_wasm_pthread_ptr(); + mono_log_warn(`JSSynchronizationContext is still installed on worker 0x${pthread_ptr.toString(16)}.`); + } else { + assertNoProxies(); + } + // signal next stage runtimeHelpers.runtimeReady = false; runtimeHelpers.afterPreRun = createPromiseController(); @@ -355,7 +363,6 @@ async function mono_wasm_init_threads() { return; } const threadName = `0x${mono_wasm_main_thread_ptr().toString(16)}-main`; - mono_set_thread_name(threadName); loaderHelpers.mono_set_thread_name(threadName); await instantiateWasmPThreadWorkerPool(); await mono_wasm_init_diagnostics(); diff --git a/src/mono/browser/runtime/types/internal.ts b/src/mono/browser/runtime/types/internal.ts index 00b02c60ad3964..c30acbb67e5e89 100644 --- a/src/mono/browser/runtime/types/internal.ts +++ b/src/mono/browser/runtime/types/internal.ts @@ -1,7 +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 type { AssetBehaviors, AssetEntry, DotnetModuleConfig, LoadBootResourceCallback, LoadingResource, MonoConfig, RuntimeAPI } from "."; +import type { AssetEntry, DotnetModuleConfig, LoadBootResourceCallback, LoadingResource, MonoConfig, RuntimeAPI, SingleAssetBehaviors } from "."; import type { PThreadLibrary } from "../pthreads/shared/emscripten-internals"; import type { CharPtr, EmscriptenModule, ManagedPointer, NativePointer, VoidPtr, Int32Ptr } from "./emscripten"; @@ -83,6 +83,13 @@ export type MonoConfigInternal = MonoConfig & { asyncFlushOnExit?: boolean exitAfterSnapshot?: number loadAllSatelliteResources?: boolean + runtimeId?: number + + // related to config hash + preferredIcuAsset?: string | null, + resourcesHash?: string, + GitHash?: string, + ProductVersion?: string, }; export type RunArguments = { @@ -118,7 +125,7 @@ export type LoaderHelpers = { scriptDirectory: string scriptUrl: string modulesUniqueQuery?: string - preferredIcuAsset: string | null, + preferredIcuAsset?: string | null, invariantMode: boolean, actual_downloaded_assets_count: number, @@ -140,7 +147,7 @@ export type LoaderHelpers = { getPromiseController: (promise: ControllablePromise) => PromiseController, assertIsControllablePromise: (promise: Promise) => asserts promise is ControllablePromise, mono_download_assets: () => Promise, - resolve_single_asset_path: (behavior: AssetBehaviors) => AssetEntryInternal, + resolve_single_asset_path: (behavior: SingleAssetBehaviors) => AssetEntryInternal, setup_proxy_console: (id: string, console: Console, origin: string) => void mono_set_thread_name: (tid: string) => void fetch_like: (url: string, init?: RequestInit) => Promise; @@ -185,8 +192,8 @@ export type RuntimeHelpers = { waitForDebugger?: number; ExitStatus: ExitStatusError; quit: Function, - mono_wasm_exit?: (code: number) => void, - abort: (reason: any) => void, + nativeExit: (code: number) => void, + nativeAbort: (reason: any) => void, javaScriptExports: JavaScriptExports, storeMemorySnapshotPending: boolean, memorySnapshotCacheKey: string, @@ -232,50 +239,6 @@ export type BrowserProfilerOptions = { export type DotnetModule = EmscriptenModule & DotnetModuleConfig; export type DotnetModuleInternal = EmscriptenModule & DotnetModuleConfig & EmscriptenModuleInternal; -// see src/mono/wasm/driver.c MARSHAL_TYPE_xxx and Runtime.cs MarshalType -export const enum MarshalType { - NULL = 0, - INT = 1, - FP64 = 2, - STRING = 3, - VT = 4, - DELEGATE = 5, - TASK = 6, - OBJECT = 7, - BOOL = 8, - ENUM = 9, - URI = 22, - SAFEHANDLE = 23, - ARRAY_BYTE = 10, - ARRAY_UBYTE = 11, - ARRAY_UBYTE_C = 12, - ARRAY_SHORT = 13, - ARRAY_USHORT = 14, - ARRAY_INT = 15, - ARRAY_UINT = 16, - ARRAY_FLOAT = 17, - ARRAY_DOUBLE = 18, - FP32 = 24, - UINT32 = 25, - INT64 = 26, - UINT64 = 27, - CHAR = 28, - STRING_INTERNED = 29, - VOID = 30, - ENUM64 = 31, - POINTER = 32, - SPAN_BYTE = 33, -} - -// see src/mono/wasm/driver.c MARSHAL_ERROR_xxx and Runtime.cs -export const enum MarshalError { - BUFFER_TOO_SMALL = 512, - NULL_CLASS_POINTER = 513, - NULL_TYPE_POINTER = 514, - UNSUPPORTED_TYPE = 515, - FIRST = BUFFER_TOO_SMALL -} - // Evaluates whether a value is nullish (same definition used as the ?? operator, // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator) export function is_nullish(value: T | null | undefined): value is null | undefined { @@ -526,4 +489,21 @@ export type NativeModuleExportsInternal = { export type WeakRefInternal = WeakRef & { dispose?: () => void -} \ No newline at end of file +} + +/// a symbol that we use as a key on messages on the global worker-to-main channel to identify our own messages +/// we can't use an actual JS Symbol because those don't transfer between workers. +export const monoMessageSymbol = "__mono_message__"; + +export const enum WorkerToMainMessageType { + monoRegistered = "monoRegistered", + monoAttached = "monoAttached", + enabledInterop = "notify_enabled_interop", + monoUnRegistered = "monoUnRegistered", + pthreadCreated = "pthreadCreated", + preload = "preload", +} + +export const enum MainToWorkerMessageType { + applyConfig = "apply_mono_config", +} diff --git a/src/mono/browser/runtime/web-socket.ts b/src/mono/browser/runtime/web-socket.ts index 8f95dee771311f..f2be9bd5d5f923 100644 --- a/src/mono/browser/runtime/web-socket.ts +++ b/src/mono/browser/runtime/web-socket.ts @@ -47,8 +47,14 @@ export function ws_wasm_create(uri: string, sub_protocols: string[] | null, rece verifyEnvironment(); assert_js_interop(); mono_assert(uri && typeof uri === "string", () => `ERR12: Invalid uri ${typeof uri}`); - - const ws = new globalThis.WebSocket(uri, sub_protocols || undefined) as WebSocketExtension; + let ws: WebSocketExtension; + try { + ws = new globalThis.WebSocket(uri, sub_protocols || undefined) as WebSocketExtension; + } + catch (e) { + mono_log_warn("WebSocket error", e); + throw e; + } const { promise_control: open_promise_control } = createPromiseController(); ws[wasm_ws_pending_receive_event_queue] = new Queue(); diff --git a/src/mono/mono/metadata/threads.c b/src/mono/mono/metadata/threads.c index 80b21be8444e19..0fe313db637cf9 100644 --- a/src/mono/mono/metadata/threads.c +++ b/src/mono/mono/metadata/threads.c @@ -781,6 +781,7 @@ mono_thread_attach_internal (MonoThread *thread, gboolean force_attach) MonoInternalThread *internal; MonoDomain *domain = mono_get_root_domain (); MonoGCHandle gchandle; + MonoNativeThreadId tid; g_assert (thread); @@ -799,7 +800,8 @@ mono_thread_attach_internal (MonoThread *thread, gboolean force_attach) internal->handle = mono_threads_open_thread_handle (info->handle); internal->native_handle = MONO_NATIVE_THREAD_HANDLE_TO_GPOINTER (mono_threads_open_native_thread_handle (info->native_handle)); - internal->tid = MONO_NATIVE_THREAD_ID_TO_UINT (mono_native_thread_id_get ()); + tid = mono_native_thread_id_get (); + internal->tid = MONO_NATIVE_THREAD_ID_TO_UINT (tid); internal->thread_info = info; internal->small_id = info->small_id; @@ -809,6 +811,10 @@ mono_thread_attach_internal (MonoThread *thread, gboolean force_attach) mono_domain_set_fast (domain); +#ifdef HOST_BROWSER + mono_threads_wasm_on_thread_attached (tid, internal->name.chars, (internal->state & ThreadState_Background) != 0, internal->threadpool_thread, internal->external_eventloop, internal->debugger_thread != 0); +#endif + mono_threads_lock (); if (shutting_down && !force_attach) { diff --git a/src/mono/mono/utils/mono-threads-wasm.c b/src/mono/mono/utils/mono-threads-wasm.c index e1fb6bfda492b1..f3c0a429ff2d02 100644 --- a/src/mono/mono/utils/mono-threads-wasm.c +++ b/src/mono/mono/utils/mono-threads-wasm.c @@ -477,12 +477,13 @@ mono_threads_wasm_ui_thread_tid (void) } #ifndef DISABLE_THREADS -extern void mono_wasm_pthread_on_pthread_attached (MonoNativeThreadId pthread_id); -extern void mono_wasm_pthread_on_pthread_detached (MonoNativeThreadId pthread_id); +extern void mono_wasm_pthread_on_pthread_attached (MonoNativeThreadId pthread_id, const char* thread_name, gboolean background_thread, gboolean threadpool_thread, gboolean external_eventloop, gboolean debugger_thread); +extern void mono_wasm_pthread_on_pthread_unregistered (MonoNativeThreadId pthread_id); +extern void mono_wasm_pthread_on_pthread_registered (MonoNativeThreadId pthread_id); #endif void -mono_threads_wasm_on_thread_attached (void) +mono_threads_wasm_on_thread_attached (pthread_t tid, const char* thread_name, gboolean background_thread, gboolean threadpool_thread, gboolean external_eventloop, gboolean debugger_thread) { #ifdef DISABLE_THREADS return; @@ -493,16 +494,16 @@ mono_threads_wasm_on_thread_attached (void) // g_assert(!mono_threads_wasm_is_ui_thread ()); return; } + // Notify JS that the pthread attached to Mono - pthread_t id = pthread_self (); MONO_ENTER_GC_SAFE; - mono_wasm_pthread_on_pthread_attached (id); + mono_wasm_pthread_on_pthread_attached (tid, thread_name, background_thread, threadpool_thread, external_eventloop, debugger_thread); MONO_EXIT_GC_SAFE; #endif } void -mono_threads_wasm_on_thread_detached (void) +mono_threads_wasm_on_thread_unregistered (void) { #ifdef DISABLE_THREADS return; @@ -513,7 +514,23 @@ mono_threads_wasm_on_thread_detached (void) // Notify JS that the pthread detached from Mono pthread_t id = pthread_self (); - mono_wasm_pthread_on_pthread_detached (id); + mono_wasm_pthread_on_pthread_unregistered (id); +#endif +} + +void +mono_threads_wasm_on_thread_registered (void) +{ +#ifdef DISABLE_THREADS + return; +#else + if (mono_threads_wasm_is_ui_thread ()) { + return; + } + // Notify JS that the pthread registered to Mono + pthread_t id = pthread_self (); + + mono_wasm_pthread_on_pthread_registered (id); #endif } diff --git a/src/mono/mono/utils/mono-threads-wasm.h b/src/mono/mono/utils/mono-threads-wasm.h index 157fc22af8859a..e5591ba939f995 100644 --- a/src/mono/mono/utils/mono-threads-wasm.h +++ b/src/mono/mono/utils/mono-threads-wasm.h @@ -74,12 +74,15 @@ extern MonoNativeTlsKey jobs_key; extern GSList *jobs; #endif /* DISABLE_THREADS */ +void +mono_threads_wasm_on_thread_registered (void); + // Called from register_thread when a pthread attaches to the runtime void -mono_threads_wasm_on_thread_attached (void); +mono_threads_wasm_on_thread_attached (pthread_t tid, const char* thread_name, gboolean background_thread, gboolean threadpool_thread, gboolean external_eventloop, gboolean debugger_thread); void -mono_threads_wasm_on_thread_detached (void); +mono_threads_wasm_on_thread_unregistered (void); #endif /* HOST_WASM*/ diff --git a/src/mono/mono/utils/mono-threads.c b/src/mono/mono/utils/mono-threads.c index 6e556fb7156ec6..515cde6eebad53 100644 --- a/src/mono/mono/utils/mono-threads.c +++ b/src/mono/mono/utils/mono-threads.c @@ -564,7 +564,7 @@ register_thread (MonoThreadInfo *info) mono_thread_info_suspend_unlock (); #ifdef HOST_BROWSER - mono_threads_wasm_on_thread_attached (); + mono_threads_wasm_on_thread_registered (); #endif return TRUE; @@ -649,7 +649,7 @@ unregister_thread (void *arg) mono_thread_info_suspend_unlock (); #ifdef HOST_BROWSER - mono_threads_wasm_on_thread_detached (); + mono_threads_wasm_on_thread_unregistered (); #endif g_byte_array_free (info->stackdata, /*free_segment=*/TRUE); diff --git a/src/mono/wasm/Wasm.Build.Tests/Templates/InterpPgoTests.cs b/src/mono/wasm/Wasm.Build.Tests/Templates/InterpPgoTests.cs index dfdd86dcd77646..8982e50f7a301d 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Templates/InterpPgoTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Templates/InterpPgoTests.cs @@ -107,6 +107,7 @@ public async Task FirstRunGeneratesTableAndSecondRunLoadsIt(string config) Assert.Contains("Hello, Browser!", output); // Verify that table data was loaded from cache + // if this breaks, it could be caused by change in config which affects the config hash and the cache storage hash key Assert.Contains(" bytes of interp_pgo data (table size == ", output); // Verify that the table was saved after the app ran Assert.Contains("Saved interp_pgo table", output);