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