// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'dart:js_interop'; import 'dart:math' as math; import 'package:benchmark_harness/benchmark_harness.dart'; // Benchmark for converting Dart functions to JS and calling them. // // Contains the following benchmarks: // - A series that determines how expensive calling `Function.toJS` is. These // are prefixed with `Convert`. These contain two dimensions in their names: // - Whether the function that is being wrapped is an `Instance` method, // `Static` method, `Closure`, or a closure that is stored in a field // (`ClosureField`). // - The arity of the function: `Zero`, `Two`, and `Eight` arguments. // - A series that determines how expensive calling the result of a // `Function.toJS`-wrapped function using interop is. These are prefixed with // `CallJS`. These contain the same dimensions as the `Convert` series, except // there is no `ClosureField` series as it would be the same as `Closure`. // - A series that determines how expensive calling a closure that's stored in a // field in Dart is. These are prefixed with `CallDartClosure`. They only // contain one dimension, which is the arity of the function. This is used to // compare the performance of calling a wrapped function versus calling it // directly. // Caching the function to a global should help avoid V8 from optimizing the // call away. @JS() external set cache(JSFunction jsFunction); extension on JSFunction { external int call( [JSAny? thisArg, int? arg1, int? arg2, int? arg3, int? arg4, int? arg5, int? arg6, int? arg7, int? arg8]); } final random = math.Random(); class ConvertInstanceZeroBenchmark extends BenchmarkBase { ConvertInstanceZeroBenchmark() : super('FunctionToJs.Convert.Instance.0'); final int randomInt = random.nextInt(10); @pragma('dart2js:never-inline') int zero() => randomInt; @override void run() { cache = zero.toJS; } } class ConvertInstanceTwoBenchmark extends BenchmarkBase { ConvertInstanceTwoBenchmark() : super('FunctionToJs.Convert.Instance.2'); final int randomInt = random.nextInt(10); @pragma('dart2js:never-inline') int two(int arg1, int arg2) => randomInt + arg1 + arg2; @override void run() { cache = two.toJS; } } class ConvertInstanceEightBenchmark extends BenchmarkBase { ConvertInstanceEightBenchmark() : super('FunctionToJs.Convert.Instance.8'); final int randomInt = random.nextInt(10); @pragma('dart2js:never-inline') int eight(int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8) => randomInt + arg1 + arg2 + arg3 + arg4 + arg5 + arg6 + arg7 + arg8; @override void run() { cache = eight.toJS; } } class ConvertStaticZeroBenchmark extends BenchmarkBase { ConvertStaticZeroBenchmark() : super('FunctionToJs.Convert.Static.0'); static final int randomInt = random.nextInt(10); static int zero() => randomInt; @override void run() { cache = zero.toJS; } } class ConvertStaticTwoBenchmark extends BenchmarkBase { ConvertStaticTwoBenchmark() : super('FunctionToJs.Convert.Static.2'); static final int randomInt = random.nextInt(10); static int two(int arg1, int arg2) => randomInt + arg1 + arg2; @override void run() { cache = two.toJS; } } class ConvertStaticEightBenchmark extends BenchmarkBase { ConvertStaticEightBenchmark() : super('FunctionToJs.Convert.Static.8'); static final int randomInt = random.nextInt(10); static int eight(int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8) => randomInt + arg1 + arg2 + arg3 + arg4 + arg5 + arg6 + arg7 + arg8; @override void run() { cache = eight.toJS; } } class ConvertClosureZeroBenchmark extends BenchmarkBase { ConvertClosureZeroBenchmark() : super('FunctionToJs.Convert.Closure.0'); final int randomInt = random.nextInt(10); @override void run() { cache = (() => randomInt).toJS; } } class ConvertClosureTwoBenchmark extends BenchmarkBase { ConvertClosureTwoBenchmark() : super('FunctionToJs.Convert.Closure.2'); final int randomInt = random.nextInt(10); @override void run() { cache = ((int arg1, int arg2) => randomInt + arg1 + arg2).toJS; } } class ConvertClosureEightBenchmark extends BenchmarkBase { ConvertClosureEightBenchmark() : super('FunctionToJs.Convert.Closure.8'); final int randomInt = random.nextInt(10); @override void run() { cache = ((int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8) => randomInt + arg1 + arg2 + arg3 + arg4 + arg5 + arg6 + arg7 + arg8).toJS; } } class ConvertClosureFieldZeroBenchmark extends BenchmarkBase { ConvertClosureFieldZeroBenchmark() : super('FunctionToJs.Convert.ClosureField.0'); late int Function() closure; final int randomInt = random.nextInt(10); @override void setup() { closure = () => randomInt; } @override void run() { cache = closure.toJS; } } class ConvertClosureFieldTwoBenchmark extends BenchmarkBase { ConvertClosureFieldTwoBenchmark() : super('FunctionToJs.Convert.ClosureField.2'); late int Function(int, int) closure; final int randomInt = random.nextInt(10); @override void setup() { closure = (int arg1, int arg2) => randomInt + arg1 + arg2; } @override void run() { cache = closure.toJS; } } class ConvertClosureFieldEightBenchmark extends BenchmarkBase { ConvertClosureFieldEightBenchmark() : super('FunctionToJs.Convert.ClosureField.8'); late int Function(int, int, int, int, int, int, int, int) closure; final int randomInt = random.nextInt(10); @override void setup() { closure = (int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8) => randomInt + arg1 + arg2 + arg3 + arg4 + arg5 + arg6 + arg7 + arg8; } @override void run() { cache = closure.toJS; } } class CallJSInstanceZeroBenchmark extends BenchmarkBase { CallJSInstanceZeroBenchmark() : super('FunctionToJs.CallJS.Instance.0'); late JSExportedDartFunction jsFunction; final int randomInt = random.nextInt(10); @pragma('dart2js:never-inline') int zero() => randomInt; @override void setup() { jsFunction = zero.toJS; } @override void run() { final val = jsFunction.call(); if (val < 0 || val >= 10) throw 'Bad result: $val'; } } class CallJSInstanceTwoBenchmark extends BenchmarkBase { CallJSInstanceTwoBenchmark() : super('FunctionToJs.CallJS.Instance.2'); late JSExportedDartFunction jsFunction; final int randomInt = random.nextInt(10); @pragma('dart2js:never-inline') int two(int arg1, int arg2) => randomInt + arg1 + arg2; @override void setup() { jsFunction = two.toJS; } @override void run() { final val = jsFunction.call(null, 1, 1); if (val < 2 || val >= 12) throw 'Bad result: $val'; } } class CallJSInstanceEightBenchmark extends BenchmarkBase { CallJSInstanceEightBenchmark() : super('FunctionToJs.CallJS.Instance.8'); late JSExportedDartFunction jsFunction; final int randomInt = random.nextInt(10); @pragma('dart2js:never-inline') int eight(int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8) => randomInt + arg1 + arg2 + arg3 + arg4 + arg5 + arg6 + arg7 + arg8; @override void setup() { jsFunction = eight.toJS; } @override void run() { final val = jsFunction.call(null, 1, 1, 1, 1, 1, 1, 1, 1); if (val < 8 || val >= 18) throw 'Bad result: $val'; } } class CallJSStaticZeroBenchmark extends BenchmarkBase { CallJSStaticZeroBenchmark() : super('FunctionToJs.CallJS.Static.0'); static final int randomInt = random.nextInt(10); static int zero() => randomInt; late JSExportedDartFunction jsFunction; @override void setup() { jsFunction = zero.toJS; } @override void run() { final val = jsFunction.call(); if (val < 0 || val >= 10) throw 'Bad result: $val'; } } class CallJSStaticTwoBenchmark extends BenchmarkBase { CallJSStaticTwoBenchmark() : super('FunctionToJs.CallJS.Static.2'); static final int randomInt = random.nextInt(10); static int two(int arg1, int arg2) => randomInt + arg1 + arg2; late JSExportedDartFunction jsFunction; @override void setup() { jsFunction = two.toJS; } @override void run() { final val = jsFunction.call(null, 1, 1); if (val < 2 || val >= 12) throw 'Bad result: $val'; } } class CallJSStaticEightBenchmark extends BenchmarkBase { CallJSStaticEightBenchmark() : super('FunctionToJs.CallJS.Static.8'); static final int randomInt = random.nextInt(10); static int eight(int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8) => randomInt + arg1 + arg2 + arg3 + arg4 + arg5 + arg6 + arg7 + arg8; late JSExportedDartFunction jsFunction; @override void setup() { jsFunction = eight.toJS; } @override void run() { final val = jsFunction.call(null, 1, 1, 1, 1, 1, 1, 1, 1); if (val < 8 || val >= 18) throw 'Bad result: $val'; } } class CallJSClosureZeroBenchmark extends BenchmarkBase { CallJSClosureZeroBenchmark() : super('FunctionToJs.CallJS.Closure.0'); late JSExportedDartFunction jsFunction; final int randomInt = random.nextInt(10); @override void setup() { jsFunction = (() => randomInt).toJS; } @override void run() { final val = jsFunction.call(); if (val < 0 || val >= 10) throw 'Bad result: $val'; } } class CallJSClosureTwoBenchmark extends BenchmarkBase { CallJSClosureTwoBenchmark() : super('FunctionToJs.CallJS.Closure.2'); late JSExportedDartFunction jsFunction; final int randomInt = random.nextInt(10); @override void setup() { jsFunction = ((int arg1, int arg2) => randomInt + arg1 + arg2).toJS; } @override void run() { final val = jsFunction.call(null, 1, 1); if (val < 2 || val >= 12) throw 'Bad result: $val'; } } class CallJSClosureEightBenchmark extends BenchmarkBase { CallJSClosureEightBenchmark() : super('FunctionToJs.CallJS.Closure.8'); late JSExportedDartFunction jsFunction; final int randomInt = random.nextInt(10); @override void setup() { jsFunction = ((int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8) => randomInt + arg1 + arg2 + arg3 + arg4 + arg5 + arg6 + arg7 + arg8).toJS; } @override void run() { final val = jsFunction.call(null, 1, 1, 1, 1, 1, 1, 1, 1); if (val < 8 || val >= 18) throw 'Bad result: $val'; } } class CallDartClosureZeroBenchmark extends BenchmarkBase { CallDartClosureZeroBenchmark() : super('FunctionToJs.CallDart.Closure.0'); late int Function() closure; final int randomInt = random.nextInt(10); @override void setup() { closure = () => randomInt; } @override void run() { final val = closure(); if (val < 0 || val >= 10) throw 'Bad result: $val'; } } class CallDartClosureTwoBenchmark extends BenchmarkBase { CallDartClosureTwoBenchmark() : super('FunctionToJs.CallDart.Closure.2'); late int Function(int, int) closure; final int randomInt = random.nextInt(10); @override void setup() { closure = (int arg1, int arg2) => randomInt + arg1 + arg2; } @override void run() { final val = closure(1, 1); if (val < 2 || val >= 12) throw 'Bad result: $val'; } } class CallDartClosureEightBenchmark extends BenchmarkBase { CallDartClosureEightBenchmark() : super('FunctionToJs.CallDart.Closure.8'); late int Function(int, int, int, int, int, int, int, int) closure; final int randomInt = random.nextInt(10); @override void setup() { closure = (int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8) => randomInt + arg1 + arg2 + arg3 + arg4 + arg5 + arg6 + arg7 + arg8; } @override void run() { final val = closure(1, 1, 1, 1, 1, 1, 1, 1); if (val < 8 || val >= 18) throw 'Bad result: $val'; } } void main() { final benchmarks = [ ConvertInstanceZeroBenchmark(), ConvertInstanceTwoBenchmark(), ConvertInstanceEightBenchmark(), ConvertStaticZeroBenchmark(), ConvertStaticTwoBenchmark(), ConvertStaticEightBenchmark(), ConvertClosureZeroBenchmark(), ConvertClosureTwoBenchmark(), ConvertClosureEightBenchmark(), ConvertClosureFieldZeroBenchmark(), ConvertClosureFieldTwoBenchmark(), ConvertClosureFieldEightBenchmark(), CallJSInstanceZeroBenchmark(), CallJSInstanceTwoBenchmark(), CallJSInstanceEightBenchmark(), CallJSStaticZeroBenchmark(), CallJSStaticTwoBenchmark(), CallJSStaticEightBenchmark(), CallJSClosureZeroBenchmark(), CallJSClosureTwoBenchmark(), CallJSClosureEightBenchmark(), CallDartClosureZeroBenchmark(), CallDartClosureTwoBenchmark(), CallDartClosureEightBenchmark(), ]; // Warmup all benchmarks so that the first benchmark doesn't get overfitted by // a JIT compiler. for (final benchmark in benchmarks) { benchmark.setup(); } for (var i = 0; i < 10; i++) { for (final benchmark in benchmarks) { benchmark.run(); } } for (final benchmark in benchmarks) { benchmark.report(); } }