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] GC, threadpool and some JS interop improvements #86759

Merged
merged 20 commits into from
Jun 12, 2023
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
1 change: 1 addition & 0 deletions eng/Subsets.props
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@
</ItemGroup>

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

Expand Down
7 changes: 7 additions & 0 deletions src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ internal static unsafe partial class Runtime
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void DeregisterGCRoot(IntPtr handle);

#if FEATURE_WASM_THREADS
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void InstallWebWorkerInterop(bool installJSSynchronizationContext);
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void UninstallWebWorkerInterop(bool uninstallJSSynchronizationContext);
#endif

#region Legacy

[MethodImplAttribute(MethodImplOptions.InternalCall)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@
<Compile Include="System\Runtime\InteropServices\JavaScript\Marshaling\JSMarshalerArgument.JSObject.cs" />
<Compile Include="System\Runtime\InteropServices\JavaScript\Marshaling\JSMarshalerArgument.String.cs" />
<Compile Include="System\Runtime\InteropServices\JavaScript\Marshaling\JSMarshalerArgument.Exception.cs" />

<Compile Include="System\Runtime\InteropServices\JavaScript\JSSynchronizationContext.cs" />
</ItemGroup>

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

<!-- only include threads support when FeatureWasmThreads is enabled -->
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'browser' and '$(FeatureWasmThreads)' == 'true'">
<Compile Include="System\Runtime\InteropServices\JavaScript\WebWorker.cs" />
<Compile Include="System\Runtime\InteropServices\JavaScript\JSSynchronizationContext.cs" />
</ItemGroup>

<ItemGroup>
<Reference Include="System.Collections" />
<Reference Include="System.Memory" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Diagnostics.CodeAnalysis;

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

// the marshaled signature is:
// void InstallSynchronizationContext()
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, "System.Runtime.InteropServices.JavaScript.WebWorker", "System.Runtime.InteropServices.JavaScript")]
public static void InstallSynchronizationContext (JSMarshalerArgument* arguments_buffer) {
ref JSMarshalerArgument arg_exc = ref arguments_buffer[0]; // initialized by caller in alloc_stack_frame()
try
{
JSSynchronizationContext.Install();
JSHostImplementation.InstallWebWorkerInterop(true);
}
catch (Exception ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public static Dictionary<int, WeakReference<JSObject>> ThreadCsOwnedObjects
{
get
{
s_csOwnedObjects ??= new ();
s_csOwnedObjects ??= new();
return s_csOwnedObjects;
}
}
Expand Down Expand Up @@ -197,5 +197,71 @@ public static JSObject CreateCSOwnedProxy(nint jsHandle)
}
return res;
}

#if FEATURE_WASM_THREADS
public static void InstallWebWorkerInterop(bool installJSSynchronizationContext)
{
Interop.Runtime.InstallWebWorkerInterop(installJSSynchronizationContext);
if (installJSSynchronizationContext)
{
var currentThreadId = GetNativeThreadId();
var ctx = JSSynchronizationContext.CurrentJSSynchronizationContext;
pavelsavara marked this conversation as resolved.
Show resolved Hide resolved
if (ctx == null)
{
ctx = new JSSynchronizationContext(Thread.CurrentThread, currentThreadId);
ctx.previousSynchronizationContext = SynchronizationContext.Current;
JSSynchronizationContext.CurrentJSSynchronizationContext = ctx;
SynchronizationContext.SetSynchronizationContext(ctx);
}
else if (ctx.TargetThreadId != currentThreadId)
{
Environment.FailFast($"JSSynchronizationContext.Install failed has wrong native thread id {ctx.TargetThreadId} != {currentThreadId}");
}
ctx.AwaitNewData();
}
}

public static void UninstallWebWorkerInterop()
{
var ctx = SynchronizationContext.Current as JSSynchronizationContext;
var uninstallJSSynchronizationContext = ctx != null;
if (uninstallJSSynchronizationContext)
{
SynchronizationContext.SetSynchronizationContext(ctx!.previousSynchronizationContext);
JSSynchronizationContext.CurrentJSSynchronizationContext = null;
ctx.isDisposed = true;
}
Interop.Runtime.UninstallWebWorkerInterop(uninstallJSSynchronizationContext);
}

private static FieldInfo? thread_id_Field;
private static FieldInfo? external_eventloop_Field;

// FIXME: after https://github.com/dotnet/runtime/issues/86040 replace with
// [UnsafeAccessor(UnsafeAccessorKind.Field, Name="external_eventloop")]
// static extern ref bool ThreadExternalEventloop(Thread @this);
[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, "System.Threading.Thread", "System.Private.CoreLib")]
public static void SetHasExternalEventLoop(Thread thread)
{
if (external_eventloop_Field == null)
{
external_eventloop_Field = typeof(Thread).GetField("external_eventloop", BindingFlags.NonPublic | BindingFlags.Instance)!;
pavelsavara marked this conversation as resolved.
Show resolved Hide resolved
}
external_eventloop_Field.SetValue(thread, true);
}

// FIXME: after https://github.com/dotnet/runtime/issues/86040
[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicFields, "System.Threading.Thread", "System.Private.CoreLib")]
public static IntPtr GetNativeThreadId()
{
if (thread_id_Field == null)
{
thread_id_Field = typeof(Thread).GetField("thread_id", BindingFlags.NonPublic | BindingFlags.Instance)!;
}
return (int)(long)thread_id_Field.GetValue(Thread.CurrentThread)!;
pavelsavara marked this conversation as resolved.
Show resolved Hide resolved
}

