Skip to content

[wasm][mt] Improve blocking wait detection and tests #98045

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@ public static partial class Debug
#if FEATURE_WASM_MANAGED_THREADS
namespace System.Threading
{
public partial class Monitor
public partial class Thread
{
[ThreadStatic]
public static bool ThrowOnBlockingWaitOnJSInteropThread;

public static void AssureBlockingPossible() { throw null; }
public static void ForceBlockingWait(Action<object?> action, object? state) { throw null; }
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ T:System.Runtime.Serialization.DeserializationToken
M:System.Runtime.Serialization.SerializationInfo.StartDeserialization
T:System.Diagnostics.DebugProvider
M:System.Diagnostics.Debug.SetProvider(System.Diagnostics.DebugProvider)
F:System.Threading.Monitor.ThrowOnBlockingWaitOnJSInteropThread
M:System.Threading.Thread.AssureBlockingPossible
F:System.Threading.Thread.ThrowOnBlockingWaitOnJSInteropThread
F:System.Threading.Thread.ForceBlockingWait
Original file line number Diff line number Diff line change
Expand Up @@ -4298,4 +4298,7 @@
<data name="NotSupported_AssemblySave" xml:space="preserve">
<value>This AssemblyBuilder instance doesn't support saving. Use AssemblyBuilder.DefinePersistedAssembly to create an AssemblyBuilder instance that supports saving.</value>
</data>
<data name="WasmThreads_BlockingWaitNotSupportedOnJSInterop" xml:space="preserve">
<value>Blocking wait is not supported on the JS interop threads.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,9 @@ public bool Wait(int millisecondsTimeout)
public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
{
CheckDispose();
#if FEATURE_WASM_MANAGED_THREADS
Thread.AssureBlockingPossible();
#endif

if (millisecondsTimeout < -1)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -671,5 +671,33 @@ public static int GetCurrentProcessorId()
// a speed check will determine refresh rate of the cache and will report if caching is not advisable.
// we will record that in a readonly static so that it could become a JIT constant and bypass caching entirely.
private static readonly bool s_isProcessorNumberReallyFast = ProcessorIdCache.ProcessorNumberSpeedCheck();

#if FEATURE_WASM_MANAGED_THREADS
[ThreadStatic]
public static bool ThrowOnBlockingWaitOnJSInteropThread;

public static void AssureBlockingPossible()
{
if (ThrowOnBlockingWaitOnJSInteropThread)
{
throw new PlatformNotSupportedException(SR.WasmThreads_BlockingWaitNotSupportedOnJSInterop);
}
}

public static void ForceBlockingWait(Action<object?> action, object? state = null)
{
var flag = ThrowOnBlockingWaitOnJSInteropThread;
try
{
ThrowOnBlockingWaitOnJSInteropThread = false;

action(state);
}
finally
{
ThrowOnBlockingWaitOnJSInteropThread = flag;
}
}
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -233,18 +233,9 @@ public static void CompleteTask(JSMarshalerArgument* arguments_buffer)

if (holder.CallbackReady != null)
{
var threadFlag = Monitor.ThrowOnBlockingWaitOnJSInteropThread;
try
{
Monitor.ThrowOnBlockingWaitOnJSInteropThread = false;
#pragma warning disable CA1416 // Validate platform compatibility
holder.CallbackReady?.Wait();
Thread.ForceBlockingWait(static (callbackReady) => ((ManualResetEventSlim)callbackReady!).Wait(), holder.CallbackReady);
#pragma warning restore CA1416 // Validate platform compatibility
}
finally
{
Monitor.ThrowOnBlockingWaitOnJSInteropThread = threadFlag;
}
}

lock (ctx)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public static JSSynchronizationContext InstallWebWorkerInterop(bool isMainThread
// - 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
Monitor.ThrowOnBlockingWaitOnJSInteropThread = true;
Thread.ThrowOnBlockingWaitOnJSInteropThread = true;

var proxyContext = ctx.ProxyContext;
JSProxyContext.CurrentThreadContext = proxyContext;
Expand Down Expand Up @@ -216,11 +216,8 @@ public override void Send(SendOrPostCallback d, object? state)
d(state);
return;
}
// TODO, refactor into single assert method
if (Monitor.ThrowOnBlockingWaitOnJSInteropThread)
{
throw new PlatformNotSupportedException("Blocking wait is not supported on the JS interop threads.");
}

Thread.AssureBlockingPossible();

using (var signal = new ManualResetEventSlim(false))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,8 @@ public async Task JSSynchronizationContext_Send_Post_Items_Cancellation()
capturedSynchronizationContext = SynchronizationContext.Current;
jswReady.SetResult();

var threadFlag = Monitor.ThrowOnBlockingWaitOnJSInteropThread;
try
{
Monitor.ThrowOnBlockingWaitOnJSInteropThread = false;

// blocking the worker, so that JSSynchronizationContext could enqueue next tasks
blocker.Wait();
}
finally
{
Monitor.ThrowOnBlockingWaitOnJSInteropThread = threadFlag;
}
// blocking the worker, so that JSSynchronizationContext could enqueue next tasks
Thread.ForceBlockingWait(static (b) => ((ManualResetEventSlim)b).Wait(), blocker);

return never.Task;
}, cts.Token);
Expand Down Expand Up @@ -453,9 +443,12 @@ public async Task WaitAssertsOnJSInteropThreads(Executor executor, NamedCall met
await executor.Execute(Task () =>
{
Exception? exception = null;
try {
try
{
method.Call(cts.Token);
} catch (Exception ex) {
}
catch (Exception ex)
{
exception = ex;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ public class NamedCall
mr.Wait(cts.Token);
} catch (OperationCanceledException) { /* ignore */ }
}},
new NamedCall { Name = "SemaphoreSlim.Wait", Call = delegate (CancellationToken ct) {
using var sem = new SemaphoreSlim(2);
var cts = new CancellationTokenSource(8);
try {
sem.Wait(cts.Token);
} catch (OperationCanceledException) { /* ignore */ }
}},
};

public static IEnumerable<object[]> GetTargetThreadsAndBlockingCalls()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,16 @@
<DiagnosticId>CP0014</DiagnosticId>
<Target>M:System.Threading.Thread.UnsafeStart(System.Object):[T:System.Runtime.Versioning.UnsupportedOSPlatformAttribute]</Target>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:System.Threading.Thread.ThrowOnBlockingWaitOnJSInteropThread</Target>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:System.Threading.Thread.AssureBlockingPossible</Target>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:System.Threading.Thread.ForceBlockingWait(System.Action{System.Object},System.Object)</Target>
</Suppression>
</Suppressions>
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,4 @@
<DiagnosticId>CP0014</DiagnosticId>
<Target>M:System.Threading.Monitor.Wait(System.Object):[T:System.Runtime.Versioning.UnsupportedOSPlatformAttribute]</Target>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:System.Threading.Monitor.ThrowOnBlockingWaitOnJSInteropThread</Target>
</Suppression>
</Suppressions>
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,6 @@ namespace System.Threading
{
public static partial class Monitor
{
#if FEATURE_WASM_MANAGED_THREADS
[ThreadStatic]
public static bool ThrowOnBlockingWaitOnJSInteropThread;
#endif

[Intrinsic]
[MethodImplAttribute(MethodImplOptions.InternalCall)] // Interpreter is missing this intrinsic
public static void Enter(object obj) => Enter(obj);
Expand Down Expand Up @@ -83,10 +78,7 @@ public static bool Wait(object obj, int millisecondsTimeout)
{
ArgumentNullException.ThrowIfNull(obj);
#if FEATURE_WASM_MANAGED_THREADS
if (ThrowOnBlockingWaitOnJSInteropThread)
{
throw new PlatformNotSupportedException("blocking Wait is not supported on the JS interop threads.");
}
Thread.AssureBlockingPossible();
#endif
return ObjWait(millisecondsTimeout, obj);
}
Expand Down