From 24d74c93a2c0c72926e190ded76be8e9833458ce Mon Sep 17 00:00:00 2001
From: pavelsavara <pavel.savara@gmail.com>
Date: Wed, 21 Feb 2024 17:27:30 +0100
Subject: [PATCH 1/8] wip

---
 eng/testing/tests.browser.targets             |  2 +-
 .../src/Interop/Browser/Interop.Runtime.cs    |  2 +-
 .../TestUtilities/System/PlatformDetection.cs |  2 +
 .../JavaScript/Interop/JavaScriptExports.cs   | 46 +++++++++++++-
 .../JavaScript/JSFunctionBinding.cs           | 63 ++++++++++++++++---
 .../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 --
 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         | 19 +++---
 src/mono/browser/runtime/loader/config.ts     | 51 ++++++++++++---
 src/mono/browser/runtime/managed-exports.ts   | 61 +++++++++++-------
 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   |  5 +-
 .../browser/runtime/pthreads/ui-thread.ts     |  8 +++
 src/mono/browser/runtime/startup.ts           | 10 ++-
 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 ++++
 34 files changed, 546 insertions(+), 114 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/testing/tests.browser.targets b/eng/testing/tests.browser.targets
index d27fa412d490b..bce044984e937 100644
--- a/eng/testing/tests.browser.targets
+++ b/eng/testing/tests.browser.targets
@@ -87,8 +87,8 @@
     <_AppArgs Condition="'$(IsFunctionalTest)' != 'true' and '$(WasmMainAssemblyFileName)' != ''">--run $(WasmMainAssemblyFileName)</_AppArgs>
     <_AppArgs Condition="'$(IsFunctionalTest)' == 'true'">--run $(AssemblyName).dll</_AppArgs>
 
-    <_XUnitBackgroundExec Condition="'$(_XUnitBackgroundExec)' == '' and '$(WasmEnableThreads)' == 'true'">true</_XUnitBackgroundExec>
     <WasmTestAppArgs Condition="'$(_XUnitBackgroundExec)' == 'true'">$(WasmTestAppArgs) -backgroundExec</WasmTestAppArgs>
+    <WasmXHarnessMonoArgs Condition="'$(_XUnitBackgroundExec)' == 'true'">$(WasmXHarnessMonoArgs) --setenv=IsWasmBackgroundExec=true</WasmXHarnessMonoArgs>
     <_AppArgs Condition="'$(WasmTestAppArgs)' != ''">$(_AppArgs) $(WasmTestAppArgs)</_AppArgs>
 
     <WasmXHarnessMonoArgs Condition="'$(XunitShowProgress)' == 'true'">$(WasmXHarnessMonoArgs) --setenv=XHARNESS_LOG_TEST_START=true</WasmXHarnessMonoArgs>
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/TestUtilities/System/PlatformDetection.cs b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs
index 054a868c25f85..e1e451b2d453f 100644
--- a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs
+++ b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs
@@ -135,6 +135,8 @@ 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 IsBinaryFormatterSupported => IsNotMobile && !IsNativeAot;
 
         public static bool IsStartingProcessesSupported => !IsiOS && !IstvOS;
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<int>? 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..9000f2ab69324 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<JSMarshal
             jsFunction.AssertNotDisposed();
 
 #if FEATURE_WASM_MANAGED_THREADS
