Skip to content

Commit

Permalink
Add benchmarks, GC handle tracking, tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jasongin committed Oct 4, 2023
1 parent 9ab1030 commit 80131d1
Show file tree
Hide file tree
Showing 15 changed files with 582 additions and 57 deletions.
243 changes: 214 additions & 29 deletions bench/Benchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,80 +7,265 @@
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;
using Microsoft.JavaScript.NodeApi.DotNetHost;
using Microsoft.JavaScript.NodeApi.Interop;
using Microsoft.JavaScript.NodeApi.Runtimes;
using static Microsoft.JavaScript.NodeApi.JSNativeApi.Interop;
using static Microsoft.JavaScript.NodeApi.Test.TestUtils;

namespace Microsoft.JavaScript.NodeApi.Bench;

/// <summary>
/// Micro-benchmarks for various .NET + JS interop operations.
/// </summary>
/// <remarks>
/// These benchmarks run both .NET and Node.js code, and call between them. The benchmark
/// runner manages the GC for the .NET runtime, but it doesn't know anything about the JS runtime.
/// To avoid heavy JS GC pressure from millions of operations (which may each allocate objects),
/// these benchmarks use the `ShortRunJob` attribute (which sacrifices some precision but also
/// doesn't take as long to run).
/// </remarks>
[IterationCount(5)]
[WarmupCount(1)]
public abstract class Benchmarks
{
public static void Main(string[] args)
{
// Example: dotnet run -c Release --filter aot
#if DEBUG
IConfig config = new DebugBuildConfig();
#else
IConfig config = DefaultConfig.Instance;
#endif

// Example: dotnet run -c Release --filter clr
// If no filter is specified, the switcher will prompt.
BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args,
ManualConfig.Create(DefaultConfig.Instance).WithOptions(ConfigOptions.JoinSummary));
}

public class NonAot : Benchmarks
{
// Non-AOT-only benchmarks may go here
}

[SimpleJob(RuntimeMoniker.NativeAot70)]
public class Aot : Benchmarks
{
// AOT-only benchmarks may go here
ManualConfig.Create(config)
.WithOptions(ConfigOptions.JoinSummary));
}

private static string LibnodePath { get; } = Path.Combine(
GetRepoRootDirectory(),
"bin",
"win-x64", // TODO
GetCurrentPlatformRuntimeIdentifier(),
"libnode" + GetSharedLibraryExtension());

private napi_env _env;
private JSValue _function;
private JSValue _callback;
private JSFunction _jsFunction;
private JSFunction _jsFunctionWithArgs;
private JSFunction _jsFunctionWithCallback;
private JSObject _jsInstance;
private JSFunction _dotnetFunction;
private JSFunction _dotnetFunctionWithArgs;
private JSObject _dotnetClass;
private JSObject _dotnetInstance;
private JSFunction _jsFunctionCreateInstance;
private JSFunction _jsFunctionCallMethod;
private JSFunction _jsFunctionCallMethodWithArgs;
private JSReference _reference = null!;

[GlobalSetup]
public void Setup()
/// <summary>
/// Simple class that is exported to JS and used in some benchmarks.
/// </summary>
private class DotnetClass
{
NodejsPlatform platform = new(LibnodePath);
public DotnetClass() { }

public string Property { get; set; } = string.Empty;

#pragma warning disable CA1822 // Method does not access instance data and can be marked as static
public static void Method() { }
#pragma warning restore CA1822
}

