Skip to content

Commit 0e23327

Browse files
authored
Add benchmarks, GC handle tracking test (#157)
1 parent 9ab1030 commit 0e23327

16 files changed

Lines changed: 602 additions & 57 deletions

bench/Benchmarks.cs

Lines changed: 214 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,80 +7,265 @@
77
using BenchmarkDotNet.Configs;
88
using BenchmarkDotNet.Jobs;
99
using BenchmarkDotNet.Running;
10+
using Microsoft.JavaScript.NodeApi.DotNetHost;
11+
using Microsoft.JavaScript.NodeApi.Interop;
1012
using Microsoft.JavaScript.NodeApi.Runtimes;
1113
using static Microsoft.JavaScript.NodeApi.JSNativeApi.Interop;
1214
using static Microsoft.JavaScript.NodeApi.Test.TestUtils;
1315

1416
namespace Microsoft.JavaScript.NodeApi.Bench;
1517

18+
/// <summary>
19+
/// Micro-benchmarks for various .NET + JS interop operations.
20+
/// </summary>
21+
/// <remarks>
22+
/// These benchmarks run both .NET and Node.js code, and call between them. The benchmark
23+
/// runner manages the GC for the .NET runtime, but it doesn't know anything about the JS runtime.
24+
/// To avoid heavy JS GC pressure from millions of operations (which may each allocate objects),
25+
/// these benchmarks use the `ShortRunJob` attribute (which sacrifices some precision but also
26+
/// doesn't take as long to run).
27+
/// </remarks>
28+
[IterationCount(5)]
29+
[WarmupCount(1)]
1630
public abstract class Benchmarks
1731
{
1832
public static void Main(string[] args)
1933
{
20-
// Example: dotnet run -c Release --filter aot
34+
#if DEBUG
35+
IConfig config = new DebugBuildConfig();
36+
#else
37+
IConfig config = DefaultConfig.Instance;
38+
#endif
39+
40+
// Example: dotnet run -c Release --filter clr
2141
// If no filter is specified, the switcher will prompt.
2242
BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args,
23-
ManualConfig.Create(DefaultConfig.Instance).WithOptions(ConfigOptions.JoinSummary));
24-
}
25-
26-
public class NonAot : Benchmarks
27-
{
28-
// Non-AOT-only benchmarks may go here
29-
}
30-
31-
[SimpleJob(RuntimeMoniker.NativeAot70)]
32-
public class Aot : Benchmarks
33-
{
34-
// AOT-only benchmarks may go here
43+
ManualConfig.Create(config)
44+
.WithOptions(ConfigOptions.JoinSummary));
3545
}
3646

3747
private static string LibnodePath { get; } = Path.Combine(
3848
GetRepoRootDirectory(),
3949
"bin",
40-
"win-x64", // TODO
50+
GetCurrentPlatformRuntimeIdentifier(),
4151
"libnode" + GetSharedLibraryExtension());
4252

4353
private napi_env _env;
44-
private JSValue _function;
45-
private JSValue _callback;
54+
private JSFunction _jsFunction;
55+
private JSFunction _jsFunctionWithArgs;
56+
private JSFunction _jsFunctionWithCallback;
57+
private JSObject _jsInstance;
58+
private JSFunction _dotnetFunction;
59+
private JSFunction _dotnetFunctionWithArgs;
60+
private JSObject _dotnetClass;
61+
private JSObject _dotnetInstance;
62+
private JSFunction _jsFunctionCreateInstance;
63+
private JSFunction _jsFunctionCallMethod;
64+
private JSFunction _jsFunctionCallMethodWithArgs;
4665
private JSReference _reference = null!;
4766

