Skip to content

Commit 273ae87

Browse files
osa1Commit Queue
authored andcommitted
[dart2wasm] JS interop: use dartify functions specific to types being converted
`dartifyRaw` converts based on the runtime type and it's slow. Use dartify functions specific to the expected Dart types when converting JS values to Dart. Benchmarks before: WasmJSInterop.call.bool.0Args(RunTimeRaw): 0.023022675490940652 ns. WasmJSInterop.call.nullableBool.0Args(RunTimeRaw): 0.02189111747851003 ns. WasmJSInterop.call.num.0Args(RunTimeRaw): 0.028148974752406197 ns. WasmJSInterop.call.nullableNum.0Args(RunTimeRaw): 0.028181565023670287 ns. WasmJSInterop.call.double.0Args(RunTimeRaw): 0.022986874778290176 ns. WasmJSInterop.call.nullableDouble.0Args(RunTimeRaw): 0.02907384054925038 ns. WasmJSInterop.call.int.0Args(RunTimeRaw): 0.029822888283378746 ns. WasmJSInterop.call.nullableInt.0Args(RunTimeRaw): 0.029088963619688168 ns. WasmJSInterop.call.string.0Args(RunTimeRaw): 0.020517746991606835 ns. WasmJSInterop.call.nullableString.0Args(RunTimeRaw): 0.02004299314157027 ns. WasmJSInterop.call.JSArray.0Args(RunTimeRaw): 0.020724174653887113 ns. WasmJSInterop.call.nullableJSArray.0Args(RunTimeRaw): 0.020034212115113702 ns. WasmJSInterop.call.JSUint8Array.0Args(RunTimeRaw): 0.030711090544501003 ns. WasmJSInterop.call.nullableJSUint8Array.0Args(RunTimeRaw): 0.030679086538461537 ns. Benchmarks after: WasmJSInterop.call.bool.0Args(RunTimeRaw): 0.019944312674022892 ns. WasmJSInterop.call.nullableBool.0Args(RunTimeRaw): 0.0206124732933157 ns. WasmJSInterop.call.num.0Args(RunTimeRaw): 0.024475179056557175 ns. WasmJSInterop.call.nullableNum.0Args(RunTimeRaw): 0.02481147236988503 ns. WasmJSInterop.call.double.0Args(RunTimeRaw): 0.01829122645842903 ns. WasmJSInterop.call.nullableDouble.0Args(RunTimeRaw): 0.024581756014165346 ns. WasmJSInterop.call.int.0Args(RunTimeRaw): 0.01788861473387381 ns. WasmJSInterop.call.nullableInt.0Args(RunTimeRaw): 0.02333933933933934 ns. WasmJSInterop.call.string.0Args(RunTimeRaw): 0.018238532110091743 ns. WasmJSInterop.call.nullableString.0Args(RunTimeRaw): 0.019454117647058823 ns. WasmJSInterop.call.JSArray.0Args(RunTimeRaw): 0.020055434567412395 ns. WasmJSInterop.call.nullableJSArray.0Args(RunTimeRaw): 0.019759825327510916 ns. WasmJSInterop.call.JSUint8Array.0Args(RunTimeRaw): 0.030023959269242288 ns. WasmJSInterop.call.nullableJSUint8Array.0Args(RunTimeRaw): 0.03057233704292528 ns. JSValue wrapper type performance (`JSUint8Array`, `JSArray`) does not change as we had a special case (before this CL) for those types and directly boxed them. Issue: #60357 Change-Id: I0ee8e49cf3ed801634e4c6e13b1c597b2e730d59 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/424021 Commit-Queue: Ömer Ağacan <[email protected]> Reviewed-by: Martin Kustermann <[email protected]>
1 parent bbb847e commit 273ae87

File tree

4 files changed

+118
-18
lines changed

4 files changed

+118
-18
lines changed

pkg/dart2wasm/lib/js/util.dart

