Skip to content

Commit b0f6444

Browse files
[browser][MT] Marshal resolved/unresolved tasks separately (#99347)
Co-authored-by: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com>
1 parent 4e86b1c commit b0f6444

File tree

5 files changed

+112
-15
lines changed

5 files changed

+112
-15
lines changed

src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'browser' and '$(FeatureWasmManagedThreads)' == 'true'">
6969
<Compile Include="System\Runtime\InteropServices\JavaScript\JSWebWorker.cs" />
7070
<Compile Include="System\Runtime\InteropServices\JavaScript\JSSynchronizationContext.cs" />
71+
<Compile Include="System\Runtime\InteropServices\JavaScript\JSAsyncTaskScheduler.cs" />
7172
</ItemGroup>
7273
<ItemGroup Condition="'$(WasmEnableThreads)' == 'true'">
7374
<ApiCompatSuppressionFile Include="CompatibilitySuppressions.xml;CompatibilitySuppressions.WasmThreads.xml" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using System.Threading.Tasks;
6+
7+
namespace System.Runtime.InteropServices.JavaScript
8+
{
9+
// executes all tasks thru queue, never inline
10+
internal sealed class JSAsyncTaskScheduler : TaskScheduler
11+
{
12+
private readonly JSSynchronizationContext m_synchronizationContext;
13+
14+
internal JSAsyncTaskScheduler(JSSynchronizationContext synchronizationContext)
15+
{
16+
m_synchronizationContext = synchronizationContext;
17+
}
18+
19+
protected override void QueueTask(Task task)
20+
{
21+
m_synchronizationContext.Post((_) =>
22+
{
23+
if (!TryExecuteTask(task))
24+
{
25+
Environment.FailFast("Unexpected failure in JSAsyncTaskScheduler" + Environment.CurrentManagedThreadId);
26+
}
27+
}, null);
28+
}
29+
30+
// this is the main difference from the SynchronizationContextTaskScheduler
31+
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
32+
{
33+
return false;
34+
}
35+
36+
protected override IEnumerable<Task>? GetScheduledTasks()
37+
{
38+
return null;
39+
}
40+
41+
public override int MaximumConcurrencyLevel => 1;
42+
}
43+
}

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
@@ -40,6 +40,7 @@ private JSProxyContext()
4040
public int ManagedTID; // current managed thread id
4141
public bool IsMainThread;
4242
public JSSynchronizationContext SynchronizationContext;
43+
public JSAsyncTaskScheduler? AsyncTaskScheduler;
4344

4445
public static MainThreadingMode MainThreadingMode = MainThreadingMode.DeputyThread;
4546
public static JSThreadBlockingMode ThreadBlockingMode = JSThreadBlockingMode.NoBlockingWait;
@@ -483,7 +484,7 @@ public static void ReleaseCSOwnedObject(JSObject jso, bool skipJS)
483484
{
484485
if (IsJSVHandle(jsHandle))
485486
{
486-
Environment.FailFast("TODO implement blocking ReleaseCSOwnedObjectSend to make sure the order of FreeJSVHandle is correct.");
487+
Environment.FailFast($"TODO implement blocking ReleaseCSOwnedObjectSend to make sure the order of FreeJSVHandle is correct, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}");
487488
}
488489

489490
// this is async message, we need to call this as the last thing
@@ -501,7 +502,7 @@ public static void ReleaseCSOwnedObject(JSObject jso, bool skipJS)
501502
}
502503
}
503504

504-
#endregion
505+
#endregion
505506

506507
#region Dispose
507508

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public static unsafe JSSynchronizationContext InstallWebWorkerInterop(bool isMai
5656
}
5757

