Skip to content

Commit

Permalink
[browser][mt] dynamic thread create (#95702)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelsavara committed Dec 13, 2023
1 parent 8b22543 commit f6a96fb
Show file tree
Hide file tree
Showing 21 changed files with 261 additions and 195 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
<TestRuntime>true</TestRuntime>
<EnableDefaultItems>true</EnableDefaultItems>
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
<_WasmPThreadPoolSize Condition="'$(MonoWasmBuildVariant)' == 'multithread'">100</_WasmPThreadPoolSize>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:System.Runtime.InteropServices.JavaScript.WebWorker</Target>
<Target>T:System.Runtime.InteropServices.JavaScript.JSWebWorker</Target>
<Left>ref/net9.0/System.Runtime.InteropServices.JavaScript.dll</Left>
<Right>runtimes/browser/lib/net9.0/System.Runtime.InteropServices.JavaScript.dll</Right>
</Suppression>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
<WasmEnableJsInteropByValue Condition="'$(TargetPlatformIdentifier)' == 'browser' and '$(WasmEnableJsInteropByValue)' == '' and '$(FeatureWasmThreads)' == 'true'">true</WasmEnableJsInteropByValue>
<WasmEnableJsInteropByValue Condition="'$(TargetPlatformIdentifier)' == 'browser' and '$(WasmEnableJsInteropByValue)' == ''">false</WasmEnableJsInteropByValue>
<WasmEnableLegacyJsInterop Condition="'$(TargetPlatformIdentifier)' == 'browser' and '$(WasmEnableLegacyJsInterop)' == ''">true</WasmEnableLegacyJsInterop>
<DefineConstants Condition="'$(FeatureWasmThreads)' == 'true'" >$(DefineConstants);FEATURE_WASM_THREADS</DefineConstants>
<DefineConstants Condition="'$(WasmEnableLegacyJsInterop)' == 'false'" >$(DefineConstants);DISABLE_LEGACY_JS_INTEROP</DefineConstants>
<DefineConstants Condition="'$(WasmEnableJsInteropByValue)' == 'true'" >$(DefineConstants);ENABLE_JS_INTEROP_BY_VALUE</DefineConstants>
<DefineConstants Condition="'$(FeatureWasmThreads)' == 'true'">$(DefineConstants);FEATURE_WASM_THREADS</DefineConstants>
<DefineConstants Condition="'$(WasmEnableLegacyJsInterop)' == 'false'">$(DefineConstants);DISABLE_LEGACY_JS_INTEROP</DefineConstants>
<DefineConstants Condition="'$(WasmEnableJsInteropByValue)' == 'true'">$(DefineConstants);ENABLE_JS_INTEROP_BY_VALUE</DefineConstants>
<EmitCompilerGeneratedFiles Condition="'$(Configuration)' == 'Debug' and '$(TargetPlatformIdentifier)' == 'browser'">true</EmitCompilerGeneratedFiles>
</PropertyGroup>

Expand Down Expand Up @@ -81,7 +81,7 @@

<!-- 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\JSWebWorker.cs" />
<Compile Include="System\Runtime\InteropServices\JavaScript\JSSynchronizationContext.cs" />
</ItemGroup>
<ItemGroup Condition="'$(MonoWasmBuildVariant)' == 'multithread'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,10 @@ public static void GetManagedStackTrace(JSMarshalerArgument* arguments_buffer)

#if FEATURE_WASM_THREADS

// this is here temporarily, until JSWebWorker becomes public API
[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, "System.Runtime.InteropServices.JavaScript.JSWebWorker", "System.Runtime.InteropServices.JavaScript")]
// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ internal static unsafe partial class JavaScriptImports
public static partial JSObject GetDotnetInstance();
[JSImport("INTERNAL.dynamic_import")]
public static partial Task<JSObject> DynamicImport(string moduleName, string moduleUrl);
#if FEATURE_WASM_THREADS
[JSImport("INTERNAL.thread_available")]
public static partial Task ThreadAvailable();
#endif

