Skip to content

Commit 6c3a197

Browse files
authored
[browser][MT] GC, threadpool and some JS interop improvements (#86759)
1 parent c6fd07b commit 6c3a197

File tree

45 files changed

+880
-145
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+880
-145
lines changed

eng/Subsets.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,7 @@
440440
</ItemGroup>
441441

442442
<ItemGroup Condition="$(_subset.Contains('+mono.wasmruntime+'))">
443+
<ProjectToBuild Include="$(LibrariesProjectRoot)\System.Runtime.InteropServices.JavaScript\src\System.Runtime.InteropServices.JavaScript.csproj" Category="mono" />
443444
<ProjectToBuild Include="$(MonoProjectRoot)wasm\wasm.proj" Category="mono" />
444445
</ItemGroup>
445446

src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ internal static unsafe partial class Runtime
2828
[MethodImpl(MethodImplOptions.InternalCall)]
2929
public static extern void DeregisterGCRoot(IntPtr handle);
3030

31+
#if FEATURE_WASM_THREADS
32+
[MethodImpl(MethodImplOptions.InternalCall)]
33+
public static extern void InstallWebWorkerInterop(bool installJSSynchronizationContext);
34+
[MethodImpl(MethodImplOptions.InternalCall)]
35+
public static extern void UninstallWebWorkerInterop(bool uninstallJSSynchronizationContext);
36+
#endif
37+
3138
#region Legacy
3239

3340
[MethodImplAttribute(MethodImplOptions.InternalCall)]

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,6 @@
5858
<Compile Include="System\Runtime\InteropServices\JavaScript\Marshaling\JSMarshalerArgument.JSObject.cs" />
5959
<Compile Include="System\Runtime\InteropServices\JavaScript\Marshaling\JSMarshalerArgument.String.cs" />
6060
<Compile Include="System\Runtime\InteropServices\JavaScript\Marshaling\JSMarshalerArgument.Exception.cs" />
61-
62-
<Compile Include="System\Runtime\InteropServices\JavaScript\JSSynchronizationContext.cs" />
6361
</ItemGroup>
6462

6563
<!-- only include legacy interop when WasmEnableLegacyJsInterop is enabled -->
@@ -74,6 +72,12 @@
7472
<Compile Include="System\Runtime\InteropServices\JavaScript\Legacy\LegacyHostImplementation.cs" />
7573
</ItemGroup>
7674

75+
<!-- only include threads support when FeatureWasmThreads is enabled -->
76+
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'browser' and '$(FeatureWasmThreads)' == 'true'">
77+
<Compile Include="System\Runtime\InteropServices\JavaScript\WebWorker.cs" />
78+
<Compile Include="System\Runtime\InteropServices\JavaScript\JSSynchronizationContext.cs" />
79+
</ItemGroup>
80+
7781
<ItemGroup>
7882
<Reference Include="System.Collections" />
7983
<Reference Include="System.Memory" />

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Reflection;
55
using System.Runtime.CompilerServices;
66
using System.Threading.Tasks;
7+
using System.Diagnostics.CodeAnalysis;
78

89
namespace System.Runtime.InteropServices.JavaScript
910
{
@@ -219,11 +220,12 @@ public static void GetManagedStackTrace(JSMarshalerArgument* arguments_buffer)
219220

220221
// the marshaled signature is:
221222
// void InstallSynchronizationContext()
223+
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, "System.Runtime.InteropServices.JavaScript.WebWorker", "System.Runtime.InteropServices.JavaScript")]
222224
public static void InstallSynchronizationContext (JSMarshalerArgument* arguments_buffer) {
223225
ref JSMarshalerArgument arg_exc = ref arguments_buffer[0]; // initialized by caller in alloc_stack_frame()
224226
try
225227
{
226-
JSSynchronizationContext.Install();
228+
JSHostImplementation.InstallWebWorkerInterop(true);
227229
}
228230
catch (Exception ex)
229231
{

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

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public static Dictionary<int, WeakReference<JSObject>> ThreadCsOwnedObjects
2424
{
2525
get
2626
{
27-
s_csOwnedObjects ??= new ();
27+
s_csOwnedObjects ??= new();
2828
return s_csOwnedObjects;
2929
}
3030
}
@@ -197,5 +197,71 @@ public static JSObject CreateCSOwnedProxy(nint jsHandle)
197197
}
198198
return res;
199199
}
200+
201+
#if FEATURE_WASM_THREADS
202+
public static void InstallWebWorkerInterop(bool installJSSynchronizationContext)
203+
{
204+
Interop.Runtime.InstallWebWorkerInterop(installJSSynchronizationContext);
205+
if (installJSSynchronizationContext)
206+
{
207+
var currentThreadId = GetNativeThreadId();
208+
var ctx = JSSynchronizationContext.CurrentJSSynchronizationContext;
209+
if (ctx == null)
210+
{
211+
ctx = new JSSynchronizationContext(Thread.CurrentThread, currentThreadId);
212+
ctx.previousSynchronizationContext = SynchronizationContext.Current;
213+
JSSynchronizationContext.CurrentJSSynchronizationContext = ctx;
214+
SynchronizationContext.SetSynchronizationContext(ctx);
215+
}
216+
else if (ctx.TargetThreadId != currentThreadId)
217+
{
218+
Environment.FailFast($"JSSynchronizationContext.Install failed has wrong native thread id {ctx.TargetThreadId} != {currentThreadId}");
219+
}
220+
ctx.AwaitNewData();
221+
}
222+
}
223+
224+
public static void UninstallWebWorkerInterop()
225+
{
226+
var ctx = SynchronizationContext.Current as JSSynchronizationContext;
227+
var uninstallJSSynchronizationContext = ctx != null;
228+
if (uninstallJSSynchronizationContext)
229+
{
230+
SynchronizationContext.SetSynchronizationContext(ctx!.previousSynchronizationContext);
231+
JSSynchronizationContext.CurrentJSSynchronizationContext = null;
232+
ctx.isDisposed = true;
233+
}
234+
Interop.Runtime.UninstallWebWorkerInterop(uninstallJSSynchronizationContext);
235+
}
236+
237+
private static FieldInfo? thread_id_Field;
238+
private static FieldInfo? external_eventloop_Field;
239+
240+
// FIXME: after https://github.com/dotnet/runtime/issues/86040 replace with
241+
// [UnsafeAccessor(UnsafeAccessorKind.Field, Name="external_eventloop")]
242+
// static extern ref bool ThreadExternalEventloop(Thread @this);
243+
[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, "System.Threading.Thread", "System.Private.CoreLib")]
244+
public static void SetHasExternalEventLoop(Thread thread)
245+
{
246+
if (external_eventloop_Field == null)
247+
{
248+
external_eventloop_Field = typeof(Thread).GetField("external_eventloop", BindingFlags.NonPublic | BindingFlags.Instance)!;
249+
}
250+
external_eventloop_Field.SetValue(thread, true);
251+
}
252+
253+
// FIXME: after https://github.com/dotnet/runtime/issues/86040
254+
[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicFields, "System.Threading.Thread", "System.Private.CoreLib")]
255+
public static IntPtr GetNativeThreadId()
256+
{
257+
if (thread_id_Field == null)
258+
{
259+
thread_id_Field = typeof(Thread).GetField("thread_id", BindingFlags.NonPublic | BindingFlags.Instance)!;
260+
}
261+
return (int)(long)thread_id_Field.GetValue(Thread.CurrentThread)!;
262+
}
263+
264+
#endif
265+
200266
}
201267
}

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

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,31 @@
33

