Optimize js_util callMethod calls for 0-4 arguments.

Change completes initial set of js_util optimizations. Some usages
of `getProperty`, `setProperty`, and `callMethod` with <= 4
arguments will have unnecessary checks removed by the compilers.

Change-Id: I94d5402ca9a64ad2818fd6d6a5f7f114f87348c5
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/204562
Commit-Queue: Riley Porter <rileyporter@google.com>
Reviewed-by: Srujan Gaddam <srujzs@google.com>
Reviewed-by: Sigmund Cherem <sigmund@google.com>
This commit is contained in:
Riley Porter 2021-06-30 20:11:44 +00:00 committed by commit-bot@chromium.org
parent c019575d48
commit 63ac437e99
8 changed files with 445 additions and 13 deletions

View file

@ -12,6 +12,8 @@ import 'package:kernel/kernel.dart';
/// emits the code as a JavaScript code fragment.
class JsUtilOptimizer extends Transformer {
final Procedure _jsTarget;
final Procedure _callMethodTarget;
final List<Procedure> _callMethodUncheckedTargets;
final Procedure _getPropertyTarget;
final Procedure _setPropertyTarget;
final Procedure _setPropertyUncheckedTarget;
@ -27,6 +29,7 @@ class JsUtilOptimizer extends Transformer {
];
final Iterable<Procedure> _allowedInteropJsUtilTargets;
final Procedure _allowInteropTarget;
final Procedure _listEmptyFactory;
final CoreTypes _coreTypes;
final StatefulStaticTypeContext _staticTypeContext;
@ -34,6 +37,12 @@ class JsUtilOptimizer extends Transformer {
JsUtilOptimizer(this._coreTypes, ClassHierarchy hierarchy)
: _jsTarget =
_coreTypes.index.getTopLevelMember('dart:_foreign_helper', 'JS'),
_callMethodTarget =
_coreTypes.index.getTopLevelMember('dart:js_util', 'callMethod'),
_callMethodUncheckedTargets = List<Procedure>.generate(
5,
(i) => _coreTypes.index
.getTopLevelMember('dart:js_util', '_callMethodUnchecked$i')),
_getPropertyTarget =
_coreTypes.index.getTopLevelMember('dart:js_util', 'getProperty'),
_setPropertyTarget =
@ -45,6 +54,8 @@ class JsUtilOptimizer extends Transformer {
_allowedInteropJsUtilTargets = _allowedInteropJsUtilMembers.map(
(member) =>
_coreTypes.index.getTopLevelMember('dart:js_util', member)),
_listEmptyFactory =
_coreTypes.index.getMember('dart:core', 'List', 'empty'),
_staticTypeContext = StatefulStaticTypeContext.stacked(
TypeEnvironment(_coreTypes, hierarchy)) {}
@ -69,12 +80,16 @@ class JsUtilOptimizer extends Transformer {
/// Lowers `getProperty` for any argument type straight to JS fragment call.
/// Lowers `setProperty` to `_setPropertyUnchecked` for values that are
/// not Function type and guaranteed to be interop allowed.
/// Lowers `callMethod` to `_callMethodUncheckedN` when the number of given
/// arguments is 0-4 and all arguments are guaranteed to be interop allowed.
@override
visitStaticInvocation(StaticInvocation node) {
if (node.target == _getPropertyTarget) {
node = _lowerGetProperty(node);
} else if (node.target == _setPropertyTarget) {
node = _lowerSetProperty(node);
} else if (node.target == _callMethodTarget) {
node = _lowerCallMethod(node);
}
node.transformChildren(this);
return node;
@ -104,6 +119,7 @@ class JsUtilOptimizer extends Transformer {
/// Lowers the given js_util `setProperty` call to `_setPropertyUnchecked`
/// when the additional validation checks in `setProperty` can be elided.
///
/// Removing the checks allows further inlining by the compilers.
StaticInvocation _lowerSetProperty(StaticInvocation node) {
Arguments arguments = node.arguments;
@ -119,6 +135,85 @@ class JsUtilOptimizer extends Transformer {
..fileOffset = node.fileOffset;
}
/// Lowers the given js_util `callMethod` call to `_callMethodUncheckedN`
/// when the additional validation checks on the arguments can be elided.
///
/// Calls will be lowered when using a List literal or constant list with 0-4
/// elements for the `callMethod` arguments, or the `List.empty()` factory.
/// Removing the checks allows further inlining by the compilers.
StaticInvocation _lowerCallMethod(StaticInvocation node) {
Arguments arguments = node.arguments;
assert(arguments.types.isEmpty);
assert(arguments.positional.length == 3);
assert(arguments.named.isEmpty);
// Lower List.empty factory call.
var argumentsList = arguments.positional.last;
if (argumentsList is StaticInvocation &&
argumentsList.target == _listEmptyFactory) {
return _createNewCallMethodNode([], arguments, node.fileOffset);
}
// Lower other kinds of Lists.
var callMethodArguments;
var entryType;
if (argumentsList is ListLiteral) {
if (argumentsList.expressions.length >=
_callMethodUncheckedTargets.length) {
return node;
}
callMethodArguments = argumentsList.expressions;
entryType = argumentsList.typeArgument;
} else if (argumentsList is ConstantExpression &&
argumentsList.constant is ListConstant) {
var argumentsListConstant = argumentsList.constant as ListConstant;
if (argumentsListConstant.entries.length >=
_callMethodUncheckedTargets.length) {
return node;
}
callMethodArguments = argumentsListConstant.entries
.map((constant) => ConstantExpression(
constant, constant.getType(_staticTypeContext)))
.toList();
entryType = argumentsListConstant.typeArgument;
} else {
// Skip lowering any other type of List.
return node;
}
// Check the overall List entry type, then verify each argument if needed.
if (!_allowedInteropType(entryType)) {
for (var argument in callMethodArguments) {
if (!_allowedInterop(argument)) {
return node;
}
}
}
return _createNewCallMethodNode(
callMethodArguments, arguments, node.fileOffset);
}
/// Creates a new StaticInvocation node for `_callMethodUncheckedN` with the
/// given 0-4 arguments.
StaticInvocation _createNewCallMethodNode(
List<Expression> callMethodArguments,
Arguments arguments,
int nodeFileOffset) {
assert(callMethodArguments.length <= 4);
return StaticInvocation(
_callMethodUncheckedTargets[callMethodArguments.length],
Arguments(
[
arguments.positional[0],
arguments.positional[1],
...callMethodArguments
],
types: [],
)..fileOffset = arguments.fileOffset)
..fileOffset = nodeFileOffset;
}
/// Returns whether the given Expression is guaranteed to be allowed to
/// interop with JS.
///
@ -138,7 +233,12 @@ class JsUtilOptimizer extends Transformer {
if (_allowedInteropJsUtilTargets.contains(node.target)) return true;
}
var type = node.getStaticType(_staticTypeContext);
return _allowedInteropType(node.getStaticType(_staticTypeContext));
}
/// Returns whether the given DartType is guaranteed to be not a function
/// and therefore allowed to interop with JS.
bool _allowedInteropType(DartType type) {
if (type is InterfaceType) {
return type.classNode != _coreTypes.functionClass &&
type.classNode != _coreTypes.objectClass;

View file

@ -68,13 +68,12 @@ dynamic newObject() => JS('=Object', '{}');
bool hasProperty(Object o, Object name) => JS('bool', '# in #', name, o);
// All usage optimized away in a CFE transformation. Changes here will not
// affect the generated JS.
// A CFE transformation will optimize all calls to `getProperty`.
dynamic getProperty(Object o, Object name) =>
JS('Object|Null', '#[#]', o, name);
// Some usage optimized away in a CFE transformation. If given value is a
// function, changes here will not affect the generated JS.
// A CFE transformation may optimize calls to `setProperty`, when [value] is
// statically known to be a non-function.
dynamic setProperty(Object o, Object name, Object? value) {
assertInterop(value);
return JS('', '#[#]=#', o, name, value);
@ -86,11 +85,48 @@ dynamic _setPropertyUnchecked(Object o, Object name, Object? value) {
return JS('', '#[#]=#', o, name, value);
}
// A CFE transformation may optimize calls to `callMethod` when [args] is a
// a list literal or const list containing at most 4 values, all of which are
// statically known to be non-functions.
dynamic callMethod(Object o, String method, List<Object?> args) {
assertInteropArgs(args);
return JS('Object|Null', '#[#].apply(#, #)', o, method, o, args);
}
/// Unchecked version for 0 arguments, only used in a CFE transformation.
@pragma('dart2js:tryInline')
dynamic _callMethodUnchecked0(Object o, String method) {
return JS('Object|Null', '#[#]()', o, method);
}
/// Unchecked version for 1 argument, only used in a CFE transformation.
@pragma('dart2js:tryInline')
dynamic _callMethodUnchecked1(Object o, String method, Object? arg1) {
return JS('Object|Null', '#[#](#)', o, method, arg1);
}
/// Unchecked version for 2 arguments, only used in a CFE transformation.
@pragma('dart2js:tryInline')
dynamic _callMethodUnchecked2(
Object o, String method, Object? arg1, Object? arg2) {
return JS('Object|Null', '#[#](#, #)', o, method, arg1, arg2);
}
/// Unchecked version for 3 arguments, only used in a CFE transformation.
@pragma('dart2js:tryInline')
dynamic _callMethodUnchecked3(
Object o, String method, Object? arg1, Object? arg2, Object? arg3) {
return JS('Object|Null', '#[#](#, #, #)', o, method, arg1, arg2, arg3);
}
/// Unchecked version for 4 arguments, only used in a CFE transformation.
@pragma('dart2js:tryInline')
dynamic _callMethodUnchecked4(Object o, String method, Object? arg1,
Object? arg2, Object? arg3, Object? arg4) {
return JS(
'Object|Null', '#[#](#, #, #, #)', o, method, arg1, arg2, arg3, arg4);
}
/// Check whether [o] is an instance of [type].
///
/// The value in [type] is expected to be a JS-interop object that

View file

@ -0,0 +1,37 @@
// Copyright (c) 2021, 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.
// Tests the functionality of object properties with the js_util library that
// involve implicit type checks.
@JS()
library js_util_properties_implicit_checks_test;
import 'package:js/js.dart';
import 'package:js/js_util.dart' as js_util;
import 'package:expect/minitest.dart';
@JS()
external void eval(String code);
@JS()
class CallMethodTest {
external CallMethodTest();
external one(a);
}
main() {
eval(r"""
function CallMethodTest() {}
CallMethodTest.prototype.one = function(a) {
return 'one';
}
""");
var o = CallMethodTest();
expect(() => js_util.callMethod(o, 'one', <String>[5 as dynamic]), throws);
expect(() => js_util.callMethod(o, 'one', <int>['foo' as dynamic]), throws);
}

View file

@ -53,6 +53,9 @@ class ExampleTypedLiteral {
class DartClass {
int x = 3;
int getX() => x;
static staticFunction() => 'static';
static const staticConstList = [1];
}
class GenericDartClass<T> {
@ -70,6 +73,18 @@ String _getBarWithSideEffect() {
return 'bar';
}
@JS()
class CallMethodTest {
external CallMethodTest();
external zero();
external one(a);
external two(a, b);
external three(a, b, c);
external four(a, b, c, d);
external five(a, b, c, d, e);
}
main() {
eval(r"""
function Foo(a) {
@ -121,6 +136,27 @@ main() {
Foo.prototype.callFn = function(fn) {
return fn();
}
function CallMethodTest() {}
CallMethodTest.prototype.zero = function() {
return 'zero';
}
CallMethodTest.prototype.one = function(a) {
return 'one';
}
CallMethodTest.prototype.two = function(a, b) {
return 'two';
}
CallMethodTest.prototype.three = function(a, b, c) {
return 'three';
}
CallMethodTest.prototype.four = function(a, b, c, d) {
return 'four';
}
CallMethodTest.prototype.five = function(a, b, c, d, e) {
return 'five';
}
""");
group('newObject', () {
@ -324,6 +360,8 @@ main() {
return 'Inline';
}));
expect(js_util.callMethod(f, 'bar', []), equals('Inline'));
js_util.setProperty(f, 'bar', allowInterop(DartClass.staticFunction));
expect(js_util.callMethod(f, 'bar', []), equals('static'));
// Set property to a JS function.
js_util.setProperty(f, 'bar', allowInterop(jsFunction));
@ -403,16 +441,20 @@ main() {
expect(js_util.callMethod(f, 'sumFn', [2, 3]), equals(5));
expect(js_util.callMethod(f, 'getA', [f]), equals(42));
expect(js_util.callMethod(f, 'callFn', [allowInterop(jsFunction)]),
equals("JS Function"));
equals('JS Function'));
expect(js_util.callMethod(f, 'callFn', [allowInterop(dartFunction)]),
equals("Dart Function"));
equals('Dart Function'));
expect(
js_util.callMethod(f, 'callFn', [
allowInterop(() {
return "inline";
return 'inline';
})
]),
equals("inline"));
equals('inline'));
expect(
js_util.callMethod(
f, 'callFn', [allowInterop(DartClass.staticFunction)]),
equals('static'));
// Using a variable for the method name.
String methodName = 'bar';
@ -420,6 +462,70 @@ main() {
String bar = _getBarWithSideEffect();
expect(js_util.callMethod(f, bar, []), equals(42));
});
test('callMethod with List edge cases', () {
var o = CallMethodTest();
expect(js_util.callMethod(o, 'zero', List.empty()), equals('zero'));
expect(js_util.callMethod(o, 'zero', List<int>.empty()), equals('zero'));
expect(
js_util.callMethod(o, 'two', List<int>.filled(2, 0)), equals('two'));
expect(js_util.callMethod(o, 'three', List<int>.generate(3, (i) => i)),
equals('three'));
Iterable<String> iterableStrings = <String>['foo', 'bar'];
expect(js_util.callMethod(o, 'two', List.of(iterableStrings)),
equals('two'));
const l1 = [1, 2];
const l2 = [3, 4];
expect(js_util.callMethod(o, 'four', List.from(l1)..addAll(l2)),
equals('four'));
expect(js_util.callMethod(o, 'four', l1 + l2), equals('four'));
expect(js_util.callMethod(o, 'four', List.unmodifiable([1, 2, 3, 4])),
equals('four'));
var setElements = {1, 2};
expect(js_util.callMethod(o, 'two', setElements.toList()), equals('two'));
var spreadList = [1, 2, 3];
expect(js_util.callMethod(o, 'four', [1, ...spreadList]), equals('four'));
});
test('edge cases for lowering to _callMethodUncheckedN', () {
var o = CallMethodTest();
expect(js_util.callMethod(o, 'zero', []), equals('zero'));
expect(js_util.callMethod(o, 'one', [1]), equals('one'));
expect(js_util.callMethod(o, 'four', [1, 2, 3, 4]), equals('four'));
expect(js_util.callMethod(o, 'five', [1, 2, 3, 4, 5]), equals('five'));
// List with a type declaration, short circuits element checking
expect(js_util.callMethod(o, 'two', <int>[1, 2]), equals('two'));
// List as a variable instead of a List Literal or constant
var list = [1, 2];
expect(js_util.callMethod(o, 'two', list), equals('two'));
// Mixed types of elements to check in the given list.
var x = 4;
var str = 'cat';
var b = false;
var evens = [2, 4, 6];
expect(js_util.callMethod(o, 'four', [x, str, b, evens]), equals('four'));
var obj = Object();
expect(js_util.callMethod(o, 'one', [obj]), equals('one'));
var nullElement = null;
expect(js_util.callMethod(o, 'one', [nullElement]), equals('one'));
// const lists.
expect(js_util.callMethod(o, 'one', const [3]), equals('one'));
const constList = [10, 20, 30];
expect(js_util.callMethod(o, 'three', constList), equals('three'));
expect(js_util.callMethod(o, 'one', DartClass.staticConstList),
equals('one'));
});
});
group('instanceof', () {

View file

@ -26,6 +26,9 @@ wasm/*: SkipByDesign # dart:wasm not currently supported on web.
[ $compiler != dart2js ]
async/dart2js_uncaught_error_test: Skip # JS-integration only test
[ $builder_tag == dart2js_production && $compiler == dart2js ]
js/js_util/properties_implicit_checks_test: SkipByDesign # No implicit checks in production mode
[ $compiler == dart2js && $runtime == chrome ]
async/slow_consumer2_test: SkipSlow # Times out. Issue 22050
convert/streamed_conversion_json_utf8_decode_test: SkipSlow # Times out. Issue 22050

View file

@ -0,0 +1,39 @@
// Copyright (c) 2021, 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.
// @dart = 2.9
// Tests the functionality of object properties with the js_util library that
// involve implicit type checks.
@JS()
library js_util_properties_implicit_checks_test;
import 'package:js/js.dart';
import 'package:js/js_util.dart' as js_util;
import 'package:expect/minitest.dart';
@JS()
external void eval(String code);
@JS()
class CallMethodTest {
external CallMethodTest();
external one(a);
}
main() {
eval(r"""
function CallMethodTest() {}
CallMethodTest.prototype.one = function(a) {
return 'one';
}
""");
var o = CallMethodTest();
expect(() => js_util.callMethod(o, 'one', <String>[5 as dynamic]), throws);
expect(() => js_util.callMethod(o, 'one', <int>['foo' as dynamic]), throws);
}

View file

@ -55,6 +55,9 @@ class ExampleTypedLiteral {
class DartClass {
int x = 3;
int getX() => x;
static staticFunction() => 'static';
static const staticConstList = [1];
}
class GenericDartClass<T> {
@ -72,6 +75,18 @@ String _getBarWithSideEffect() {
return 'bar';
}
@JS()
class CallMethodTest {
external CallMethodTest();
external zero();
external one(a);
external two(a, b);
external three(a, b, c);
external four(a, b, c, d);
external five(a, b, c, d, e);
}
main() {
eval(r"""
function Foo(a) {
@ -123,6 +138,27 @@ main() {
Foo.prototype.callFn = function(fn) {
return fn();
}
function CallMethodTest() {}
CallMethodTest.prototype.zero = function() {
return 'zero';
}
CallMethodTest.prototype.one = function(a) {
return 'one';
}
CallMethodTest.prototype.two = function(a, b) {
return 'two';
}
CallMethodTest.prototype.three = function(a, b, c) {
return 'three';
}
CallMethodTest.prototype.four = function(a, b, c, d) {
return 'four';
}
CallMethodTest.prototype.five = function(a, b, c, d, e) {
return 'five';
}
""");
group('newObject', () {
@ -326,6 +362,8 @@ main() {
return 'Inline';
}));
expect(js_util.callMethod(f, 'bar', []), equals('Inline'));
js_util.setProperty(f, 'bar', allowInterop(DartClass.staticFunction));
expect(js_util.callMethod(f, 'bar', []), equals('static'));
// Set property to a JS function.
js_util.setProperty(f, 'bar', allowInterop(jsFunction));
@ -405,16 +443,20 @@ main() {
expect(js_util.callMethod(f, 'sumFn', [2, 3]), equals(5));
expect(js_util.callMethod(f, 'getA', [f]), equals(42));
expect(js_util.callMethod(f, 'callFn', [allowInterop(jsFunction)]),
equals("JS Function"));
equals('JS Function'));
expect(js_util.callMethod(f, 'callFn', [allowInterop(dartFunction)]),
equals("Dart Function"));
equals('Dart Function'));
expect(
js_util.callMethod(f, 'callFn', [
allowInterop(() {
return "inline";
return 'inline';
})
]),
equals("inline"));
equals('inline'));
expect(
js_util.callMethod(
f, 'callFn', [allowInterop(DartClass.staticFunction)]),
equals('static'));
// Using a variable for the method name.
String methodName = 'bar';
@ -422,6 +464,72 @@ main() {
String bar = _getBarWithSideEffect();
expect(js_util.callMethod(f, bar, []), equals(42));
});
test('callMethod with List edge cases', () {
var o = CallMethodTest();
expect(js_util.callMethod(o, 'zero', List()), equals('zero'));
expect(js_util.callMethod(o, 'zero', List<int>()), equals('zero'));
expect(js_util.callMethod(o, 'zero', List.empty()), equals('zero'));
expect(js_util.callMethod(o, 'zero', List<int>.empty()), equals('zero'));
expect(
js_util.callMethod(o, 'two', List<int>.filled(2, 0)), equals('two'));
expect(js_util.callMethod(o, 'three', List<int>.generate(3, (i) => i)),
equals('three'));
Iterable<String> iterableStrings = <String>['foo', 'bar'];
expect(js_util.callMethod(o, 'two', List.of(iterableStrings)),
equals('two'));
const l1 = [1, 2];
const l2 = [3, 4];
expect(js_util.callMethod(o, 'four', List.from(l1)..addAll(l2)),
equals('four'));
expect(js_util.callMethod(o, 'four', l1 + l2), equals('four'));
expect(js_util.callMethod(o, 'four', List.unmodifiable([1, 2, 3, 4])),
equals('four'));
var setElements = {1, 2};
expect(js_util.callMethod(o, 'two', setElements.toList()), equals('two'));
var spreadList = [1, 2, 3];
expect(js_util.callMethod(o, 'four', [1, ...spreadList]), equals('four'));
});
test('edge cases for lowering to _callMethodUncheckedN', () {
var o = CallMethodTest();
expect(js_util.callMethod(o, 'zero', []), equals('zero'));
expect(js_util.callMethod(o, 'one', [1]), equals('one'));
expect(js_util.callMethod(o, 'four', [1, 2, 3, 4]), equals('four'));
expect(js_util.callMethod(o, 'five', [1, 2, 3, 4, 5]), equals('five'));
// List with a type declaration, short circuits element checking
expect(js_util.callMethod(o, 'two', <int>[1, 2]), equals('two'));
// List as a variable instead of a List Literal or constant
var list = [1, 2];
expect(js_util.callMethod(o, 'two', list), equals('two'));
// Mixed types of elements to check in the given list.
var x = 4;
var str = 'cat';
var b = false;
var evens = [2, 4, 6];
expect(js_util.callMethod(o, 'four', [x, str, b, evens]), equals('four'));
var obj = Object();
expect(js_util.callMethod(o, 'one', [obj]), equals('one'));
var nullElement = null;
expect(js_util.callMethod(o, 'one', [nullElement]), equals('one'));
// const lists.
expect(js_util.callMethod(o, 'one', const [3]), equals('one'));
const constList = [10, 20, 30];
expect(js_util.callMethod(o, 'three', constList), equals('three'));
expect(js_util.callMethod(o, 'one', DartClass.staticConstList),
equals('one'));
});
});
group('instanceof', () {

View file

@ -24,6 +24,9 @@ wasm/*: SkipByDesign # dart:wasm not currently supported on web.
[ $compiler != dart2js ]
async/dart2js_uncaught_error_test: Skip # JS-integration only test
[ $builder_tag == dart2js_production && $compiler == dart2js ]
js/js_util/properties_implicit_checks_test: SkipByDesign # No implicit checks in production mode
[ $compiler == dart2js && $runtime == chrome ]
async/slow_consumer2_test: SkipSlow # Times out. Issue 22050
convert/streamed_conversion_json_utf8_decode_test: SkipSlow # Times out. Issue 22050