diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/CancelablePromise.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/CancelablePromise.cs index 795f3311cba97..aac9035997871 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/CancelablePromise.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/CancelablePromise.cs @@ -29,7 +29,7 @@ public static void CancelPromise(Task promise) } _CancelPromise(holder.GCHandle); #else - // this need to e manually dispatched via holder.ProxyContext, because we don't pass JSObject with affinity + // this need to be manually dispatched via holder.ProxyContext, because we don't pass JSObject with affinity holder.ProxyContext.SynchronizationContext.Post(static (object? h) => { var holder = (JSHostImplementation.PromiseHolder)h!; @@ -63,6 +63,7 @@ public static void CancelPromise(Task promise, Action callback, T state) _CancelPromise(holder.GCHandle); callback.Invoke(state); #else + // this need to be manually dispatched via holder.ProxyContext, because we don't pass JSObject with affinity holder.ProxyContext.SynchronizationContext.Post(_ => { lock (holder.ProxyContext) 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 d90a43cddf721..72cd8f87700e3 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 @@ -197,7 +197,9 @@ public static JSFunctionBinding BindManagedFunction(string fullyQualifiedName, i return BindManagedFunctionImpl(fullyQualifiedName, signatureHash, signatures); } +#if !DEBUG [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif internal static unsafe void InvokeJSFunction(JSObject jsFunction, Span arguments) { jsFunction.AssertNotDisposed(); @@ -218,7 +220,9 @@ internal static unsafe void InvokeJSFunction(JSObject jsFunction, Span arguments) { var functionHandle = (int)jsFunction.JSHandle; @@ -235,7 +239,9 @@ internal static unsafe void InvokeJSFunctionCurrent(JSObject jsFunction, Span arguments) { var args = (nint)Unsafe.AsPointer(ref arguments[0]); @@ -255,7 +261,9 @@ internal static unsafe void DispatchJSFunctionSync(JSObject jsFunction, Span arguments) { #if FEATURE_WASM_THREADS @@ -311,7 +319,9 @@ internal static unsafe void InvokeJSImportImpl(JSFunctionBinding signature, Span #endif } +#if !DEBUG [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif internal static unsafe void InvokeJSImportCurrent(JSFunctionBinding signature, Span arguments) { fixed (JSMarshalerArgument* args = arguments) @@ -332,7 +342,9 @@ internal static unsafe void InvokeJSImportCurrent(JSFunctionBinding signature, S #if FEATURE_WASM_THREADS +#if !DEBUG [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif internal static unsafe void DispatchJSImportSync(JSFunctionBinding signature, JSProxyContext targetContext, Span arguments) { var args = (nint)Unsafe.AsPointer(ref arguments[0]); @@ -351,7 +363,9 @@ internal static unsafe void DispatchJSImportSync(JSFunctionBinding signature, JS } } +#if !DEBUG [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif internal static unsafe void DispatchJSImportAsync(JSFunctionBinding signature, JSProxyContext targetContext, Span arguments) { // this copy is freed in mono_wasm_invoke_import_async @@ -418,7 +432,9 @@ internal static unsafe void ResolveOrRejectPromise(Span arg } } #else +#if !DEBUG [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif internal static unsafe void ResolveOrRejectPromise(JSProxyContext targetContext, Span arguments) { // this copy is freed in mono_wasm_invoke_import_async diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs index e4d8042c05032..4bf4aeb36353e 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs @@ -237,6 +237,8 @@ public static void UninstallWebWorkerInterop() { SynchronizationContext.SetSynchronizationContext(syncContext.previousSynchronizationContext); } + JSProxyContext.CurrentThreadContext = null; + JSProxyContext.ExecutionContext = null; ctx.Dispose(); } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSObject.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSObject.cs index d6c7b8ab21083..ec54c147da4f6 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSObject.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSObject.cs @@ -16,13 +16,7 @@ public partial class JSObject : IDisposable /// /// Returns true if the proxy was already disposed. /// - public bool IsDisposed - { - get - { - return _isDisposed; - } - } + public bool IsDisposed => _isDisposed; /// /// Checks whether the target object or one of its prototypes has a property with the specified name. 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 73937af88bf3d..d81f1cc1bcd56 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 @@ -134,9 +134,17 @@ public static JSProxyContext SealJSImportCapturing() var executionContext = ExecutionContext; if (executionContext != null) { - // we could will call JS on the current thread (or child task), if it has the JS interop installed + // we could will call JS on the task's AsyncLocal context, if it has the JS interop installed return executionContext; } + + var currentThreadContext = CurrentThreadContext; + if (currentThreadContext != null) + { + // we could will call JS on the current thread (or child task), if it has the JS interop installed + return currentThreadContext; + } + // otherwise we will call JS on the main thread, which always has JS interop return MainThreadContext; } @@ -150,13 +158,13 @@ public static void CaptureContextFromParameter(JSProxyContext parameterContext) Environment.FailFast($"Method only allowed during JSImport capturing phase, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); } - var capturedContext = _CapturedOperationContext; + var alreadyCapturedContext = _CapturedOperationContext; - if (capturedContext == null) + if (alreadyCapturedContext == null) { - _CapturedOperationContext = capturedContext; + _CapturedOperationContext = parameterContext; } - else if (parameterContext != capturedContext) + else if (parameterContext != alreadyCapturedContext) { _CapturedOperationContext = null; _CapturingState = JSImportOperationState.None; 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 d04fa731a49aa..ae1c8d4f1b98a 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 @@ -163,7 +163,7 @@ private void Pump() } catch (Exception e) { - Environment.FailFast("JSSynchronizationContext.BackgroundJobHandler failed", e); + Environment.FailFast($"JSSynchronizationContext.BackgroundJobHandler failed, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {e.StackTrace}"); } finally { diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSWebWorker.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSWebWorker.cs index 9cc19c7406b96..4e1108b4afcf9 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSWebWorker.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSWebWorker.cs @@ -51,7 +51,8 @@ public static async Task RunAsync(Func body, CancellationToken cancellatio private static Task RunAsyncImpl(Func> body, CancellationToken cancellationToken) { var parentContext = SynchronizationContext.Current ?? new SynchronizationContext(); - var tcs = new TaskCompletionSource(); + // continuation should not be running synchronously in the JSWebWorker thread because we are about to kill it after we resolve/reject the Task. + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var capturedContext = SynchronizationContext.Current; var t = new Thread(() => { @@ -88,7 +89,8 @@ private static Task RunAsyncImpl(Func> body, CancellationToken can private static Task RunAsyncImpl(Func body, CancellationToken cancellationToken) { var parentContext = SynchronizationContext.Current ?? new SynchronizationContext(); - var tcs = new TaskCompletionSource(); + // continuation should not be running synchronously in the JSWebWorker thread because we are about to kill it after we resolve/reject the Task. + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var capturedContext = SynchronizationContext.Current; var t = new Thread(() => { diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Exception.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Exception.cs index a64b045fd55e3..d26d4e58dcadd 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Exception.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Exception.cs @@ -67,11 +67,9 @@ public unsafe void ToJS(Exception? value) if (jse != null && jse.jsException != null) { var jsException = jse.jsException; -#if !FEATURE_WASM_THREADS jsException.AssertNotDisposed(); -#else +#if FEATURE_WASM_THREADS var ctx = jsException.ProxyContext; - jsException.AssertNotDisposed(); if (JSProxyContext.CapturingState == JSProxyContext.JSImportOperationState.JSImportParams) { diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.JSObject.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.JSObject.cs index bc8fb323775fe..7a1d8a2695e55 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.JSObject.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.JSObject.cs @@ -40,10 +40,8 @@ public void ToJS(JSObject? value) } else { -#if !FEATURE_WASM_THREADS - value.AssertNotDisposed(); -#else value.AssertNotDisposed(); +#if FEATURE_WASM_THREADS var ctx = value.ProxyContext; if (JSProxyContext.CapturingState == JSProxyContext.JSImportOperationState.JSImportParams) 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 3a65cc9f6ca47..b2c035e6a8ad4 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 @@ -24,7 +24,7 @@ namespace System.Runtime.InteropServices.JavaScript.Tests // thread allocation, many threads // TLS // ProxyContext flow, child thread, child task - // use JSObject after JSWebWorker finished + // use JSObject after JSWebWorker finished, especially HTTP // JSWebWorker JS setTimeout till after close // WS on JSWebWorker // Yield will hit event loop 3x @@ -32,6 +32,7 @@ namespace System.Runtime.InteropServices.JavaScript.Tests // WS continue on TP // event pipe // FS + // unit test for problem **7)** public class WebWorkerTest { @@ -42,7 +43,13 @@ public static IEnumerable GetTargetThreads() return Enum.GetValues().Select(type => new object[] { new Executor(type) }); } - public static IEnumerable GetTargetThreads2x2() + public static IEnumerable GetSpecificTargetThreads() + { + yield return new object[] { new Executor(ExecutorType.JSWebWorker), new Executor(ExecutorType.Main) }; + yield break; + } + + public static IEnumerable GetTargetThreads2x() { return Enum.GetValues().SelectMany( type1 => Enum.GetValues().Select( @@ -143,12 +150,13 @@ await executor.Execute(async () => } [Theory, MemberData(nameof(GetTargetThreads))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/96493")] public async Task ManagedDelay_ContinueWith(Executor executor) { await executor.Execute(async () => { executor.AssertTargetThread(); - await Task.Delay(1).ContinueWith(_ => + await Task.Delay(10).ContinueWith(_ => { // continue on the context of the Timer's thread pool thread Assert.True(Thread.CurrentThread.IsThreadPoolThread); @@ -185,5 +193,68 @@ await executor.Execute(async () => #endregion + #region affinity + + private async Task ActionsInDifferentThreads(Executor executor1, Executor executor2, Func, Task> e1Job, Func e2Job) + { + TaskCompletionSource readyTCS = new TaskCompletionSource(); + TaskCompletionSource doneTCS = new TaskCompletionSource(); + + var e1 = executor1.Execute(async () => + { + await e1Job(doneTCS.Task, readyTCS); + if (!readyTCS.Task.IsCompleted) + { + readyTCS.SetResult(default); + } + await doneTCS.Task; + }); + + var r1 = await readyTCS.Task.ConfigureAwait(true); + + var e2 = executor2.Execute(async () => + { + + executor2.AssertTargetThread(); + + await e2Job(r1); + + doneTCS.SetResult(); + }); + + await e2; + await e1; + } + + [Theory, MemberData(nameof(GetTargetThreads2x))] + public async Task JSObject_CapturesAffinity(Executor executor1, Executor executor2) + { + var e1Job = async (Task e2done, TaskCompletionSource e1State) => + { + await WebWorkerTestHelper.InitializeAsync(); + + executor1.AssertAwaitCapturedContext(); + + var jsState = await WebWorkerTestHelper.PromiseState(); + + // share the state with the E2 continuation + e1State.SetResult(jsState); + + await e2done; + + // cleanup + await WebWorkerTestHelper.DisposeAsync(); + }; + + var e2Job = async (JSObject e1State) => + { + bool valid = await WebWorkerTestHelper.PromiseValidateState(e1State); + Assert.True(valid); + }; + + await ActionsInDifferentThreads(executor1, executor2, e1Job, e2Job); + } + + #endregion } } 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 dfccfd6b609c1..63cf9fff69e7f 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 @@ -26,6 +26,18 @@ public partial class WebWorkerTestHelper [JSImport("getTid", "WebWorkerTestHelper")] public static partial int GetTid(); + [JSImport("getState", "WebWorkerTestHelper")] + public static partial JSObject GetState(); + + [JSImport("promiseState", "WebWorkerTestHelper")] + public static partial Task PromiseState(); + + [JSImport("validateState", "WebWorkerTestHelper")] + public static partial bool ValidateState(JSObject state); + + [JSImport("promiseValidateState", "WebWorkerTestHelper")] + public static partial Task PromiseValidateState(JSObject state); + public static string GetOriginUrl() { using var globalThis = JSHost.GlobalThis; @@ -42,9 +54,9 @@ public static Task ImportModuleFromString(string jsModule) #region Execute - public async static Task RunOnNewThread(Func job) + public static Task RunOnNewThread(Func job) { - TaskCompletionSource tcs = new TaskCompletionSource(); + TaskCompletionSource tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var t = new Thread(() => { try @@ -59,46 +71,6 @@ public async static Task RunOnNewThread(Func job) } }); t.Start(); - await tcs.Task; - t.Join(); - } - - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern")] - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern")] - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060:UnrecognizedReflectionPattern")] - public async static Task RunOnMainAsync(Func job) - { - if (MainSynchronizationContext == null) - { - var jsProxyContext = typeof(JSObject).Assembly.GetType("System.Runtime.InteropServices.JavaScript.JSProxyContext"); - var mainThreadContext = jsProxyContext.GetField("_MainThreadContext", BindingFlags.NonPublic | BindingFlags.Static); - var synchronizationContext = jsProxyContext.GetField("SynchronizationContext", BindingFlags.Public | BindingFlags.Instance); - var mainCtx = mainThreadContext.GetValue(null); - MainSynchronizationContext = (SynchronizationContext)synchronizationContext.GetValue(mainCtx); - } - await RunOnTargetAsync(MainSynchronizationContext, job); - } - - public static Task RunOnTargetAsync(SynchronizationContext ctx, Func job) - { - TaskCompletionSource tcs = new TaskCompletionSource(); - ctx.Post(async _ => - { - await InitializeAsync(); - try - { - await job().ConfigureAwait(true); - tcs.SetResult(); - } - catch (Exception ex) - { - tcs.SetException(ex); - } - finally - { - await DisposeAsync(); - } - }, null); return tcs.Task; } @@ -111,8 +83,6 @@ public static Task RunOnTargetAsync(SynchronizationContext ctx, Func job) [ThreadStatic] public static JSObject InlineTestHelperModule; - public static SynchronizationContext MainSynchronizationContext; - [JSImport("setup", "WebWorkerTestHelper")] internal static partial Task Setup(); @@ -162,7 +132,7 @@ public static Task DisposeAsync() #endregion } - #region + #region Executor public enum ExecutorType { @@ -171,9 +141,32 @@ public enum ExecutorType NewThread, JSWebWorker, } + public class Executor { public int ExecutorTID; + public SynchronizationContext ExecutorSynchronizationContext; + private static SynchronizationContext _mainSynchronizationContext; + public static SynchronizationContext MainSynchronizationContext + { + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060:UnrecognizedReflectionPattern")] + get + { + if (_mainSynchronizationContext != null) + { + return _mainSynchronizationContext; + } + var jsProxyContext = typeof(JSObject).Assembly.GetType("System.Runtime.InteropServices.JavaScript.JSProxyContext"); + var mainThreadContext = jsProxyContext.GetField("_MainThreadContext", BindingFlags.NonPublic | BindingFlags.Static); + var synchronizationContext = jsProxyContext.GetField("SynchronizationContext", BindingFlags.Public | BindingFlags.Instance); + var mainCtx = mainThreadContext.GetValue(null); + _mainSynchronizationContext = (SynchronizationContext)synchronizationContext.GetValue(mainCtx); + return _mainSynchronizationContext; + } + } public ExecutorType Type; @@ -187,13 +180,14 @@ public Task Execute(Func job) Task wrapExecute() { ExecutorTID = Environment.CurrentManagedThreadId; + ExecutorSynchronizationContext = SynchronizationContext.Current ?? MainSynchronizationContext; return job(); } switch (Type) { case ExecutorType.Main: - return WebWorkerTestHelper.RunOnMainAsync(wrapExecute); + return RunOnTargetAsync(MainSynchronizationContext, wrapExecute); case ExecutorType.ThreadPool: return Task.Run(wrapExecute); case ExecutorType.NewThread: @@ -217,11 +211,11 @@ public void AssertTargetThread() } if (Type == ExecutorType.ThreadPool) { - Assert.True(Thread.CurrentThread.IsThreadPoolThread); + Assert.True(Thread.CurrentThread.IsThreadPoolThread, "IsThreadPoolThread:" + Thread.CurrentThread.IsThreadPoolThread + " Type " + Type); } else { - Assert.False(Thread.CurrentThread.IsThreadPoolThread); + Assert.False(Thread.CurrentThread.IsThreadPoolThread, "IsThreadPoolThread:" + Thread.CurrentThread.IsThreadPoolThread + " Type " + Type); } } @@ -297,6 +291,24 @@ public async Task StickyAwait(Task task) } AssertTargetThread(); } + + public static Task RunOnTargetAsync(SynchronizationContext ctx, Func job) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + ctx.Post(async _ => + { + try + { + await job().ConfigureAwait(true); + tcs.SetResult(); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }, null); + return tcs.Task; + } } #endregion diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.mjs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.mjs index d7912c0480be5..ee4aa20094281 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.mjs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.mjs @@ -2,10 +2,45 @@ let dllExports; const runtime = getDotnetRuntime(0); +let jsState = {}; + export async function setup() { dllExports = await runtime.getAssemblyExports("System.Runtime.InteropServices.JavaScript.Tests.dll"); + jsState.id = getRndInteger(0, 1000); + jsState.tid = getTid(); +} + +export function getState() { + return jsState; +} + +export function validateState(state) { + const isvalid = state.tid === jsState.tid && state.id === jsState.id; + if (!isvalid) { + console.log("Expected: ", JSON.stringify(jsState)); + console.log("Actual: ", JSON.stringify(state)); + } + return isvalid; +} + +export async function promiseState() { + await delay(10); + return getState(); +} + +export async function promiseValidateState(state) { + await delay(10); + return validateState(state); } export function getTid() { return runtime.Module["_pthread_self"](); } + +export function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +export function getRndInteger(min, max) { + return Math.floor(Math.random() * (max - min)) + min; +} diff --git a/src/mono/browser/runtime/invoke-cs.ts b/src/mono/browser/runtime/invoke-cs.ts index 03caffeecea74..22282a31b2c43 100644 --- a/src/mono/browser/runtime/invoke-cs.ts +++ b/src/mono/browser/runtime/invoke-cs.ts @@ -2,8 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. import BuildConfiguration from "consts:configuration"; - import MonoWasmThreads from "consts:monoWasmThreads"; + import { Module, loaderHelpers, mono_assert, runtimeHelpers } from "./globals"; import { bind_arg_marshal_to_cs } from "./marshal-to-cs"; import { marshal_exception_to_js, bind_arg_marshal_to_js, end_marshal_task_to_js } from "./marshal-to-js"; diff --git a/src/mono/browser/runtime/invoke-js.ts b/src/mono/browser/runtime/invoke-js.ts index 9aa56b1e745a5..f36d2ee1aa02b 100644 --- a/src/mono/browser/runtime/invoke-js.ts +++ b/src/mono/browser/runtime/invoke-js.ts @@ -10,13 +10,14 @@ import { setI32_unchecked, receiveWorkerHeapViews } from "./memory"; import { stringToMonoStringRoot } from "./strings"; import { MonoObject, MonoObjectRef, JSFunctionSignature, JSMarshalerArguments, WasmRoot, BoundMarshalerToJs, JSFnHandle, BoundMarshalerToCs, JSHandle, MarshalerType } from "./types/internal"; import { Int32Ptr } from "./types/emscripten"; -import { INTERNAL, Module, loaderHelpers, mono_assert, runtimeHelpers } from "./globals"; +import { ENVIRONMENT_IS_WORKER, INTERNAL, Module, loaderHelpers, mono_assert, runtimeHelpers } from "./globals"; import { bind_arg_marshal_to_js } from "./marshal-to-js"; import { mono_wasm_new_external_root } from "./roots"; import { mono_log_debug, mono_wasm_symbolicate_string } from "./logging"; import { mono_wasm_get_jsobj_from_js_handle } from "./gc-handles"; import { endMeasure, MeasuredBlock, startMeasure } from "./profiler"; import { wrap_as_cancelable_promise } from "./cancelable-promise"; +import { is_thread_available } from "./pthreads/shared/emscripten-replacements"; export const js_import_wrapper_by_fn_handle: Function[] = [null];// 0th slot is dummy, main thread we free them on shutdown. On web worker thread we free them when worker is detached. @@ -48,10 +49,26 @@ export function mono_wasm_invoke_import_async(args: JSMarshalerArguments, signat } mono_assert(bound_fn, () => `Imported function handle expected ${function_handle}`); - bound_fn(args); + let max_postpone_count = 10; + function postpone_invoke_import_async() { + if (max_postpone_count < 0 || is_thread_available()) { + bound_fn(args); + Module._free(args as any); + } else { + max_postpone_count--; + Module.safeSetTimeout(postpone_invoke_import_async, 10); + } + } - // this works together with AllocHGlobal in JSFunctionBinding.DispatchJSImportAsync - Module._free(args as any); + if (MonoWasmThreads && !ENVIRONMENT_IS_WORKER) { + // give thread chance to load before we run more synchronous code on UI thread + postpone_invoke_import_async(); + } + else { + bound_fn(args); + // this works together with AllocHGlobal in JSFunctionBinding.DispatchJSImportAsync + Module._free(args as any); + } } export function mono_wasm_invoke_import_sync(args: JSMarshalerArguments, signature: JSFunctionSignature) { @@ -323,7 +340,11 @@ function mono_wasm_lookup_js_import(function_name: string, js_module_name: strin const parts = function_name.split("."); if (js_module_name) { scope = importedModules.get(js_module_name); - mono_assert(scope, () => `ES6 module ${js_module_name} was not imported yet, please call JSHost.ImportAsync() first.`); + if (MonoWasmThreads) { + mono_assert(scope, () => `ES6 module ${js_module_name} was not imported yet, please call JSHost.ImportAsync() on the UI or JSWebWorker thread first.`); + } else { + mono_assert(scope, () => `ES6 module ${js_module_name} was not imported yet, please call JSHost.ImportAsync() first.`); + } } else if (parts[0] === "INTERNAL") { scope = INTERNAL; diff --git a/src/mono/browser/runtime/loader/logging.ts b/src/mono/browser/runtime/loader/logging.ts index df6723397f87d..108c354e23a66 100644 --- a/src/mono/browser/runtime/loader/logging.ts +++ b/src/mono/browser/runtime/loader/logging.ts @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. /* eslint-disable no-console */ + +import MonoWasmThreads from "consts:monoWasmThreads"; + import { ENVIRONMENT_IS_WORKER, loaderHelpers } from "./globals"; const methods = ["debug", "log", "trace", "warn", "info", "error"]; @@ -59,12 +62,16 @@ function proxyConsoleMethod(prefix: string, func: any, asJson: boolean) { if (typeof payload === "string") { if (payload[0] == "[") { const now = new Date().toISOString(); - if (ENVIRONMENT_IS_WORKER) { + if (MonoWasmThreads && ENVIRONMENT_IS_WORKER) { payload = `[${threadNamePrefix}][${now}] ${payload}`; } else { payload = `[${now}] ${payload}`; } - } else if (ENVIRONMENT_IS_WORKER) { + } else if (MonoWasmThreads && ENVIRONMENT_IS_WORKER) { + if (payload.indexOf("keeping the worker alive for asynchronous operation") !== -1) { + // muting emscripten noise + return; + } payload = `[${threadNamePrefix}] ${payload}`; } } diff --git a/src/mono/browser/runtime/pthreads/shared/emscripten-replacements.ts b/src/mono/browser/runtime/pthreads/shared/emscripten-replacements.ts index dcbb767ebe2c4..30e150df22133 100644 --- a/src/mono/browser/runtime/pthreads/shared/emscripten-replacements.ts +++ b/src/mono/browser/runtime/pthreads/shared/emscripten-replacements.ts @@ -7,7 +7,7 @@ import { onWorkerLoadInitiated } from "../browser"; import { afterThreadInitTLS } from "../worker"; import { Internals, PThreadLibrary, PThreadWorker } from "./emscripten-internals"; import { loaderHelpers, mono_assert } from "../../globals"; -import { mono_log_debug } from "../../logging"; +import { mono_log_warn } from "../../logging"; /** @module emscripten-replacements Replacements for individual functions in the emscripten PThreads library. * These have a hard dependency on the version of Emscripten that we are using and may need to be kept in sync with @@ -23,6 +23,9 @@ export function replaceEmscriptenPThreadLibrary(modulePThread: PThreadLibrary): modulePThread.loadWasmModuleToWorker = (worker: Worker): Promise => { const afterLoaded = originalLoadWasmModuleToWorker(worker); + afterLoaded.then(() => { + availableThreadCount++; + }); onWorkerLoadInitiated(worker, afterLoaded); return afterLoaded; }; @@ -44,17 +47,25 @@ export function replaceEmscriptenPThreadLibrary(modulePThread: PThreadLibrary): } })); } else { + availableThreadCount++; originalReturnWorkerToPool(worker); } }; } +let availableThreadCount = 0; +export function is_thread_available() { + return availableThreadCount > 0; +} + function getNewWorker(modulePThread: PThreadLibrary): PThreadWorker { if (!MonoWasmThreads) return null as any; if (modulePThread.unusedWorkers.length == 0) { + mono_log_warn("Failed to find unused WebWorker, this may deadlock. Please increase the pthreadPoolSize."); const worker = allocateUnusedWorker(); modulePThread.loadWasmModuleToWorker(worker); + availableThreadCount--; return worker; } @@ -68,10 +79,12 @@ function getNewWorker(modulePThread: PThreadLibrary): PThreadWorker { const worker = modulePThread.unusedWorkers[i]; if (worker.loaded) { modulePThread.unusedWorkers.splice(i, 1); + availableThreadCount--; return worker; } } - mono_log_debug("Failed to find loaded WebWorker, this may deadlock. Please increase the pthreadPoolSize."); + mono_log_warn("Failed to find loaded WebWorker, this may deadlock. Please increase the pthreadPoolSize."); + availableThreadCount--; // negative value return modulePThread.unusedWorkers.pop()!; } diff --git a/src/mono/browser/runtime/scheduling.ts b/src/mono/browser/runtime/scheduling.ts index 080c7f2c732c0..5b598861e4938 100644 --- a/src/mono/browser/runtime/scheduling.ts +++ b/src/mono/browser/runtime/scheduling.ts @@ -4,7 +4,8 @@ import MonoWasmThreads from "consts:monoWasmThreads"; import cwraps from "./cwraps"; -import { Module, loaderHelpers } from "./globals"; +import { ENVIRONMENT_IS_WORKER, Module, loaderHelpers } from "./globals"; +import { is_thread_available } from "./pthreads/shared/emscripten-replacements"; let spread_timers_maximum = 0; let pump_count = 0; @@ -50,7 +51,23 @@ function mono_background_exec_until_done() { export function schedule_background_exec(): void { ++pump_count; - Module.safeSetTimeout(mono_background_exec_until_done, 0); + let max_postpone_count = 10; + function postpone_schedule_background() { + if (max_postpone_count < 0 || is_thread_available()) { + Module.safeSetTimeout(mono_background_exec_until_done, 0); + } else { + max_postpone_count--; + Module.safeSetTimeout(postpone_schedule_background, 10); + } + } + + if (MonoWasmThreads && !ENVIRONMENT_IS_WORKER) { + // give threads chance to load before we run more synchronous code on UI thread + postpone_schedule_background(); + } + else { + Module.safeSetTimeout(mono_background_exec_until_done, 0); + } } let lastScheduledTimeoutId: any = undefined;