Skip to content

Commit 738be39

Browse files
committed
- _XUnitBackgroundExec not default
- new JSThreadBlockingMode.AllowBlockingWaitInAsyncCode, make it default - free GCHandles during JSProxyContext dispose - SetSynchronizationContext for JSWebWorker - ToManaged(Task) creates Task with TaskCreationOptions.RunContinuationsAsynchronously - delete System.Runtime.InteropServices.JavaScript.BackgroundExec.Tests - mono_wasm_create_io_thread, mono_wasm_register_io_thread, mono_wasm_start_io_thread_async - don't call Managed during forceDisposeProxies for GCHandles via force_dispose_proxies_in_progress - dispatch Promise/Task resolution to I/O Thread - dispatch release_js_owned_object_by_gc_handle to I/O thread from UI thread - fix tests
1 parent 309009e commit 738be39

File tree

30 files changed

+291
-124
lines changed

30 files changed

+291
-124
lines changed

eng/testing/tests.browser.targets

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@
8787
<_AppArgs Condition="'$(IsFunctionalTest)' != 'true' and '$(WasmMainAssemblyFileName)' != ''">--run $(WasmMainAssemblyFileName)</_AppArgs>
8888
<_AppArgs Condition="'$(IsFunctionalTest)' == 'true'">--run $(AssemblyName).dll</_AppArgs>
8989

90-
<_XUnitBackgroundExec Condition="'$(_XUnitBackgroundExec)' == '' and '$(WasmEnableThreads)' == 'true'">true</_XUnitBackgroundExec>
9190
<WasmTestAppArgs Condition="'$(_XUnitBackgroundExec)' == 'true'">$(WasmTestAppArgs) -backgroundExec</WasmTestAppArgs>
9291
<WasmXHarnessMonoArgs Condition="'$(_XUnitBackgroundExec)' == 'true'">$(WasmXHarnessMonoArgs) --setenv=IsWasmBackgroundExec=true</WasmXHarnessMonoArgs>
9392
<_AppArgs Condition="'$(WasmTestAppArgs)' != ''">$(_AppArgs) $(WasmTestAppArgs)</_AppArgs>

src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ public static void ReleaseJSOwnedObjectByGCHandle(JSMarshalerArgument* arguments
104104

105105
try
106106
{
107-
// when we arrive here, we are on the thread which owns the proxies
108-
var ctx = arg_exc.AssertCurrentThreadContext();
107+
// when we arrive here, we are on the thread which owns the proxies or on IO thread
108+
var ctx = arg_exc.ToManagedContext;
109109
ctx.ReleaseJSOwnedObjectByGCHandle(arg_1.slot.GCHandle);
110110
}
111111
catch (Exception ex)
@@ -131,6 +131,11 @@ public static void CallDelegate(JSMarshalerArgument* arguments_buffer)
131131
// we may need to consider how to solve blocking of the synchronous call
132132
// see also https://github.com/dotnet/runtime/issues/76958#issuecomment-1921418290
133133
arg_exc.AssertCurrentThreadContext();
134+
135+
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.AllowBlockingWaitInAsyncCode)
136+
{
137+
Thread.ThrowOnBlockingWaitOnJSInteropThread = true;
138+
}
134139
#endif
135140

136141
GCHandle callback_gc_handle = (GCHandle)arg_1.slot.GCHandle;
@@ -148,6 +153,15 @@ public static void CallDelegate(JSMarshalerArgument* arguments_buffer)
148153
{
149154
arg_exc.ToJS(ex);
150155
}
156+
#if FEATURE_WASM_MANAGED_THREADS
157+
finally
158+
{
159+
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.AllowBlockingWaitInAsyncCode)
160+
{
161+
Thread.ThrowOnBlockingWaitOnJSInteropThread = false;
162+
}
163+
}
164+
#endif
151165
}
152166

