| // Copyright (c) 2025, 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. |
| |
| // Benchmarks passing small and large integers to JS via `js_interop`. |
| // |
| // In Wasm, integers that fit into 31 bits can be passed without allocation by |
| // by passing them as `i31ref`. To take advantage of this, dart2wasm checks the |
| // size of the integer before passing to JS and passes the integer as `i31ref` |
| // when possible. |
| // |
| // This benchmark compares performance of `int` passing for integers that fit |
| // into 31 bits and those that don't. |
| |
| import 'dart:js_interop'; |
| |
| import 'package:benchmark_harness/benchmark_harness.dart'; |
| |
| @JS() |
| external void eval(String code); |
| |
| // These return `void` to avoid adding `dartify` overheads to the benchmark |
| // results. |
| // V8 can't figure out that these don't do anything so the loops and JS calls |
| // aren't eliminated. |
| @JS() |
| external void intFun(int i); |
| |
| @JS() |
| external void doubleFun(double d); |
| |
| // Run benchmarked code for at least 2 seconds. |
| const int minimumMeasureDurationMillis = 2000; |
| |
| class IntPassingBenchmark { |
| final int start; |
| final int end; |
| |
| IntPassingBenchmark(this.start, this.end); |
| |
| double measure() => |
| BenchmarkBase.measureFor(() { |
| for (int i = start; i < end; i += 1) { |
| intFun(i); |
| } |
| }, minimumMeasureDurationMillis) / |
| (end - start); |
| } |
| |
| class DoublePassingBenchmark { |
| final double start; |
| final double step; |
| final int calls; |
| |
| DoublePassingBenchmark(this.start, this.step, this.calls); |
| |
| double measure() => |
| BenchmarkBase.measureFor(() { |
| double d = start; |
| for (int i = 0; i < calls; i += 1) { |
| doubleFun(d); |
| d *= step; |
| } |
| }, minimumMeasureDurationMillis) / |
| calls; |
| } |
| |
| @JS() |
| external bool dartifyBool(); |
| |
| @JS() |
| external bool? dartifyNullableBool(); |
| |
| @JS() |
| external num dartifyNum(); |
| |
| @JS() |
| external num? dartifyNullableNum(); |
| |
| @JS() |
| external double dartifyDouble(); |
| |
| @JS() |
| external double? dartifyNullableDouble(); |
| |
| @JS() |
| external int dartifyInt(); |
| |
| @JS() |
| external int? dartifyNullableInt(); |
| |
| @JS() |
| external String dartifyString(); |
| |
| @JS() |
| external String? dartifyNullableString(); |
| |
| @JS() |
| external JSArray dartifyJSArray(); |
| |
| @JS() |
| external JSArray? dartifyNullableJSArray(); |
| |
| // `dart:typed_data` types are all boxed the same way, so no need to test each |
| // one of them separately. |
| @JS() |
| external JSUint8Array dartifyJSUint8Array(); |
| |
| @JS() |
| external JSUint8Array? dartifyNullableJSUint8Array(); |
| |
| const int ITERATIONS = 10000; |
| |
| bool boolSink = false; |
| |
| double dartifyBoolBenchmark() => |
| BenchmarkBase.measureFor(() { |
| for (int i = 0; i < ITERATIONS; i += 1) { |
| boolSink = dartifyBool(); |
| } |
| }, minimumMeasureDurationMillis) / |
| ITERATIONS; |
| |
| bool? nullableBoolSink; |
| |
| double dartifyNullableBoolBenchmark() => |
| BenchmarkBase.measureFor(() { |
| for (int i = 0; i < ITERATIONS; i += 1) { |
| nullableBoolSink = dartifyNullableBool(); |
| } |
| }, minimumMeasureDurationMillis) / |
| ITERATIONS; |
| |
| num numSink = 1; |
| |
| double dartifyNumBenchmark() => |
| BenchmarkBase.measureFor(() { |
| for (int i = 0; i < ITERATIONS; i += 1) { |
| numSink = dartifyNum(); |
| } |
| }, minimumMeasureDurationMillis) / |
| ITERATIONS; |
| |
| num? nullableNumSink; |
| |
| double dartifyNullableNumBenchmark() => |
| BenchmarkBase.measureFor(() { |
| for (int i = 0; i < ITERATIONS; i += 1) { |
| nullableNumSink = dartifyNullableNum(); |
| } |
| }, minimumMeasureDurationMillis) / |
| ITERATIONS; |
| |
| double doubleSink = 0.0; |
| |
| double dartifyDoubleBenchmark() => |
| BenchmarkBase.measureFor(() { |
| for (int i = 0; i < ITERATIONS; i += 1) { |
| doubleSink = dartifyDouble(); |
| } |
| }, minimumMeasureDurationMillis) / |
| ITERATIONS; |
| |
| double? nullableDoubleSink; |
| |
| double dartifyNullableDoubleBenchmark() => |
| BenchmarkBase.measureFor(() { |
| for (int i = 0; i < ITERATIONS; i += 1) { |
| nullableDoubleSink = dartifyNullableDouble(); |
| } |
| }, minimumMeasureDurationMillis) / |
| ITERATIONS; |
| |
| int intSink = 0; |
| |
| double dartifyIntBenchmark() => |
| BenchmarkBase.measureFor(() { |
| for (int i = 0; i < ITERATIONS; i += 1) { |
| intSink = dartifyInt(); |
| } |
| }, minimumMeasureDurationMillis) / |
| ITERATIONS; |
| |
| int? nullableIntSink; |
| |
| double dartifyNullableIntBenchmark() => |
| BenchmarkBase.measureFor(() { |
| for (int i = 0; i < ITERATIONS; i += 1) { |
| nullableIntSink = dartifyNullableInt(); |
| } |
| }, minimumMeasureDurationMillis) / |
| ITERATIONS; |
| |
| String stringSink = ''; |
| |
| double dartifyStringBenchmark() => |
| BenchmarkBase.measureFor(() { |
| for (int i = 0; i < ITERATIONS; i += 1) { |
| stringSink = dartifyString(); |
| } |
| }, minimumMeasureDurationMillis) / |
| ITERATIONS; |
| |
| String? nullableStringSink; |
| |
| double dartifyNullableStringBenchmark() => |
| BenchmarkBase.measureFor(() { |
| for (int i = 0; i < ITERATIONS; i += 1) { |
| nullableStringSink = dartifyNullableString(); |
| } |
| }, minimumMeasureDurationMillis) / |
| ITERATIONS; |
| |
| JSArray jsArraySink = JSArray(); |
| |
| double dartifyJSArrayBenchmark() => |
| BenchmarkBase.measureFor(() { |
| for (int i = 0; i < ITERATIONS; i += 1) { |
| jsArraySink = dartifyJSArray(); |
| } |
| }, minimumMeasureDurationMillis) / |
| ITERATIONS; |
| |
| JSArray? nullableJsArraySink; |
| |
| double dartifyNullableJSArrayBenchmark() => |
| BenchmarkBase.measureFor(() { |
| for (int i = 0; i < ITERATIONS; i += 1) { |
| nullableJsArraySink = dartifyNullableJSArray(); |
| } |
| }, minimumMeasureDurationMillis) / |
| ITERATIONS; |
| |
| JSUint8Array jsUint8ArraySink = JSUint8Array(); |
| |
| double dartifyJSUint8ArrayBenchmark() => |
| BenchmarkBase.measureFor(() { |
| for (int i = 0; i < ITERATIONS; i += 1) { |
| jsUint8ArraySink = dartifyJSUint8Array(); |
| } |
| }, minimumMeasureDurationMillis) / |
| ITERATIONS; |
| |
| JSUint8Array? nullableJsUint8ArraySink; |
| |
| double dartifyNullableJSUint8ArrayBenchmark() => |
| BenchmarkBase.measureFor(() { |
| for (int i = 0; i < ITERATIONS; i += 1) { |
| nullableJsUint8ArraySink = dartifyNullableJSUint8Array(); |
| } |
| }, minimumMeasureDurationMillis) / |
| ITERATIONS; |
| |
| void main() { |
| eval(''' |
| self.intFun = (i) => i; |
| self.doubleFun = (d) => d; |
| self.dartifyBool = () => true; |
| self.dartifyNullableBool = () => false; |
| self.dartifyNum = () => 12.34; |
| self.dartifyNullableNum = () => 56.78; |
| self.dartifyDouble = () => 10.20; |
| self.dartifyNullableDouble = () => 30.40; |
| self.dartifyInt = () => 123; |
| self.dartifyNullableInt = () => 456; |
| self.dartifyString = () => "abc"; |
| self.dartifyNullableString = () => "def"; |
| |
| // Arrays are all empty to not add element conversion overheads to the |
| // benchmark results. |
| self.dartifyJSArray = () => new Array(); |
| self.dartifyNullableJSArray = () => new Array(); |
| self.dartifyJSUint8Array = () => new Uint8Array(0); |
| self.dartifyNullableJSUint8Array = () => new Uint8Array(0); |
| '''); |
| |
| final maxI31 = (1 << 30) - 1; |
| |
| final small = IntPassingBenchmark(maxI31 - 1000000, maxI31).measure(); |
| report('WasmJSInterop.call.void.1ArgsSmi', small); |
| |
| final large = IntPassingBenchmark(maxI31 + 1, maxI31 + 1000001).measure(); |
| report('WasmJSInterop.call.void.1ArgsInt', large); |
| |
| // Have more than one call site to the `double` benchmark to avoid inlining |
| // too much, and for fair comparison with the `int` benchmark above. |
| DoublePassingBenchmark(1.0, 1.0, 10).measure(); |
| final double = DoublePassingBenchmark(1.0, 12.34, 1000000).measure(); |
| report('WasmJSInterop.call.void.1ArgsDouble', double); |
| |
| report('WasmJSInterop.call.bool.0Args', dartifyBoolBenchmark()); |
| report( |
| 'WasmJSInterop.call.nullableBool.0Args', |
| dartifyNullableBoolBenchmark(), |
| ); |
| report('WasmJSInterop.call.num.0Args', dartifyNumBenchmark()); |
| report('WasmJSInterop.call.nullableNum.0Args', dartifyNullableNumBenchmark()); |
| report('WasmJSInterop.call.double.0Args', dartifyDoubleBenchmark()); |
| report( |
| 'WasmJSInterop.call.nullableDouble.0Args', |
| dartifyNullableDoubleBenchmark(), |
| ); |
| report('WasmJSInterop.call.int.0Args', dartifyIntBenchmark()); |
| report('WasmJSInterop.call.nullableInt.0Args', dartifyNullableIntBenchmark()); |
| report('WasmJSInterop.call.string.0Args', dartifyStringBenchmark()); |
| report( |
| 'WasmJSInterop.call.nullableString.0Args', |
| dartifyNullableStringBenchmark(), |
| ); |
| report('WasmJSInterop.call.JSArray.0Args', dartifyJSArrayBenchmark()); |
| report( |
| 'WasmJSInterop.call.nullableJSArray.0Args', |
| dartifyNullableJSArrayBenchmark(), |
| ); |
| report( |
| 'WasmJSInterop.call.JSUint8Array.0Args', |
| dartifyJSUint8ArrayBenchmark(), |
| ); |
| report( |
| 'WasmJSInterop.call.nullableJSUint8Array.0Args', |
| dartifyNullableJSUint8ArrayBenchmark(), |
| ); |
| |
| // To keep the sinks alive |
| if (int.parse('1') == 0) { |
| print(boolSink); |
| print(nullableBoolSink); |
| print(numSink); |
| print(nullableNumSink); |
| print(doubleSink); |
| print(nullableDoubleSink); |
| print(intSink); |
| print(nullableIntSink); |
| print(stringSink); |
| print(nullableStringSink); |
| print(jsArraySink); |
| print(nullableJsArraySink); |
| print(jsUint8ArraySink); |
| print(nullableJsUint8ArraySink); |
| } |
| } |
| |
| /// Reports in Golem-specific format. |
| void report(String name, double usPerCall) { |
| print('$name(RunTimeRaw): ${usPerCall * 1000} ns.'); |
| } |