/// <summary>
/// Setup shared by both CLR and AOT benchmarks.
/// </summary>
protected void Setup()
{
NodejsPlatform platform = new(LibnodePath/*, args: new[] { "node", "--expose-gc" }*/);

// This setup avoids using NodejsEnvironment so benchmarks can run on the same thread.
// NodejsEnvironment creates a separate thread that would slow down the micro-benchmarks.
_env = JSNativeApi.CreateEnvironment(
(napi_platform)platform, (error) => Console.WriteLine(error), null);
_env = JSNativeApi.CreateEnvironment(platform, (error) => Console.WriteLine(error), null);

// The new scope instance saves itself as the thread-local JSValueScope.Current.
JSValueScope scope = new(JSValueScopeType.Root, _env);

// Create some JS values that will be used by the benchmarks.

_function = JSNativeApi.RunScript("function callMeBack(cb) { cb(); }; callMeBack");
_callback = JSValue.CreateFunction("callback", (args) => JSValue.Undefined);
_jsFunction = (JSFunction)JSNativeApi.RunScript("function jsFunction() { }; jsFunction");
_jsFunctionWithArgs = (JSFunction)JSNativeApi.RunScript(
"function jsFunctionWithArgs(a, b, c) { }; jsFunctionWithArgs");
_jsFunctionWithCallback = (JSFunction)JSNativeApi.RunScript(
"function jsFunctionWithCallback(cb, ...args) { cb(...args); }; " +
"jsFunctionWithCallback");
_jsInstance = (JSObject)JSNativeApi.RunScript(
"const jsInstance = { method: (...args) => {} }; jsInstance");

_dotnetFunction = (JSFunction)JSValue.CreateFunction(
"dotnetFunction", (args) => JSValue.Undefined);
_dotnetFunctionWithArgs = (JSFunction)JSValue.CreateFunction(
"dotnetFunctionWithArgs", (args) =>
{
for (int i = 0; i < args.Length; i++)
{
_ = args[i];
}
return JSValue.Undefined;
});

var classBuilder = new JSClassBuilder<DotnetClass>(
nameof(DotnetClass), () => new DotnetClass());
classBuilder.AddProperty(
"property",
(x) => x.Property,
(x, value) => x.Property = (string)value);
classBuilder.AddMethod("method", (x) => (args) => DotnetClass.Method());
_dotnetClass = (JSObject)classBuilder.DefineClass();
_dotnetInstance = (JSObject)JSNativeApi.CallAsConstructor(_dotnetClass);

_jsFunctionCreateInstance = (JSFunction)JSNativeApi.RunScript(
"function jsFunctionCreateInstance(Class) { new Class() }; " +
"jsFunctionCreateInstance");
_jsFunctionCallMethod = (JSFunction)JSNativeApi.RunScript(
"function jsFunctionCallMethod(instance) { instance.method(); }; " +
"jsFunctionCallMethod");
_jsFunctionCallMethodWithArgs = (JSFunction)JSNativeApi.RunScript(
"function jsFunctionCallMethodWithArgs(instance, ...args) " +
"{ instance.method(...args); }; " +
"jsFunctionCallMethodWithArgs");

_reference = new JSReference(_jsFunction);
}

private static JSValueScope NewJSScope() => new(JSValueScopeType.Callback);

// Benchmarks in the base class run in both CLR and AOT environments.

[Benchmark]
public void CallJSFunction()
{
_jsFunction.CallAsStatic();
}

[Benchmark]
public void CallJSFunctionWithArgs()
{
_jsFunctionWithArgs.CallAsStatic("1", "2", "3");
}

[Benchmark]
public void CallJSMethod()
{
_jsInstance.CallMethod("method");
}

[Benchmark]
public void CallJSMethodWithArgs()
{
_jsInstance.CallMethod("method", "1", "2", "3");
}

_reference = new JSReference(_function);
[Benchmark]
public void CallDotnetFunction()
{
_jsFunctionWithCallback.CallAsStatic(_dotnetFunction);
}

[Benchmark]
public void CallJS()
public void CallDotnetFunctionWithArgs()
{
_function.Call(thisArg: default, _callback);
_jsFunctionWithCallback.CallAsStatic(_dotnetFunctionWithArgs, "1", "2", "3");
}

[Benchmark]
public void GetReference()
public void CallDotnetConstructor()
{
_jsFunctionCreateInstance.CallAsStatic(_dotnetClass);
}

[Benchmark]
public void CallDotnetMethod()
{
_jsFunctionCallMethod.CallAsStatic(_dotnetInstance);
}

[Benchmark]
public void CallDotnetMethodWithArgs()
{
_jsFunctionCallMethodWithArgs.CallAsStatic(_dotnetInstance, "1", "2", "3");
}

[Benchmark]
public void ReferenceGet()
{
_ = _reference.GetValue()!.Value;
}

[Benchmark]
public void CreateAndDiposeReference()
public void ReferenceCreateAndDipose()
{
using JSReference reference = new(_jsFunction);
}

