| // 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(); | |
| } | |
| } |