#endif

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,31 @@

#if FEATURE_WASM_THREADS

using System;
using System.Threading;
using System.Threading.Channels;
using System.Runtime;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using QueueType = System.Threading.Channels.Channel<System.Runtime.InteropServices.JavaScript.JSSynchronizationContext.WorkItem>;
using WorkItemQueueType = System.Threading.Channels.Channel<System.Runtime.InteropServices.JavaScript.JSSynchronizationContext.WorkItem>;

namespace System.Runtime.InteropServices.JavaScript
{
/// <summary>
/// Provides a thread-safe default SynchronizationContext for the browser that will automatically
/// route callbacks to the main browser thread where they can interact with the DOM and other
/// route callbacks to the original browser thread where they can interact with the DOM and other
/// thread-affinity-having APIs like WebSockets, fetch, WebGL, etc.
/// Callbacks are processed during event loop turns via the runtime's background job system.
/// See also https://github.com/dotnet/runtime/blob/main/src/mono/wasm/threads.md#JS-interop-on-dedicated-threads
/// </summary>
internal sealed class JSSynchronizationContext : SynchronizationContext
{
public readonly Thread MainThread;
private readonly Action _DataIsAvailable;// don't allocate Action on each call to UnsafeOnCompleted
public readonly Thread TargetThread;
public readonly IntPtr TargetThreadId;
private readonly WorkItemQueueType Queue;

[ThreadStatic]
internal static JSSynchronizationContext? CurrentJSSynchronizationContext;
internal SynchronizationContext? previousSynchronizationContext;
internal bool isDisposed;

internal readonly struct WorkItem
{
Expand All @@ -37,34 +43,33 @@ public WorkItem(SendOrPostCallback callback, object? data, ManualResetEventSlim?
}
}

private static JSSynchronizationContext? MainThreadSynchronizationContext;
private readonly QueueType Queue;
private readonly Action _DataIsAvailable;// don't allocate Action on each call to UnsafeOnCompleted

private JSSynchronizationContext()
internal JSSynchronizationContext(Thread targetThread, IntPtr targetThreadId)
: this(
Thread.CurrentThread,
targetThread, targetThreadId,
Channel.CreateUnbounded<WorkItem>(
new UnboundedChannelOptions { SingleWriter = false, SingleReader = true, AllowSynchronousContinuations = true }
)
)
{
}

private JSSynchronizationContext(Thread mainThread, QueueType queue)
private JSSynchronizationContext(Thread targetThread, IntPtr targetThreadId, WorkItemQueueType queue)
pavelsavara marked this conversation as resolved.
Show resolved Hide resolved
{
MainThread = mainThread;
TargetThread = targetThread;
TargetThreadId = targetThreadId;
Queue = queue;
_DataIsAvailable = DataIsAvailable;
}

public override SynchronizationContext CreateCopy()
{
return new JSSynchronizationContext(MainThread, Queue);
return new JSSynchronizationContext(TargetThread, TargetThreadId, Queue);
}

private void AwaitNewData()
internal void AwaitNewData()
{
ObjectDisposedException.ThrowIf(isDisposed, this);

var vt = Queue.Reader.WaitToReadAsync();
if (vt.IsCompleted)
{
Expand All @@ -84,11 +89,13 @@ private unsafe void DataIsAvailable()
{
// While we COULD pump here, we don't want to. We want the pump to happen on the next event loop turn.
// Otherwise we could get a chain where a pump generates a new work item and that makes us pump again, forever.
MainThreadScheduleBackgroundJob((void*)(delegate* unmanaged[Cdecl]<void>)&BackgroundJobHandler);
TargetThreadScheduleBackgroundJob(TargetThreadId, (void*)(delegate* unmanaged[Cdecl]<void>)&BackgroundJobHandler);
}

public override void Post(SendOrPostCallback d, object? state)
{
ObjectDisposedException.ThrowIf(isDisposed, this);

var workItem = new WorkItem(d, state, null);
if (!Queue.Writer.TryWrite(workItem))
throw new Exception("Internal error");
Expand All @@ -99,7 +106,9 @@ public override void Post(SendOrPostCallback d, object? state)

public override void Send(SendOrPostCallback d, object? state)
{
if (Thread.CurrentThread == MainThread)
ObjectDisposedException.ThrowIf(isDisposed, this);

if (Thread.CurrentThread == TargetThread)
{
d(state);
return;
Expand All @@ -115,27 +124,25 @@ public override void Send(SendOrPostCallback d, object? state)
}
}

internal static void Install()
{
MainThreadSynchronizationContext ??= new JSSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(MainThreadSynchronizationContext);
MainThreadSynchronizationContext.AwaitNewData();
}

[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern unsafe void MainThreadScheduleBackgroundJob(void* callback);
internal static extern unsafe void TargetThreadScheduleBackgroundJob(IntPtr targetThread, void* callback);

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

private void Pump()
{
if (isDisposed)
{
// FIXME: there could be abandoned work, but here we have no way how to propagate the failure
return;
}
try
{
while (Queue.Reader.TryRead(out var item))
Expand All @@ -160,7 +167,7 @@ private void Pump()
finally
{
// If an item throws, we want to ensure that the next pump gets scheduled appropriately regardless.
AwaitNewData();
if(!isDisposed) AwaitNewData();
}
}
}
Expand Down
Loading