5858
var proxyContext = ctx.ProxyContext;
59+
proxyContext.AsyncTaskScheduler = new JSAsyncTaskScheduler(ctx);
5960
JSProxyContext.CurrentThreadContext = proxyContext;
6061
JSProxyContext.ExecutionContext = proxyContext;
6162
if (isMainThread)

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

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.ComponentModel;
88
using System.Threading;
99
using static System.Runtime.InteropServices.JavaScript.JSHostImplementation;
10+
using System.Runtime.CompilerServices;
1011

1112
namespace System.Runtime.InteropServices.JavaScript
1213
{
@@ -140,13 +141,20 @@ internal void ToJSDynamic(Task? value)
140141
{
141142
Task? task = value;
142143

144+
var ctx = ToJSContext;
145+
var canMarshalTaskResultOnSameCall = CanMarshalTaskResultOnSameCall(ctx);
146+
143147
if (task == null)
144148
{
149+
if (!canMarshalTaskResultOnSameCall)
150+
{
151+
Environment.FailFast("Marshalling null return Task to JS is not supported in MT");
152+
}
145153
slot.Type = MarshalerType.None;
146154
return;
147155
}
148156

149-
if (task.IsCompleted)
157+
if (canMarshalTaskResultOnSameCall && task.IsCompleted)
150158
{
151159
if (task.Exception != null)
152160
{
@@ -172,7 +180,6 @@ internal void ToJSDynamic(Task? value)
172180
}
173181
}
174182

175-
var ctx = ToJSContext;
176183

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

191198
#if FEATURE_WASM_MANAGED_THREADS
192-
task.ContinueWith(Complete, taskHolder, CancellationToken.None, TaskContinuationOptions.RunContinuationsAsynchronously, TaskScheduler.FromCurrentSynchronizationContext());
199+
// AsyncTaskScheduler will make sure that the resolve message is always sent after this call is completed
200+
// that is: synchronous marshaling and eventually message to the target thread, which need to arrive before the resolve message
201+
task.ContinueWith(Complete, taskHolder, ctx.AsyncTaskScheduler!);
193202
#else
194203
task.ContinueWith(Complete, taskHolder, TaskScheduler.Current);
195204
#endif
@@ -229,18 +238,18 @@ public void ToJS(Task? value)
229238
{
230239
Task? task = value;
231240
var ctx = ToJSContext;
232-
var isCurrentThread = ctx.IsCurrentThread();
241+
var canMarshalTaskResultOnSameCall = CanMarshalTaskResultOnSameCall(ctx);
233242

234243
if (task == null)
235244
{
236-
if (!isCurrentThread)
245+
if (!canMarshalTaskResultOnSameCall)
237246
{
238-
Environment.FailFast("Marshalling null task to JS is not supported in MT");
247+
Environment.FailFast("Marshalling null return Task to JS is not supported in MT");
239248
}
240249
slot.Type = MarshalerType.None;
241250
return;
242251
}
243-
if (isCurrentThread && task.IsCompleted)
252+
if (canMarshalTaskResultOnSameCall && task.IsCompleted)
244253
{
245254
if (task.Exception != null)
246255
{
@@ -273,7 +282,9 @@ public void ToJS(Task? value)
273282
var taskHolder = ctx.CreateCSOwnedProxy(slot.JSHandle);
274283

275284
#if FEATURE_WASM_MANAGED_THREADS
276-
task.ContinueWith(Complete, taskHolder, CancellationToken.None, TaskContinuationOptions.RunContinuationsAsynchronously, TaskScheduler.FromCurrentSynchronizationContext());
285+
// AsyncTaskScheduler will make sure that the resolve message is always sent after this call is completed
286+
// that is: synchronous marshaling and eventually message to the target thread, which need to arrive before the resolve message
287+
task.ContinueWith(Complete, taskHolder, ctx.AsyncTaskScheduler!);
277288
#else
278289
task.ContinueWith(Complete, taskHolder, TaskScheduler.Current);
279290
#endif
@@ -303,19 +314,19 @@ public void ToJS<T>(Task<T>? value, ArgumentToJSCallback<T> marshaler)
303314
{
304315
Task<T>? task = value;
305316
var ctx = ToJSContext;
306-
var isCurrentThread = ctx.IsCurrentThread();
317+
var canMarshalTaskResultOnSameCall = CanMarshalTaskResultOnSameCall(ctx);
307318

308319
if (task == null)
309320
{
310-
if (!isCurrentThread)
321+
if (!canMarshalTaskResultOnSameCall)
311322
{
312-
Environment.FailFast("NULL not supported in MT");
323+
Environment.FailFast("Marshalling null return Task to JS is not supported in MT");
313324
}
314325
slot.Type = MarshalerType.None;
315326
return;
316327
}
317328

318-
if (isCurrentThread && task.IsCompleted)
329+
if (canMarshalTaskResultOnSameCall && task.IsCompleted)
319330
{
320331
if (task.Exception != null)
321332
{
@@ -350,7 +361,9 @@ public void ToJS<T>(Task<T>? value, ArgumentToJSCallback<T> marshaler)
350361
var taskHolder = ctx.CreateCSOwnedProxy(slot.JSHandle);
351362

352363
#if FEATURE_WASM_MANAGED_THREADS
353-
task.ContinueWith(Complete, new HolderAndMarshaler<T>(taskHolder, marshaler), CancellationToken.None, TaskContinuationOptions.RunContinuationsAsynchronously, TaskScheduler.FromCurrentSynchronizationContext());
364+
// AsyncTaskScheduler will make sure that the resolve message is always sent after this call is completed
365+
// that is: synchronous marshaling and eventually message to the target thread, which need to arrive before the resolve message
366+
task.ContinueWith(Complete, new HolderAndMarshaler<T>(taskHolder, marshaler), ctx.AsyncTaskScheduler!);
354367
#else
355368
task.ContinueWith(Complete, new HolderAndMarshaler<T>(taskHolder, marshaler), TaskScheduler.Current);
356369
#endif
@@ -370,6 +383,44 @@ static void Complete(Task<T> task, object? thm)
370383
}
371384
}
372385

386+
#if !DEBUG
387+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
388+
#endif
389+
#if FEATURE_WASM_MANAGED_THREADS
390+
// We can't marshal resolved/rejected/null Task.Result directly into current argument when this is marshaling return of JSExport across threads
391+
private bool CanMarshalTaskResultOnSameCall(JSProxyContext ctx)
392+
{
393+
if (slot.Type != MarshalerType.TaskPreCreated)
394+
{
395+
// this means that we are not in the return value of JSExport
396+
// we are marshaling parameter of JSImport
397+
return true;
398+
}
399+
400+
if (ctx.IsCurrentThread())
401+
{
402+
// If the JS and Managed is running on the same thread we can use the args buffer,
403+
// because the call is synchronous and the buffer will be processed.
404+
// In that case the pre-allocated Promise would be discarded as necessary
405+
// and the result will be marshaled by `try_marshal_sync_task_to_js`
406+
return true;
407+
}
408+
409+
// Otherwise this is JSExport return value and we can't use the args buffer, because the args buffer arrived in async message and nobody is reading after this.
410+
// In such case the JS side already pre-created the Promise and we have to use it, to resolve it in separate call via `mono_wasm_resolve_or_reject_promise_post`
411+
// there is JSVHandle in this arg
412+
return false;
413+
}
414+
#else
415+
#pragma warning disable CA1822 // Mark members as static
416+
private bool CanMarshalTaskResultOnSameCall(JSProxyContext _)
417+
{
418+
// in ST build this is always synchronous and we can marshal the result directly
419+
return true;
420+
}
421+
#pragma warning restore CA1822 // Mark members as static
422+
#endif
423+
373424
private sealed record HolderAndMarshaler<T>(JSObject TaskHolder, ArgumentToJSCallback<T> Marshaler);
374425

375426
private static void RejectPromise(JSObject holder, Exception ex)

0 commit comments

Comments
 (0)