Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'browser' and '$(FeatureWasmManagedThreads)' == 'true'">
<Compile Include="System\Runtime\InteropServices\JavaScript\JSWebWorker.cs" />
<Compile Include="System\Runtime\InteropServices\JavaScript\JSSynchronizationContext.cs" />
<Compile Include="System\Runtime\InteropServices\JavaScript\JSAsyncTaskScheduler.cs" />
</ItemGroup>
<ItemGroup Condition="'$(WasmEnableThreads)' == 'true'">
<ApiCompatSuppressionFile Include="CompatibilitySuppressions.xml;CompatibilitySuppressions.WasmThreads.xml" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Threading.Tasks;

namespace System.Runtime.InteropServices.JavaScript
{
// executes all tasks thru queue, never inline
internal sealed class JSAsyncTaskScheduler : TaskScheduler
{
private readonly JSSynchronizationContext m_synchronizationContext;

internal JSAsyncTaskScheduler(JSSynchronizationContext synchronizationContext)
{
m_synchronizationContext = synchronizationContext;
}

protected override void QueueTask(Task task)
{
m_synchronizationContext.Post((_) =>
{
if (!TryExecuteTask(task))
{
Environment.FailFast("Unexpected failure in JSAsyncTaskScheduler" + Environment.CurrentManagedThreadId);
}
}, null);
}

// this is the main difference from the SynchronizationContextTaskScheduler
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
return false;
}

protected override IEnumerable<Task>? GetScheduledTasks()
{
return null;
}

public override int MaximumConcurrencyLevel => 1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using static System.Runtime.InteropServices.JavaScript.JSHostImplementation;

namespace System.Runtime.InteropServices.JavaScript
Expand Down Expand Up @@ -40,6 +41,7 @@ private JSProxyContext()
public int ManagedTID; // current managed thread id
public bool IsMainThread;
public JSSynchronizationContext SynchronizationContext;
public TaskScheduler? AsyncTaskScheduler;

public static MainThreadingMode MainThreadingMode = MainThreadingMode.DeputyThread;
public static JSThreadBlockingMode ThreadBlockingMode = JSThreadBlockingMode.NoBlockingWait;
Expand Down Expand Up @@ -483,7 +485,7 @@ public static void ReleaseCSOwnedObject(JSObject jso, bool skipJS)
{
if (IsJSVHandle(jsHandle))
{
Environment.FailFast("TODO implement blocking ReleaseCSOwnedObjectSend to make sure the order of FreeJSVHandle is correct.");
Environment.FailFast($"TODO implement blocking ReleaseCSOwnedObjectSend to make sure the order of FreeJSVHandle is correct, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}");
}

// this is async message, we need to call this as the last thing
Expand All @@ -501,7 +503,7 @@ public static void ReleaseCSOwnedObject(JSObject jso, bool skipJS)
}
}

#endregion
#endregion

#region Dispose

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#if FEATURE_WASM_MANAGED_THREADS

using System.Threading;
using System.Threading.Tasks;
using System.Threading.Channels;
using System.Runtime.CompilerServices;
using WorkItemQueueType = System.Threading.Channels.Channel<System.Runtime.InteropServices.JavaScript.JSSynchronizationContext.WorkItem>;
Expand Down Expand Up @@ -56,6 +57,7 @@ public static unsafe JSSynchronizationContext InstallWebWorkerInterop(bool isMai
}