[ShortRunJob]
[MemoryDiagnoser(displayGenColumns: false)]
public class Clr : Benchmarks
{
private JSObject _jsHost;
private JSFunction _jsFunctionCallMethodDynamic;
private JSFunction _jsFunctionCallMethodDynamicInterface;

[GlobalSetup]
public new void Setup()
{
base.Setup();

// CLR-only (non-AOT) setup

JSObject hostModule = new();
_ = new ManagedHost(hostModule);
_jsHost = hostModule;
_jsFunctionCallMethodDynamic = (JSFunction)JSNativeApi.RunScript(
"function jsFunctionCallMethodDynamic(dotnet) " +
"{ dotnet.System.Object.ReferenceEquals(null, null); }; " +
"jsFunctionCallMethodDynamic");

// Implement IFormatProvider in JS and pass it to a .NET method.
_jsFunctionCallMethodDynamicInterface = (JSFunction)JSNativeApi.RunScript(
"function jsFunctionCallMethodDynamicInterface(dotnet) {" +
" const formatProvider = { GetFormat: (type) => null };" +
" dotnet.System.String.Format(formatProvider, '', null, null);" +
"}; " +
"jsFunctionCallMethodDynamicInterface");
}

// CLR-only (non-AOT) benchmarks

[Benchmark]
public void DynamicCallDotnetMethod()
{
_jsFunctionCallMethodDynamic.CallAsStatic(_jsHost);
}

[Benchmark]
public void DynamicCallDotnetMethodWithInterface()
{
_jsFunctionCallMethodDynamicInterface.CallAsStatic(_jsHost);
}
}

[ShortRunJob(RuntimeMoniker.NativeAot80)]
public class Aot : Benchmarks
{
using JSReference reference = new(_function);
[GlobalSetup]
public new void Setup()
{
base.Setup();
}

// AOT-only benchmarks
}
}
1 change: 1 addition & 0 deletions bench/NodeApi.Bench.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

<ItemGroup>
<Compile Include="../test/TestUtils.cs" Link="TestUtils.cs" />
<None Remove="BenchmarkDotNet.Artifacts/**" />
</ItemGroup>

<ItemGroup>
Expand Down
26 changes: 26 additions & 0 deletions bench/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Micro-benchmarks for node-api-dotnet APIs

This project contains a set of micro-benchmarks for .NET + JS interop operations, driven by
[BenchmarkDotNet](https://benchmarkdotnet.org/). Most benchmarks run in both CLR and AOT modes,
though the "Dynamic" benchmarks are CLR-only.

### Run all benchmarks
```
dotnet run -c Release -f net8.0 --filter *
```

### Run only CLR or only AOT benchmarks
```
dotnet run -c Release -f net8.0 --filter *clr.*
dotnet run -c Release -f net8.0 --filter *aot.*
```

### Run a specific benchmark
```
dotnet run -c Release -f net8.0 --filter *clr.CallDotnetFunction
```

### List benchmarks
```
dotnet run -c Release -f net8.0 --list flat
```
4 changes: 2 additions & 2 deletions src/NodeApi.DotNetHost/JSMarshaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1860,10 +1860,10 @@ private LambdaExpression BuildConvertFromJSValueExpression(Type toType)
// public type is passed to JS and then passed back to .NET as `object` type.

/*
* (T)(value.TryUnwrap() ?? value.GetValueExternal());
* (T)(value.TryUnwrap() ?? value.TryGetValueExternal());
*/
MethodInfo getExternalMethod =
typeof(JSNativeApi).GetStaticMethod(nameof(JSNativeApi.GetValueExternal));
typeof(JSNativeApi).GetStaticMethod(nameof(JSNativeApi.TryGetValueExternal));
statements = new[]
{
Expression.Convert(
Expand Down
7 changes: 6 additions & 1 deletion src/NodeApi.DotNetHost/ManagedHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,12 @@ public sealed class ManagedHost : JSEventEmitter, IDisposable
/// </remarks>
private readonly Dictionary<string, JSReference> _loadedModules = new();

private ManagedHost(JSObject exports)
/// <summary>
/// Creates a new instance of a <see cref="ManagedHost" /> that supports loading and
/// invoking managed .NET assemblies in a JavaScript process.
/// </summary>
/// <param name="exports">JS object on which the managed host APIs will be exported.</param>
public ManagedHost(JSObject exports)
{
#if NETFRAMEWORK
AppDomain.CurrentDomain.AssemblyResolve += OnResolvingAssembly;
Expand Down
1 change: 0 additions & 1 deletion src/NodeApi.DotNetHost/TypeExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,6 @@ private static bool IsSupportedType(Type type)

if (type.IsPointer ||
type == typeof(void) ||
type == typeof(Type) ||
type.Namespace == "System.Reflection" ||
(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Memory<>)) ||
(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ReadOnlyMemory<>)) ||
Expand Down
Loading

0 comments on commit 80131d1

Please sign in to comment.