Skip to content

Commit d65353c

Browse files
committed
wip
1 parent a2ce8db commit d65353c

File tree

26 files changed

+279
-286
lines changed

26 files changed

+279
-286
lines changed

src/libraries/System.Private.CoreLib/ref/System.Private.CoreLib.ExtraApis.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public partial class Thread
4545
{
4646
[ThreadStatic]
4747
public static bool ThrowOnBlockingWaitOnJSInteropThread;
48+
[ThreadStatic]
49+
public static bool WarnOnBlockingWaitOnJSInteropThread;
4850

4951
public static void AssureBlockingPossible() { throw null; }
5052
public static void ForceBlockingWait(Action<object?> action, object? state) { throw null; }

src/libraries/System.Private.CoreLib/ref/System.Private.CoreLib.ExtraApis.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ T:System.Diagnostics.DebugProvider
77
M:System.Diagnostics.Debug.SetProvider(System.Diagnostics.DebugProvider)
88
M:System.Threading.Thread.AssureBlockingPossible
99
F:System.Threading.Thread.ThrowOnBlockingWaitOnJSInteropThread
10+
F:System.Threading.Thread.WarnOnBlockingWaitOnJSInteropThread
1011
F:System.Threading.Thread.ForceBlockingWait

src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,10 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
485485

486486
ArgumentOutOfRangeException.ThrowIfLessThan(millisecondsTimeout, -1);
487487

488+
#if FEATURE_WASM_MANAGED_THREADS
489+
Thread.AssureBlockingPossible();
490+
#endif
491+
488492
if (!IsSet)
489493
{
490494
if (millisecondsTimeout == 0)

src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -729,26 +729,46 @@ public static int GetCurrentProcessorId()
729729
[ThreadStatic]
730730
public static bool ThrowOnBlockingWaitOnJSInteropThread;
731731

732-
public static void AssureBlockingPossible()
732+
[ThreadStatic]
733+
public static bool WarnOnBlockingWaitOnJSInteropThread;
734+
735+
#pragma warning disable CS3001
736+
[MethodImplAttribute(MethodImplOptions.InternalCall)]
737+
private static extern unsafe void WarnAboutBlockingWait(char* stack, int length);
738+
739+
public static unsafe void AssureBlockingPossible()
733740
{
734741
if (ThrowOnBlockingWaitOnJSInteropThread)
735742
{
736743
throw new PlatformNotSupportedException(SR.WasmThreads_BlockingWaitNotSupportedOnJSInterop);
737744
}
745+
else if (WarnOnBlockingWaitOnJSInteropThread)
746+
{
747+
var st = $"Blocking the thread with JS interop is dangerous and could lead to deadlock. ManagedThreadId: {Environment.CurrentManagedThreadId}\n{Environment.StackTrace}";
748+
fixed (char* stack = st)
749+
{
750+
WarnAboutBlockingWait(stack, st.Length);
751+
}
752+
}
738753
}
739754

755+
#pragma warning restore CS3001
756+
740757
public static void ForceBlockingWait(Action<object?> action, object? state = null)
741758
{
742759
var flag = ThrowOnBlockingWaitOnJSInteropThread;
760+
var wflag = WarnOnBlockingWaitOnJSInteropThread;
743761
try
744762
{
745763
ThrowOnBlockingWaitOnJSInteropThread = false;
764+
WarnOnBlockingWaitOnJSInteropThread = false;
746765

747766
action(state);
748767
}
749768
finally
750769
{
751770
ThrowOnBlockingWaitOnJSInteropThread = flag;
771+
WarnOnBlockingWaitOnJSInteropThread = wflag;
752772
}
753773
}
754774
#endif

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

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -123,18 +123,25 @@ public static void CallDelegate(JSMarshalerArgument* arguments_buffer)
123123
// arg_2 set by JS caller when there are arguments
124124
// arg_3 set by JS caller when there are arguments
125125
// arg_4 set by JS caller when there are arguments
126+
#if !FEATURE_WASM_MANAGED_THREADS
126127
try
127128
{
128-
#if FEATURE_WASM_MANAGED_THREADS
129-
// when we arrive here, we are on the thread which owns the proxies
130-
// if we need to dispatch the call to another thread in the future
131-
// we may need to consider how to solve blocking of the synchronous call
132-
// see also https://github.com/dotnet/runtime/issues/76958#issuecomment-1921418290
133-
arg_exc.AssertCurrentThreadContext();
129+
#else
130+
// when we arrive here, we are on the thread which owns the proxies
131+
var ctx = arg_exc.AssertCurrentThreadContext();
134132

135-
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.AllowBlockingWaitInAsyncCode)
133+
try
134+
{
135+
if (ctx.IsMainThread)
136136
{
137-
Thread.ThrowOnBlockingWaitOnJSInteropThread = true;
137+
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.ThrowWhenBlockingWait)
138+
{
139+
Thread.ThrowOnBlockingWaitOnJSInteropThread = true;
140+
}
141+
else if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.WarnWhenBlockingWait)
142+
{
143+
Thread.WarnOnBlockingWaitOnJSInteropThread = true;
144+
}
138145
}
139146
#endif
140147

@@ -156,9 +163,16 @@ public static void CallDelegate(JSMarshalerArgument* arguments_buffer)
156163
#if FEATURE_WASM_MANAGED_THREADS
157164
finally
158165
{
159-
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.AllowBlockingWaitInAsyncCode)
166+
if (ctx.IsMainThread)
160167
{
161-
Thread.ThrowOnBlockingWaitOnJSInteropThread = false;
168+
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.ThrowWhenBlockingWait)
169+
{
170+
Thread.ThrowOnBlockingWaitOnJSInteropThread = false;
171+
}
172+
else if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.WarnWhenBlockingWait)
173+
{
174+
Thread.WarnOnBlockingWaitOnJSInteropThread = false;
175+
}
162176
}
163177
}
164178
#endif
@@ -189,12 +203,9 @@ public static void CompleteTask(JSMarshalerArgument* arguments_buffer)
189203
}
190204
}
191205