var proxyContext = ctx.ProxyContext;
proxyContext.AsyncTaskScheduler = new JSAsyncTaskScheduler(ctx);
JSProxyContext.CurrentThreadContext = proxyContext;
JSProxyContext.ExecutionContext = proxyContext;
if (isMainThread)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,21 @@ internal void ToJSDynamic(Task? value)
{
Task? task = value;

var ctx = ToJSContext;

var isCurrentThreadOrParameter = ctx.IsCurrentThread() || slot.Type != MarshalerType.TaskPreCreated;

if (task == null)
{
if (!isCurrentThreadOrParameter)
{
Environment.FailFast("Marshalling null return Task to JS is not supported in MT");
}
slot.Type = MarshalerType.None;
return;
}

if (task.IsCompleted)
if (task.IsCompleted && isCurrentThreadOrParameter)
{
if (task.Exception != null)
{
Expand All @@ -172,7 +180,6 @@ internal void ToJSDynamic(Task? value)
}
}

var ctx = ToJSContext;

if (slot.Type != MarshalerType.TaskPreCreated)
{
Expand All @@ -189,7 +196,9 @@ internal void ToJSDynamic(Task? value)
var taskHolder = ctx.CreateCSOwnedProxy(slot.JSHandle);

#if FEATURE_WASM_MANAGED_THREADS
task.ContinueWith(Complete, taskHolder, CancellationToken.None, TaskContinuationOptions.RunContinuationsAsynchronously, TaskScheduler.FromCurrentSynchronizationContext());
// AsyncTaskScheduler will make sure that the resolve message is always sent after this call is completed
// that is: synchronous marshaling and eventually message to the target thread, which need to arrive before the resolve message
task.ContinueWith(Complete, taskHolder, ctx.AsyncTaskScheduler!);
#else
task.ContinueWith(Complete, taskHolder, TaskScheduler.Current);
#endif
Expand Down Expand Up @@ -229,18 +238,18 @@ public void ToJS(Task? value)
{
Task? task = value;
var ctx = ToJSContext;
var isCurrentThread = ctx.IsCurrentThread();
var isCurrentThreadOrParameter = ctx.IsCurrentThread() || slot.Type != MarshalerType.TaskPreCreated;

if (task == null)
{
if (!isCurrentThread)
if (!isCurrentThreadOrParameter)
{
Environment.FailFast("Marshalling null task to JS is not supported in MT");
Environment.FailFast("Marshalling null return Task to JS is not supported in MT");
}
slot.Type = MarshalerType.None;
return;
}
if (isCurrentThread && task.IsCompleted)
if (isCurrentThreadOrParameter && task.IsCompleted)
{
if (task.Exception != null)
{
Expand Down Expand Up @@ -273,7 +282,9 @@ public void ToJS(Task? value)
var taskHolder = ctx.CreateCSOwnedProxy(slot.JSHandle);

#if FEATURE_WASM_MANAGED_THREADS
task.ContinueWith(Complete, taskHolder, CancellationToken.None, TaskContinuationOptions.RunContinuationsAsynchronously, TaskScheduler.FromCurrentSynchronizationContext());
// AsyncTaskScheduler will make sure that the resolve message is always sent after this call is completed
// that is: synchronous marshaling and eventually message to the target thread, which need to arrive before the resolve message
task.ContinueWith(Complete, taskHolder, ctx.AsyncTaskScheduler!);
#else
task.ContinueWith(Complete, taskHolder, TaskScheduler.Current);
#endif
Expand Down Expand Up @@ -303,19 +314,19 @@ public void ToJS<T>(Task<T>? value, ArgumentToJSCallback<T> marshaler)
{
Task<T>? task = value;
var ctx = ToJSContext;
var isCurrentThread = ctx.IsCurrentThread();
var isCurrentThreadOrParameter = ctx.IsCurrentThread() || slot.Type != MarshalerType.TaskPreCreated;

if (task == null)
{
if (!isCurrentThread)
if (!isCurrentThreadOrParameter)
{
Environment.FailFast("NULL not supported in MT");
Environment.FailFast("Marshalling null return Task to JS is not supported in MT");
}
slot.Type = MarshalerType.None;
return;
}

if (isCurrentThread && task.IsCompleted)
if (isCurrentThreadOrParameter && task.IsCompleted)
{
if (task.Exception != null)
{
Expand Down Expand Up @@ -350,7 +361,9 @@ public void ToJS<T>(Task<T>? value, ArgumentToJSCallback<T> marshaler)
var taskHolder = ctx.CreateCSOwnedProxy(slot.JSHandle);

#if FEATURE_WASM_MANAGED_THREADS
task.ContinueWith(Complete, new HolderAndMarshaler<T>(taskHolder, marshaler), CancellationToken.None, TaskContinuationOptions.RunContinuationsAsynchronously, TaskScheduler.FromCurrentSynchronizationContext());
// AsyncTaskScheduler will make sure that the resolve message is always sent after this call is completed
// that is: synchronous marshaling and eventually message to the target thread, which need to arrive before the resolve message
task.ContinueWith(Complete, new HolderAndMarshaler<T>(taskHolder, marshaler), ctx.AsyncTaskScheduler!);
#else
task.ContinueWith(Complete, new HolderAndMarshaler<T>(taskHolder, marshaler), TaskScheduler.Current);
#endif
Expand Down