153167
// the marshaled signature is: void CompleteTask<T>(GCHandle holder, Exception? exceptionResult, T? result)
@@ -161,8 +175,8 @@ public static void CompleteTask(JSMarshalerArgument* arguments_buffer)
161175

162176
try
163177
{
164-
// when we arrive here, we are on the thread which owns the proxies
165-
var ctx = arg_exc.AssertCurrentThreadContext();
178+
// when we arrive here, we are on the thread which owns the proxies or on IO thread
179+
var ctx = arg_exc.ToManagedContext;
166180
var holder = ctx.GetPromiseHolder(arg_1.slot.GCHandle);
167181
JSHostImplementation.ToManagedCallback callback;
168182

@@ -270,6 +284,10 @@ public static void BeforeSyncJSExport(JSMarshalerArgument* arguments_buffer)
270284
{
271285
var ctx = arg_exc.AssertCurrentThreadContext();
272286
ctx.IsPendingSynchronousCall = true;
287+
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.AllowBlockingWaitInAsyncCode)
288+
{
289+
Thread.ThrowOnBlockingWaitOnJSInteropThread = true;
290+
}
273291
}
274292
catch (Exception ex)
275293
{
@@ -288,6 +306,10 @@ public static void AfterSyncJSExport(JSMarshalerArgument* arguments_buffer)
288306
{
289307
var ctx = arg_exc.AssertCurrentThreadContext();
290308
ctx.IsPendingSynchronousCall = false;
309+
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.AllowBlockingWaitInAsyncCode)
310+
{
311+
Thread.ThrowOnBlockingWaitOnJSInteropThread = false;
312+
}
291313
}
292314
catch (Exception ex)
293315
{

src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.Types.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ public enum MainThreadingMode : int
7272
UIThread = 0,
7373
// Running the managed main thread on dedicated WebWorker. Marshaling all JavaScript calls to and from the main thread.
7474
DeputyThread = 1,
75+
// TODO comments
76+
DeputyAndIOThreads = 2,
7577
}
7678

7779
// keep in sync with types\internal.ts
@@ -80,6 +82,8 @@ public enum JSThreadBlockingMode : int
8082
// throw PlatformNotSupportedException if blocking .Wait is called on threads with JS interop, like JSWebWorker and Main thread.
8183
// Avoids deadlocks (typically with pending JS promises on the same thread) by throwing exceptions.
8284
NoBlockingWait = 0,
85+
// TODO comments
86+
AllowBlockingWaitInAsyncCode = 1,
8387
// allow .Wait on all threads.
8488
// Could cause deadlocks with blocking .Wait on a pending JS Task/Promise on the same thread or similar Task/Promise chain.
8589
AllowBlockingWait = 100,

src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ private JSProxyContext()
4343
public JSAsyncTaskScheduler? AsyncTaskScheduler;
4444

4545
public static MainThreadingMode MainThreadingMode = MainThreadingMode.DeputyThread;
46-
public static JSThreadBlockingMode ThreadBlockingMode = JSThreadBlockingMode.NoBlockingWait;
46+
public static JSThreadBlockingMode ThreadBlockingMode = JSThreadBlockingMode.AllowBlockingWaitInAsyncCode;
4747
public static JSThreadInteropMode ThreadInteropMode = JSThreadInteropMode.SimpleSynchronousJSInterop;
4848
public bool IsPendingSynchronousCall;
4949

@@ -517,7 +517,6 @@ private void Dispose(bool disposing)
517517
{
518518
Environment.FailFast($"JSProxyContext must be disposed on the thread which owns it, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}");
519519
}
520-
((GCHandle)ContextHandle).Free();
521520
#endif
522521

523522
List<WeakReference<JSObject>> copy = new(ThreadCsOwnedObjects.Values);
@@ -531,6 +530,7 @@ private void Dispose(bool disposing)
531530

532531
#if FEATURE_WASM_MANAGED_THREADS
533532
Interop.Runtime.UninstallWebWorkerInterop();
533+
((GCHandle)ContextHandle).Free();
534534
#endif
535535