Lines changed: 92 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ class CoreTypesUtil {
3535
final Member wasmExternRefNullRef;
3636
final Class wasmI32Class;
3737
final Procedure wasmI32ToIntSigned;
38+
final Procedure isDartNullTarget;
39+
final Procedure throwArgumentNullErrorTarget;
3840

3941
// Dart value to JS converters.
4042
final Procedure toJSBoolean;
@@ -94,7 +96,7 @@ class CoreTypesUtil {
9496

9597
// NB. We rely on iteration ordering being insertion order to handle subtypes
9698
// before supertypes to convert as `int` and `double` before `num`.
97-
late final Map<Class, Procedure> _externRefConverterMap = {
99+
late final Map<Class, Procedure> _jsifyMap = {
98100
coreTypes.boolClass: toJSBoolean,
99101
coreTypes.intClass: jsifyInt,
100102
coreTypes.doubleClass: toJSNumber,
@@ -127,6 +129,44 @@ class CoreTypesUtil {
127129
coreTypes.functionClass: jsifyFunction,
128130
};
129131

132+
late final Map<Class, Procedure> _dartifyMap = {
133+
coreTypes.boolClass:
134+
coreTypes.index.getTopLevelProcedure('dart:_js_helper', 'toDartBool'),
135+
coreTypes.intClass:
136+
coreTypes.index.getTopLevelProcedure('dart:_js_helper', 'dartifyInt'),
137+
coreTypes.doubleClass:
138+
coreTypes.index.getTopLevelProcedure('dart:_js_helper', 'toDartNumber'),
139+
coreTypes.numClass:
140+
coreTypes.index.getTopLevelProcedure('dart:_js_helper', 'toDartNumber'),
141+
coreTypes.stringClass:
142+
coreTypes.index.getProcedure('dart:_string', 'JSStringImpl', 'fromRef'),
143+
coreTypes.listClass:
144+
coreTypes.index.getTopLevelProcedure('dart:_js_helper', 'toDartList'),
145+
coreTypes.index.getClass('dart:typed_data', 'Int8List'): coreTypes.index
146+
.getProcedure('dart:_js_types', 'JSInt8ArrayImpl', 'fromRef'),
147+
coreTypes.index.getClass('dart:typed_data', 'Uint8List'): coreTypes.index
148+
.getProcedure('dart:_js_types', 'JSUint8ArrayImpl', 'fromRef'),
149+
coreTypes.index.getClass('dart:typed_data', 'Uint8ClampedList'): coreTypes
150+
.index
151+
.getProcedure('dart:_js_types', 'JSUint8ClampedArrayImpl', 'fromRef'),
152+
coreTypes.index.getClass('dart:typed_data', 'Int16List'): coreTypes.index
153+
.getProcedure('dart:_js_types', 'JSInt16ArrayImpl', 'fromRef'),
154+
coreTypes.index.getClass('dart:typed_data', 'Uint16List'): coreTypes.index
155+
.getProcedure('dart:_js_types', 'JSUint16ArrayImpl', 'fromRef'),
156+
coreTypes.index.getClass('dart:typed_data', 'Int32List'): coreTypes.index
157+
.getProcedure('dart:_js_types', 'JSInt32ArrayImpl', 'fromRef'),
158+
coreTypes.index.getClass('dart:typed_data', 'Uint32List'): coreTypes.index
159+
.getProcedure('dart:_js_types', 'JSUint32ArrayImpl', 'fromRef'),
160+
coreTypes.index.getClass('dart:typed_data', 'Float32List'): coreTypes.index
161+
.getProcedure('dart:_js_types', 'JSFloat32ArrayImpl', 'fromRef'),
162+
coreTypes.index.getClass('dart:typed_data', 'Float64List'): coreTypes.index
163+
.getProcedure('dart:_js_types', 'JSFloat64ArrayImpl', 'fromRef'),
164+
coreTypes.index.getClass('dart:typed_data', 'ByteBuffer'): coreTypes.index
165+
.getProcedure('dart:_js_types', 'JSArrayBufferImpl', 'fromRef'),
166+
coreTypes.index.getClass('dart:typed_data', 'ByteData'): coreTypes.index
167+
.getProcedure('dart:_js_types', 'JSDataViewImpl', 'fromRef'),
168+
};
169+
130170
CoreTypesUtil(this.coreTypes, this.extensionIndex)
131171
: allowInteropTarget = coreTypes.index
132172
.getTopLevelProcedure('dart:js_util', 'allowInterop'),
@@ -272,7 +312,11 @@ class CoreTypesUtil {
272312
byteBufferClass =
273313
coreTypes.index.getClass('dart:typed_data', 'ByteBuffer'),
274314
jsArrayBufferImplClass =
275-
coreTypes.index.getClass('dart:_js_types', 'JSArrayBufferImpl');
315+
coreTypes.index.getClass('dart:_js_types', 'JSArrayBufferImpl'),
316+
isDartNullTarget = coreTypes.index
317+
.getTopLevelProcedure('dart:_js_helper', 'isDartNull'),
318+
throwArgumentNullErrorTarget = coreTypes.index.getTopLevelProcedure(
319+
'dart:_error_utils', '_throwArgumentNullError');
276320

277321
DartType get nonNullableObjectType =>
278322
coreTypes.objectRawType(Nullability.nonNullable);
@@ -322,11 +366,11 @@ class CoreTypesUtil {
322366
functionType: greaterThanOrEqualToTarget.getterType as FunctionType,
323367
);
324368

325-
/// Cast the [invocation] if needed to conform to the expected [returnType].
369+
/// Cast the [invocation] if needed to conform to the expected [expectedType].
326370
Expression castInvocationForReturn(
327-
Expression invocation, DartType returnType) {
371+
Expression invocation, DartType expectedType) {
328372
Expression expression;
329-
if (returnType is VoidType) {
373+
if (expectedType is VoidType) {
330374
// Technically a `void` return value can still be used, by casting the
331375
// return type to `dynamic` or `Object?`. However this case should be
332376
// extremely rare, and `dartifyRaw` overhead for return values that should
@@ -336,27 +380,42 @@ class CoreTypesUtil {
336380
Block([ExpressionStatement(invocation)]), NullLiteral());
337381
}
338382

339-
if (isJSValueType(returnType)) {
383+
if (isJSValueType(expectedType)) {
340384
// TODO(joshualitt): Expose boxed `JSNull` and `JSUndefined` to Dart
341385
// code after migrating existing users of js interop on Dart2Wasm.
342386
// expression = _createJSValue(invocation);
343387
// Casts are expensive, so we stick to a null-assertion if needed. If
344388
// the nullability can't be determined, cast.
345389
expression = invokeOneArg(jsValueBoxTarget, invocation);
346-
final nullability = returnType.extensionTypeErasure.nullability;
390+
final nullability = expectedType.extensionTypeErasure.nullability;
347391
if (nullability == Nullability.nonNullable) {
348392
expression = NullCheck(expression);
349393
} else if (nullability == Nullability.undetermined) {
350-
expression = AsExpression(expression, returnType);
394+
expression = AsExpression(expression, expectedType);
351395
}
352396
} else {
353-
// Because we simply don't have enough information, we leave all JS
354-
// numbers as doubles. However, in cases where we know the user expects
355-
// an `int` we check that the double is an integer, and then insert a
356-
// cast. We also let static interop types flow through without
357-
// conversion, both as arguments, and as the return type.
358-
expression = convertAndCast(
359-
returnType, invokeOneArg(dartifyRawTarget, invocation));
397+
final expectedTypeExtensionTypeErasure =
398+
expectedType.extensionTypeErasure;
399+
final expectNullable =
400+
expectedTypeExtensionTypeErasure.isPotentiallyNullable;
401+
final conversionProcedure =
402+
_dartConversionProcedure(expectedTypeExtensionTypeErasure);
403+
final invocationValueVar = VariableDeclaration('#jsInvocation',
404+
initializer: invocation,
405+
type: nullableWasmExternRefType,
406+
isSynthesized: true);
407+
expression = Let(
408+
invocationValueVar,
409+
ConditionalExpression(
410+
StaticInvocation(
411+
isDartNullTarget, Arguments([VariableGet(invocationValueVar)])),
412+
expectNullable
413+
? NullLiteral()
414+
: StaticInvocation(throwArgumentNullErrorTarget, Arguments([])),
415+
invokeOneArg(conversionProcedure, VariableGet(invocationValueVar)),
416+
expectedType,
417+
),
418+
);
360419
}
361420
return expression;
362421
}
@@ -419,7 +478,7 @@ class CoreTypesUtil {
419478
/// non-nullable. This function does not check the nullability of [valueType]
420479
/// and assume that the argument passed to the conversion function won't be
421480
/// `null`.
422-
Procedure? _conversionProcedure(
481+
Procedure? _jsConversionProcedure(
423482
DartType valueType, DartType expectedType, TypeEnvironment typeEnv) {
424483
if (expectedType == coreTypes.doubleNonNullableRawType) {
425484
assert(valueType is InterfaceType &&
@@ -430,7 +489,7 @@ class CoreTypesUtil {
430489
assert(expectedType == nullableWasmExternRefType,
431490
'Unexpected expected type: $expectedType');
432491

433-
for (final entry in _externRefConverterMap.entries) {
492+
for (final entry in _jsifyMap.entries) {
434493
if (typeEnv.isSubtypeOf(
435494
valueType,
436495
InterfaceType(entry.key, Nullability.nonNullable),
@@ -442,6 +501,21 @@ class CoreTypesUtil {
442501
// `dynamic` or `Object?`, convert based on runtime type.
443502
return jsifyRawTarget;
444503
}
504+
505+
/// Return the function to convert a value returned by an interop procedure
506+
/// generated by [_Specializer.getRawInteropProcedure] to the expected Dart
507+
/// type.
508+
///
509+
/// The value passed to the returned conversion function should be an
510+
/// `externref` and should be tested for `null` and `undefined`. The returned
511+
/// procedures do not handle `null`s and `undefined`s.
512+
Procedure _dartConversionProcedure(DartType expectedType) {
513+
Procedure? conversionProcedure;
514+
if (expectedType is InterfaceType) {
515+
conversionProcedure = _dartifyMap[expectedType.classNode];
516+
}
517+
return conversionProcedure ?? dartifyRawTarget;
518+
}
445519
}
446520

447521
StaticInvocation invokeOneArg(Procedure target, Expression arg) =>
@@ -467,7 +541,7 @@ Expression jsifyValue(VariableDeclaration variable, DartType expectedType,
467541
conversionProcedure = coreTypes.jsValueUnboxTarget;
468542
} else {
469543
conversionProcedure =
470-
coreTypes._conversionProcedure(variable.type, expectedType, typeEnv);
544+
coreTypes._jsConversionProcedure(variable.type, expectedType, typeEnv);
471545
}
472546

473547
final conversion = conversionProcedure == null

pkg/dart2wasm/lib/target.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ class WasmTarget extends Target {
169169
'dart:_boxed_double',
170170
'dart:_boxed_int',
171171
'dart:_compact_hash',
172+
'dart:_error_utils',
172173
'dart:_js_helper',
173174
'dart:_js_types',
174175
'dart:_list',

sdk/lib/_internal/wasm/lib/error_utils.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,13 @@ Never _throwNegativeOrZeroError(int value, [String? name]) {
161161
throw RangeError.range(value, 1, null, name);
162162
}
163163

164+
@pragma("wasm:never-inline")
165+
@pragma("wasm:entry-point")
166+
Never _throwArgumentNullError() {
167+
if (minify) throw _nullErrorWithoutDetails;
168+
throw ArgumentError.notNull();
169+
}
170+
164171
const _indexErrorWithoutDetails = _ErrorWithoutDetails(
165172
'IndexError (details omitted due to --minify)',
166173
);
@@ -176,6 +183,9 @@ const _negativeValueErrorWithoutDetails = _ErrorWithoutDetails(
176183
const _negativeOrZeroValueErrorWithoutDetails = _ErrorWithoutDetails(
177184
'Value was negative or zero (details omitted due to --minify)',
178185
);
186+
const _nullErrorWithoutDetails = _ErrorWithoutDetails(
187+
'Value must not be null (details omitted due to --minify)',
188+
);
179189

180190
class _ErrorWithoutDetails implements Error {
181191
final String _message;

sdk/lib/_internal/wasm/lib/js_helper.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ extension JSAnyToExternRef on JSAny? {
9292

9393
// For `dartify` and `jsify`, we match the conflation of `JSUndefined`, `JSNull`
9494
// and `null`.
95+
@pragma('wasm:entry-point')
9596
bool isDartNull(WasmExternRef? ref) => ref.isNull || isJSUndefined(ref);
9697

9798
class JSArrayIteratorAdapter<T> implements Iterator<T> {
@@ -165,6 +166,7 @@ double toDartNumber(WasmExternRef? o) => JS<double>("o => o", o);
165166
@pragma('wasm:entry-point')
166167
WasmExternRef? toJSNumber(double o) => JS<WasmExternRef?>("o => o", o);
167168

169+
@pragma('wasm:entry-point')
168170
bool toDartBool(WasmExternRef? o) => JS<bool>("o => o", o);
169171

170172
WasmExternRef? toJSBoolean(bool b) => JS<WasmExternRef?>("b => !!b", b);
@@ -529,6 +531,18 @@ Object? dartifyRaw(WasmExternRef? ref, [int? refType]) {
529531
};
530532
}
531533

534+
@pragma('wasm:entry-pint')
535+
int dartifyInt(WasmExternRef? ref) {
536+
final dartDouble = toDartNumber(ref);
537+
if (dartDouble.isFinite) {
538+
final dartInt = dartDouble.toInt();
539+
if (dartInt.toDouble() == dartDouble) {
540+
return dartInt;
541+
}
542+
}
543+
throw ArgumentError('JS value is not integer');
544+
}
545+
532546
List<double> jsFloatTypedArrayToDartFloatTypedData(
533547
WasmExternRef? ref,
534548
List<double> makeTypedData(int size),
@@ -603,6 +617,7 @@ JSArray<T> toJSArray<T extends JSAny?>(List<T> list) {
603617
return result;
604618
}
605619

620+
@pragma('wasm:entry-point')
606621
List<Object?> toDartList(WasmExternRef? ref) => List<Object?>.generate(
607622
objectLength(ref),
608623
(int n) => dartifyRaw(objectReadIndex(ref, n)),

0 commit comments

Comments
 (0)