Skip to content
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

[browser][MT] Marshal resolved/unresolved tasks separately #99347

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;
pavelsavara marked this conversation as resolved.
Show resolved Hide resolved
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;
pavelsavara marked this conversation as resolved.
Show resolved Hide resolved

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
Loading