#if DEBUG
[JSImport("globalThis.console.log")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,37 @@ namespace System.Runtime.InteropServices.JavaScript
/// This is draft for possible public API of browser thread (web worker) dedicated to JS interop workloads.
/// The method names are unique to make it easy to call them via reflection for now. All of them should be just `RunAsync` probably.
/// </summary>
public static class WebWorker
public static class JSWebWorker
{
public static Task<T> RunAsync<T>(Func<Task<T>> body, CancellationToken cancellationToken)
// temporary, for easy reflection
internal static Task RunAsyncVoid(Func<Task> body, CancellationToken cancellationToken) => RunAsync(body, cancellationToken);
internal static Task<T> RunAsyncGeneric<T>(Func<Task<T>> body, CancellationToken cancellationToken) => RunAsync(body, cancellationToken);

public static Task<T> RunAsync<T>(Func<Task<T>> body)
{
return RunAsync(body, CancellationToken.None);
}

public static Task RunAsync(Func<Task> body)
{
return RunAsync(body, CancellationToken.None);
}

public static async Task<T> RunAsync<T>(Func<Task<T>> body, CancellationToken cancellationToken)
{
// TODO remove main thread condition later
if (Thread.CurrentThread.ManagedThreadId == 1) await JavaScriptImports.ThreadAvailable().ConfigureAwait(false);
return await RunAsyncImpl(body, cancellationToken).ConfigureAwait(false);
}

public static async Task RunAsync(Func<Task> body, CancellationToken cancellationToken)
{
// TODO remove main thread condition later
if (Thread.CurrentThread.ManagedThreadId == 1) await JavaScriptImports.ThreadAvailable().ConfigureAwait(false);
await RunAsyncImpl(body, cancellationToken).ConfigureAwait(false);
}

private static Task<T> RunAsyncImpl<T>(Func<Task<T>> body, CancellationToken cancellationToken)
{
var parentContext = SynchronizationContext.Current ?? new SynchronizationContext();
var tcs = new TaskCompletionSource<T>();
Expand Down Expand Up @@ -53,7 +81,7 @@ public static Task<T> RunAsync<T>(Func<Task<T>> body, CancellationToken cancella
return tcs.Task;
}

public static Task RunAsyncVoid(Func<Task> body, CancellationToken cancellationToken)
private static Task RunAsyncImpl(Func<Task> body, CancellationToken cancellationToken)
{
var parentContext = SynchronizationContext.Current ?? new SynchronizationContext();
var tcs = new TaskCompletionSource();
Expand Down Expand Up @@ -90,44 +118,6 @@ public static Task RunAsyncVoid(Func<Task> body, CancellationToken cancellationT
return tcs.Task;
}

public static Task Run(Action body, CancellationToken cancellationToken)
{
var parentContext = SynchronizationContext.Current ?? new SynchronizationContext();
var tcs = new TaskCompletionSource();
var capturedContext = SynchronizationContext.Current;
var t = new Thread(() =>
{
try
{
if (cancellationToken.IsCancellationRequested)
{
PostWhenCancellation(parentContext, tcs);
return;
}
JSHostImplementation.InstallWebWorkerInterop(false);
try
{
body();
SendWhenDone(parentContext, tcs);
}
catch (Exception ex)
{
SendWhenException(parentContext, tcs, ex);
}
JSHostImplementation.UninstallWebWorkerInterop();
}
catch (Exception ex)
{
SendWhenException(parentContext, tcs, ex);
}
});
JSHostImplementation.SetHasExternalEventLoop(t);
t.Start();
return tcs.Task;
}

#region posting result to the original thread when handling exception

private static void PostWhenCancellation(SynchronizationContext ctx, TaskCompletionSource tcs)
Expand All @@ -138,7 +128,7 @@ private static void PostWhenCancellation(SynchronizationContext ctx, TaskComplet
}
catch (Exception e)
{
Environment.FailFast("WebWorker.RunAsync failed", e);
Environment.FailFast("JSWebWorker.RunAsync failed", e);
}
}

Expand All @@ -150,7 +140,7 @@ private static void PostWhenCancellation<T>(SynchronizationContext ctx, TaskComp
}
catch (Exception e)
{
Environment.FailFast("WebWorker.RunAsync failed", e);
Environment.FailFast("JSWebWorker.RunAsync failed", e);
}
}

Expand All @@ -165,19 +155,7 @@ private static void SendWhenDone(SynchronizationContext ctx, TaskCompletionSourc
}
catch (Exception e)
{
Environment.FailFast("WebWorker.RunAsync failed", e);
}
}

private static void SendWhenDone(SynchronizationContext ctx, TaskCompletionSource tcs)
{
try
{
ctx.Send((_) => tcs.SetResult(), null);
}
catch (Exception e)
{
Environment.FailFast("WebWorker.RunAsync failed", e);
Environment.FailFast("JSWebWorker.RunAsync failed", e);
}
}

Expand All @@ -189,7 +167,7 @@ private static void SendWhenException(SynchronizationContext ctx, TaskCompletion
}
catch (Exception e)
{
Environment.FailFast("WebWorker.RunAsync failed", e);
Environment.FailFast("JSWebWorker.RunAsync failed", e);
}
}

Expand All @@ -201,7 +179,7 @@ private static void SendWhenException<T>(SynchronizationContext ctx, TaskComplet
}
catch (Exception e)
{
Environment.FailFast("WebWorker.RunAsync failed", e);
Environment.FailFast("JSWebWorker.RunAsync failed", e);
}
}

Expand All @@ -216,15 +194,15 @@ private static void SendWhenDone<T>(SynchronizationContext ctx, TaskCompletionSo
}
catch (Exception e)
{
Environment.FailFast("WebWorker.RunAsync failed", e);
Environment.FailFast("JSWebWorker.RunAsync failed", e);
}
}

