blob: 03d30ee4a0712a448fcab23d90085b87baf5688f [file] [log] [blame] [edit]
// 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.');
}