blob: c3d2cbaa875286bf2617699d740f7261e06b7436 [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.
import 'dart:js_interop';
import 'package:benchmark_harness/benchmark_harness.dart';
// Static js-interop benchmarks.
//
// Type checks:
//
// JSInterop.as.Foo - x as Foo
// JSInterop.as.T.Foo - x as T, where T is Foo
//
// Type tests:
//
// JSInterop.isA.Foo - x.isA<Foo>()
//
// Calls.
//
// JSInterop.call.moveFoo -
// Repeatedly copy the same Foo (some kind of JSObject) to JavaScript and
// back.
//
// JSInterop.call.{inline,hoisted,implicit}3ArgsSSS
// Passes three String arguments, using different positioning of `.toJS`
// conversions.
//
// JSInterop.call.{inline,hoisted,implicit}3ArgsIII
// Similar to JSInterop.call.*ArgsSSS but passing integer Numbers instead.
//
// JSInterop.call.inline7Args
// Compare with JSInterop.call.inline3ArgsSSS to assess the cost of more
// arguments.
/// Aliases for `Object.assign` with different types.
///
/// `Object.assign` with one argument returns the argument. It is a fast
/// JavaScript method that returns a JSObject.
@JS('Object')
extension type ObjectMethods(JSObject _) implements JSObject {
@JS('assign')
external static JSObject moveJSObject(JSObject o);
@JS('assign')
external static JSArray moveJSArray(JSArray o);
@JS('assign')
external static JSUint8Array moveJSUint8Array(JSUint8Array o);
}
// Alias for `.toString()` with different types.
//
// `x.toString()`, when called on a JSString receiver, calls
// `String.prototype.toString`, which is fast, since it returns the
// receiver. Additional arguments are ignored.
extension ToStringMethod on JSAny {
@JS('toString')
external JSString toString1(JSAny? a);
@JS('toString')
external JSString toString3SSS(JSString a, JSString b, JSString c);
@JS('toString')
external JSString toString3NNN(JSNumber a, JSNumber b, JSNumber c);
// This version uses Dart primitive types which implies an implicit
// conversion.
@JS('toString')
external String toString3ConvertedSSS(String a, String b, String c);
// This version uses Dart primitive types which implies an implicit
// conversion.
@JS('toString')
external String toString3ConvertedIII(int a, int b, int c);
@JS('toString')
external JSString toString7(
JSAny? a1,
JSAny? a2,
JSAny? a3,
JSAny? a4,
JSAny? a5,
JSAny? a6,
JSAny? a7,
);
}
extension type Date._(JSObject _) implements JSObject {
external Date.forTimestamp(int _);
external static int now();
}
const int N = 1000;
class Base extends BenchmarkBase {
Base(super.name);
}
List<Object?> allJSObjects() {
return List<Object?>.generate(N, (i) => createObject(), growable: false);
}
List<Object?> allJSAnys() {
return allJSObjects()
..[1] = 123.toJS
..[2] = 'x'.toJS;
}
List<Object?> allJSAnyQs() {
return allJSObjects()
..[0] = null
..[1] = 123.toJS
..[2] = 'x'.toJS;
}
final List<Object?> allJSNumbers = List<Object?>.generate(
N,
(i) => i.toJS,
growable: false,
);
final List<Object?> allJSStrings = List<Object?>.generate(
N,
(i) => '$i'.toJS,
growable: false,
);
final List<Object?> allJSUint8Array = List<Object?>.generate(
N,
(_) => JSUint8Array.withLength(2),
growable: false,
);
abstract class CastsBase extends Base {
final List<Object?> data;
CastsBase(String name, {required this.data}) : super('JSInterop.as.$name');
}
class Casts1 extends CastsBase {
Casts1() : super('JSObject', data: allJSObjects());
@override
void run() {
for (final o in data) {
sink = o as JSObject;
}
}
}
class Casts2 extends CastsBase {
Casts2() : super('JSAny', data: allJSAnys());
@override
void run() {
for (final o in data) {
sink = o as JSAny;
}
}
}
class Casts3 extends CastsBase {
Casts3() : super('JSAnyQ', data: allJSAnyQs());
@override
void run() {
for (final o in data) {
sink = o as JSAny?;
}
}
}
class Casts4 extends CastsBase {
Casts4() : super('JSNumber', data: allJSNumbers);
@override
void run() {
for (final o in data) {
sink = o as JSNumber;
}
}
}
class Casts5 extends CastsBase {
Casts5() : super('JSString', data: allJSStrings);
@override
void run() {
for (final o in data) {
sink = o as JSString;
}
}
}
class Casts6 extends CastsBase {
Casts6() : super('JSUint8Array', data: allJSUint8Array);
@override
void run() {
for (final o in data) {
sink = o as JSUint8Array;
}
}
}
class CastsT<T> extends CastsBase {
CastsT(String name, {required super.data}) : super('T.$name');
@override
void run() {
for (final o in data) {
sink = o as T;
}
}
}
class Calls1SSS extends Base {
Calls1SSS() : super('JSInterop.calls.inline3ArgsSSS');
@override
void run() {
String s = 'hello';
for (int i = 0; i < N; i++) {
// Pass in arguments with '.toJS' conversion.
// There is potential for a compiler to hoist the conversions.
// Implicit conversion of result of `toString3` to JSString.
// Explicit `.toDart` conversion of result JSString to Dart String.
sink = s = s.toJS.toString3SSS('a'.toJS, 'b'.toJS, 'c'.toJS).toDart;
}
}
}
class Calls1NNN extends Base {
Calls1NNN() : super('JSInterop.calls.inline3ArgsIII');
@override
void run() {
String s = 'hello';
for (int i = 0; i < N; i++) {
// Pass in arguments with '.toJS' conversion.
// There is potential for a compiler to hoist the conversions.
// Implicit conversion of result of `toString3` to JSString.
// Explicit `.toDart` conversion of result JSString to Dart String.
sink = s = s.toJS.toString3NNN(1.toJS, 2.toJS, 3.toJS).toDart;
}
}
}
class Calls2SSS extends Base {
Calls2SSS() : super('JSInterop.calls.hoisted3ArgsSSS');
static final _a = 'a'.toJS;
static final _b = 'b'.toJS;
static final _c = 'c'.toJS;
@override
void run() {
String s = 'hello';
for (int i = 0; i < N; i++) {
// Manually hoisted argument conversions.
// Implicit conversion of result of `toString3` to JSString.
// Explicit `.toDart` conversion of result JSString to Dart String.
sink = s = s.toJS.toString3SSS(_a, _b, _c).toDart;
}
}
}
class Calls2NNN extends Base {
Calls2NNN() : super('JSInterop.calls.hoisted3ArgsIII');
static final _a = 1.toJS;
static final _b = 2.toJS;
static final _c = 3.toJS;
@override
void run() {
String s = 'hello';
for (int i = 0; i < N; i++) {
// Manually hoisted argument conversions.
// Implicit conversion of result of `toString3` to JSString.
// Explicit `.toDart` conversion of result JSString to Dart String.
sink = s = s.toJS.toString3NNN(_a, _b, _c).toDart;
}
}
}
class Calls3SSS extends Base {
Calls3SSS() : super('JSInterop.calls.implicit3ArgsSSS');
@override
void run() {
String s = 'hello';
for (int i = 0; i < N; i++) {
// Implicit conversion of arguments and result as they have a primitive
// type.
sink = s = s.toJS.toString3ConvertedSSS('a', 'b', 'c');
}
}
}
class Calls3NNN extends Base {
Calls3NNN() : super('JSInterop.calls.implicit3ArgsIII');
@override
void run() {
String s = 'hello';
for (int i = 0; i < N; i++) {
// Implicit conversion of arguments and result as they have a primitive
// type.
sink = s = s.toJS.toString3ConvertedIII(1, 2, 3);
}
}
}
class Calls4 extends Base {
Calls4() : super('JSInterop.calls.inline7Args');
@override
void run() {
String s = 'hello';
for (int i = 0; i < N; i++) {
sink = s = s.toJS
.toString7(
'1'.toJS,
'2'.toJS,
'3'.toJS,
'4'.toJS,
'5'.toJS,
'6'.toJS,
'7'.toJS,
)
.toDart;
}
}
}
class Calls5 extends Base {
Calls5() : super('JSInterop.calls.moveJSObject');
static final _o = JSObject();
@override
void run() {
JSObject o = _o;
for (int i = 0; i < N; i++) {
// Implicit conversion of result to JSObject.
sink = o = ObjectMethods.moveJSObject(o);
}
}
}
class Calls6 extends Base {
Calls6() : super('JSInterop.calls.moveJSArray');
static final _o = JSArray.withLength(2);
@override
void run() {
JSArray o = _o;
for (int i = 0; i < N; i++) {
// Implicit conversion of result to JSArray.
sink = o = ObjectMethods.moveJSArray(o);
}
}
}
class Calls7 extends Base {
Calls7() : super('JSInterop.calls.moveJSUint8Array');
static final _o = JSUint8Array.withLength(2);
@override
void run() {
JSUint8Array o = _o;
for (int i = 0; i < N; i++) {
// Implicit conversion of result to JSUint8Array.
sink = o = ObjectMethods.moveJSUint8Array(o);
}
}
}
List<JSAny?> _objects() => List<JSAny?>.generate(
N,
(i) => switch (i % 7) {
0 => JSObject(),
1 => i.toJS,
2 => JSArray(),
3 => '$i'.toJS,
4 => Date.forTimestamp(Date.now()),
5 => null,
6 => JSUint8Array.withLength(2),
int() => throw StateError('unexpected: $i'),
},
growable: false,
);
abstract class IsABase extends Base {
IsABase(String name) : super('JSInterop.isA.$name');
final List<JSAny?> objects = _objects();
}
class IsA1 extends IsABase {
IsA1() : super('JSObject');
@override
void run() {
for (final o in objects) {
sink = o.isA<JSObject>();
}
}
}
class IsA2 extends IsABase {
IsA2() : super('JSAny');
@override
void run() {
for (final o in objects) {
sink = o.isA<JSAny>();
}
}
}
class IsA3 extends IsABase {
IsA3() : super('JSString');
@override
void run() {
for (final o in objects) {
sink = o.isA<JSString>();
}
}
}
class IsA4 extends IsABase {
IsA4() : super('JSArray');
@override
void run() {
for (final o in objects) {
sink = o.isA<JSArray>();
}
}
}
class IsA5 extends IsABase {
IsA5() : super('Date');
@override
void run() {
for (final o in objects) {
sink = o.isA<Date>();
}
}
}
class IsA6 extends IsABase {
IsA6() : super('DateQ');
@override
void run() {
for (final o in objects) {
sink = o.isA<Date?>();
}
}
}
class IsA7 extends IsABase {
IsA7() : super('JSUint8Array');
@override
void run() {
for (final o in objects) {
sink = o.isA<JSUint8Array>();
}
}
}
/// Returns a new JSObject, but with a type that is hard for an optimizing
/// compiler to know exactly.
Object? createObject() {
if (inscrutableTrue) return JSObject();
if (inscrutableTrue) return 123.toJS;
if (inscrutableTrue) return 'x'.toJS;
if (inscrutableTrue) return true.toJS;
if (inscrutableTrue) return StringBuffer();
return null;
}
bool get inscrutableTrue => DateTime.now().millisecondsSinceEpoch > 42;
Object? sink;
void main() {
print(Date.forTimestamp(Date.now()));
final benchmarks = [
IsA1(),
IsA2(),
IsA3(),
IsA4(),
IsA5(),
IsA6(),
IsA7(),
Calls1SSS(),
Calls1NNN(),
Calls2SSS(),
Calls2NNN(),
Calls3SSS(),
Calls3NNN(),
Calls4(),
Calls5(),
Calls6(),
Calls7(),
Casts1(),
Casts2(),
Casts3(),
Casts4(),
Casts5(),
Casts6(),
CastsT<JSObject>('JSObject', data: allJSObjects()),
CastsT<JSAny>('JSAny', data: allJSAnys()),
CastsT<JSAny?>('JSAnyQ', data: allJSAnyQs()),
CastsT<JSNumber>('JSNumber', data: allJSNumbers),
CastsT<JSString>('JSString', data: allJSStrings),
CastsT<JSUint8Array>('JSUint8Array', data: allJSUint8Array),
];
// Warmup all benchmarks to ensure JIT compilers see full polymorphism.
for (var benchmark in benchmarks) {
benchmark.setup();
benchmark.run();
benchmark.run();
if (sink == null) throw StateError('unexpected');
}
for (var benchmark in benchmarks) {
benchmark.run();
}
for (var benchmark in benchmarks) {
benchmark.report();
}
}