internal static void PropagateCompletion<T>(TaskCompletionSource<T> tcs, Task<T> done)
{
if (done.IsFaulted)
{
if(done.Exception is AggregateException ag && ag.InnerException!=null)
if (done.Exception is AggregateException ag && ag.InnerException != null)
{
tcs.SetException(ag.InnerException);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
<TestRuntime>true</TestRuntime>
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
<TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
<_WasmPThreadPoolSize Condition="'$(MonoWasmBuildVariant)' == 'multithread'">64</_WasmPThreadPoolSize>
</PropertyGroup>
<ItemGroup>
<Compile Include="Helpers.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,70 +7,53 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

namespace System.Runtime.InteropServices.JavaScript
namespace Sample
{
// this is just temporary thin wrapper to expose future public API
public partial class WebWorker
public partial class JSWebWorker
{
private static MethodInfo runAsyncMethod;
private static MethodInfo runAsyncVoidMethod;
private static MethodInfo runMethod;

[DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, "System.Runtime.InteropServices.JavaScript.WebWorker", "System.Runtime.InteropServices.JavaScript")]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern", Justification = "work in progress")]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", Justification = "work in progress")]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060:UnrecognizedReflectionPattern", Justification = "work in progress")]
public static Task<T> RunAsync<T>(Func<Task<T>> body, CancellationToken cancellationToken)
public static Task RunAsync(Func<Task> body)
{
if(runAsyncMethod == null)
{
var webWorker = typeof(JSObject).Assembly.GetType("System.Runtime.InteropServices.JavaScript.WebWorker");
runAsyncMethod = webWorker.GetMethod("RunAsync", BindingFlags.Public|BindingFlags.Static);
}

var genericRunAsyncMethod = runAsyncMethod.MakeGenericMethod(typeof(T));
return (Task<T>)genericRunAsyncMethod.Invoke(null, new object[] { body, cancellationToken });
return RunAsync(body, CancellationToken.None);
}

public static Task<T> RunAsync<T>(Func<Task<T>> body)
{
return RunAsync(body, CancellationToken.None);
}

[DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, "System.Runtime.InteropServices.JavaScript.WebWorker", "System.Runtime.InteropServices.JavaScript")]

[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, "System.Runtime.InteropServices.JavaScript.JSWebWorker", "System.Runtime.InteropServices.JavaScript")]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern", Justification = "work in progress")]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", Justification = "work in progress")]
public static Task RunAsync(Func<Task> body, CancellationToken cancellationToken)
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060:UnrecognizedReflectionPattern", Justification = "work in progress")]
public static Task<T> RunAsync<T>(Func<Task<T>> body, CancellationToken cancellationToken)
{
if(runAsyncVoidMethod == null)
if(runAsyncMethod == null)
{
var webWorker = typeof(JSObject).Assembly.GetType("System.Runtime.InteropServices.JavaScript.WebWorker");
runAsyncVoidMethod = webWorker.GetMethod("RunAsyncVoid", BindingFlags.Public|BindingFlags.Static);
var webWorker = typeof(JSObject).Assembly.GetType("System.Runtime.InteropServices.JavaScript.JSWebWorker");
runAsyncMethod = webWorker.GetMethod("RunAsyncGeneric", BindingFlags.NonPublic|BindingFlags.Static);
}
return (Task)runAsyncVoidMethod.Invoke(null, new object[] { body, cancellationToken });
}

public static Task RunAsync(Func<Task> body)
{
return RunAsync(body, CancellationToken.None);
var genericRunAsyncMethod = runAsyncMethod.MakeGenericMethod(typeof(T));
return (Task<T>)genericRunAsyncMethod.Invoke(null, new object[] { body, cancellationToken });
}

[DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, "System.Runtime.InteropServices.JavaScript.WebWorker", "System.Runtime.InteropServices.JavaScript")]
[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, "System.Runtime.InteropServices.JavaScript.JSWebWorker", "System.Runtime.InteropServices.JavaScript")]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern", Justification = "work in progress")]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", Justification = "work in progress")]
public static Task RunAsync(Action body, CancellationToken cancellationToken)
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060:UnrecognizedReflectionPattern", Justification = "work in progress")]
public static Task RunAsync(Func<Task> body, CancellationToken cancellationToken)
{
if(runMethod == null)
if(runAsyncVoidMethod == null)
{
var webWorker = typeof(JSObject).Assembly.GetType("System.Runtime.InteropServices.JavaScript.WebWorker");
runMethod = webWorker.GetMethod("Run", BindingFlags.Public|BindingFlags.Static);
var webWorker = typeof(JSObject).Assembly.GetType("System.Runtime.InteropServices.JavaScript.JSWebWorker");
runAsyncVoidMethod = webWorker.GetMethod("RunAsyncVoid", BindingFlags.NonPublic|BindingFlags.Static);
}
return (Task)runMethod.Invoke(null, new object[] { body, cancellationToken });
}

public static Task RunAsync(Action body)
{
return RunAsync(body, CancellationToken.None);
return (Task)runAsyncVoidMethod.Invoke(null, new object[] { body, cancellationToken });
}
}
}
Loading

0 comments on commit f6a96fb

Please sign in to comment.