48-
[GlobalSetup]
49-
public void Setup()
67+
/// <summary>
68+
/// Simple class that is exported to JS and used in some benchmarks.
69+
/// </summary>
70+
private class DotnetClass
5071
{
51-
NodejsPlatform platform = new(LibnodePath);
72+
public DotnetClass() { }
73+
74+
public string Property { get; set; } = string.Empty;
75+
76+
#pragma warning disable CA1822 // Method does not access instance data and can be marked as static
77+
public static void Method() { }
78+
#pragma warning restore CA1822
79+
}
80+
81+
/// <summary>
82+
/// Setup shared by both CLR and AOT benchmarks.
83+
/// </summary>
84+
protected void Setup()
85+
{
86+
NodejsPlatform platform = new(LibnodePath/*, args: new[] { "node", "--expose-gc" }*/);
5287

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

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

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

63-
_function = JSNativeApi.RunScript("function callMeBack(cb) { cb(); }; callMeBack");
64-
_callback = JSValue.CreateFunction("callback", (args) => JSValue.Undefined);
97+
_jsFunction = (JSFunction)JSNativeApi.RunScript("function jsFunction() { }; jsFunction");
98+
_jsFunctionWithArgs = (JSFunction)JSNativeApi.RunScript(
99+
"function jsFunctionWithArgs(a, b, c) { }; jsFunctionWithArgs");
100+
_jsFunctionWithCallback = (JSFunction)JSNativeApi.RunScript(
101+
"function jsFunctionWithCallback(cb, ...args) { cb(...args); }; " +
102+
"jsFunctionWithCallback");
103+
_jsInstance = (JSObject)JSNativeApi.RunScript(
104+
"const jsInstance = { method: (...args) => {} }; jsInstance");
105+
106+
_dotnetFunction = (JSFunction)JSValue.CreateFunction(
107+
"dotnetFunction", (args) => JSValue.Undefined);
108+
_dotnetFunctionWithArgs = (JSFunction)JSValue.CreateFunction(
109+
"dotnetFunctionWithArgs", (args) =>
110+
{
111+
for (int i = 0; i < args.Length; i++)
112+
{
113+
_ = args[i];
114+
}
115+
116+
return JSValue.Undefined;
117+
});
118+
119+
var classBuilder = new JSClassBuilder<DotnetClass>(
120+
nameof(DotnetClass), () => new DotnetClass());
121+
classBuilder.AddProperty(
122+
"property",
123+
(x) => x.Property,
124+
(x, value) => x.Property = (string)value);
125+
classBuilder.AddMethod("method", (x) => (args) => DotnetClass.Method());
126+
_dotnetClass = (JSObject)classBuilder.DefineClass();
127+
_dotnetInstance = (JSObject)JSNativeApi.CallAsConstructor(_dotnetClass);
128+
129+
_jsFunctionCreateInstance = (JSFunction)JSNativeApi.RunScript(
130+
"function jsFunctionCreateInstance(Class) { new Class() }; " +
131+
"jsFunctionCreateInstance");
132+
_jsFunctionCallMethod = (JSFunction)JSNativeApi.RunScript(
133+
"function jsFunctionCallMethod(instance) { instance.method(); }; " +
134+
"jsFunctionCallMethod");
135+
_jsFunctionCallMethodWithArgs = (JSFunction)JSNativeApi.RunScript(
136+
"function jsFunctionCallMethodWithArgs(instance, ...args) " +
137+
"{ instance.method(...args); }; " +
138+
"jsFunctionCallMethodWithArgs");
139+
140+
_reference = new JSReference(_jsFunction);
141+
}
142+
143+
private static JSValueScope NewJSScope() => new(JSValueScopeType.Callback);
144+
145+
// Benchmarks in the base class run in both CLR and AOT environments.
146+
147+
[Benchmark]
148+
public void CallJSFunction()
149+
{
150+
_jsFunction.CallAsStatic();
151+
}
152+
153+
[Benchmark]
154+
public void CallJSFunctionWithArgs()
155+
{
156+
_jsFunctionWithArgs.CallAsStatic("1", "2", "3");
157+
}
158+
159+
[Benchmark]
160+
public void CallJSMethod()
161+
{
162+
_jsInstance.CallMethod("method");
163+
}
164+
165+
[Benchmark]
166+
public void CallJSMethodWithArgs()
167+
{
168+
_jsInstance.CallMethod("method", "1", "2", "3");
169+
}
65170

66-
_reference = new JSReference(_function);
171+
[Benchmark]
172+
public void CallDotnetFunction()
173+
{
174+
_jsFunctionWithCallback.CallAsStatic(_dotnetFunction);
67175
}
68176

69177
[Benchmark]
70-
public void CallJS()
178+
public void CallDotnetFunctionWithArgs()
71179
{
72-
_function.Call(thisArg: default, _callback);
180+
_jsFunctionWithCallback.CallAsStatic(_dotnetFunctionWithArgs, "1", "2", "3");
73181
}
74182

75183
[Benchmark]
76-
public void GetReference()
184+
public void CallDotnetConstructor()
185+
{
186+
_jsFunctionCreateInstance.CallAsStatic(_dotnetClass);
187+
}
188+
189+
[Benchmark]
190+
public void CallDotnetMethod()
191+
{
192+
_jsFunctionCallMethod.CallAsStatic(_dotnetInstance);
193+
}
194+
195+
[Benchmark]
196+
public void CallDotnetMethodWithArgs()
197+
{
198+
_jsFunctionCallMethodWithArgs.CallAsStatic(_dotnetInstance, "1", "2", "3");
199+
}
200+
201+
[Benchmark]
202+
public void ReferenceGet()
77203
{
78204
_ = _reference.GetValue()!.Value;
79205
}
80206