44
#if FEATURE_WASM_THREADS
55

6-
using System;
76
using System.Threading;
87
using System.Threading.Channels;
9-
using System.Runtime;
10-
using System.Runtime.InteropServices;
118
using System.Runtime.CompilerServices;
12-
using QueueType = System.Threading.Channels.Channel<System.Runtime.InteropServices.JavaScript.JSSynchronizationContext.WorkItem>;
9+
using WorkItemQueueType = System.Threading.Channels.Channel<System.Runtime.InteropServices.JavaScript.JSSynchronizationContext.WorkItem>;
1310

1411
namespace System.Runtime.InteropServices.JavaScript
1512
{
1613
/// <summary>
1714
/// Provides a thread-safe default SynchronizationContext for the browser that will automatically
18-
/// route callbacks to the main browser thread where they can interact with the DOM and other
15+
/// route callbacks to the original browser thread where they can interact with the DOM and other
1916
/// thread-affinity-having APIs like WebSockets, fetch, WebGL, etc.
2017
/// Callbacks are processed during event loop turns via the runtime's background job system.
18+
/// See also https://github.com/dotnet/runtime/blob/main/src/mono/wasm/threads.md#JS-interop-on-dedicated-threads
2119
/// </summary>
2220
internal sealed class JSSynchronizationContext : SynchronizationContext
2321
{
24-
public readonly Thread MainThread;
22+
private readonly Action _DataIsAvailable;// don't allocate Action on each call to UnsafeOnCompleted
23+
public readonly Thread TargetThread;
24+
public readonly IntPtr TargetThreadId;
25+
private readonly WorkItemQueueType Queue;
26+
27+
[ThreadStatic]
28+
internal static JSSynchronizationContext? CurrentJSSynchronizationContext;
29+
internal SynchronizationContext? previousSynchronizationContext;
30+
internal bool isDisposed;
2531

2632
internal readonly struct WorkItem
2733
{
@@ -37,34 +43,33 @@ public WorkItem(SendOrPostCallback callback, object? data, ManualResetEventSlim?
3743
}
3844
}
3945

40-
private static JSSynchronizationContext? MainThreadSynchronizationContext;
41-
private readonly QueueType Queue;
42-
private readonly Action _DataIsAvailable;// don't allocate Action on each call to UnsafeOnCompleted
43-
44-
private JSSynchronizationContext()
46+
internal JSSynchronizationContext(Thread targetThread, IntPtr targetThreadId)
4547
: this(
46-
Thread.CurrentThread,
48+
targetThread, targetThreadId,
4749
Channel.CreateUnbounded<WorkItem>(
4850
new UnboundedChannelOptions { SingleWriter = false, SingleReader = true, AllowSynchronousContinuations = true }
4951
)
5052
)
5153
{
5254
}
5355

54-
private JSSynchronizationContext(Thread mainThread, QueueType queue)
56+
private JSSynchronizationContext(Thread targetThread, IntPtr targetThreadId, WorkItemQueueType queue)
5557
{
56-
MainThread = mainThread;
58+
TargetThread = targetThread;
59+
TargetThreadId = targetThreadId;
5760
Queue = queue;
5861
_DataIsAvailable = DataIsAvailable;
5962
}
6063

6164
public override SynchronizationContext CreateCopy()
6265
{
63-
return new JSSynchronizationContext(MainThread, Queue);
66+
return new JSSynchronizationContext(TargetThread, TargetThreadId, Queue);
6467
}
6568

66-
private void AwaitNewData()
69+
internal void AwaitNewData()
6770
{
71+
ObjectDisposedException.ThrowIf(isDisposed, this);
72+
6873
var vt = Queue.Reader.WaitToReadAsync();
6974
if (vt.IsCompleted)
7075
{
@@ -84,11 +89,13 @@ private unsafe void DataIsAvailable()
8489
{
8590
// While we COULD pump here, we don't want to. We want the pump to happen on the next event loop turn.
8691
// Otherwise we could get a chain where a pump generates a new work item and that makes us pump again, forever.
87-
MainThreadScheduleBackgroundJob((void*)(delegate* unmanaged[Cdecl]<void>)&BackgroundJobHandler);
92+
TargetThreadScheduleBackgroundJob(TargetThreadId, (void*)(delegate* unmanaged[Cdecl]<void>)&BackgroundJobHandler);
8893
}
8994

9095
public override void Post(SendOrPostCallback d, object? state)
9196
{
97+
ObjectDisposedException.ThrowIf(isDisposed, this);
98+
9299
var workItem = new WorkItem(d, state, null);
93100
if (!Queue.Writer.TryWrite(workItem))
94101
throw new Exception("Internal error");
@@ -99,7 +106,9 @@ public override void Post(SendOrPostCallback d, object? state)
99106

100107
public override void Send(SendOrPostCallback d, object? state)
101108
{
102-
if (Thread.CurrentThread == MainThread)
109+
ObjectDisposedException.ThrowIf(isDisposed, this);
110+
111+
if (Thread.CurrentThread == TargetThread)
103112
{
104113
d(state);
105114
return;
@@ -115,27 +124,25 @@ public override void Send(SendOrPostCallback d, object? state)
115124
}
116125
}
117126

118-
internal static void Install()
119-
{
120-
MainThreadSynchronizationContext ??= new JSSynchronizationContext();
121-
SynchronizationContext.SetSynchronizationContext(MainThreadSynchronizationContext);
122-
MainThreadSynchronizationContext.AwaitNewData();
123-
}
124-
125127
[MethodImplAttribute(MethodImplOptions.InternalCall)]
126-
internal static extern unsafe void MainThreadScheduleBackgroundJob(void* callback);
128+
internal static extern unsafe void TargetThreadScheduleBackgroundJob(IntPtr targetThread, void* callback);
127129

128130
#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant
129131
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
130132
#pragma warning restore CS3016
131-
// this callback will arrive on the bound thread, called from mono_background_exec
133+
// this callback will arrive on the target thread, called from mono_background_exec
132134
private static void BackgroundJobHandler()
133135
{
134-
MainThreadSynchronizationContext!.Pump();
136+
CurrentJSSynchronizationContext!.Pump();
135137
}
136138

137139
private void Pump()
138140
{
141+
if (isDisposed)
142+
{
143+
// FIXME: there could be abandoned work, but here we have no way how to propagate the failure
144+
return;
145+
}
139146
try
140147
{
141148
while (Queue.Reader.TryRead(out var item))
@@ -160,7 +167,7 @@ private void Pump()
160167
finally
161168
{
162169
// If an item throws, we want to ensure that the next pump gets scheduled appropriately regardless.
163-
AwaitNewData();
170+
if(!isDisposed) AwaitNewData();
164171
}
165172
}
166173
}

0 commit comments

Comments
 (0)