+            ref JSMarshalerArgument exc = ref arguments[0];
+            exc.slot.CallerNativeTID = jsFunction.ProxyContext.NativeTID;
+
             // if we are on correct thread already, just call it
             if (jsFunction.ProxyContext.IsCurrentThread())
             {
@@ -226,6 +229,17 @@ internal static unsafe void InvokeJSFunction(JSObject jsFunction, Span<JSMarshal
 #endif
         internal static unsafe void InvokeJSFunctionCurrent(JSObject jsFunction, Span<JSMarshalerArgument> 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<JS
 #endif
         internal static unsafe void DispatchJSFunctionSync(JSObject jsFunction, Span<JSMarshalerArgument> 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<JSM
 #endif
         internal static unsafe void InvokeJSImportImpl(JSFunctionBinding signature, Span<JSMarshalerArgument> 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);
             }
         }
 
@@ -427,11 +468,12 @@ internal static unsafe void ResolveOrRejectPromise(JSProxyContext targetContext,
             {
                 fixed (JSMarshalerArgument* ptr = arguments)
                 {
+                    ref JSMarshalerArgument exc = ref arguments[0];
+                    exc.slot.CallerNativeTID = targetContext.NativeTID;
                     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);
                     }
                 }
             }
@@ -441,6 +483,7 @@ internal static unsafe void ResolveOrRejectPromise(JSProxyContext targetContext,
                 // meaning JS side needs to dispose it
                 ref JSMarshalerArgument exc = ref arguments[0];
                 exc.slot.ReceiverShouldFree = true;
+                exc.slot.CallerNativeTID = targetContext.NativeTID;
 
                 // this copy is freed in mono_wasm_resolve_or_reject_promise
                 var bytes = sizeof(JSMarshalerArgument) * arguments.Length;
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]<JSMarshalerArgument*, void>)&JavaScriptExports.BeforeSyncJSExport,
+                (delegate* unmanaged[Cdecl]<JSMarshalerArgument*, void>)&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 @@
+<Project>
+  <PropertyGroup>
+    <_XUnitBackgroundExec Condition="'$(_XUnitBackgroundExec)' == ''">true</_XUnitBackgroundExec>
+    <AssemblyName>System.Runtime.InteropServices.JavaScript.Tests</AssemblyName>
+  </PropertyGroup>
+  <Import Project="$(MSBuildThisFileDirectory)..\System.Runtime.InteropServices.JavaScript.Tests.csproj" />
+</Project>
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<JSException>(()=>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<Task> job, CancellationToken cancellatio
 
         public static Task RunOnNewThread(Func<Task> 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/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 @@
   </ItemGroup>
 
   <ItemGroup Condition="'$(TargetOS)' == 'browser' and '$(WasmEnableThreads)' == 'true' and '$(RunDisabledWasmTests)' != 'true'">
-    <!-- Until: https://github.com/dotnet/runtime/pull/97441 -->
-    <ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Runtime.InteropServices.JavaScript\tests\System.Runtime.InteropServices.JavaScript.UnitTests\System.Runtime.InteropServices.JavaScript.Tests.csproj" />
     <!-- Issue: https://github.com/dotnet/runtime/issues/95795 -->
     <ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Runtime\tests\System.Globalization.Tests\Hybrid\System.Globalization.Hybrid.WASM.Tests.csproj" />
     <ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Runtime\tests\System.Globalization.Calendars.Tests\Hybrid\System.Globalization.Calendars.Hybrid.WASM.Tests.csproj" />
   </ItemGroup>
 
+  <ItemGroup Condition="'$(TargetOS)' == 'browser' and '$(WasmEnableThreads)' != 'true' and '$(RunDisabledWasmTests)' != 'true'">
+    <ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Runtime.InteropServices.JavaScript\tests\System.Runtime.InteropServices.JavaScript.UnitTests\BackgroundExec\System.Runtime.InteropServices.JavaScript.BackgroundExec.Tests.csproj" />
+  </ItemGroup>
+
   <!-- Aggressive Trimming related failures -->
   <ItemGroup Condition="('$(TargetOS)' != 'browser' and '$(RunAOTCompilation)' == 'true' and '$(MonoForceInterpreter)' != 'true') or ('$(TargetOS)' == 'browser' and '$(BuildAOTTestsOnHelix)' == 'true' and '$(RunDisabledWasmTests)' != 'true')">
   </ItemGroup>
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<string | null> {
     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..8641b4db82285 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";
@@ -133,7 +133,7 @@ function bind_js_import(signature: JSFunctionSignature): Function {
     }
 
     function async_bound_fn(args: JSMarshalerArguments): void {
-        forceThreadMemoryViewRefresh();
+            forceThreadMemoryViewRefresh();
         bound_fn(args);
     }
 
@@ -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 {
@@ -151,12 +152,12 @@ function bind_js_import(signature: JSFunctionSignature): Function {
 
     let wrapped_fn: WrappedJSFunction = bound_fn;
     if (WasmEnableThreads) {
-        if (is_async || is_discard_no_wait) {
-            wrapped_fn = async_bound_fn;
-        }
-        else {
-            wrapped_fn = sync_bound_fn;
-        }
+    if (is_async || is_discard_no_wait) {
+        wrapped_fn = async_bound_fn;
+    }
+    else {
+        wrapped_fn = sync_bound_fn;
+    }
     }
 
     // this is just to make debugging easier by naming the function in the stack trace.
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..60d4f6207d062 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;
 
@@ -122,8 +123,8 @@ export function release_js_owned_object_by_gc_handle(gc_handle: GCHandle) {
         set_arg_type(arg1, MarshalerType.Object);
         set_gc_handle(arg1, gc_handle);
         if (is_gcv_handle(gc_handle)) {
-            // this must stay synchronous for free_gcv_handle sake
-            invoke_sync_jsexport(managedExports.ReleaseJSOwnedObjectByGCHandle, args);
+        // this must stay synchronous for free_gcv_handle sake
+        invoke_sync_jsexport(managedExports.ReleaseJSOwnedObjectByGCHandle, args);
         } else {
             invoke_async_jsexport(managedExports.ReleaseJSOwnedObjectByGCHandle, args, size);
         }
@@ -164,6 +165,14 @@ export function complete_task(holder_gc_handle: GCHandle, isCanceling: boolean,
 // the marshaled signature is: TRes? CallDelegate<T1,T2,T3,TRes>(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(<any>args + JSMarshalerArgumentOffsets.ReceiverShouldFree);
 }
 
+export function get_caller_native_tid(args: JSMarshalerArguments): PThreadPtr {
+    if (!WasmEnableThreads) return PThreadPtrNull;
+    mono_assert(args, "Null args");
+    return getI32(<any>args + JSMarshalerArgumentOffsets.CallerNativeTID) as any;
+}
+
 export function set_receiver_should_free(args: JSMarshalerArguments): void {
     mono_assert(args, "Null args");
     setB32(<any>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..53a57df5b44ea 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,6 +70,7 @@ export function update_thread_info(): void {
     if (!WasmEnableThreads) return;
     const threadType = !monoThreadInfo.isRegistered ? "emsc"
         : monoThreadInfo.isUI ? "-UI-"
+            : monoThreadInfo.isDeputy ? "dpty"
             : monoThreadInfo.isTimer ? "timr"
                 : monoThreadInfo.isLongRunning ? "long"
                     : monoThreadInfo.isThreadPoolGate ? "gate"
@@ -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<any>):
             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..3dd0c1b8acf82 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";
@@ -126,7 +126,6 @@ async function instantiateWasmWorker(
     await loaderHelpers.afterConfigLoaded.promise;
 
     replace_linker_placeholders(imports);
-    replaceEmscriptenPThreadInit();
 
     // Instantiate from the module posted from the main thread.
     // We can just use sync instantiation in the worker.
@@ -270,9 +269,8 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) {
 
         Module.runtimeKeepalivePush();
 
-        // load mono runtime and apply environment settings (if necessary)
+        // load runtime and apply environment settings (if necessary)
         await start_runtime();
-
         if (!ENVIRONMENT_IS_WORKER) {
             Module.runtimeKeepalivePush();
         }
@@ -518,7 +516,7 @@ export async function start_runtime() {
             runtimeHelpers.currentThreadTID = monoThreadInfo.pthreadId = runtimeHelpers.managedThreadTID = mono_wasm_pthread_ptr();
             update_thread_info();
             runtimeHelpers.proxyGCHandle = install_main_synchronization_context();
-            runtimeHelpers.isManagedRunningOnCurrentThread = true;
+            runtimeHelpers.isCurrentThread = true;
 
             // start finalizer thread, lazy
             init_finalizer_thread();
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<void>;
 export type configureWorkerStartupType = (module: DotnetModuleInternal) => Promise<void>
@@ -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));
 

From 9b007bdf9d7ab11f1baa0b7384b128261b992291 Mon Sep 17 00:00:00 2001
From: pavelsavara <pavel.savara@gmail.com>
Date: Wed, 21 Feb 2024 17:49:17 +0100
Subject: [PATCH 2/8] fix merge

---
 src/mono/browser/runtime/invoke-js.ts | 14 ++++++------
 src/mono/browser/runtime/startup.ts   | 31 +++++++++++++++++++++------
 2 files changed, 31 insertions(+), 14 deletions(-)

diff --git a/src/mono/browser/runtime/invoke-js.ts b/src/mono/browser/runtime/invoke-js.ts
index 8641b4db82285..b2a352cf5e94e 100644
--- a/src/mono/browser/runtime/invoke-js.ts
+++ b/src/mono/browser/runtime/invoke-js.ts
@@ -133,7 +133,7 @@ function bind_js_import(signature: JSFunctionSignature): Function {
     }
 
     function async_bound_fn(args: JSMarshalerArguments): void {
-            forceThreadMemoryViewRefresh();
+        forceThreadMemoryViewRefresh();
         bound_fn(args);
     }
 
@@ -152,12 +152,12 @@ function bind_js_import(signature: JSFunctionSignature): Function {
 
     let wrapped_fn: WrappedJSFunction = bound_fn;
     if (WasmEnableThreads) {
-    if (is_async || is_discard_no_wait) {
-        wrapped_fn = async_bound_fn;
-    }
-    else {
-        wrapped_fn = sync_bound_fn;
-    }
+        if (is_async || is_discard_no_wait) {
+            wrapped_fn = async_bound_fn;
+        }
+        else {
+            wrapped_fn = sync_bound_fn;
+        }
     }
 
     // this is just to make debugging easier by naming the function in the stack trace.
diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts
index 3dd0c1b8acf82..4cef37a78a16b 100644
--- a/src/mono/browser/runtime/startup.ts
+++ b/src/mono/browser/runtime/startup.ts
@@ -32,7 +32,6 @@ import { assertNoProxies } from "./gc-handles";
 import { runtimeList } from "./exports";
 import { nativeAbort, nativeExit } from "./run";
 import { mono_wasm_init_diagnostics } from "./diagnostics";
-import { replaceEmscriptenPThreadInit } from "./pthreads/worker-thread";
 
 export async function configureRuntimeStartup(): Promise<void> {
     await init_polyfills_async();
@@ -269,10 +268,25 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) {
 
         Module.runtimeKeepalivePush();
 
-        // load runtime and apply environment settings (if necessary)
-        await start_runtime();
-        if (!ENVIRONMENT_IS_WORKER) {
-            Module.runtimeKeepalivePush();
+        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;
+
+            // 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) {
@@ -515,8 +529,11 @@ 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.isCurrentThread = true;
+            runtimeHelpers.proxyGCHandle = install_main_synchronization_context(
+                runtimeHelpers.config.jsThreadBlockingMode!,
+                runtimeHelpers.config.jsThreadInteropMode!,
+                runtimeHelpers.config.mainThreadingMode!);
+            runtimeHelpers.isManagedRunningOnCurrentThread = true;
 
             // start finalizer thread, lazy
             init_finalizer_thread();

From 669a7cc3c8cb151f8aa32bc3341ab3b0da8e1ba1 Mon Sep 17 00:00:00 2001
From: pavelsavara <pavel.savara@gmail.com>
Date: Wed, 21 Feb 2024 17:54:46 +0100
Subject: [PATCH 3/8] fix bad merge

---
 src/mono/browser/runtime/managed-exports.ts |  4 ++--
 src/mono/browser/runtime/pthreads/shared.ts | 16 ++++++++--------
 src/mono/browser/runtime/startup.ts         |  2 ++
 3 files changed, 12 insertions(+), 10 deletions(-)

diff --git a/src/mono/browser/runtime/managed-exports.ts b/src/mono/browser/runtime/managed-exports.ts
index 60d4f6207d062..88081b4e4319a 100644
--- a/src/mono/browser/runtime/managed-exports.ts
+++ b/src/mono/browser/runtime/managed-exports.ts
@@ -123,8 +123,8 @@ export function release_js_owned_object_by_gc_handle(gc_handle: GCHandle) {
         set_arg_type(arg1, MarshalerType.Object);
         set_gc_handle(arg1, gc_handle);
         if (is_gcv_handle(gc_handle)) {
-        // this must stay synchronous for free_gcv_handle sake
-        invoke_sync_jsexport(managedExports.ReleaseJSOwnedObjectByGCHandle, args);
+            // this must stay synchronous for free_gcv_handle sake
+            invoke_sync_jsexport(managedExports.ReleaseJSOwnedObjectByGCHandle, args);
         } else {
             invoke_async_jsexport(managedExports.ReleaseJSOwnedObjectByGCHandle, args, size);
         }
diff --git a/src/mono/browser/runtime/pthreads/shared.ts b/src/mono/browser/runtime/pthreads/shared.ts
index 53a57df5b44ea..5eb76b7f8fc37 100644
--- a/src/mono/browser/runtime/pthreads/shared.ts
+++ b/src/mono/browser/runtime/pthreads/shared.ts
@@ -71,14 +71,14 @@ export function update_thread_info(): void {
     const threadType = !monoThreadInfo.isRegistered ? "emsc"
         : monoThreadInfo.isUI ? "-UI-"
             : 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";
+                : 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}`;
diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts
index 4cef37a78a16b..41ce07e37d3c4 100644
--- a/src/mono/browser/runtime/startup.ts
+++ b/src/mono/browser/runtime/startup.ts
@@ -32,6 +32,7 @@ import { assertNoProxies } from "./gc-handles";
 import { runtimeList } from "./exports";
 import { nativeAbort, nativeExit } from "./run";
 import { mono_wasm_init_diagnostics } from "./diagnostics";
+import { replaceEmscriptenPThreadInit } from "./pthreads/worker-thread";
 
 export async function configureRuntimeStartup(): Promise<void> {
     await init_polyfills_async();
@@ -125,6 +126,7 @@ async function instantiateWasmWorker(
     await loaderHelpers.afterConfigLoaded.promise;
 
     replace_linker_placeholders(imports);
+    replaceEmscriptenPThreadInit();
 
     // Instantiate from the module posted from the main thread.
     // We can just use sync instantiation in the worker.

From 73e9c6aa0ab1d05c5e8f28c13352fc64b7600bc6 Mon Sep 17 00:00:00 2001
From: pavelsavara <pavel.savara@gmail.com>
Date: Wed, 21 Feb 2024 18:21:33 +0100
Subject: [PATCH 4/8] fix ST

---
 .../InteropServices/JavaScript/JSFunctionBinding.cs        | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

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 9000f2ab69324..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
@@ -462,14 +462,15 @@ internal static unsafe JSFunctionBinding BindJSImportImpl(string functionName, s
 #endif
         internal static unsafe void ResolveOrRejectPromise(JSProxyContext targetContext, Span<JSMarshalerArgument> 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)
                 {
-                    ref JSMarshalerArgument exc = ref arguments[0];
-                    exc.slot.CallerNativeTID = targetContext.NativeTID;
                     Interop.Runtime.ResolveOrRejectPromise((nint)ptr);
                     if (exc.slot.Type != MarshalerType.None)
                     {
@@ -481,9 +482,7 @@ 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;
-                exc.slot.CallerNativeTID = targetContext.NativeTID;
 
                 // this copy is freed in mono_wasm_resolve_or_reject_promise
                 var bytes = sizeof(JSMarshalerArgument) * arguments.Length;

From eae0342818d80a7e7c87b3dfa54812d5222adaeb Mon Sep 17 00:00:00 2001
From: pavelsavara <pavel.savara@gmail.com>
Date: Wed, 21 Feb 2024 20:45:13 +0100
Subject: [PATCH 5/8] conditional tests

Active Issue https://github.com/dotnet/runtime/issues/98771
---
 .../runtime-extra-platforms-wasm.yml          | 23 +++++------
 .../Common/tests/System/TimeProviderTests.cs  |  2 +-
 .../TestUtilities/System/PlatformDetection.cs |  1 +
 .../ServiceLookup/CallSiteFactoryTest.cs      |  2 +-
 .../DI.Tests/ServiceProviderContainerTests.cs |  4 +-
 .../tests/HostFactoryResolverTests.cs         | 38 +++++++++----------
 .../ArrayPool/UnitTests.cs                    |  2 +-
 7 files changed, 37 insertions(+), 35 deletions(-)

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/src/libraries/Common/tests/System/TimeProviderTests.cs b/src/libraries/Common/tests/System/TimeProviderTests.cs
index 428c5b13fecc5..cc46dabad9a96 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.IsNotBrowserDeputyThread))]
         [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 e1e451b2d453f..4fd79f1bdeedc 100644
--- a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs
+++ b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs
@@ -137,6 +137,7 @@ public static int SlowRuntimeTimeoutModifier
         public static bool IsNotWasmThreadingSupported => !IsWasmThreadingSupported;
         public static bool IsWasmBackgroundExec => IsBrowser && IsEnvironmentVariableTrue("IsWasmBackgroundExec");
         public static bool IsWasmBackgroundExecOrSingleThread => IsWasmBackgroundExec || IsNotWasmThreadingSupported;
+        public static bool IsNotBrowserDeputyThread => IsWasmBackgroundExec || IsNotWasmThreadingSupported || !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..2b663acb42e73 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<FakeService>), callSite.Cache.Key.ServiceIdentifier.ServiceType);
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
         public void CallSitesAreUniquePerServiceTypeAndSlot()
         {
             // 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..e59409745b8db 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.IsNotBrowserDeputyThread))]
         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.IsNotBrowserDeputyThread))]
         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..8e09a689cb107 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<IServiceProvider>(factory(Array.Empty<string>()));
         }
 
-        [Fact]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
         [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.IsNotBrowserDeputyThread))]
         [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.IsNotBrowserDeputyThread))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CreateWebHostBuilderPatternTestSite.Program))]
         public void CreateWebHostBuilderPattern_CanFindWebHostBuilder()
         {
@@ -65,7 +65,7 @@ public void CreateWebHostBuilderPattern_CanFindWebHostBuilder()
             Assert.IsAssignableFrom<IWebHostBuilder>(factory(Array.Empty<string>()));
         }
 
-        [Fact]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CreateWebHostBuilderPatternTestSite.Program))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(IWebHost))]
         public void CreateWebHostBuilderPattern_CanFindServiceProvider()
@@ -85,7 +85,7 @@ public void CreateWebHostBuilderPattern__Invalid_CantFindWebHostBuilder()
             Assert.Null(factory);
         }
 
-        [Fact]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
         [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<string>()));
         }
 
-        [Fact]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CreateHostBuilderPatternTestSite.Program))]
         public void CreateHostBuilderPattern_CanFindHostBuilder()
         {
@@ -105,7 +105,7 @@ public void CreateHostBuilderPattern_CanFindHostBuilder()
             Assert.IsAssignableFrom<IHostBuilder>(factory(Array.Empty<string>()));
         }
 
-        [Fact]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
         [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<IServiceProvider>(factory(Array.Empty<string>()));
         }
 
-        [Fact]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CreateHostBuilderInvalidSignature.Program))]
         public void CreateHostBuilderPattern__Invalid_CantFindHostBuilder()
         {
@@ -135,7 +135,7 @@ public void CreateHostBuilderPattern__Invalid_CantFindServiceProvider()
             Assert.Throws<InvalidOperationException>(() => factory(Array.Empty<string>()));
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPattern.Program))]
         public void NoSpecialEntryPointPattern()
         {
@@ -145,7 +145,7 @@ public void NoSpecialEntryPointPattern()
             Assert.IsAssignableFrom<IServiceProvider>(factory(Array.Empty<string>()));
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
         [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.IsNotBrowserDeputyThread))]
         [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.IsNotBrowserDeputyThread))]
         [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.IsNotBrowserDeputyThread))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternThrows.Program))]
         public void NoSpecialEntryPointPatternThrows()
         {
@@ -213,7 +213,7 @@ public void NoSpecialEntryPointPatternThrows()
             Assert.Throws<Exception>(() => factory(Array.Empty<string>()));
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternExits.Program))]
         public void NoSpecialEntryPointPatternExits()
         {
@@ -223,7 +223,7 @@ public void NoSpecialEntryPointPatternExits()
             Assert.Throws<InvalidOperationException>(() => factory(Array.Empty<string>()));
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternHangs.Program))]
         public void NoSpecialEntryPointPatternHangs()
         {
@@ -233,7 +233,7 @@ public void NoSpecialEntryPointPatternHangs()
             Assert.Throws<InvalidOperationException>(() => factory(Array.Empty<string>()));
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternMainNoArgs.Program))]
         public void NoSpecialEntryPointPatternMainNoArgs()
         {
@@ -243,7 +243,7 @@ public void NoSpecialEntryPointPatternMainNoArgs()
             Assert.IsAssignableFrom<IServiceProvider>(factory(Array.Empty<string>()));
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, "Program", "TopLevelStatements")]
         public void TopLevelStatements()
         {
@@ -254,7 +254,7 @@ public void TopLevelStatements()
             Assert.IsAssignableFrom<IServiceProvider>(factory(Array.Empty<string>()));
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, "Program", "TopLevelStatementsTestsTimeout")]
         public void TopLevelStatementsTestsTimeout()
         {
@@ -265,7 +265,7 @@ public void TopLevelStatementsTestsTimeout()
             Assert.Throws<InvalidOperationException>(() => factory(Array.Empty<string>()));
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, "Program", "ApplicationNameSetFromArgument")]
         public void ApplicationNameSetFromArgument()
         {
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..bf377598bcb94 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.IsNotBrowserDeputyThread))]
         [MemberData(nameof(BytePoolInstances))]
         public static void CanRentManySizedBuffers(ArrayPool<byte> pool)
         {

From 0e566e27f6e9c71ae5b02cd12a5efc1631893734 Mon Sep 17 00:00:00 2001
From: pavelsavara <pavel.savara@gmail.com>
Date: Wed, 21 Feb 2024 21:09:26 +0100
Subject: [PATCH 6/8] more

---
 .../tests/DI.Tests/ServiceLookup/CallSiteFactoryTest.cs       | 2 +-
 .../tests/HostFactoryResolverTests.cs                         | 4 ++--
 .../HttpClientFactoryServiceCollectionExtensionsTest.cs       | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

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 2b663acb42e73..744160912b4f4 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
@@ -828,7 +828,7 @@ public void CallSitesAreUniquePerServiceTypeAndSlot()
             }
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
         public void CallSitesAreUniquePerServiceTypeAndSlotWithOpenGenericInGraph()
         {
             // Connected graph
diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/HostFactoryResolverTests.cs b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/HostFactoryResolverTests.cs
index 8e09a689cb107..b4ddd3417ee50 100644
--- a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/HostFactoryResolverTests.cs
+++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/HostFactoryResolverTests.cs
@@ -125,7 +125,7 @@ public void CreateHostBuilderPattern__Invalid_CantFindHostBuilder()
             Assert.Null(factory);
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CreateHostBuilderInvalidSignature.Program))]
         public void CreateHostBuilderPattern__Invalid_CantFindServiceProvider()
         {
@@ -277,7 +277,7 @@ public void ApplicationNameSetFromArgument()
             Assert.Contains("ApplicationNameSetFromArgument", configuration["applicationName"]);
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
         [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..4ea8fa0a97d0c 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.IsNotBrowserDeputyThread), nameof(PlatformDetection.IsReflectionEmitSupported))]
         public void AddHttpClient_GetAwaiterAndResult_InSingleThreadedSynchronizationContext_ShouldNotHangs()
         {
             // Arrange

From ae51573246b8c27340cae4fc6b0e2b4f7e04a335 Mon Sep 17 00:00:00 2001
From: pavelsavara <pavel.savara@gmail.com>
Date: Wed, 21 Feb 2024 22:17:55 +0100
Subject: [PATCH 7/8] fix

---
 .../Common/tests/System/TimeProviderTests.cs  |  2 +-
 .../TestUtilities/System/PlatformDetection.cs |  2 +-
 .../ServiceLookup/CallSiteFactoryTest.cs      |  4 +-
 .../DI.Tests/ServiceProviderContainerTests.cs |  4 +-
 .../tests/HostFactoryResolverTests.cs         | 44 +++++++++----------
 ...tFactoryServiceCollectionExtensionsTest.cs |  2 +-
 .../ArrayPool/UnitTests.cs                    |  2 +-
 7 files changed, 30 insertions(+), 30 deletions(-)

diff --git a/src/libraries/Common/tests/System/TimeProviderTests.cs b/src/libraries/Common/tests/System/TimeProviderTests.cs
index cc46dabad9a96..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.IsNotBrowserDeputyThread))]
+        [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 4fd79f1bdeedc..510b4b8e32180 100644
--- a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs
+++ b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs
@@ -137,7 +137,7 @@ public static int SlowRuntimeTimeoutModifier
         public static bool IsNotWasmThreadingSupported => !IsWasmThreadingSupported;
         public static bool IsWasmBackgroundExec => IsBrowser && IsEnvironmentVariableTrue("IsWasmBackgroundExec");
         public static bool IsWasmBackgroundExecOrSingleThread => IsWasmBackgroundExec || IsNotWasmThreadingSupported;
-        public static bool IsNotBrowserDeputyThread => IsWasmBackgroundExec || IsNotWasmThreadingSupported || !IsBrowser;
+        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 744160912b4f4..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<FakeService>), callSite.Cache.Key.ServiceIdentifier.ServiceType);
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))]
         public void CallSitesAreUniquePerServiceTypeAndSlot()
         {
             // Connected graph
@@ -828,7 +828,7 @@ public void CallSitesAreUniquePerServiceTypeAndSlot()
             }
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
+        [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 e59409745b8db..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.IsNotBrowserDeputyThread))]
+        [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.IsNotBrowserDeputyThread))]
+        [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 b4ddd3417ee50..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<IServiceProvider>(factory(Array.Empty<string>()));
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
+        [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);
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
+        [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);
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
+        [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<IWebHostBuilder>(factory(Array.Empty<string>()));
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
+        [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<IServiceProvider>(factory(Array.Empty<string>()));
         }
 
-        [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);
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
+        [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<string>()));
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
+        [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<IHostBuilder>(factory(Array.Empty<string>()));
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
+        [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<IServiceProvider>(factory(Array.Empty<string>()));
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
+        [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.IsNotBrowserDeputyThread))]
+        [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<InvalidOperationException>(() => factory(Array.Empty<string>()));
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPattern.Program))]
         public void NoSpecialEntryPointPattern()
         {
@@ -145,7 +145,7 @@ public void NoSpecialEntryPointPattern()
             Assert.IsAssignableFrom<IServiceProvider>(factory(Array.Empty<string>()));
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
+        [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.IsNotBrowserDeputyThread))]
+        [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.IsNotBrowserDeputyThread))]
+        [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.IsNotBrowserDeputyThread))]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternThrows.Program))]
         public void NoSpecialEntryPointPatternThrows()
         {
@@ -213,7 +213,7 @@ public void NoSpecialEntryPointPatternThrows()
             Assert.Throws<Exception>(() => factory(Array.Empty<string>()));
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternExits.Program))]
         public void NoSpecialEntryPointPatternExits()
         {
@@ -223,7 +223,7 @@ public void NoSpecialEntryPointPatternExits()
             Assert.Throws<InvalidOperationException>(() => factory(Array.Empty<string>()));
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternHangs.Program))]
         public void NoSpecialEntryPointPatternHangs()
         {
@@ -233,7 +233,7 @@ public void NoSpecialEntryPointPatternHangs()
             Assert.Throws<InvalidOperationException>(() => factory(Array.Empty<string>()));
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternMainNoArgs.Program))]
         public void NoSpecialEntryPointPatternMainNoArgs()
         {
@@ -243,7 +243,7 @@ public void NoSpecialEntryPointPatternMainNoArgs()
             Assert.IsAssignableFrom<IServiceProvider>(factory(Array.Empty<string>()));
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, "Program", "TopLevelStatements")]
         public void TopLevelStatements()
         {
@@ -254,7 +254,7 @@ public void TopLevelStatements()
             Assert.IsAssignableFrom<IServiceProvider>(factory(Array.Empty<string>()));
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, "Program", "TopLevelStatementsTestsTimeout")]
         public void TopLevelStatementsTestsTimeout()
         {
@@ -265,7 +265,7 @@ public void TopLevelStatementsTestsTimeout()
             Assert.Throws<InvalidOperationException>(() => factory(Array.Empty<string>()));
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
+        [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.IsNotBrowserDeputyThread))]
+        [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 4ea8fa0a97d0c..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.IsNotBrowserDeputyThread), 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/tests/System.Buffers.Tests/ArrayPool/UnitTests.cs b/src/libraries/System.Runtime/tests/System.Buffers.Tests/ArrayPool/UnitTests.cs
index bf377598bcb94..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());
         }
 
-        [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowserDeputyThread))]
+        [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupportedOrBrowserBackgroundExec))]
         [MemberData(nameof(BytePoolInstances))]
         public static void CanRentManySizedBuffers(ArrayPool<byte> pool)
         {

From 515d769910a9d28cde90a4ce436dc85b63bceae1 Mon Sep 17 00:00:00 2001
From: pavelsavara <pavel.savara@gmail.com>
Date: Thu, 22 Feb 2024 07:23:26 +0100
Subject: [PATCH 8/8] put pack _XUnitBackgroundExec

---
 eng/testing/tests.browser.targets | 1 +
 1 file changed, 1 insertion(+)

diff --git a/eng/testing/tests.browser.targets b/eng/testing/tests.browser.targets
index bce044984e937..06f07c898eabc 100644
--- a/eng/testing/tests.browser.targets
+++ b/eng/testing/tests.browser.targets
@@ -87,6 +87,7 @@
     <_AppArgs Condition="'$(IsFunctionalTest)' != 'true' and '$(WasmMainAssemblyFileName)' != ''">--run $(WasmMainAssemblyFileName)</_AppArgs>
     <_AppArgs Condition="'$(IsFunctionalTest)' == 'true'">--run $(AssemblyName).dll</_AppArgs>
 
+    <_XUnitBackgroundExec Condition="'$(_XUnitBackgroundExec)' == '' and '$(WasmEnableThreads)' == 'true'">true</_XUnitBackgroundExec>
     <WasmTestAppArgs Condition="'$(_XUnitBackgroundExec)' == 'true'">$(WasmTestAppArgs) -backgroundExec</WasmTestAppArgs>
     <WasmXHarnessMonoArgs Condition="'$(_XUnitBackgroundExec)' == 'true'">$(WasmXHarnessMonoArgs) --setenv=IsWasmBackgroundExec=true</WasmXHarnessMonoArgs>
     <_AppArgs Condition="'$(WasmTestAppArgs)' != ''">$(_AppArgs) $(WasmTestAppArgs)</_AppArgs>