192-
if (holder.CallbackReady != null)
193-
{
194-
#pragma warning disable CA1416 // Validate platform compatibility
195-
Thread.ForceBlockingWait(static (callbackReady) => ((ManualResetEventSlim)callbackReady!).Wait(), holder.CallbackReady);
196-
#pragma warning restore CA1416 // Validate platform compatibility
197-
}
206+
// this is always running on I/O thread, so it will not throw PNSE
207+
// it's also OK to block here, because we know we will only block shortly, as this is just race with the other thread.
208+
holder.CallbackReady?.Wait();
198209

199210
lock (ctx)
200211
{
@@ -247,21 +258,17 @@ public static void GetManagedStackTrace(JSMarshalerArgument* arguments_buffer)
247258

248259
// this is here temporarily, until JSWebWorker becomes public API
249260
[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, "System.Runtime.InteropServices.JavaScript.JSWebWorker", "System.Runtime.InteropServices.JavaScript")]
250-
// the marshaled signature is: GCHandle InstallMainSynchronizationContext(nint jsNativeTID, JSThreadBlockingMode jsThreadBlockingMode, JSThreadInteropMode jsThreadInteropMode, MainThreadingMode mainThreadingMode)
261+
// the marshaled signature is: GCHandle InstallMainSynchronizationContext(nint jsNativeTID, JSThreadBlockingMode jsThreadBlockingMode)
251262
public static void InstallMainSynchronizationContext(JSMarshalerArgument* arguments_buffer)
252263
{
253264
ref JSMarshalerArgument arg_exc = ref arguments_buffer[0]; // initialized by caller in alloc_stack_frame()
254265
ref JSMarshalerArgument arg_res = ref arguments_buffer[1];// initialized and set by caller
255266
ref JSMarshalerArgument arg_1 = ref arguments_buffer[2];// initialized and set by caller
256267
ref JSMarshalerArgument arg_2 = ref arguments_buffer[3];// initialized and set by caller
257-
ref JSMarshalerArgument arg_3 = ref arguments_buffer[4];// initialized and set by caller
258-
ref JSMarshalerArgument arg_4 = ref arguments_buffer[5];// initialized and set by caller
259268

260269
try
261270
{
262271
JSProxyContext.ThreadBlockingMode = (JSHostImplementation.JSThreadBlockingMode)arg_2.slot.Int32Value;
263-
JSProxyContext.ThreadInteropMode = (JSHostImplementation.JSThreadInteropMode)arg_3.slot.Int32Value;
264-
JSProxyContext.MainThreadingMode = (JSHostImplementation.MainThreadingMode)arg_4.slot.Int32Value;
265272
var jsSynchronizationContext = JSSynchronizationContext.InstallWebWorkerInterop(true, CancellationToken.None);
266273
jsSynchronizationContext.ProxyContext.JSNativeTID = arg_1.slot.IntPtrValue;
267274
arg_res.slot.GCHandle = jsSynchronizationContext.ProxyContext.ContextHandle;
@@ -283,9 +290,16 @@ public static void BeforeSyncJSExport(JSMarshalerArgument* arguments_buffer)
283290
{
284291
var ctx = arg_exc.AssertCurrentThreadContext();
285292
ctx.IsPendingSynchronousCall = true;
286-
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.AllowBlockingWaitInAsyncCode)
293+
if (ctx.IsMainThread)
287294
{
288-
Thread.ThrowOnBlockingWaitOnJSInteropThread = true;
295+
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.ThrowWhenBlockingWait)
296+
{
297+
Thread.ThrowOnBlockingWaitOnJSInteropThread = true;
298+
}
299+
else if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.WarnWhenBlockingWait)
300+
{
301+
Thread.WarnOnBlockingWaitOnJSInteropThread = true;
302+
}
289303
}
290304
}
291305
catch (Exception ex)
@@ -305,9 +319,16 @@ public static void AfterSyncJSExport(JSMarshalerArgument* arguments_buffer)
305319
{
306320
var ctx = arg_exc.AssertCurrentThreadContext();
307321
ctx.IsPendingSynchronousCall = false;
308-
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.AllowBlockingWaitInAsyncCode)
322+
if (ctx.IsMainThread)
309323
{
310-
Thread.ThrowOnBlockingWaitOnJSInteropThread = false;
324+
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.ThrowWhenBlockingWait)
325+
{
326+
Thread.ThrowOnBlockingWaitOnJSInteropThread = false;
327+
}
328+
else if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.WarnWhenBlockingWait)
329+
{
330+
Thread.WarnOnBlockingWaitOnJSInteropThread = false;
331+
}
311332
}
312333
}
313334
catch (Exception ex)

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

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -230,11 +230,7 @@ internal static unsafe void InvokeJSFunction(JSObject jsFunction, Span<JSMarshal
230230
internal static unsafe void InvokeJSFunctionCurrent(JSObject jsFunction, Span<JSMarshalerArgument> arguments)
231231
{
232232
#if FEATURE_WASM_MANAGED_THREADS
233-
if (JSProxyContext.ThreadInteropMode == JSHostImplementation.JSThreadInteropMode.NoSyncJSInterop)
234-
{
235-
throw new PlatformNotSupportedException("Cannot call synchronous JS functions.");
236-
}
237-
else if (jsFunction.ProxyContext.IsPendingSynchronousCall)
233+
if (jsFunction.ProxyContext.IsPendingSynchronousCall && jsFunction.ProxyContext.IsMainThread)
238234
{
239235
throw new PlatformNotSupportedException("Cannot call synchronous JS function from inside a synchronous call to a C# method.");
240236
}
@@ -260,11 +256,7 @@ internal static unsafe void InvokeJSFunctionCurrent(JSObject jsFunction, Span<JS
260256
internal static unsafe void DispatchJSFunctionSync(JSObject jsFunction, Span<JSMarshalerArgument> arguments)
261257
{
262258
#if FEATURE_WASM_MANAGED_THREADS
263-
if (JSProxyContext.ThreadInteropMode == JSHostImplementation.JSThreadInteropMode.NoSyncJSInterop)
264-
{
265-
throw new PlatformNotSupportedException("Cannot call synchronous JS functions.");
266-
}
267-
else if (jsFunction.ProxyContext.IsPendingSynchronousCall)
259+
if (jsFunction.ProxyContext.IsPendingSynchronousCall && jsFunction.ProxyContext.IsMainThread)
268260
{
269261
throw new PlatformNotSupportedException("Cannot call synchronous JS function from inside a synchronous call to a C# method.");
270262
}
@@ -274,10 +266,8 @@ internal static unsafe void DispatchJSFunctionSync(JSObject jsFunction, Span<JSM
274266

275267
// we already know that we are not on the right thread
276268
// this will be blocking until resolved by that thread
277-
// we don't have to disable ThrowOnBlockingWaitOnJSInteropThread, because this is lock in native code
278-
// we also don't throw PNSE here, because we know that the target has JS interop installed and that it could not block
269+
// we know that the target has JS interop installed and that it could not block
279270
// so it could take some time, while target is CPU busy, but not forever
280-
// see also https://github.com/dotnet/runtime/issues/76958#issuecomment-1921418290
281271
Interop.Runtime.InvokeJSFunctionSend(jsFunction.ProxyContext.JSNativeTID, functionHandle, args);
282272

283273
ref JSMarshalerArgument exceptionArg = ref arguments[0];
@@ -317,11 +307,7 @@ internal static unsafe void InvokeJSImportImpl(JSFunctionBinding signature, Span
317307
#if FEATURE_WASM_MANAGED_THREADS
318308
else
319309
{
320-
if (JSProxyContext.ThreadInteropMode == JSHostImplementation.JSThreadInteropMode.NoSyncJSInterop)
321-
{
322-
throw new PlatformNotSupportedException("Cannot call synchronous JS functions.");
323-
}
324-
else if (targetContext.IsPendingSynchronousCall)
310+
if (targetContext.IsPendingSynchronousCall && targetContext.IsMainThread)
325311
{
326312
throw new PlatformNotSupportedException("Cannot call synchronous JS function from inside a synchronous call to a C# method.");
327313
}
@@ -407,10 +393,6 @@ internal static unsafe void DispatchJSImportSyncSend(JSFunctionBinding signature
407393

408394
// we already know that we are not on the right thread
409395
// this will be blocking until resolved by that thread
410-
// we don't have to disable ThrowOnBlockingWaitOnJSInteropThread, because this is lock in native code
411-
// we also don't throw PNSE here, because we know that the target has JS interop installed and that it could not block
412-
// so it could take some time, while target is CPU busy, but not forever
413-
// see also https://github.com/dotnet/runtime/issues/76958#issuecomment-1921418290
414396
Interop.Runtime.InvokeJSImportSyncSend(targetContext.JSNativeTID, sig, args);
415397

416398
if (exc.slot.Type != MarshalerType.None)

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

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -63,41 +63,13 @@ public struct IntPtrAndHandle
6363
internal RuntimeTypeHandle typeHandle;
6464
}
6565

66-
// keep in sync with types\internal.ts
67-
public enum MainThreadingMode : int
68-
{
69-
// Running the managed main thread on UI thread.
70-
// Managed GC and similar scenarios could be blocking the UI.
71-
// Easy to deadlock. Not recommended for production.
72-
UIThread = 0,
73-
// Running the managed main thread on dedicated WebWorker. Marshaling all JavaScript calls to and from the main thread.
74-
DeputyThread = 1,
75-
// TODO comments
76-
DeputyAndIOThreads = 2,
77-
}
78-
7966
// keep in sync with types\internal.ts
8067
public enum JSThreadBlockingMode : int
8168
{
82-
// throw PlatformNotSupportedException if blocking .Wait is called on threads with JS interop, like JSWebWorker and Main thread.
83-
// Avoids deadlocks (typically with pending JS promises on the same thread) by throwing exceptions.
84-
NoBlockingWait = 0,
85-
// TODO comments
86-
AllowBlockingWaitInAsyncCode = 1,
87-
// allow .Wait on all threads.
88-
// Could cause deadlocks with blocking .Wait on a pending JS Task/Promise on the same thread or similar Task/Promise chain.
89-
AllowBlockingWait = 100,
90-
}
91-
92-
// keep in sync with types\internal.ts
93-
public enum JSThreadInteropMode : int
94-
{
95-
// throw PlatformNotSupportedException if synchronous JSImport/JSExport is called on threads with JS interop, like JSWebWorker and Main thread.
96-
// calling synchronous JSImport on thread pool or new threads is allowed.
97-
NoSyncJSInterop = 0,
98-
// allow non-re-entrant synchronous blocking calls to and from JS on JSWebWorker on threads with JS interop, like JSWebWorker and Main thread.
99-
// calling synchronous JSImport on thread pool or new threads is allowed.
100-
SimpleSynchronousJSInterop = 1,
69+
PreventSynchronousJSExport = 0,
70+
ThrowWhenBlockingWait = 1,
71+
WarnWhenBlockingWait = 2,
72+
DangerousAllowBlockingWait = 100,
10173
}
10274
}
10375
}

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,15 @@ private JSProxyContext()
4242
public JSSynchronizationContext SynchronizationContext;
4343
public JSAsyncTaskScheduler? AsyncTaskScheduler;
4444

45-
public static MainThreadingMode MainThreadingMode = MainThreadingMode.DeputyThread;
46-
public static JSThreadBlockingMode ThreadBlockingMode = JSThreadBlockingMode.AllowBlockingWaitInAsyncCode;
47-
public static JSThreadInteropMode ThreadInteropMode = JSThreadInteropMode.SimpleSynchronousJSInterop;
45+
public static JSThreadBlockingMode ThreadBlockingMode = JSThreadBlockingMode.PreventSynchronousJSExport;
4846
public bool IsPendingSynchronousCall;
4947

5048
#if !DEBUG
5149
[MethodImpl(MethodImplOptions.AggressiveInlining)]
5250
#endif
5351
public bool IsCurrentThread()
5452
{
55-
return ManagedTID == Environment.CurrentManagedThreadId && (!IsMainThread || MainThreadingMode == MainThreadingMode.UIThread);
53+
return ManagedTID == Environment.CurrentManagedThreadId && !IsMainThread;
5654
}
5755

5856
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "thread_id")]

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,18 @@ public static unsafe JSSynchronizationContext InstallWebWorkerInterop(bool isMai
5050
ctx.previousSynchronizationContext = SynchronizationContext.Current;
5151
SynchronizationContext.SetSynchronizationContext(ctx);
5252

53-
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.NoBlockingWait)
53+
if (!isMainThread)
5454
{
55-
Thread.ThrowOnBlockingWaitOnJSInteropThread = true;
55+
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.ThrowWhenBlockingWait)
56+
{
57+
Thread.ThrowOnBlockingWaitOnJSInteropThread = true;
58+
}
59+
else if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.WarnWhenBlockingWait
60+
|| JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.PreventSynchronousJSExport
61+
)
62+
{
63+
Thread.WarnOnBlockingWaitOnJSInteropThread = true;
64+
}
5665
}
5766

5867
var proxyContext = ctx.ProxyContext;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ await Assert.ThrowsAsync<TaskCanceledException>(async () =>
8282
{
8383
CancellationTokenSource cts = new CancellationTokenSource();
8484
var promise = response.Content.ReadAsStringAsync(cts.Token);
85-
Console.WriteLine("HttpClient_CancelInDifferentThread: ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId);
85+
WebWorkerTestHelper.Log("HttpClient_CancelInDifferentThread: ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId);
8686
cts.Cancel();
8787
var res = await promise;
8888
throw new Exception("This should be unreachable: " + res);

0 commit comments

Comments
 (0)