536536
foreach (var gch in ThreadJsOwnedObjects.Values)
@@ -544,6 +544,7 @@ private void Dispose(bool disposing)
544544
{
545545
holder.Callback!.Invoke(null);
546546
}
547+
((GCHandle)holder.GCHandle).Free();
547548
}
548549

549550
ThreadCsOwnedObjects.Clear();

src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSSynchronizationContext.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,10 @@ private void Pump()
257257
}
258258
try
259259
{
260+
if (SynchronizationContext.Current == null)
261+
{
262+
SetSynchronizationContext(this);
263+
}
260264
while (Queue.Reader.TryRead(out var item))
261265
{
262266
try

src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Task.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,7 @@ public unsafe void ToManaged(out Task? value)
5050
lock (ctx)
5151
{
5252
PromiseHolder holder = ctx.GetPromiseHolder(slot.GCHandle);
53-
// we want to run the continuations on the original thread which called the JSImport, so RunContinuationsAsynchronously, rather than ExecuteSynchronously
54-
// TODO TaskCreationOptions.RunContinuationsAsynchronously
55-
TaskCompletionSource tcs = new TaskCompletionSource(holder);
53+
TaskCompletionSource tcs = new TaskCompletionSource(holder, TaskCreationOptions.RunContinuationsAsynchronously);
5654
ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) =>
5755
{
5856
if (arguments_buffer == null)
@@ -101,9 +99,7 @@ public unsafe void ToManaged<T>(out Task<T>? value, ArgumentToManagedCallback<T>
10199
lock (ctx)
102100
{
103101
var holder = ctx.GetPromiseHolder(slot.GCHandle);
104-
// we want to run the continuations on the original thread which called the JSImport, so RunContinuationsAsynchronously, rather than ExecuteSynchronously
105-
// TODO TaskCreationOptions.RunContinuationsAsynchronously
106-
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>(holder);
102+
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>(holder, TaskCreationOptions.RunContinuationsAsynchronously);
107103
ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) =>
108104
{
109105
if (arguments_buffer == null)

src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/BackgroundExec/System.Runtime.InteropServices.JavaScript.BackgroundExec.Tests.csproj

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JSExportTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public async Task JsExportInt32DiscardNoWait(int value)
3737
{
3838
JavaScriptTestHelper.optimizedReached=0;
3939
JavaScriptTestHelper.invoke1O(value);
40-
await JavaScriptTestHelper.Delay(0);
40+
await JavaScriptTestHelper.Delay(50);
4141
Assert.Equal(value, JavaScriptTestHelper.optimizedReached);
4242
}
4343

src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ await executor.Execute(async () =>
382382
}
383383

384384
[Theory, MemberData(nameof(GetTargetThreads))]
385-
public async Task JSDelay_ContinueWith(Executor executor)
385+
public async Task JSDelay_ContinueWith_Async(Executor executor)
386386
{
387387
using var cts = CreateTestCaseTimeoutSource();
388388
await executor.Execute(async () =>
@@ -391,9 +391,23 @@ await executor.Execute(async () =>
391391

392392
await WebWorkerTestHelper.JSDelay(10).ContinueWith(_ =>
393393
{
394-
// continue on the context of the target JS interop
395-
executor.AssertInteropThread();
396-
}, TaskContinuationOptions.ExecuteSynchronously);
394+
Assert.True(Thread.CurrentThread.IsThreadPoolThread);
395+
}, TaskContinuationOptions.RunContinuationsAsynchronously);
396+
}, cts.Token);
397+
}
398+
399+
[Theory, MemberData(nameof(GetTargetThreads))]
400+
public async Task JSDelay_ContinueWith_Sync(Executor executor)
401+
{
402+
using var cts = CreateTestCaseTimeoutSource();
403+
await executor.Execute(async () =>
404+
{
405+
await executor.StickyAwait(WebWorkerTestHelper.CreateDelay(), cts.Token);
406+
407+
await WebWorkerTestHelper.JSDelay(10).ContinueWith(_ =>
408+
{
409+
Assert.True(Thread.CurrentThread.IsThreadPoolThread);
410+
}, TaskContinuationOptions.ExecuteSynchronously); // ExecuteSynchronously is ignored
397411
}, cts.Token);
398412
}
399413

@@ -411,6 +425,21 @@ await executor.Execute(async () =>
411425
}, cts.Token);
412426
}
413427

428+
[Theory, MemberData(nameof(GetTargetThreads))]
429+
public async Task JSDelay_ConfigureAwait_False(Executor executor)
430+
{
431+
using var cts = CreateTestCaseTimeoutSource();
432+
await executor.Execute(async () =>
433+
{
434+
await executor.StickyAwait(WebWorkerTestHelper.CreateDelay(), cts.Token);
435+
436+
await WebWorkerTestHelper.JSDelay(10).ConfigureAwait(false);
437+
438+
// resolve/reject on I/O thread -> thread pool
439+
Assert.True(Thread.CurrentThread.IsThreadPoolThread);
440+
}, cts.Token);
441+
}
442+
414443
[Theory, MemberData(nameof(GetTargetThreads))]
415444
public async Task ManagedDelay_ContinueWith(Executor executor)
416445
{
@@ -442,21 +471,27 @@ await executor.Execute(async () =>
442471
public async Task WaitAssertsOnJSInteropThreads(Executor executor, NamedCall method)
443472
{
444473
using var cts = CreateTestCaseTimeoutSource();
445-
await executor.Execute(Task () =>
474+
await executor.Execute(async () =>
446475
{
476+
await executor.StickyAwait(WebWorkerTestHelper.InitializeAsync(), cts.Token);
477+
447478
Exception? exception = null;
448-
try
449-
{
450-
method.Call(cts.Token);
451-
}
452-
catch (Exception ex)
453-
{
454-
exception = ex;
455-
}
456-
Console.WriteLine("WaitAssertsOnJSInteropThreads: ExecuterType: " + executor.Type + " ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId);
457-
executor.AssertBlockingWait(exception);
479+
// the callback will hit Main or JSWebWorker, not the original executor thread
480+
await WebWorkerTestHelper.CallMeBackSync(() => {
481+
// when we are inside of synchronous callback, all blocking .Wait is forbidden
482+
try
483+
{
484+
method.Call(cts.Token);
485+
}
486+
catch (Exception ex)
487+
{
488+
exception = ex;
489+
}
490+
});
458491

459-
return Task.CompletedTask;
492+
Console.WriteLine("WaitAssertsOnJSInteropThreads: ExecuterType: " + executor.Type + " ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId);
493+
Assert.NotNull(exception);
494+
Assert.IsType<PlatformNotSupportedException>(exception);
460495
}, cts.Token);
461496
}
462497

src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.cs

Lines changed: 13 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public partial class WebWorkerTestHelper
1818
public static readonly string LocalWsEcho = "ws://" + Environment.GetEnvironmentVariable("DOTNET_TEST_WEBSOCKETHOST") + "/WebSocket/EchoWebSocket.ashx";
1919

2020
[JSImport("globalThis.console.log")]
21+
[return: JSMarshalAs<JSType.DiscardNoWait>]
2122
public static partial void Log(string message);
2223

2324
[JSImport("delay", "InlineTestHelper")]
@@ -38,6 +39,9 @@ public partial class WebWorkerTestHelper
3839
[JSImport("promiseValidateState", "WebWorkerTestHelper")]
3940
public static partial Task<bool> PromiseValidateState(JSObject state);
4041

42+
[JSImport("callMeBackSync", "WebWorkerTestHelper")]
43+
public static partial Task CallMeBackSync([JSMarshalAs<JSType.Function>] Action syncCallback);
44+
4145
public static string GetOriginUrl()
4246
{
4347
using var globalThis = JSHost.GlobalThis;
@@ -121,7 +125,6 @@ public enum ExecutorType
121125
public class Executor
122126
{
123127
public int ExecutorTID;
124-
public SynchronizationContext ExecutorSynchronizationContext;
125128
private static SynchronizationContext _mainSynchronizationContext;
126129
public static SynchronizationContext MainSynchronizationContext
127130
{
@@ -156,7 +159,6 @@ public Task Execute(Func<Task> job, CancellationToken cancellationToken)
156159
Task wrapExecute()
157160
{
158161
ExecutorTID = Environment.CurrentManagedThreadId;
159-
ExecutorSynchronizationContext = SynchronizationContext.Current ?? MainSynchronizationContext;
160162
AssertTargetThread();
161163
return job();
162164
}
@@ -194,6 +196,15 @@ public void AssertTargetThread()
194196
{
195197
Assert.False(Thread.CurrentThread.IsThreadPoolThread, "IsThreadPoolThread:" + Thread.CurrentThread.IsThreadPoolThread + " Type " + Type);
196198
}
199+
if (Type == ExecutorType.Main || Type == ExecutorType.JSWebWorker)
200+
{
201+
Assert.NotNull(SynchronizationContext.Current);
202+
Assert.Equal("System.Runtime.InteropServices.JavaScript.JSSynchronizationContext", SynchronizationContext.Current.GetType().FullName);
203+
}
204+
else
205+
{
206+
Assert.Null(SynchronizationContext.Current);
207+
}
197208
}
198209

199210
public void AssertAwaitCapturedContext()
@@ -230,51 +241,6 @@ public void AssertAwaitCapturedContext()
230241
}
231242
}
232243

233-
public void AssertBlockingWait(Exception? exception)
234-
{
235-
switch (Type)
236-
{
237-
case ExecutorType.Main:
238-
case ExecutorType.JSWebWorker:
239-
Assert.NotNull(exception);
240-
Assert.IsType<PlatformNotSupportedException>(exception);
241-
break;
242-
case ExecutorType.NewThread:
243-
case ExecutorType.ThreadPool:
244-
Assert.Null(exception);
245-
break;
246-
}
247-
}
248-
249-
public void AssertInteropThread()
250-
{
251-
switch (Type)
252-
{
253-
case ExecutorType.Main:
254-
Assert.Equal(1, Environment.CurrentManagedThreadId);
255-
Assert.Equal(ExecutorTID, Environment.CurrentManagedThreadId);
256-
Assert.False(Thread.CurrentThread.IsThreadPoolThread);
257-
break;
258-
case ExecutorType.JSWebWorker:
259-
Assert.NotEqual(1, Environment.CurrentManagedThreadId);
260-
Assert.Equal(ExecutorTID, Environment.CurrentManagedThreadId);
261-
Assert.False(Thread.CurrentThread.IsThreadPoolThread);
262-
break;
263-
case ExecutorType.NewThread:
264-
// it will synchronously continue on the UI thread
265-
Assert.Equal(1, Environment.CurrentManagedThreadId);
266-
Assert.NotEqual(ExecutorTID, Environment.CurrentManagedThreadId);
267-
Assert.False(Thread.CurrentThread.IsThreadPoolThread);
268-
break;
269-
case ExecutorType.ThreadPool:
270-
// it will synchronously continue on the UI thread
271-
Assert.Equal(1, Environment.CurrentManagedThreadId);
272-
Assert.NotEqual(ExecutorTID, Environment.CurrentManagedThreadId);
273-
Assert.False(Thread.CurrentThread.IsThreadPoolThread);
274-
break;
275-
}
276-
}
277-
278244
public override string ToString() => Type.ToString();
279245

280246
// make sure we stay on the executor

0 commit comments

Comments
 (0)