From 7ec95140934219a0e98cfb924aeddb41614deb30 Mon Sep 17 00:00:00 2001 From: Srujan Gaddam Date: Fri, 14 Jun 2024 19:42:10 +0000 Subject: [PATCH] [benchmarks] Add Function.toJS benchmark Adds a benchmark to test converting Dart functions to and from JS as well as calling them. Specifically, tests some combinations of the following: - Converting vs calling functions - JS vs Dart functions - Calling instance methods vs static methods vs closure vs closures stored in fields Change-Id: I8f5b63781201042c4068437fe84c3043d6dfb446 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/368064 Reviewed-by: Stephen Adams Commit-Queue: Srujan Gaddam --- .../FunctionToJs/dart/FunctionToJs.dart | 507 ++++++++++++++++++ 1 file changed, 507 insertions(+) create mode 100644 benchmarks/FunctionToJs/dart/FunctionToJs.dart diff --git a/benchmarks/FunctionToJs/dart/FunctionToJs.dart b/benchmarks/FunctionToJs/dart/FunctionToJs.dart new file mode 100644 index 00000000000..c714cd0684d --- /dev/null +++ b/benchmarks/FunctionToJs/dart/FunctionToJs.dart @@ -0,0 +1,507 @@ +// 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(); + } +}