81207
[Benchmark]
82-
public void CreateAndDiposeReference()
208+
public void ReferenceCreateAndDipose()
209+
{
210+
using JSReference reference = new(_jsFunction);
211+
}
212+
213+
[ShortRunJob]
214+
[MemoryDiagnoser(displayGenColumns: false)]
215+
public class Clr : Benchmarks
216+
{
217+
private JSObject _jsHost;
218+
private JSFunction _jsFunctionCallMethodDynamic;
219+
private JSFunction _jsFunctionCallMethodDynamicInterface;
220+
221+
[GlobalSetup]
222+
public new void Setup()
223+
{
224+
base.Setup();
225+
226+
// CLR-only (non-AOT) setup
227+
228+
JSObject hostModule = new();
229+
_ = new ManagedHost(hostModule);
230+
_jsHost = hostModule;
231+
_jsFunctionCallMethodDynamic = (JSFunction)JSNativeApi.RunScript(
232+
"function jsFunctionCallMethodDynamic(dotnet) " +
233+
"{ dotnet.System.Object.ReferenceEquals(null, null); }; " +
234+
"jsFunctionCallMethodDynamic");
235+
236+
// Implement IFormatProvider in JS and pass it to a .NET method.
237+
_jsFunctionCallMethodDynamicInterface = (JSFunction)JSNativeApi.RunScript(
238+
"function jsFunctionCallMethodDynamicInterface(dotnet) {" +
239+
" const formatProvider = { GetFormat: (type) => null };" +
240+
" dotnet.System.String.Format(formatProvider, '', null, null);" +
241+
"}; " +
242+
"jsFunctionCallMethodDynamicInterface");
243+
}
244+
245+
// CLR-only (non-AOT) benchmarks
246+
247+
[Benchmark]
248+
public void DynamicCallDotnetMethod()
249+
{
250+
_jsFunctionCallMethodDynamic.CallAsStatic(_jsHost);
251+
}
252+
253+
[Benchmark]
254+
public void DynamicCallDotnetMethodWithInterface()
255+
{
256+
_jsFunctionCallMethodDynamicInterface.CallAsStatic(_jsHost);
257+
}
258+
}
259+
260+
[ShortRunJob(RuntimeMoniker.NativeAot80)]
261+
public class Aot : Benchmarks
83262
{
84-
using JSReference reference = new(_function);
263+
[GlobalSetup]
264+
public new void Setup()
265+
{
266+
base.Setup();
267+
}
268+
269+
// AOT-only benchmarks
85270
}
86271
}

bench/NodeApi.Bench.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
<ItemGroup>
1515
<Compile Include="../test/TestUtils.cs" Link="TestUtils.cs" />
16+
<None Remove="BenchmarkDotNet.Artifacts/**" />
1617
</ItemGroup>
1718

1819
<ItemGroup>

bench/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Micro-benchmarks for node-api-dotnet APIs
2+
3+
This project contains a set of micro-benchmarks for .NET + JS interop operations, driven by
4+
[BenchmarkDotNet](https://benchmarkdotnet.org/). Most benchmarks run in both CLR and AOT modes,
5+
though the "Dynamic" benchmarks are CLR-only.
6+
7+
### Run all benchmarks
8+
```
9+
dotnet run -c Release -f net8.0 --filter *
10+
```
11+
12+
### Run only CLR or only AOT benchmarks
13+
```
14+
dotnet run -c Release -f net8.0 --filter *clr.*
15+
dotnet run -c Release -f net8.0 --filter *aot.*
16+
```
17+
18+
### Run a specific benchmark
19+
```
20+
dotnet run -c Release -f net8.0 --filter *clr.CallDotnetFunction
21+
```
22+
23+
### List benchmarks
24+
```
25+
dotnet run -c Release -f net8.0 --list flat
26+
```

src/NodeApi.DotNetHost/JSMarshaller.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1860,10 +1860,10 @@ private LambdaExpression BuildConvertFromJSValueExpression(Type toType)
18601860
// public type is passed to JS and then passed back to .NET as `object` type.
18611861

18621862
/*
1863-
* (T)(value.TryUnwrap() ?? value.GetValueExternal());
1863+
* (T)(value.TryUnwrap() ?? value.TryGetValueExternal());
18641864
*/
18651865
MethodInfo getExternalMethod =
1866-
typeof(JSNativeApi).GetStaticMethod(nameof(JSNativeApi.GetValueExternal));
1866+
typeof(JSNativeApi).GetStaticMethod(nameof(JSNativeApi.TryGetValueExternal));
18671867
statements = new[]
18681868
{
18691869
Expression.Convert(

src/NodeApi.DotNetHost/ManagedHost.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,12 @@ public sealed class ManagedHost : JSEventEmitter, IDisposable
9898
/// </remarks>
9999
private readonly Dictionary<string, JSReference> _loadedModules = new();
100100

101-
private ManagedHost(JSObject exports)
101+
/// <summary>
102+
/// Creates a new instance of a <see cref="ManagedHost" /> that supports loading and
103+
/// invoking managed .NET assemblies in a JavaScript process.
104+
/// </summary>
105+
/// <param name="exports">JS object on which the managed host APIs will be exported.</param>
106+
public ManagedHost(JSObject exports)
102107
{
103108
#if NETFRAMEWORK
104109
AppDomain.CurrentDomain.AssemblyResolve += OnResolvingAssembly;

src/NodeApi.DotNetHost/TypeExporter.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,6 @@ private static bool IsSupportedType(Type type)
477477

478478
if (type.IsPointer ||
479479
type == typeof(void) ||
480-
type == typeof(Type) ||
481480
type.Namespace == "System.Reflection" ||
482481
(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Memory<>)) ||
483482
(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ReadOnlyMemory<>)) ||

0 commit comments

Comments
 (0)