From 771afaf38cf465b89f401adf1457dad8aad26b23 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Thu, 22 Feb 2024 09:39:41 +0100 Subject: [PATCH] [browser][MT] deputy thread (#98118) --- .../runtime-extra-platforms-wasm.yml | 23 +++---- eng/testing/tests.browser.targets | 1 + .../src/Interop/Browser/Interop.Runtime.cs | 2 +- .../Common/tests/System/TimeProviderTests.cs | 2 +- .../TestUtilities/System/PlatformDetection.cs | 3 + .../ServiceLookup/CallSiteFactoryTest.cs | 4 +- .../DI.Tests/ServiceProviderContainerTests.cs | 4 +- .../tests/HostFactoryResolverTests.cs | 44 ++++++------- ...tFactoryServiceCollectionExtensionsTest.cs | 2 +- .../JavaScript/Interop/JavaScriptExports.cs | 46 ++++++++++++- .../JavaScript/JSFunctionBinding.cs | 64 +++++++++++++++---- .../JavaScript/JSHostImplementation.Types.cs | 33 ++++++++++ .../JavaScript/JSMarshalerArgument.cs | 3 + .../JavaScript/JSProxyContext.cs | 7 +- .../JavaScript/JSSynchronizationContext.cs | 26 +++----- ...ces.JavaScript.BackgroundExec.Tests.csproj | 7 ++ .../JavaScript/JSExportTest.cs | 6 +- .../JavaScript/JSImportTest.cs | 32 +++++----- .../JavaScript/WebWorkerTest.cs | 2 +- .../JavaScript/WebWorkerTestHelper.cs | 4 -- .../ArrayPool/UnitTests.cs | 2 +- src/libraries/tests.proj | 6 +- src/mono/browser/runtime/corebindings.c | 13 +++- src/mono/browser/runtime/cwraps.ts | 4 ++ src/mono/browser/runtime/driver.c | 17 ++++- src/mono/browser/runtime/exports-binding.ts | 10 ++- src/mono/browser/runtime/interp-pgo.ts | 3 + src/mono/browser/runtime/invoke-js.ts | 5 +- src/mono/browser/runtime/loader/config.ts | 51 ++++++++++++--- src/mono/browser/runtime/managed-exports.ts | 57 +++++++++++------ src/mono/browser/runtime/marshal.ts | 9 ++- src/mono/browser/runtime/multi-threading.md | 45 +++++++++++++ .../browser/runtime/pthreads/deputy-thread.ts | 60 +++++++++++++++++ src/mono/browser/runtime/pthreads/index.ts | 2 + src/mono/browser/runtime/pthreads/shared.ts | 21 +++--- .../browser/runtime/pthreads/ui-thread.ts | 8 +++ src/mono/browser/runtime/startup.ts | 31 +++++++-- src/mono/browser/runtime/types/internal.ts | 39 +++++++++++ src/mono/mono/component/diagnostics_server.c | 1 + src/mono/mono/utils/mono-threads-wasm.c | 50 ++++++++++++++- src/mono/mono/utils/mono-threads-wasm.h | 12 ++++ 41 files changed, 607 insertions(+), 154 deletions(-) create mode 100644 src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/BackgroundExec/System.Runtime.InteropServices.JavaScript.BackgroundExec.Tests.csproj create mode 100644 src/mono/browser/runtime/multi-threading.md create mode 100644 src/mono/browser/runtime/pthreads/deputy-thread.ts diff --git a/eng/pipelines/extra-platforms/runtime-extra-platforms-wasm.yml b/eng/pipelines/extra-platforms/runtime-extra-platforms-wasm.yml index 31d15946c50da..fc8d757233cd4 100644 --- a/eng/pipelines/extra-platforms/runtime-extra-platforms-wasm.yml +++ b/eng/pipelines/extra-platforms/runtime-extra-platforms-wasm.yml @@ -290,17 +290,18 @@ jobs: # ff tests are unstable currently shouldContinueOnError: true - - template: /eng/pipelines/common/templates/wasm-debugger-tests.yml - parameters: - platforms: - - Browser_wasm - - Browser_wasm_win - extraBuildArgs: /p:WasmEnableThreads=true /p:AotHostArchitecture=x64 /p:AotHostOS=$(_hostedOS) - nameSuffix: DebuggerTests_MultiThreaded - alwaysRun: ${{ parameters.isWasmOnlyBuild }} - isExtraPlatformsBuild: ${{ parameters.isExtraPlatformsBuild }} - isWasmOnlyBuild: ${{ parameters.isWasmOnlyBuild }} - runOnlyOnWasmOnlyPipelines: true + # Active Issue https://github.com/dotnet/runtime/issues/98771 + # - template: /eng/pipelines/common/templates/wasm-debugger-tests.yml + # parameters: + # platforms: + # - Browser_wasm + # - Browser_wasm_win + # extraBuildArgs: /p:WasmEnableThreads=true /p:AotHostArchitecture=x64 /p:AotHostOS=$(_hostedOS) + # nameSuffix: DebuggerTests_MultiThreaded + # alwaysRun: ${{ parameters.isWasmOnlyBuild }} + # isExtraPlatformsBuild: ${{ parameters.isExtraPlatformsBuild }} + # isWasmOnlyBuild: ${{ parameters.isWasmOnlyBuild }} + # runOnlyOnWasmOnlyPipelines: true # Disable for now #- template: /eng/pipelines/coreclr/perf-wasm-jobs.yml diff --git a/eng/testing/tests.browser.targets b/eng/testing/tests.browser.targets index d27fa412d490b..06f07c898eabc 100644 --- a/eng/testing/tests.browser.targets +++ b/eng/testing/tests.browser.targets @@ -89,6 +89,7 @@ <_XUnitBackgroundExec Condition="'$(_XUnitBackgroundExec)' == '' and '$(WasmEnableThreads)' == 'true'">true $(WasmTestAppArgs) -backgroundExec + $(WasmXHarnessMonoArgs) --setenv=IsWasmBackgroundExec=true <_AppArgs Condition="'$(WasmTestAppArgs)' != ''">$(_AppArgs) $(WasmTestAppArgs) $(WasmXHarnessMonoArgs) --setenv=XHARNESS_LOG_TEST_START=true diff --git a/src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs b/src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs index f66afe40b4661..53e3fc148a46e 100644 --- a/src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs +++ b/src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs @@ -42,7 +42,7 @@ internal static unsafe partial class Runtime #if FEATURE_WASM_MANAGED_THREADS [MethodImpl(MethodImplOptions.InternalCall)] - public static extern void InstallWebWorkerInterop(nint proxyContextGCHandle); + public static extern void InstallWebWorkerInterop(nint proxyContextGCHandle, void* beforeSyncJSImport, void* afterSyncJSImport); [MethodImpl(MethodImplOptions.InternalCall)] public static extern void UninstallWebWorkerInterop(); diff --git a/src/libraries/Common/tests/System/TimeProviderTests.cs b/src/libraries/Common/tests/System/TimeProviderTests.cs index 428c5b13fecc5..7a0cb33eb74d7 100644 --- a/src/libraries/Common/tests/System/TimeProviderTests.cs +++ b/src/libraries/Common/tests/System/TimeProviderTests.cs @@ -214,7 +214,7 @@ private static void CancelAfter(TimeProvider provider, CancellationTokenSource c } #endif // NETFRAMEWORK - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [MemberData(nameof(TimersProvidersListData))] public static void CancellationTokenSourceWithTimer(TimeProvider provider) { diff --git a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs index 054a868c25f85..510b4b8e32180 100644 --- a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs +++ b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs @@ -135,6 +135,9 @@ public static int SlowRuntimeTimeoutModifier public static bool IsThreadingSupported => (!IsWasi && !IsBrowser) || IsWasmThreadingSupported; public static bool IsWasmThreadingSupported => IsBrowser && IsEnvironmentVariableTrue("IsBrowserThreadingSupported"); public static bool IsNotWasmThreadingSupported => !IsWasmThreadingSupported; + public static bool IsWasmBackgroundExec => IsBrowser && IsEnvironmentVariableTrue("IsWasmBackgroundExec"); + public static bool IsWasmBackgroundExecOrSingleThread => IsWasmBackgroundExec || IsNotWasmThreadingSupported; + public static bool IsThreadingSupportedOrBrowserBackgroundExec => IsWasmBackgroundExec || !IsBrowser; public static bool IsBinaryFormatterSupported => IsNotMobile && !IsNativeAot; public static bool IsStartingProcessesSupported => !IsiOS && !IstvOS; diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceLookup/CallSiteFactoryTest.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceLookup/CallSiteFactoryTest.cs index 919dee6475909..74f85296af952 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceLookup/CallSiteFactoryTest.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceLookup/CallSiteFactoryTest.cs @@ -792,7 +792,7 @@ public void CreateCallSite_EnumberableCachedAtLowestLevel(ServiceDescriptor[] de Assert.Equal(typeof(IEnumerable), callSite.Cache.Key.ServiceIdentifier.ServiceType); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] public void CallSitesAreUniquePerServiceTypeAndSlot() { // Connected graph @@ -828,7 +828,7 @@ public void CallSitesAreUniquePerServiceTypeAndSlot() } } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] public void CallSitesAreUniquePerServiceTypeAndSlotWithOpenGenericInGraph() { // Connected graph diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderContainerTests.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderContainerTests.cs index 3e08a16db282e..f668cee41efc3 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderContainerTests.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderContainerTests.cs @@ -371,7 +371,7 @@ public void GetService_DisposeOnSameThread_Throws() }); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] public void GetAsyncService_DisposeAsyncOnSameThread_ThrowsAndDoesNotHangAndDisposeAsyncGetsCalled() { // Arrange @@ -398,7 +398,7 @@ public void GetAsyncService_DisposeAsyncOnSameThread_ThrowsAndDoesNotHangAndDisp Assert.True(asyncDisposableResource.DisposeAsyncCalled); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] public void GetService_DisposeOnSameThread_ThrowsAndDoesNotHangAndDisposeGetsCalled() { // Arrange diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/HostFactoryResolverTests.cs b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/HostFactoryResolverTests.cs index 3982ae2984686..f123b65bb58bc 100644 --- a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/HostFactoryResolverTests.cs +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/HostFactoryResolverTests.cs @@ -37,7 +37,7 @@ public void BuildWebHostPattern_CanFindServiceProvider() Assert.IsAssignableFrom(factory(Array.Empty())); } - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BuildWebHostInvalidSignature.Program))] public void BuildWebHostPattern__Invalid_CantFindWebHost() { @@ -46,7 +46,7 @@ public void BuildWebHostPattern__Invalid_CantFindWebHost() Assert.Null(factory); } - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BuildWebHostInvalidSignature.Program))] public void BuildWebHostPattern__Invalid_CantFindServiceProvider() { @@ -55,7 +55,7 @@ public void BuildWebHostPattern__Invalid_CantFindServiceProvider() Assert.NotNull(factory); } - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CreateWebHostBuilderPatternTestSite.Program))] public void CreateWebHostBuilderPattern_CanFindWebHostBuilder() { @@ -65,7 +65,7 @@ public void CreateWebHostBuilderPattern_CanFindWebHostBuilder() Assert.IsAssignableFrom(factory(Array.Empty())); } - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CreateWebHostBuilderPatternTestSite.Program))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(IWebHost))] public void CreateWebHostBuilderPattern_CanFindServiceProvider() @@ -76,7 +76,7 @@ public void CreateWebHostBuilderPattern_CanFindServiceProvider() Assert.IsAssignableFrom(factory(Array.Empty())); } - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CreateWebHostBuilderInvalidSignature.Program))] public void CreateWebHostBuilderPattern__Invalid_CantFindWebHostBuilder() { @@ -85,7 +85,7 @@ public void CreateWebHostBuilderPattern__Invalid_CantFindWebHostBuilder() Assert.Null(factory); } - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CreateWebHostBuilderInvalidSignature.Program))] public void CreateWebHostBuilderPattern__InvalidReturnType_CanFindServiceProvider() { @@ -95,7 +95,7 @@ public void CreateWebHostBuilderPattern__InvalidReturnType_CanFindServiceProvide Assert.Null(factory(Array.Empty())); } - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CreateHostBuilderPatternTestSite.Program))] public void CreateHostBuilderPattern_CanFindHostBuilder() { @@ -105,7 +105,7 @@ public void CreateHostBuilderPattern_CanFindHostBuilder() Assert.IsAssignableFrom(factory(Array.Empty())); } - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CreateHostBuilderPatternTestSite.Program))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Host))] public void CreateHostBuilderPattern_CanFindServiceProvider() @@ -116,7 +116,7 @@ public void CreateHostBuilderPattern_CanFindServiceProvider() Assert.IsAssignableFrom(factory(Array.Empty())); } - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CreateHostBuilderInvalidSignature.Program))] public void CreateHostBuilderPattern__Invalid_CantFindHostBuilder() { @@ -125,7 +125,7 @@ public void CreateHostBuilderPattern__Invalid_CantFindHostBuilder() Assert.Null(factory); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CreateHostBuilderInvalidSignature.Program))] public void CreateHostBuilderPattern__Invalid_CantFindServiceProvider() { @@ -135,7 +135,7 @@ public void CreateHostBuilderPattern__Invalid_CantFindServiceProvider() Assert.Throws(() => factory(Array.Empty())); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPattern.Program))] public void NoSpecialEntryPointPattern() { @@ -145,7 +145,7 @@ public void NoSpecialEntryPointPattern() Assert.IsAssignableFrom(factory(Array.Empty())); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPattern.Program))] public void NoSpecialEntryPointPatternHostBuilderConfigureHostBuilderCallbackIsCalled() { @@ -163,7 +163,7 @@ void ConfigureHostBuilder(object hostBuilder) Assert.True(called); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPattern.Program))] public void NoSpecialEntryPointPatternBuildsThenThrowsCallsEntryPointCompletedCallback() { @@ -183,7 +183,7 @@ void EntryPointCompleted(Exception? exception) Assert.Null(entryPointException); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternBuildsThenThrows.Program))] public void NoSpecialEntryPointPatternBuildsThenThrowsCallsEntryPointCompletedCallbackWithException() { @@ -203,7 +203,7 @@ void EntryPointCompleted(Exception? exception) Assert.NotNull(entryPointException); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternThrows.Program))] public void NoSpecialEntryPointPatternThrows() { @@ -213,7 +213,7 @@ public void NoSpecialEntryPointPatternThrows() Assert.Throws(() => factory(Array.Empty())); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternExits.Program))] public void NoSpecialEntryPointPatternExits() { @@ -223,7 +223,7 @@ public void NoSpecialEntryPointPatternExits() Assert.Throws(() => factory(Array.Empty())); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternHangs.Program))] public void NoSpecialEntryPointPatternHangs() { @@ -233,7 +233,7 @@ public void NoSpecialEntryPointPatternHangs() Assert.Throws(() => factory(Array.Empty())); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternMainNoArgs.Program))] public void NoSpecialEntryPointPatternMainNoArgs() { @@ -243,7 +243,7 @@ public void NoSpecialEntryPointPatternMainNoArgs() Assert.IsAssignableFrom(factory(Array.Empty())); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, "Program", "TopLevelStatements")] public void TopLevelStatements() { @@ -254,7 +254,7 @@ public void TopLevelStatements() Assert.IsAssignableFrom(factory(Array.Empty())); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, "Program", "TopLevelStatementsTestsTimeout")] public void TopLevelStatementsTestsTimeout() { @@ -265,7 +265,7 @@ public void TopLevelStatementsTestsTimeout() Assert.Throws(() => factory(Array.Empty())); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, "Program", "ApplicationNameSetFromArgument")] public void ApplicationNameSetFromArgument() { @@ -277,7 +277,7 @@ public void ApplicationNameSetFromArgument() Assert.Contains("ApplicationNameSetFromArgument", configuration["applicationName"]); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPattern.Program))] public void NoSpecialEntryPointPatternCanRunInParallel() { diff --git a/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/DependencyInjection/HttpClientFactoryServiceCollectionExtensionsTest.cs b/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/DependencyInjection/HttpClientFactoryServiceCollectionExtensionsTest.cs index c72b17e728848..81bb589fcfb31 100644 --- a/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/DependencyInjection/HttpClientFactoryServiceCollectionExtensionsTest.cs +++ b/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/DependencyInjection/HttpClientFactoryServiceCollectionExtensionsTest.cs @@ -1203,7 +1203,7 @@ public async Task AddHttpClient_MessageHandler_Scope_TransientDependency() } } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported), nameof(PlatformDetection.IsReflectionEmitSupported))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec), nameof(PlatformDetection.IsReflectionEmitSupported))] public void AddHttpClient_GetAwaiterAndResult_InSingleThreadedSynchronizationContext_ShouldNotHangs() { // Arrange diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs index e083c09a41f6f..b423f9b585bed 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs @@ -13,7 +13,7 @@ namespace System.Runtime.InteropServices.JavaScript { // this maps to src\mono\browser\runtime\managed-exports.ts // the public methods are protected from trimming by DynamicDependency on JSFunctionBinding.BindJSFunction - // TODO: all the calls here should be running on deputy or TP in MT, not in UI thread + // TODO: change all of these to [UnmanagedCallersOnly] and drop the reflection in mono_wasm_invoke_jsexport internal static unsafe partial class JavaScriptExports { // the marshaled signature is: Task? CallEntrypoint(char* assemblyNamePtr, string[] args) @@ -240,15 +240,21 @@ public static void GetManagedStackTrace(JSMarshalerArgument* arguments_buffer) // this is here temporarily, until JSWebWorker becomes public API [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, "System.Runtime.InteropServices.JavaScript.JSWebWorker", "System.Runtime.InteropServices.JavaScript")] - // the marshaled signature is: GCHandle InstallMainSynchronizationContext(nint jsNativeTID) + // the marshaled signature is: GCHandle InstallMainSynchronizationContext(nint jsNativeTID, JSThreadBlockingMode jsThreadBlockingMode, JSThreadInteropMode jsThreadInteropMode, MainThreadingMode mainThreadingMode) public static void InstallMainSynchronizationContext(JSMarshalerArgument* arguments_buffer) { ref JSMarshalerArgument arg_exc = ref arguments_buffer[0]; // initialized by caller in alloc_stack_frame() ref JSMarshalerArgument arg_res = ref arguments_buffer[1];// initialized and set by caller ref JSMarshalerArgument arg_1 = ref arguments_buffer[2];// initialized and set by caller + ref JSMarshalerArgument arg_2 = ref arguments_buffer[3];// initialized and set by caller + ref JSMarshalerArgument arg_3 = ref arguments_buffer[4];// initialized and set by caller + ref JSMarshalerArgument arg_4 = ref arguments_buffer[5];// initialized and set by caller try { + JSProxyContext.ThreadBlockingMode = (JSHostImplementation.JSThreadBlockingMode)arg_2.slot.Int32Value; + JSProxyContext.ThreadInteropMode = (JSHostImplementation.JSThreadInteropMode)arg_3.slot.Int32Value; + JSProxyContext.MainThreadingMode = (JSHostImplementation.MainThreadingMode)arg_4.slot.Int32Value; var jsSynchronizationContext = JSSynchronizationContext.InstallWebWorkerInterop(true, CancellationToken.None); jsSynchronizationContext.ProxyContext.JSNativeTID = arg_1.slot.IntPtrValue; arg_res.slot.GCHandle = jsSynchronizationContext.ProxyContext.ContextHandle; @@ -259,6 +265,42 @@ public static void InstallMainSynchronizationContext(JSMarshalerArgument* argume } } +#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#pragma warning restore CS3016 + // TODO ideally this would be public API callable from generated C# code for JSExport + public static void BeforeSyncJSExport(JSMarshalerArgument* arguments_buffer) + { + ref JSMarshalerArgument arg_exc = ref arguments_buffer[0]; + try + { + var ctx = arg_exc.AssertCurrentThreadContext(); + ctx.IsPendingSynchronousCall = true; + } + catch (Exception ex) + { + Environment.FailFast($"BeforeSyncJSExport: Unexpected synchronous failure (ManagedThreadId {Environment.CurrentManagedThreadId}): " + ex); + } + } + +#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#pragma warning restore CS3016 + // TODO ideally this would be public API callable from generated C# code for JSExport + public static void AfterSyncJSExport(JSMarshalerArgument* arguments_buffer) + { + ref JSMarshalerArgument arg_exc = ref arguments_buffer[0]; + try + { + var ctx = arg_exc.AssertCurrentThreadContext(); + ctx.IsPendingSynchronousCall = false; + } + catch (Exception ex) + { + Environment.FailFast($"AfterSyncJSExport: Unexpected synchronous failure (ManagedThreadId {Environment.CurrentManagedThreadId}): " + ex); + } + } + #endif // the marshaled signature is: Task BindAssemblyExports(string assemblyName) diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSFunctionBinding.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSFunctionBinding.cs index 974b0ce7e3eb7..4f14a575fb1c0 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSFunctionBinding.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSFunctionBinding.cs @@ -206,6 +206,9 @@ internal static unsafe void InvokeJSFunction(JSObject jsFunction, Span arguments) { +#if FEATURE_WASM_MANAGED_THREADS + if (JSProxyContext.ThreadInteropMode == JSHostImplementation.JSThreadInteropMode.NoSyncJSInterop) + { + throw new PlatformNotSupportedException("Cannot call synchronous JS functions."); + } + else if (jsFunction.ProxyContext.IsPendingSynchronousCall) + { + throw new PlatformNotSupportedException("Cannot call synchronous JS function from inside a synchronous call to a C# method."); + } +#endif + var functionHandle = (int)jsFunction.JSHandle; fixed (JSMarshalerArgument* ptr = arguments) { @@ -245,6 +259,16 @@ internal static unsafe void InvokeJSFunctionCurrent(JSObject jsFunction, Span arguments) { +#if FEATURE_WASM_MANAGED_THREADS + if (JSProxyContext.ThreadInteropMode == JSHostImplementation.JSThreadInteropMode.NoSyncJSInterop) + { + throw new PlatformNotSupportedException("Cannot call synchronous JS functions."); + } + else if (jsFunction.ProxyContext.IsPendingSynchronousCall) + { + throw new PlatformNotSupportedException("Cannot call synchronous JS function from inside a synchronous call to a C# method."); + } +#endif var args = (nint)Unsafe.AsPointer(ref arguments[0]); var functionHandle = jsFunction.JSHandle; @@ -269,10 +293,13 @@ internal static unsafe void DispatchJSFunctionSync(JSObject jsFunction, Span arguments) { + ref JSMarshalerArgument exc = ref arguments[0]; + ref JSMarshalerArgument res = ref arguments[1]; #if FEATURE_WASM_MANAGED_THREADS var targetContext = JSProxyContext.SealJSImportCapturing(); - arguments[0].slot.ContextHandle = targetContext.ContextHandle; - arguments[1].slot.ContextHandle = targetContext.ContextHandle; + exc.slot.CallerNativeTID = targetContext.NativeTID; + exc.slot.ContextHandle = targetContext.ContextHandle; + res.slot.ContextHandle = targetContext.ContextHandle; #else var targetContext = JSProxyContext.MainThreadContext; #endif @@ -281,9 +308,22 @@ internal static unsafe void InvokeJSImportImpl(JSFunctionBinding signature, Span { // pre-allocate the result handle and Task var holder = targetContext.CreatePromiseHolder(); - arguments[1].slot.Type = MarshalerType.TaskPreCreated; - arguments[1].slot.GCHandle = holder.GCHandle; + res.slot.Type = MarshalerType.TaskPreCreated; + res.slot.GCHandle = holder.GCHandle; } +#if FEATURE_WASM_MANAGED_THREADS + else + { + if (JSProxyContext.ThreadInteropMode == JSHostImplementation.JSThreadInteropMode.NoSyncJSInterop) + { + throw new PlatformNotSupportedException("Cannot call synchronous JS functions."); + } + else if (targetContext.IsPendingSynchronousCall) + { + throw new PlatformNotSupportedException("Cannot call synchronous JS function from inside a synchronous call to a C# method."); + } + } +#endif if (signature.IsDiscardNoWait) { @@ -360,6 +400,8 @@ internal static unsafe void DispatchJSImportSyncSend(JSFunctionBinding signature var args = (nint)Unsafe.AsPointer(ref arguments[0]); var sig = (nint)signature.Header; + ref JSMarshalerArgument exc = ref arguments[0]; + // we already know that we are not on the right thread // this will be blocking until resolved by that thread // we don't have to disable ThrowOnBlockingWaitOnJSInteropThread, because this is lock in native code @@ -368,10 +410,9 @@ internal static unsafe void DispatchJSImportSyncSend(JSFunctionBinding signature // see also https://github.com/dotnet/runtime/issues/76958#issuecomment-1921418290 Interop.Runtime.InvokeJSImportSyncSend(targetContext.JSNativeTID, sig, args); - ref JSMarshalerArgument exceptionArg = ref arguments[0]; - if (exceptionArg.slot.Type != MarshalerType.None) + if (exc.slot.Type != MarshalerType.None) { - JSHostImplementation.ThrowException(ref exceptionArg); + JSHostImplementation.ThrowException(ref exc); } } @@ -421,17 +462,19 @@ internal static unsafe JSFunctionBinding BindJSImportImpl(string functionName, s #endif internal static unsafe void ResolveOrRejectPromise(JSProxyContext targetContext, Span arguments) { + ref JSMarshalerArgument exc = ref arguments[0]; #if FEATURE_WASM_MANAGED_THREADS + exc.slot.CallerNativeTID = targetContext.NativeTID; + if (targetContext.IsCurrentThread()) #endif { fixed (JSMarshalerArgument* ptr = arguments) { Interop.Runtime.ResolveOrRejectPromise((nint)ptr); - ref JSMarshalerArgument exceptionArg = ref arguments[0]; - if (exceptionArg.slot.Type != MarshalerType.None) + if (exc.slot.Type != MarshalerType.None) { - JSHostImplementation.ThrowException(ref exceptionArg); + JSHostImplementation.ThrowException(ref exc); } } } @@ -439,7 +482,6 @@ internal static unsafe void ResolveOrRejectPromise(JSProxyContext targetContext, else { // meaning JS side needs to dispose it - ref JSMarshalerArgument exc = ref arguments[0]; exc.slot.ReceiverShouldFree = true; // this copy is freed in mono_wasm_resolve_or_reject_promise diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.Types.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.Types.cs index 2aa59d1814d45..c949901a803cf 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.Types.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.Types.cs @@ -46,5 +46,38 @@ public struct IntPtrAndHandle [FieldOffset(0)] internal RuntimeTypeHandle typeHandle; } + + // keep in sync with types\internal.ts + public enum MainThreadingMode : int + { + // Running the managed main thread on UI thread. + // Managed GC and similar scenarios could be blocking the UI. + // Easy to deadlock. Not recommended for production. + UIThread = 0, + // Running the managed main thread on dedicated WebWorker. Marshaling all JavaScript calls to and from the main thread. + DeputyThread = 1, + } + + // keep in sync with types\internal.ts + public enum JSThreadBlockingMode : int + { + // throw PlatformNotSupportedException if blocking .Wait is called on threads with JS interop, like JSWebWorker and Main thread. + // Avoids deadlocks (typically with pending JS promises on the same thread) by throwing exceptions. + NoBlockingWait = 0, + // allow .Wait on all threads. + // Could cause deadlocks with blocking .Wait on a pending JS Task/Promise on the same thread or similar Task/Promise chain. + AllowBlockingWait = 100, + } + + // keep in sync with types\internal.ts + public enum JSThreadInteropMode : int + { + // throw PlatformNotSupportedException if synchronous JSImport/JSExport is called on threads with JS interop, like JSWebWorker and Main thread. + // calling synchronous JSImport on thread pool or new threads is allowed. + NoSyncJSInterop = 0, + // allow non-re-entrant synchronous blocking calls to and from JS on JSWebWorker on threads with JS interop, like JSWebWorker and Main thread. + // calling synchronous JSImport on thread pool or new threads is allowed. + SimpleSynchronousJSInterop = 1, + } } } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSMarshalerArgument.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSMarshalerArgument.cs index 9402aa5e8b80e..afd0325800dc4 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSMarshalerArgument.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSMarshalerArgument.cs @@ -65,6 +65,9 @@ internal struct JSMarshalerArgumentImpl [FieldOffset(20)] internal bool ReceiverShouldFree; + + [FieldOffset(24)] + internal IntPtr CallerNativeTID; #endif } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs index 7a86f173d11e1..7b481d24658a0 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs @@ -41,12 +41,17 @@ private JSProxyContext() public bool IsMainThread; public JSSynchronizationContext SynchronizationContext; + public static MainThreadingMode MainThreadingMode = MainThreadingMode.DeputyThread; + public static JSThreadBlockingMode ThreadBlockingMode = JSThreadBlockingMode.NoBlockingWait; + public static JSThreadInteropMode ThreadInteropMode = JSThreadInteropMode.SimpleSynchronousJSInterop; + public bool IsPendingSynchronousCall; + #if !DEBUG [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public bool IsCurrentThread() { - return ManagedTID == Environment.CurrentManagedThreadId; + return ManagedTID == Environment.CurrentManagedThreadId && (!IsMainThread || MainThreadingMode == MainThreadingMode.UIThread); } [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "thread_id")] diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSSynchronizationContext.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSSynchronizationContext.cs index b9c151ac79d08..ed7fffbae25de 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSSynchronizationContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSSynchronizationContext.cs @@ -44,28 +44,16 @@ public WorkItem(SendOrPostCallback callback, object? data, ManualResetEventSlim? } // this need to be called from JSWebWorker or UI thread - public static JSSynchronizationContext InstallWebWorkerInterop(bool isMainThread, CancellationToken cancellationToken) + public static unsafe JSSynchronizationContext InstallWebWorkerInterop(bool isMainThread, CancellationToken cancellationToken) { var ctx = new JSSynchronizationContext(isMainThread, cancellationToken); ctx.previousSynchronizationContext = SynchronizationContext.Current; SynchronizationContext.SetSynchronizationContext(ctx); - // FIXME: make this configurable - // we could have 3 different modes of this - // 1) throwing on UI + JSWebWorker - // 2) throwing only on UI - small risk, more convenient. - // 3) not throwing at all - quite risky - // deadlock scenarios are: - // - .Wait for more than 5000ms and deadlock the GC suspend - // - .Wait on the Task from HTTP client, on the same thread as the HTTP client needs to resolve the Task/Promise. This could be also be a chain of promises. - // - try to create new pthread when UI thread is blocked and we run out of posix/emscripten pool of loaded workers. - // Things which lead to it are - // - Task.Wait, Signal.Wait etc - // - Monitor.Enter etc, if the lock is held by another thread for long time - // - synchronous [JSExport] into managed code, which would block - // - synchronous [JSImport] to another thread, which would block - // see also https://github.com/dotnet/runtime/issues/76958#issuecomment-1921418290 - Thread.ThrowOnBlockingWaitOnJSInteropThread = true; + if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.NoBlockingWait) + { + Thread.ThrowOnBlockingWaitOnJSInteropThread = true; + } var proxyContext = ctx.ProxyContext; JSProxyContext.CurrentThreadContext = proxyContext; @@ -77,7 +65,9 @@ public static JSSynchronizationContext InstallWebWorkerInterop(bool isMainThread ctx.AwaitNewData(); - Interop.Runtime.InstallWebWorkerInterop(proxyContext.ContextHandle); + Interop.Runtime.InstallWebWorkerInterop(proxyContext.ContextHandle, + (delegate* unmanaged[Cdecl])&JavaScriptExports.BeforeSyncJSExport, + (delegate* unmanaged[Cdecl])&JavaScriptExports.AfterSyncJSExport); return ctx; } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/BackgroundExec/System.Runtime.InteropServices.JavaScript.BackgroundExec.Tests.csproj b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/BackgroundExec/System.Runtime.InteropServices.JavaScript.BackgroundExec.Tests.csproj new file mode 100644 index 0000000000000..506fcc01f008a --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/BackgroundExec/System.Runtime.InteropServices.JavaScript.BackgroundExec.Tests.csproj @@ -0,0 +1,7 @@ + + + <_XUnitBackgroundExec Condition="'$(_XUnitBackgroundExec)' == ''">true + System.Runtime.InteropServices.JavaScript.Tests + + + diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JSExportTest.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JSExportTest.cs index bcc2c85e9e785..8fe5e467eca06 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JSExportTest.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JSExportTest.cs @@ -43,7 +43,7 @@ public async Task JsExportInt32DiscardNoWait(int value) } } - //TODO [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] // this test doesn't make sense with deputy + [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsWasmBackgroundExecOrSingleThread))] public class JSExportTest : JSInteropTestBase, IAsyncLifetime { [Theory] @@ -383,7 +383,7 @@ public async Task JsExportTaskOfInt(int value) //GC.Collect(); } - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] public void JsExportCallback_FunctionIntInt() { int called = -1; @@ -399,7 +399,7 @@ public void JsExportCallback_FunctionIntInt() Assert.Equal(42, called); } - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] public void JsExportCallback_FunctionIntIntThrow() { int called = -1; diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JSImportTest.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JSImportTest.cs index 96b89ffa9e4f4..89ec5cbb08978 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JSImportTest.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JSImportTest.cs @@ -113,7 +113,7 @@ public unsafe void OutOfRange() Assert.Contains("Overflow: value 9007199254740991 is out of -2147483648 2147483647 range", ex.Message); } - [Fact] //TODO [Fact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] // this test doesn't make sense with deputy + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWasmBackgroundExecOrSingleThread))] public unsafe void OptimizedPaths() { JavaScriptTestHelper.optimizedReached = 0; @@ -922,7 +922,7 @@ public async Task JsImportSleep() await JavaScriptTestHelper.sleep(100); } - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] // slow public async Task JsImportTaskTypes() { for (int i = 0; i < 100; i++) @@ -1117,7 +1117,7 @@ public async Task JsImportTaskAwait() #region Action - [Fact] //TODO [Fact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] // this test doesn't make sense with deputy + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] public void JsImportCallback_EchoAction() { bool called = false; @@ -1132,7 +1132,6 @@ public void JsImportCallback_EchoAction() Assert.True(called); } - /* TODO deputy [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWasmThreadingSupported))] public void JsImportCallback_EchoActionThrows_MT() { @@ -1148,7 +1147,6 @@ public void JsImportCallback_EchoActionThrows_MT() Assert.Throws(()=>actual()); Assert.False(called); } - */ [Fact] public async Task JsImportCallback_Async() @@ -1166,7 +1164,7 @@ public async Task JsImportCallback_Async() } - [Fact] //TODO [Fact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] // this test doesn't make sense with deputy + [Fact] [OuterLoop] public async Task JsImportCallback_EchoActionMany() { @@ -1187,7 +1185,7 @@ public async Task JsImportCallback_EchoActionMany() } } - [Fact] //TODO [Fact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] // this test doesn't make sense with deputy + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] public void JsImportCallback_Action() { bool called = false; @@ -1198,7 +1196,7 @@ public void JsImportCallback_Action() Assert.True(called); } - [Fact] //TODO [Fact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] // this test doesn't make sense with deputy + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] public void JsImportEcho_ActionAction() { bool called = false; @@ -1211,7 +1209,7 @@ public void JsImportEcho_ActionAction() Assert.True(called); } - [Fact] //TODO [Fact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] // this test doesn't make sense with deputy + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] public void JsImportEcho_ActionIntActionInt() { int calledA = -1; @@ -1224,7 +1222,7 @@ public void JsImportEcho_ActionIntActionInt() Assert.Equal(42, calledA); } - [Fact] //TODO [Fact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] // this test doesn't make sense with deputy + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] public void JsImportCallback_ActionInt() { int called = -1; @@ -1235,7 +1233,7 @@ public void JsImportCallback_ActionInt() Assert.Equal(42, called); } - [Fact] //TODO [Fact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] // this test doesn't make sense with deputy + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] public void JsImportCallback_FunctionIntInt() { int called = -1; @@ -1248,7 +1246,7 @@ public void JsImportCallback_FunctionIntInt() Assert.Equal(42, res); } - [Fact] //TODO [Fact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] // this test doesn't make sense with deputy + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] public void JsImportBackCallback_FunctionIntInt() { int called = -1; @@ -1263,7 +1261,7 @@ public void JsImportBackCallback_FunctionIntInt() Assert.Equal(84, called); } - [Fact] //TODO [Fact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] // this test doesn't make sense with deputy + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] public void JsImportBackCallback_FunctionIntIntIntInt() { int calledA = -1; @@ -1282,7 +1280,7 @@ public void JsImportBackCallback_FunctionIntIntIntInt() Assert.Equal(84, calledB); } - [Fact] //TODO [Fact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] // this test doesn't make sense with deputy + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] public void JsImportCallback_ActionIntInt() { int calledA = -1; @@ -1296,7 +1294,7 @@ public void JsImportCallback_ActionIntInt() Assert.Equal(43, calledB); } - [Fact] //TODO [Fact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] // this test doesn't make sense with deputy + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] public void JsImportCallback_ActionLongLong() { long calledA = -1; @@ -1310,7 +1308,7 @@ public void JsImportCallback_ActionLongLong() Assert.Equal(43, calledB); } - [Fact] //TODO [Fact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] // this test doesn't make sense with deputy + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] public void JsImportCallback_ActionIntLong() { int calledA = -1; @@ -1324,7 +1322,7 @@ public void JsImportCallback_ActionIntLong() Assert.Equal(43, calledB); } - [Fact] //TODO [Fact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] // this test doesn't make sense with deputy + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWasmThreadingSupported))] public void JsImportCallback_ActionIntThrow() { int called = -1; 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 514741a6b8753..f0a4047924eaa 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 @@ -343,7 +343,7 @@ await executor.Execute(async () => var jsTid = WebWorkerTestHelper.GetTid(); var csTid = WebWorkerTestHelper.NativeThreadId; - if (executor.Type == ExecutorType.Main || executor.Type == ExecutorType.JSWebWorker) + if (executor.Type == ExecutorType.JSWebWorker) { Assert.Equal(jsTid, csTid); } 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 96312c7aba35c..35cdd8ff1858b 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 @@ -325,10 +325,6 @@ public static Task RunOnThreadPool(Func job, CancellationToken cancellatio public static Task RunOnNewThread(Func job, CancellationToken cancellationToken) { - if( Environment.CurrentManagedThreadId == 1) - { - throw new Exception("This unit test should be executed with -backgroundExec otherwise it's prone to consume all threads too quickly"); - } TaskCompletionSource tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var thread = new Thread(() => { diff --git a/src/libraries/System.Runtime/tests/System.Buffers.Tests/ArrayPool/UnitTests.cs b/src/libraries/System.Runtime/tests/System.Buffers.Tests/ArrayPool/UnitTests.cs index cc704c68fac13..d58a8838d90a9 100644 --- a/src/libraries/System.Runtime/tests/System.Buffers.Tests/ArrayPool/UnitTests.cs +++ b/src/libraries/System.Runtime/tests/System.Buffers.Tests/ArrayPool/UnitTests.cs @@ -272,7 +272,7 @@ public static void RentingReturningThenRentingABufferShouldNotAllocate() Assert.Equal(id, bt.GetHashCode()); } - [Theory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))] [MemberData(nameof(BytePoolInstances))] public static void CanRentManySizedBuffers(ArrayPool pool) { diff --git a/src/libraries/tests.proj b/src/libraries/tests.proj index 42afa2ece4fba..4ab8fa568e3f3 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -397,13 +397,15 @@ - - + + + + diff --git a/src/mono/browser/runtime/corebindings.c b/src/mono/browser/runtime/corebindings.c index abaa7cab7b6f6..1b0c0a809f63e 100644 --- a/src/mono/browser/runtime/corebindings.c +++ b/src/mono/browser/runtime/corebindings.c @@ -42,6 +42,7 @@ void mono_wasm_resolve_or_reject_promise_post (pthread_t target_tid, void *args) void mono_wasm_cancel_promise_post (pthread_t target_tid, int task_holder_gc_handle); extern void mono_wasm_install_js_worker_interop (int context_gc_handle); +void mono_wasm_install_js_worker_interop_wrapper (int context_gc_handle, void* beforeSyncJSImport, void* afterSyncJSImport); extern void mono_wasm_uninstall_js_worker_interop (); extern void mono_wasm_invoke_jsimport (void* signature, void* args); void mono_wasm_invoke_jsimport_async_post (pthread_t target_tid, void* signature, void* args); @@ -77,7 +78,7 @@ void bindings_initialize_internals (void) #ifndef DISABLE_THREADS mono_add_internal_call ("Interop/Runtime::ReleaseCSOwnedObjectPost", mono_wasm_release_cs_owned_object_post); mono_add_internal_call ("Interop/Runtime::ResolveOrRejectPromisePost", mono_wasm_resolve_or_reject_promise_post); - mono_add_internal_call ("Interop/Runtime::InstallWebWorkerInterop", mono_wasm_install_js_worker_interop); + mono_add_internal_call ("Interop/Runtime::InstallWebWorkerInterop", mono_wasm_install_js_worker_interop_wrapper); mono_add_internal_call ("Interop/Runtime::UninstallWebWorkerInterop", mono_wasm_uninstall_js_worker_interop); mono_add_internal_call ("Interop/Runtime::InvokeJSImportSync", mono_wasm_invoke_jsimport); mono_add_internal_call ("Interop/Runtime::InvokeJSImportSyncSend", mono_wasm_invoke_jsimport_sync_send); @@ -253,6 +254,16 @@ void mono_wasm_get_assembly_export (char *assembly_name, char *namespace, char * #ifndef DISABLE_THREADS +void* before_sync_js_import; +void* after_sync_js_import; + +void mono_wasm_install_js_worker_interop_wrapper (int context_gc_handle, void* beforeSyncJSImport, void* afterSyncJSImport) +{ + before_sync_js_import = beforeSyncJSImport; + after_sync_js_import = afterSyncJSImport; + mono_wasm_install_js_worker_interop (context_gc_handle); +} + // async void mono_wasm_release_cs_owned_object_post (pthread_t target_tid, int js_handle) { diff --git a/src/mono/browser/runtime/cwraps.ts b/src/mono/browser/runtime/cwraps.ts index dbb0babce8231..23c0330d6eced 100644 --- a/src/mono/browser/runtime/cwraps.ts +++ b/src/mono/browser/runtime/cwraps.ts @@ -27,6 +27,8 @@ const threading_cwraps: SigLine[] = WasmEnableThreads ? [ [false, "mono_wasm_init_finalizer_thread", null, []], [false, "mono_wasm_invoke_jsexport_async_post", "void", ["number", "number", "number"]], [false, "mono_wasm_invoke_jsexport_sync_send", "void", ["number", "number", "number"]], + [true, "mono_wasm_create_deputy_thread", "number", []], + [true, "mono_wasm_register_ui_thread", "void", []], ] : []; // when the method is assigned/cached at usage, instead of being invoked directly from cwraps, it can't be marked lazy, because it would be re-bound on each call @@ -144,6 +146,8 @@ export interface t_ThreadingCwraps { mono_wasm_init_finalizer_thread(): void; mono_wasm_invoke_jsexport_async_post(targetTID: PThreadPtr, method: MonoMethod, args: VoidPtr): void; mono_wasm_invoke_jsexport_sync_send(targetTID: PThreadPtr, method: MonoMethod, args: VoidPtr): void; + mono_wasm_create_deputy_thread(): PThreadPtr; + mono_wasm_register_ui_thread(): void; } export interface t_ProfilerCwraps { diff --git a/src/mono/browser/runtime/driver.c b/src/mono/browser/runtime/driver.c index c5135a7e44b34..d8a3da3f100ac 100644 --- a/src/mono/browser/runtime/driver.c +++ b/src/mono/browser/runtime/driver.c @@ -258,6 +258,7 @@ mono_wasm_invoke_jsexport (MonoMethod *method, void* args) extern void mono_threads_wasm_async_run_in_target_thread_vii (void* target_thread, void (*func) (gpointer, gpointer), gpointer user_data1, gpointer user_data2); extern void mono_threads_wasm_sync_run_in_target_thread_vii (void* target_thread, void (*func) (gpointer, gpointer), gpointer user_data1, gpointer user_data2); +// this is running on the target thread static void mono_wasm_invoke_jsexport_async_post_cb (MonoMethod *method, void* args) { @@ -274,11 +275,25 @@ mono_wasm_invoke_jsexport_async_post (void* target_thread, MonoMethod *method, v mono_threads_wasm_async_run_in_target_thread_vii(target_thread, (void (*)(gpointer, gpointer))mono_wasm_invoke_jsexport_async_post_cb, method, args); } + +typedef void (*js_interop_event)(void* args); +extern js_interop_event before_sync_js_import; +extern js_interop_event after_sync_js_import; + +// this is running on the target thread +static void +mono_wasm_invoke_jsexport_sync_send_cb (MonoMethod *method, void* args) +{ + before_sync_js_import (args); + mono_wasm_invoke_jsexport (method, args); + after_sync_js_import (args); +} + // sync EMSCRIPTEN_KEEPALIVE void mono_wasm_invoke_jsexport_sync_send (void* target_thread, MonoMethod *method, void* args /*JSMarshalerArguments*/) { - mono_threads_wasm_sync_run_in_target_thread_vii(target_thread, (void (*)(gpointer, gpointer))mono_wasm_invoke_jsexport, method, args); + mono_threads_wasm_sync_run_in_target_thread_vii(target_thread, (void (*)(gpointer, gpointer))mono_wasm_invoke_jsexport_sync_send_cb, method, args); } #endif /* DISABLE_THREADS */ diff --git a/src/mono/browser/runtime/exports-binding.ts b/src/mono/browser/runtime/exports-binding.ts index 6ab2b430ab048..3e65378483df3 100644 --- a/src/mono/browser/runtime/exports-binding.ts +++ b/src/mono/browser/runtime/exports-binding.ts @@ -10,7 +10,6 @@ import { mono_interp_tier_prepare_jiterpreter, mono_jiterp_free_method_data_js } import { mono_interp_jit_wasm_entry_trampoline, mono_interp_record_interp_entry } from "./jiterpreter-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"; 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"; @@ -27,8 +26,12 @@ import { mono_wasm_get_first_day_of_week, mono_wasm_get_first_week_of_year } fro import { mono_wasm_browser_entropy } from "./crypto"; import { mono_wasm_cancel_promise } from "./cancelable-promise"; -import { mono_wasm_pthread_on_pthread_attached, mono_wasm_pthread_on_pthread_unregistered, mono_wasm_pthread_on_pthread_registered, mono_wasm_pthread_set_name } from "./pthreads"; -import { mono_wasm_install_js_worker_interop, mono_wasm_uninstall_js_worker_interop } from "./pthreads"; +import { + mono_wasm_eventloop_has_unsettled_interop_promises, mono_wasm_start_deputy_thread_async, + mono_wasm_pthread_on_pthread_attached, mono_wasm_pthread_on_pthread_unregistered, + mono_wasm_pthread_on_pthread_registered, mono_wasm_pthread_set_name, mono_wasm_install_js_worker_interop, mono_wasm_uninstall_js_worker_interop +} from "./pthreads"; + // the JS methods would be visible to EMCC linker and become imports of the WASM module @@ -38,6 +41,7 @@ export const mono_wasm_threads_imports = !WasmEnableThreads ? [] : [ mono_wasm_pthread_on_pthread_attached, mono_wasm_pthread_on_pthread_unregistered, mono_wasm_pthread_set_name, + mono_wasm_start_deputy_thread_async, // threads.c mono_wasm_eventloop_has_unsettled_interop_promises, diff --git a/src/mono/browser/runtime/interp-pgo.ts b/src/mono/browser/runtime/interp-pgo.ts index 79ea1e29ab5df..e7ec6198fbf51 100644 --- a/src/mono/browser/runtime/interp-pgo.ts +++ b/src/mono/browser/runtime/interp-pgo.ts @@ -207,6 +207,9 @@ export async function getCacheKey(prefix: string): Promise { delete inputs.enableDownloadRetry; delete inputs.extensions; delete inputs.runtimeId; + delete inputs.mainThreadingMode; + delete inputs.jsThreadBlockingMode; + delete inputs.jsThreadInteropMode; inputs.GitHash = loaderHelpers.gitHash; inputs.ProductVersion = ProductVersion; diff --git a/src/mono/browser/runtime/invoke-js.ts b/src/mono/browser/runtime/invoke-js.ts index d4c0459a0f1a3..b2a352cf5e94e 100644 --- a/src/mono/browser/runtime/invoke-js.ts +++ b/src/mono/browser/runtime/invoke-js.ts @@ -5,7 +5,7 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; import BuildConfiguration from "consts:configuration"; import { marshal_exception_to_cs, bind_arg_marshal_to_cs } from "./marshal-to-cs"; -import { get_signature_argument_count, bound_js_function_symbol, get_sig, get_signature_version, get_signature_type, imported_js_function_symbol, get_signature_handle, get_signature_function_name, get_signature_module_name, is_receiver_should_free } from "./marshal"; +import { get_signature_argument_count, bound_js_function_symbol, get_sig, get_signature_version, get_signature_type, imported_js_function_symbol, get_signature_handle, get_signature_function_name, get_signature_module_name, is_receiver_should_free, get_caller_native_tid } from "./marshal"; import { setI32_unchecked, receiveWorkerHeapViews, forceThreadMemoryViewRefresh } from "./memory"; import { stringToMonoStringRoot } from "./strings"; import { MonoObject, MonoObjectRef, JSFunctionSignature, JSMarshalerArguments, WasmRoot, BoundMarshalerToJs, JSFnHandle, BoundMarshalerToCs, JSHandle, MarshalerType } from "./types/internal"; @@ -141,7 +141,8 @@ function bind_js_import(signature: JSFunctionSignature): Function { const previous = runtimeHelpers.isPendingSynchronousCall; try { forceThreadMemoryViewRefresh(); - runtimeHelpers.isPendingSynchronousCall = true; + const caller_tid = get_caller_native_tid(args); + runtimeHelpers.isPendingSynchronousCall = runtimeHelpers.currentThreadTID === caller_tid; bound_fn(args); } finally { diff --git a/src/mono/browser/runtime/loader/config.ts b/src/mono/browser/runtime/loader/config.ts index 8aec355743721..e9ae9e1dd3e69 100644 --- a/src/mono/browser/runtime/loader/config.ts +++ b/src/mono/browser/runtime/loader/config.ts @@ -4,7 +4,7 @@ import BuildConfiguration from "consts:configuration"; import WasmEnableThreads from "consts:wasmEnableThreads"; -import type { DotnetModuleInternal, MonoConfigInternal } from "../types/internal"; +import { MainThreadingMode, type DotnetModuleInternal, type MonoConfigInternal, JSThreadBlockingMode, JSThreadInteropMode } from "../types/internal"; import type { DotnetModuleConfig, MonoConfig, ResourceGroups, ResourceList } from "../types"; import { ENVIRONMENT_IS_WEB, exportedRuntimeAPI, loaderHelpers, runtimeHelpers } from "./globals"; import { mono_log_error, mono_log_debug } from "./logging"; @@ -12,6 +12,7 @@ import { importLibraryInitializers, invokeLibraryInitializers } from "./libraryI import { mono_exit } from "./exit"; import { makeURLAbsoluteWithApplicationBase } from "./polyfills"; import { appendUniqueQuery } from "./assets"; +import { mono_log_warn } from "./logging"; export function deep_merge_config(target: MonoConfigInternal, source: MonoConfigInternal): MonoConfigInternal { // no need to merge the same object @@ -188,14 +189,46 @@ export function normalizeConfig() { } // ActiveIssue https://github.com/dotnet/runtime/issues/75602 - if (WasmEnableThreads && !Number.isInteger(config.pthreadPoolInitialSize)) { - config.pthreadPoolInitialSize = 7; - } - if (WasmEnableThreads && !Number.isInteger(config.pthreadPoolUnusedSize)) { - config.pthreadPoolUnusedSize = 3; - } - if (WasmEnableThreads && !Number.isInteger(config.finalizerThreadStartDelayMs)) { - config.finalizerThreadStartDelayMs = 200; + if (WasmEnableThreads) { + + if (!Number.isInteger(config.pthreadPoolInitialSize)) { + config.pthreadPoolInitialSize = 7; + } + if (!Number.isInteger(config.pthreadPoolUnusedSize)) { + config.pthreadPoolUnusedSize = 3; + } + if (!Number.isInteger(config.finalizerThreadStartDelayMs)) { + config.finalizerThreadStartDelayMs = 200; + } + if (config.mainThreadingMode == undefined) { + config.mainThreadingMode = MainThreadingMode.DeputyThread; + } + if (config.jsThreadBlockingMode == undefined) { + config.jsThreadBlockingMode = JSThreadBlockingMode.NoBlockingWait; + } + if (config.jsThreadInteropMode == undefined) { + config.jsThreadInteropMode = JSThreadInteropMode.SimpleSynchronousJSInterop; + } + let validModes = false; + if (config.mainThreadingMode == MainThreadingMode.DeputyThread + && config.jsThreadBlockingMode == JSThreadBlockingMode.NoBlockingWait + && config.jsThreadInteropMode == JSThreadInteropMode.SimpleSynchronousJSInterop + ) { + validModes = true; + } + else if (config.mainThreadingMode == MainThreadingMode.DeputyThread + && config.jsThreadBlockingMode == JSThreadBlockingMode.AllowBlockingWait + && config.jsThreadInteropMode == JSThreadInteropMode.SimpleSynchronousJSInterop + ) { + validModes = true; + } + if (!validModes) { + mono_log_warn("Unsupported threading configuration", { + mainThreadingMode: config.mainThreadingMode, + jsThreadBlockingMode: config.jsThreadBlockingMode, + jsThreadInteropMode: config.jsThreadInteropMode + }); + } } // this is how long the Mono GC will try to wait for all threads to be suspended before it gives up and aborts the process diff --git a/src/mono/browser/runtime/managed-exports.ts b/src/mono/browser/runtime/managed-exports.ts index 62fee7656e282..88081b4e4319a 100644 --- a/src/mono/browser/runtime/managed-exports.ts +++ b/src/mono/browser/runtime/managed-exports.ts @@ -3,17 +3,18 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; -import { GCHandle, GCHandleNull, JSMarshalerArguments, MarshalerToCs, MarshalerToJs, MarshalerType, MonoMethod } from "./types/internal"; -import cwraps from "./cwraps"; +import { GCHandle, GCHandleNull, JSMarshalerArguments, JSThreadInteropMode, MarshalerToCs, MarshalerToJs, MarshalerType, MonoMethod } from "./types/internal"; +import cwraps, { threads_c_functions as twraps } from "./cwraps"; import { runtimeHelpers, Module, loaderHelpers, mono_assert } from "./globals"; -import { JavaScriptMarshalerArgSize, alloc_stack_frame, get_arg, get_arg_gc_handle, is_args_exception, set_arg_intptr, set_arg_type, set_gc_handle } from "./marshal"; +import { JavaScriptMarshalerArgSize, alloc_stack_frame, get_arg, get_arg_gc_handle, is_args_exception, set_arg_i32, set_arg_intptr, set_arg_type, set_gc_handle, set_receiver_should_free } from "./marshal"; import { marshal_array_to_cs, marshal_array_to_cs_impl, marshal_bool_to_cs, marshal_exception_to_cs, marshal_intptr_to_cs, marshal_string_to_cs } from "./marshal-to-cs"; import { marshal_int32_to_js, end_marshal_task_to_js, marshal_string_to_js, begin_marshal_task_to_js, marshal_exception_to_js } from "./marshal-to-js"; import { do_not_force_dispose, is_gcv_handle } from "./gc-handles"; import { assert_c_interop, assert_js_interop } from "./invoke-js"; import { mono_wasm_main_thread_ptr } from "./pthreads"; -import { _zero_region } from "./memory"; +import { _zero_region, copyBytes } from "./memory"; import { stringToUTF8Ptr } from "./strings"; +import { mono_log_debug } from "./logging"; const managedExports: ManagedExports = {} as any; @@ -164,6 +165,14 @@ export function complete_task(holder_gc_handle: GCHandle, isCanceling: boolean, // the marshaled signature is: TRes? CallDelegate(GCHandle callback, T1? arg1, T2? arg2, T3? arg3) export function call_delegate(callback_gc_handle: GCHandle, arg1_js: any, arg2_js: any, arg3_js: any, res_converter?: MarshalerToJs, arg1_converter?: MarshalerToCs, arg2_converter?: MarshalerToCs, arg3_converter?: MarshalerToCs) { loaderHelpers.assert_runtime_running(); + if (WasmEnableThreads) { + if (runtimeHelpers.config.jsThreadInteropMode == JSThreadInteropMode.NoSyncJSInterop) { + throw new Error("Cannot call synchronous C# methods."); + } + else if (runtimeHelpers.isPendingSynchronousCall) { + throw new Error("Cannot call synchronous C# method from inside a synchronous call to a JS method."); + } + } const sp = Module.stackSave(); try { const size = 6; @@ -218,21 +227,26 @@ export function get_managed_stack_trace(exception_gc_handle: GCHandle) { } } -// GCHandle InstallMainSynchronizationContext(nint jsNativeTID) -export function install_main_synchronization_context(): GCHandle { +// GCHandle InstallMainSynchronizationContext(nint jsNativeTID, JSThreadBlockingMode jsThreadBlockingMode, JSThreadInteropMode jsThreadInteropMode, MainThreadingMode mainThreadingMode) +export function install_main_synchronization_context(jsThreadBlockingMode: number, jsThreadInteropMode: number, mainThreadingMode: number): GCHandle { if (!WasmEnableThreads) return GCHandleNull; assert_c_interop(); - const sp = Module.stackSave(); try { // this block is like alloc_stack_frame() but without set_args_context() - const bytes = JavaScriptMarshalerArgSize * 3; + const bytes = JavaScriptMarshalerArgSize * 6; const args = Module.stackAlloc(bytes) as any; _zero_region(args, bytes); const res = get_arg(args, 1); const arg1 = get_arg(args, 2); + const arg2 = get_arg(args, 3); + const arg3 = get_arg(args, 4); + const arg4 = get_arg(args, 5); set_arg_intptr(arg1, mono_wasm_main_thread_ptr() as any); + set_arg_i32(arg2, jsThreadBlockingMode); + set_arg_i32(arg3, jsThreadInteropMode); + set_arg_i32(arg4, mainThreadingMode); // this block is like invoke_sync_jsexport() but without assert_js_interop() cwraps.mono_wasm_invoke_jsexport(managedExports.InstallMainSynchronizationContext!, args); @@ -241,8 +255,9 @@ export function install_main_synchronization_context(): GCHandle { throw marshal_exception_to_js(exc); } return get_arg_gc_handle(res) as any; - } finally { - Module.stackRestore(sp); + } catch (e) { + mono_log_debug("install_main_synchronization_context failed", e); + throw e; } } @@ -255,31 +270,33 @@ export function invoke_async_jsexport(method: MonoMethod, args: JSMarshalerArgum throw marshal_exception_to_js(exc); } } else { - throw new Error("Should be unreachable until we implement deputy." + size); - /* set_receiver_should_free(args); const bytes = JavaScriptMarshalerArgSize * size; const cpy = Module._malloc(bytes) as any; copyBytes(args as any, cpy, bytes); twraps.mono_wasm_invoke_jsexport_async_post(runtimeHelpers.managedThreadTID, method, cpy); - */ } } export function invoke_sync_jsexport(method: MonoMethod, args: JSMarshalerArguments): void { assert_js_interop(); - if (!WasmEnableThreads || runtimeHelpers.isManagedRunningOnCurrentThread) { + if (!WasmEnableThreads) { cwraps.mono_wasm_invoke_jsexport(method, args as any); } else { - throw new Error("Should be unreachable until we implement deputy."); - /* - if (!runtimeHelpers.isManagedRunningOnCurrentThread && runtimeHelpers.isPendingSynchronousCall) { + if (runtimeHelpers.config.jsThreadInteropMode == JSThreadInteropMode.NoSyncJSInterop) { + throw new Error("Cannot call synchronous C# methods."); + } + else if (runtimeHelpers.isPendingSynchronousCall) { throw new Error("Cannot call synchronous C# method from inside a synchronous call to a JS method."); } - // this is blocking too - twraps.mono_wasm_invoke_jsexport_sync_send(runtimeHelpers.managedThreadTID, method, args as any); - */ + if (runtimeHelpers.isManagedRunningOnCurrentThread) { + cwraps.mono_wasm_invoke_jsexport(method, args as any); + } else { + // this is blocking too + twraps.mono_wasm_invoke_jsexport_sync_send(runtimeHelpers.managedThreadTID, method, args as any); + } } + if (is_args_exception(args)) { const exc = get_arg(args, 0); throw marshal_exception_to_js(exc); diff --git a/src/mono/browser/runtime/marshal.ts b/src/mono/browser/runtime/marshal.ts index b6f825c4f5738..35e001c8665e0 100644 --- a/src/mono/browser/runtime/marshal.ts +++ b/src/mono/browser/runtime/marshal.ts @@ -7,7 +7,7 @@ import { js_owned_gc_handle_symbol, teardown_managed_proxy } from "./gc-handles" import { Module, loaderHelpers, mono_assert, runtimeHelpers } from "./globals"; import { getF32, getF64, getI16, getI32, getI64Big, getU16, getU32, getU8, setF32, setF64, setI16, setI32, setI64Big, setU16, setU32, setU8, localHeapViewF64, localHeapViewI32, localHeapViewU8, _zero_region, getB32, setB32, forceThreadMemoryViewRefresh } from "./memory"; import { mono_wasm_new_external_root } from "./roots"; -import { GCHandle, JSHandle, MonoObject, MonoString, GCHandleNull, JSMarshalerArguments, JSFunctionSignature, JSMarshalerType, JSMarshalerArgument, MarshalerToJs, MarshalerToCs, WasmRoot, MarshalerType } from "./types/internal"; +import { GCHandle, JSHandle, MonoObject, MonoString, GCHandleNull, JSMarshalerArguments, JSFunctionSignature, JSMarshalerType, JSMarshalerArgument, MarshalerToJs, MarshalerToCs, WasmRoot, MarshalerType, PThreadPtr, PThreadPtrNull } from "./types/internal"; import { TypedArray, VoidPtr } from "./types/emscripten"; import { utf16ToString } from "./strings"; import { get_managed_stack_trace } from "./managed-exports"; @@ -38,6 +38,7 @@ const enum JSMarshalerArgumentOffsets { ElementType = 13, ContextHandle = 16, ReceiverShouldFree = 20, + CallerNativeTID = 24, } export const JSMarshalerTypeSize = 32; // keep in sync with JSFunctionBinding.JSBindingType @@ -90,6 +91,12 @@ export function is_receiver_should_free(args: JSMarshalerArguments): boolean { return getB32(args + JSMarshalerArgumentOffsets.ReceiverShouldFree); } +export function get_caller_native_tid(args: JSMarshalerArguments): PThreadPtr { + if (!WasmEnableThreads) return PThreadPtrNull; + mono_assert(args, "Null args"); + return getI32(args + JSMarshalerArgumentOffsets.CallerNativeTID) as any; +} + export function set_receiver_should_free(args: JSMarshalerArguments): void { mono_assert(args, "Null args"); setB32(args + JSMarshalerArgumentOffsets.ReceiverShouldFree, true); diff --git a/src/mono/browser/runtime/multi-threading.md b/src/mono/browser/runtime/multi-threading.md new file mode 100644 index 0000000000000..4e308852e5034 --- /dev/null +++ b/src/mono/browser/runtime/multi-threading.md @@ -0,0 +1,45 @@ +# Multi-threading with JavaScript interop + +## Meaningful configurations are: + + * Single-threaded mode as you know it since .Net 6 + - default, safe, tested, supported + - from .Net 8 it could be easily started also as a web worker, but you need your own messaging between main and worker + * `MainThreadingMode.DeputyThread` + `JSThreadBlockingMode.NoBlockingWait` + `JSThreadInteropMode.SimpleSynchronousJSInterop` + + **default threading**, safe, tested, supported + + blocking `.Wait` is allowed on thread pool and new threads + - blocking `.Wait` throws `PlatformNotSupportedException` on `JSWebWorker` and main thread + - DOM events like `onClick` need to be asynchronous, if the handler needs use synchronous `[JSImport]` + - synchronous calls to `[JSImport]`/`[JSExport]` can't synchronously call back + + * `MainThreadingMode.DeputyThread` + `JSThreadBlockingMode.AllowBlockingWait` + `JSThreadInteropMode.SimpleSynchronousJSInterop` + + pragmatic for legacy codebase, which contains blocking code and can't be fully executed on thread pool or new threads + - ** could cause deadlocks !!!** + - Use your own judgment before you opt in. + - blocking .Wait is allowed on all threads! + - blocking .Wait on pending JS `Task`/`Promise` (like HTTP/WS requests) could cause deadlocks! + - reason is that blocked thread can't process the browser event loop + - so it can't resolve the promises + - even when it's longer `Promise`/`Task` chain + - DOM events like `onClick` need to be asynchronous, if the handler needs use synchronous `[JSImport]` + - synchronous calls to `[JSImport]`/`[JSExport]` can't synchronously call back + +## Unsupported combinations are: + * `MainThreadingMode.DeputyThread` + `JSThreadBlockingMode.NoBlockingWait` + `JSThreadInteropMode.NoSyncJSInterop` + + very safe + - HTTP/WS requests are not possible because it currently uses synchronous JS interop + - Blazor doesn't work because it currently uses synchronous JS interop + * `MainThreadingMode.UIThread` + - not recommended, not tested, not supported! + - can deadlock on creating new threads + - can deadlock on blocking `.Wait` for a pending JS `Promise`/`Task`, including HTTP/WS requests + - .Wait is spin-waiting - it blocks debugger, network, UI rendering, ... + + JS interop to UI is faster, synchronous and re-entrant + +### There could be more JSThreadInteropModes: + - allow re-entrant synchronous JS interop on `JSWebWorker`. + - This is possible because managed code is running on same thread as JS. + - But it's nuanced to debug it, when things go wrong. + - allow re-entrant synchronous JS interop also on deputy thread. + - This is not possible for deputy, because it would deadlock on call back to different thread. + - The thread receiving the callback is still blocked waiting for the first synchronous call to finish. diff --git a/src/mono/browser/runtime/pthreads/deputy-thread.ts b/src/mono/browser/runtime/pthreads/deputy-thread.ts new file mode 100644 index 0000000000000..1020aaf98620d --- /dev/null +++ b/src/mono/browser/runtime/pthreads/deputy-thread.ts @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import WasmEnableThreads from "consts:wasmEnableThreads"; +import BuildConfiguration from "consts:configuration"; + +import { mono_log_error, mono_log_info } from "../logging"; +import { monoThreadInfo, postMessageToMain, update_thread_info } from "./shared"; +import { Module, loaderHelpers, runtimeHelpers } from "../globals"; +import { start_runtime } from "../startup"; +import { WorkerToMainMessageType } from "../types/internal"; + +export function mono_wasm_start_deputy_thread_async() { + if (!WasmEnableThreads) return; + + if (BuildConfiguration === "Debug" && globalThis.setInterval) globalThis.setInterval(() => { + mono_log_info("Deputy thread is alive!"); + }, 3000); + + try { + monoThreadInfo.isDeputy = true; + monoThreadInfo.threadName = "Managed Main Deputy"; + update_thread_info(); + postMessageToMain({ + monoCmd: WorkerToMainMessageType.deputyCreated, + info: monoThreadInfo, + }); + Module.runtimeKeepalivePush(); + Module.safeSetTimeout(async () => { + try { + + await start_runtime(); + + postMessageToMain({ + monoCmd: WorkerToMainMessageType.deputyStarted, + info: monoThreadInfo, + deputyProxyGCHandle: runtimeHelpers.proxyGCHandle, + }); + } + catch (err) { + postMessageToMain({ + monoCmd: WorkerToMainMessageType.deputyFailed, + info: monoThreadInfo, + error: "mono_wasm_start_deputy_thread_async() failed" + err, + }); + mono_log_error("mono_wasm_start_deputy_thread_async() failed", err); + loaderHelpers.mono_exit(1, err); + throw err; + } + }, 0); + } + catch (err) { + mono_log_error("mono_wasm_start_deputy_thread_async() failed", err); + loaderHelpers.mono_exit(1, err); + throw err; + } + + // same as emscripten_exit_with_live_runtime() + throw "unwind"; +} \ No newline at end of file diff --git a/src/mono/browser/runtime/pthreads/index.ts b/src/mono/browser/runtime/pthreads/index.ts index 74d9a4be7561b..a97f0f0f06082 100644 --- a/src/mono/browser/runtime/pthreads/index.ts +++ b/src/mono/browser/runtime/pthreads/index.ts @@ -16,3 +16,5 @@ export { mono_wasm_pthread_on_pthread_registered, mono_wasm_pthread_set_name, currentWorkerThreadEvents, dotnetPthreadCreated, initWorkerThreadEvents, replaceEmscriptenTLSInit, pthread_self } from "./worker-thread"; + +export { mono_wasm_start_deputy_thread_async } from "./deputy-thread"; diff --git a/src/mono/browser/runtime/pthreads/shared.ts b/src/mono/browser/runtime/pthreads/shared.ts index eda95db4c6226..5eb76b7f8fc37 100644 --- a/src/mono/browser/runtime/pthreads/shared.ts +++ b/src/mono/browser/runtime/pthreads/shared.ts @@ -38,7 +38,7 @@ export function mono_wasm_install_js_worker_interop(context_gc_handle: GCHandle) mono_assert(!runtimeHelpers.proxyGCHandle, "JS interop should not be already installed on this worker."); runtimeHelpers.proxyGCHandle = context_gc_handle; if (ENVIRONMENT_IS_PTHREAD) { - runtimeHelpers.managedThreadTID = mono_wasm_pthread_ptr(); + runtimeHelpers.managedThreadTID = runtimeHelpers.currentThreadTID; runtimeHelpers.isManagedRunningOnCurrentThread = true; } Module.runtimeKeepalivePush(); @@ -70,14 +70,15 @@ export function update_thread_info(): void { if (!WasmEnableThreads) return; const threadType = !monoThreadInfo.isRegistered ? "emsc" : monoThreadInfo.isUI ? "-UI-" - : monoThreadInfo.isTimer ? "timr" - : monoThreadInfo.isLongRunning ? "long" - : monoThreadInfo.isThreadPoolGate ? "gate" - : monoThreadInfo.isDebugger ? "dbgr" - : monoThreadInfo.isThreadPoolWorker ? "pool" - : monoThreadInfo.isExternalEventLoop ? "jsww" - : monoThreadInfo.isBackground ? "back" - : "norm"; + : monoThreadInfo.isDeputy ? "dpty" + : monoThreadInfo.isTimer ? "timr" + : monoThreadInfo.isLongRunning ? "long" + : monoThreadInfo.isThreadPoolGate ? "gate" + : monoThreadInfo.isDebugger ? "dbgr" + : monoThreadInfo.isThreadPoolWorker ? "pool" + : monoThreadInfo.isExternalEventLoop ? "jsww" + : monoThreadInfo.isBackground ? "back" + : "norm"; const hexPtr = (monoThreadInfo.pthreadId as any).toString(16).padStart(8, "0"); const hexPrefix = monoThreadInfo.isRegistered ? "0x" : "--"; monoThreadInfo.threadPrefix = `${hexPrefix}${hexPtr}-${threadType}`; @@ -124,6 +125,8 @@ export interface MonoWorkerToMainMessage { monoCmd: WorkerToMainMessageType; info: PThreadInfo; port?: MessagePort; + error?: string; + deputyProxyGCHandle?: GCHandle; } /// Identification of the current thread executing on a worker diff --git a/src/mono/browser/runtime/pthreads/ui-thread.ts b/src/mono/browser/runtime/pthreads/ui-thread.ts index d145af3c50a6c..c7fb54a66e48c 100644 --- a/src/mono/browser/runtime/pthreads/ui-thread.ts +++ b/src/mono/browser/runtime/pthreads/ui-thread.ts @@ -95,12 +95,20 @@ function monoWorkerMessageHandler(worker: PThreadWorker, ev: MessageEvent): worker.thread = thread; worker.info.isRunning = true; resolveThreadPromises(pthreadId, thread); + worker.info = Object.assign(worker.info!, message.info, {}); + break; + case WorkerToMainMessageType.deputyStarted: + runtimeHelpers.afterMonoStarted.promise_control.resolve(message.deputyProxyGCHandle); + break; + case WorkerToMainMessageType.deputyFailed: + runtimeHelpers.afterMonoStarted.promise_control.reject(new Error(message.error)); break; case WorkerToMainMessageType.monoRegistered: case WorkerToMainMessageType.monoAttached: case WorkerToMainMessageType.enabledInterop: case WorkerToMainMessageType.monoUnRegistered: case WorkerToMainMessageType.updateInfo: + case WorkerToMainMessageType.deputyCreated: // just worker.info updates above break; default: diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts index 8d98635b55637..41ce07e37d3c4 100644 --- a/src/mono/browser/runtime/startup.ts +++ b/src/mono/browser/runtime/startup.ts @@ -3,9 +3,9 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; -import { DotnetModuleInternal, CharPtrNull } from "./types/internal"; +import { DotnetModuleInternal, CharPtrNull, MainThreadingMode } from "./types/internal"; import { ENVIRONMENT_IS_NODE, exportedRuntimeAPI, INTERNAL, loaderHelpers, Module, runtimeHelpers, createPromiseController, mono_assert, ENVIRONMENT_IS_WORKER } from "./globals"; -import cwraps, { init_c_exports } from "./cwraps"; +import cwraps, { init_c_exports, threads_c_functions as tcwraps } from "./cwraps"; import { mono_wasm_raise_debug_event, mono_wasm_runtime_ready } from "./debug"; import { toBase64StringImpl } from "./base64"; import { mono_wasm_init_aot_profiler, mono_wasm_init_browser_profiler } from "./profiler"; @@ -270,11 +270,25 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { Module.runtimeKeepalivePush(); - // load mono runtime and apply environment settings (if necessary) - await start_runtime(); + if (WasmEnableThreads && runtimeHelpers.config.mainThreadingMode == MainThreadingMode.DeputyThread) { + // this will create thread and call start_runtime() on it + runtimeHelpers.monoThreadInfo = monoThreadInfo; + runtimeHelpers.isManagedRunningOnCurrentThread = false; + update_thread_info(); + runtimeHelpers.managedThreadTID = tcwraps.mono_wasm_create_deputy_thread(); + runtimeHelpers.proxyGCHandle = await runtimeHelpers.afterMonoStarted.promise; - if (!ENVIRONMENT_IS_WORKER) { - Module.runtimeKeepalivePush(); + // TODO make UI thread not managed + tcwraps.mono_wasm_register_ui_thread(); + monoThreadInfo.isAttached = true; + monoThreadInfo.isRegistered = true; + + runtimeHelpers.runtimeReady = true; + update_thread_info(); + bindings_init(); + } else { + // load mono runtime and apply environment settings (if necessary) + await start_runtime(); } if (ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER) { @@ -517,7 +531,10 @@ export async function start_runtime() { monoThreadInfo.isRegistered = true; runtimeHelpers.currentThreadTID = monoThreadInfo.pthreadId = runtimeHelpers.managedThreadTID = mono_wasm_pthread_ptr(); update_thread_info(); - runtimeHelpers.proxyGCHandle = install_main_synchronization_context(); + runtimeHelpers.proxyGCHandle = install_main_synchronization_context( + runtimeHelpers.config.jsThreadBlockingMode!, + runtimeHelpers.config.jsThreadInteropMode!, + runtimeHelpers.config.mainThreadingMode!); runtimeHelpers.isManagedRunningOnCurrentThread = true; // start finalizer thread, lazy diff --git a/src/mono/browser/runtime/types/internal.ts b/src/mono/browser/runtime/types/internal.ts index e92c24faa03b2..4c9fa7babc62f 100644 --- a/src/mono/browser/runtime/types/internal.ts +++ b/src/mono/browser/runtime/types/internal.ts @@ -95,6 +95,10 @@ export type MonoConfigInternal = MonoConfig & { resourcesHash?: string, GitHash?: string, ProductVersion?: string, + + mainThreadingMode?: MainThreadingMode, + jsThreadBlockingMode?: JSThreadBlockingMode, + jsThreadInteropMode?: JSThreadInteropMode, }; export type RunArguments = { @@ -455,6 +459,7 @@ export type passEmscriptenInternalsType = (internals: EmscriptenInternals, emscr export type setGlobalObjectsType = (globalObjects: GlobalObjects) => void; export type initializeExportsType = (globalObjects: GlobalObjects) => RuntimeAPI; export type initializeReplacementsType = (replacements: EmscriptenReplacements) => void; +export type afterInitializeType = (module: EmscriptenModuleInternal) => void; export type configureEmscriptenStartupType = (module: DotnetModuleInternal) => void; export type configureRuntimeStartupType = () => Promise; export type configureWorkerStartupType = (module: DotnetModuleInternal) => Promise @@ -489,6 +494,9 @@ export const enum WorkerToMainMessageType { enabledInterop = "notify_enabled_interop", monoUnRegistered = "monoUnRegistered", pthreadCreated = "pthreadCreated", + deputyCreated = "createdDeputy", + deputyFailed = "deputyFailed", + deputyStarted = "monoStarted", preload = "preload", } @@ -518,6 +526,7 @@ export interface PThreadInfo { isRegistered?: boolean, isRunning?: boolean, isAttached?: boolean, + isDeputy?: boolean, isExternalEventLoop?: boolean, isUI?: boolean; isBackground?: boolean, @@ -557,3 +566,33 @@ export interface MonoThreadMessage { // A particular kind of message. For example, "started", "stopped", "stopped_with_error", etc. cmd: string; } + +// keep in sync with JSHostImplementation.Types.cs +export const enum MainThreadingMode { + // Running the managed main thread on UI thread. + // Managed GC and similar scenarios could be blocking the UI. + // Easy to deadlock. Not recommended for production. + UIThread = 0, + // Running the managed main thread on dedicated WebWorker. Marshaling all JavaScript calls to and from the main thread. + DeputyThread = 1, +} + +// keep in sync with JSHostImplementation.Types.cs +export const enum JSThreadBlockingMode { + // throw PlatformNotSupportedException if blocking .Wait is called on threads with JS interop, like JSWebWorker and Main thread. + // Avoids deadlocks (typically with pending JS promises on the same thread) by throwing exceptions. + NoBlockingWait = 0, + // allow .Wait on all threads. + // Could cause deadlocks with blocking .Wait on a pending JS Task/Promise on the same thread or similar Task/Promise chain. + AllowBlockingWait = 100, +} + +// keep in sync with JSHostImplementation.Types.cs +export const enum JSThreadInteropMode { + // throw PlatformNotSupportedException if synchronous JSImport/JSExport is called on threads with JS interop, like JSWebWorker and Main thread. + // calling synchronous JSImport on thread pool or new threads is allowed. + NoSyncJSInterop = 0, + // allow non-re-entrant synchronous blocking calls to and from JS on JSWebWorker on threads with JS interop, like JSWebWorker and Main thread. + // calling synchronous JSImport on thread pool or new threads is allowed. + SimpleSynchronousJSInterop = 1, +} \ No newline at end of file diff --git a/src/mono/mono/component/diagnostics_server.c b/src/mono/mono/component/diagnostics_server.c index 4bea3d722625c..02179f785eb35 100644 --- a/src/mono/mono/component/diagnostics_server.c +++ b/src/mono/mono/component/diagnostics_server.c @@ -250,6 +250,7 @@ queue_push_sync (WasmIpcStreamQueue *q, const uint8_t *buf, uint32_t buf_size, u gboolean is_browser_thread = FALSE; while (mono_atomic_load_i32 (&q->buf_full) != 0) { if (G_UNLIKELY (!is_browser_thread_inited)) { + // FIXME for deputy is_browser_thread = mono_threads_wasm_is_ui_thread (); is_browser_thread_inited = TRUE; } diff --git a/src/mono/mono/utils/mono-threads-wasm.c b/src/mono/mono/utils/mono-threads-wasm.c index 67d3e7a1c6a79..1746c7213e96d 100644 --- a/src/mono/mono/utils/mono-threads-wasm.c +++ b/src/mono/mono/utils/mono-threads-wasm.c @@ -458,7 +458,7 @@ mono_threads_platform_is_main_thread (void) #ifdef DISABLE_THREADS return TRUE; #else - return emscripten_is_main_runtime_thread (); + return mono_threads_wasm_is_deputy_thread (); #endif } @@ -541,6 +541,54 @@ mono_threads_wasm_on_thread_registered (void) } #ifndef DISABLE_THREADS +extern void mono_wasm_start_deputy_thread_async (void); +extern void mono_wasm_trace_logger (const char *log_domain, const char *log_level, const char *message, mono_bool fatal, void *user_data); +static pthread_t deputy_thread_tid; + +gboolean +mono_threads_wasm_is_deputy_thread (void) +{ + return pthread_self () == deputy_thread_tid; +} + +MonoNativeThreadId +mono_threads_wasm_deputy_thread_tid (void) +{ + return (MonoNativeThreadId) deputy_thread_tid; +} + +// this is running in deputy thread +static gsize +deputy_thread_fn (void* unused_arg G_GNUC_UNUSED) +{ + deputy_thread_tid = pthread_self (); + + // this will throw JS "unwind" + mono_wasm_start_deputy_thread_async(); + + return 0;// never reached +} + +EMSCRIPTEN_KEEPALIVE MonoNativeThreadId +mono_wasm_create_deputy_thread (void) +{ + pthread_create (&deputy_thread_tid, NULL, (void *(*)(void *)) deputy_thread_fn, NULL); + return deputy_thread_tid; +} + +// TODO ideally we should not need to have UI thread registered as managed +EMSCRIPTEN_KEEPALIVE void +mono_wasm_register_ui_thread (void) +{ + MonoThread *thread = mono_thread_internal_attach (mono_get_root_domain ()); + mono_thread_set_state (thread, ThreadState_Background); + mono_thread_info_set_flags (MONO_THREAD_INFO_FLAGS_NONE); + + MonoThreadInfo *info = mono_thread_info_current_unchecked (); + g_assert (info); + info->runtime_thread = TRUE; + MONO_ENTER_GC_SAFE_UNBALANCED; +} void mono_threads_wasm_async_run_in_target_thread (pthread_t target_thread, void (*func) (void)) diff --git a/src/mono/mono/utils/mono-threads-wasm.h b/src/mono/mono/utils/mono-threads-wasm.h index 13d0b6cb76a6c..13668709357db 100644 --- a/src/mono/mono/utils/mono-threads-wasm.h +++ b/src/mono/mono/utils/mono-threads-wasm.h @@ -28,6 +28,18 @@ mono_threads_wasm_ui_thread_tid (void); #ifndef DISABLE_THREADS +gboolean +mono_threads_wasm_is_deputy_thread (void); + +MonoNativeThreadId +mono_threads_wasm_deputy_thread_tid (void); + +MonoNativeThreadId +mono_wasm_create_deputy_thread (void); + +void +mono_wasm_register_ui_thread (void); + void mono_threads_wasm_async_run_in_target_thread (pthread_t target_thread, void (*func) (void));