[dart2wasm] Add deferred loading support to dart2wasm (5/X).

Add support for a StaticTable which holds references to known functions that need to be called across modules. For calls that target the DispatchTable we will still go through there if possible. But for any functions not referenced in that table (including any compiler generated functions) we add a separate static table.

Also adds import/export support to both the DispatchTable and the StaticTable. The table will always be defined in the main module and imported into subsequent modules.

Change-Id: Iedc683d1ecfe721393900913826010cdd9b2c3c4
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/381323
Reviewed-by: Martin Kustermann <kustermann@google.com>
This commit is contained in:
Nate Biggs
2024-09-04 21:58:12 +00:00
committed by Commit Queue
parent b5b335fb75
commit a7f5845a4e
20 changed files with 250 additions and 39 deletions
+12 -3
View File
@@ -707,7 +707,15 @@ abstract class AstCodeGenerator
}
List<w.ValueType> call(Reference target, {bool useUncheckedEntry = false}) {
return b.invoke(translator.directCallTarget(target, useUncheckedEntry));
final targetModule = translator.moduleForReference(target);
final isLocalModuleCall = targetModule == b.module;
if (isLocalModuleCall) {
return b.invoke(translator.directCallTarget(target, useUncheckedEntry));
} else {
b.comment('Indirect call to $target');
return translator.callReference(target, b);
}
}
@override
@@ -1912,7 +1920,8 @@ abstract class AstCodeGenerator
b.i32_const(offset);
b.i32_add();
}
b.call_indirect(selector.signature, translator.dispatchTable.wasmTable);
b.call_indirect(
selector.signature, translator.dispatchTable.getWasmTable(b.module));
translator.functions.recordSelectorUse(selector);
}
@@ -4516,7 +4525,7 @@ abstract class CallTarget {
/// The wasm target function to call.
///
/// This should only be accessed if caller intents to call it, as it will
/// This should only be accessed if caller intends to call it, as it will
/// enqueue the function in the compilation queue.
w.BaseFunction get function;
}
+35 -7
View File
@@ -69,8 +69,6 @@ class SelectorInfo {
/// class member for this selector.
int? offset;
w.ModuleBuilder get m => translator.m;
/// The selector's member's name.
String get name => paramInfo.member!.name.text;
@@ -215,6 +213,9 @@ class SelectorInfo {
/// Builds the dispatch table for member calls.
class DispatchTable {
static const _tableName = 'dispatch';
static const _functionType = w.RefType.func(nullable: true);
final Translator translator;
final List<TableSelectorInfo> _selectorMetadata;
final Map<TreeNode, ProcedureAttributesMetadata> _procedureAttributeMetadata;
@@ -236,10 +237,14 @@ class DispatchTable {
/// member for the selector.
late final List<Reference?> _table;
/// The Wasm table for the dispatch table.
late final w.TableBuilder wasmTable;
late final w.TableBuilder _definedWasmTable;
final Map<w.ModuleBuilder, w.ImportedTable> _importedWasmTables = {};
w.ModuleBuilder get m => translator.m;
/// The Wasm table for the dispatch table.
w.Table getWasmTable(w.ModuleBuilder module) =>
translator.isMainModule(module)
? _definedWasmTable
: _importedWasmTables[module]!;
DispatchTable(this.translator)
: _selectorMetadata =
@@ -499,7 +504,17 @@ class DispatchTable {
selectors[i].offset = rows[i].offset;
}
wasmTable = m.tables.define(w.RefType.func(nullable: true), _table.length);
_definedWasmTable =
translator.mainModule.tables.define(_functionType, _table.length);
if (translator.hasMultipleModules) {
final mainModuleName = translator.nameForModule(translator.mainModule);
translator.mainModule.exports.export(_tableName, _definedWasmTable);
for (final module in translator.modules) {
if (translator.isMainModule(module)) continue;
_importedWasmTables[module] = module.tables
.import(mainModuleName, _tableName, _functionType, _table.length);
}
}
}
void output() {
@@ -507,8 +522,21 @@ class DispatchTable {
Reference? target = _table[i];
if (target != null) {
w.BaseFunction? fun = translator.functions.getExistingFunction(target);
// Any call to the dispatch table is guaranteed to hit a target.
//
// If a target is in a deferred module and that deferred module hasn't
// been loaded yet, then the entry is `null`.
//
// Though we can only hit a target if that target's class has been
// allocated. In order for the class to be allocated, the deferred
// module must've been loaded to call the constructor.
if (fun != null) {
wasmTable.setElement(i, fun);
final targetModule = translator.moduleForReference(target);
if (translator.isMainModule(targetModule)) {
_definedWasmTable.setElement(i, fun);
} else {
_importedWasmTables[targetModule]!.setElements[fun] = i;
}
}
}
}
@@ -0,0 +1,99 @@
// Copyright (c) 2024, 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:collection';
import 'package:wasm_builder/wasm_builder.dart' as w;
import 'translator.dart';
class StaticDispatchTables {
final Translator translator;
final Map<w.FunctionType, StaticDispatchTableForSignature> _tables =
LinkedHashMap(
hashCode: (t) =>
Object.hash(Object.hashAll(t.inputs), Object.hashAll(t.outputs)),
equals: (t1, t2) => t1.isStructurallyEqualTo(t2));
StaticDispatchTables(this.translator);
StaticDispatchTableForSignature getTableForType(w.FunctionType type) {
return _tables[type] ??=
StaticDispatchTableForSignature(translator, type, _tables.length);
}
void outputTables() {
for (final table in _tables.values) {
table.output();
}
}
}
/// Builds a static dispatch table for a specific function type signature.
///
/// All calls to this table will have the same signature and so `call_indirect`
/// instructions that reference this table can omit the type check.
class StaticDispatchTableForSignature {
final String _tableName;
final w.FunctionType _functionType;
final Translator translator;
/// Contents of wasm table.
final Map<w.BaseFunction, int> _table = {};
late final w.TableBuilder _definedWasmTable;
final Map<w.ModuleBuilder, w.ImportedTable> _importedWasmTables = {};
StaticDispatchTableForSignature(
this.translator, this._functionType, int nameCounter)
: _tableName = 'static$nameCounter' {
_definedWasmTable = translator.mainModule.tables
.define(w.RefType(_functionType, nullable: true), _table.length);
}
/// Gets the wasm table used to reference this static dispatch table in
/// [module].
///
/// This can either be the table definition itself or an import of it. Imports
/// the table into [module] if it is not imported yet.
w.Table getWasmTable(w.ModuleBuilder module) {
if (translator.isMainModule(module)) {
return _definedWasmTable;
}
if (_importedWasmTables.isEmpty) {
translator.mainModule.exports.export(_tableName, _definedWasmTable);
}
return _importedWasmTables.putIfAbsent(module, () {
final mainModuleName = translator.nameForModule(translator.mainModule);
return module.tables.import(mainModuleName, _tableName,
w.RefType(_functionType, nullable: true), _table.length);
});
}
/// Returns the index for [function] in the table allocating one if necessary.
int indexForFunction(w.BaseFunction function) {
assert(function.type.isStructurallyEqualTo(function.type));
return _table[function] ??= _table.length;
}
void output() {
final importedTables = _importedWasmTables;
_table.forEach((fun, index) {
final targetModule = fun.enclosingModule;
if (translator.isMainModule(targetModule)) {
_definedWasmTable.setElement(index, fun);
} else {
(getWasmTable(targetModule) as w.ImportedTable).setElements[fun] =
index;
}
});
_definedWasmTable.minSize = _table.length;
for (final table in importedTables.values) {
table.minSize = _table.length;
}
}
}
+30 -3
View File
@@ -26,6 +26,7 @@ import 'kernel_nodes.dart';
import 'param_info.dart';
import 'records.dart';
import 'reference_extensions.dart';
import 'static_dispatch_table.dart';
import 'tags.dart';
import 'types.dart';
import 'util.dart' as util;
@@ -99,6 +100,7 @@ class Translator with KernelNodes {
final LibraryIndex index;
late final ClosureLayouter closureLayouter;
late final ClassInfoCollector classInfoCollector;
late final StaticDispatchTables staticTablesPerType;
late final DispatchTable dispatchTable;
late final Globals globals;
late final Constants constants;
@@ -305,6 +307,7 @@ class Translator with KernelNodes {
subtypes = hierarchy.computeSubtypesInformation();
closureLayouter = ClosureLayouter(this);
classInfoCollector = ClassInfoCollector(this);
staticTablesPerType = StaticDispatchTables(this);
dispatchTable = DispatchTable(this);
compilationQueue = CompilationQueue();
functions = FunctionCollector(this);
@@ -337,6 +340,7 @@ class Translator with KernelNodes {
constructorClosures.clear();
dispatchTable.output();
staticTablesPerType.outputTables();
initFunction.body.end();
for (ConstantInfo info in constants.constantInfo.values) {
@@ -375,14 +379,36 @@ class Translator with KernelNodes {
return callFunction(functions.getFunction(reference), b);
}
final Map<w.BaseFunction, Map<w.ModuleBuilder, w.BaseFunction>>
_importedFunctions = {};
/// Generates a set of instructions to call [function] adding indirection
/// if the call crosses a module boundary. Calls the function directly if it
/// is local. Imports the function and calls it directly if is in the main
/// module. Otherwise does an indirect call through the static dispatch table.
List<w.ValueType> callFunction(
w.BaseFunction function, w.InstructionsBuilder b) {
// TODO(natebiggs): Add indirect call.
b.call(function);
final targetModule = function.enclosingModule;
// TODO(natebiggs): Consider inlining function body in some scenarios.
if (targetModule == b.module) {
b.call(function);
} else if (isMainModule(targetModule)) {
final importedFunctions = _importedFunctions.putIfAbsent(function, () {
final importName = 'func${_importedFunctions.length}';
targetModule.exports.export(importName, function);
return {};
});
final importedFunction = importedFunctions[b.module] ??=
b.module.functions.import(nameForModule(targetModule),
function.exportedName!, function.type);
b.call(importedFunction);
} else {
final staticTable = staticTablesPerType.getTableForType(function.type);
b.i32_const(staticTable.indexForFunction(function));
b.table_get(staticTable.getWasmTable(b.module));
b.ref_as_non_null();
b.call_ref(function.type);
}
return function.type.outputs;
}
@@ -1774,7 +1800,8 @@ class PolymorphicDispatcherCodeGenerator implements CodeGenerator {
b.struct_get(translator.topInfo.struct, FieldIndex.classId);
b.i32_const(selector.offset!);
b.i32_add();
b.call_indirect(signature, translator.dispatchTable.wasmTable);
b.call_indirect(
signature, translator.dispatchTable.getWasmTable(b.module));
translator.functions.recordSelectorUse(selector);
}
@@ -13,14 +13,14 @@ class FunctionBuilder extends ir.BaseFunction
/// The body of the function.
late final InstructionsBuilder body;
FunctionBuilder(ModuleBuilder module, super.index, super.type,
FunctionBuilder(super.enclosingModule, super.index, super.type,
[super.functionName]) {
body = InstructionsBuilder(module, type.inputs, type.outputs);
body = InstructionsBuilder(enclosingModule, type.inputs, type.outputs);
}
@override
ir.DefinedFunction forceBuild() =>
ir.DefinedFunction(body.build(), finalizableIndex, type, functionName);
ir.DefinedFunction forceBuild() => ir.DefinedFunction(
enclosingModule, body.build(), finalizableIndex, type, functionName);
@override
String toString() => exportedName ?? "#$finalizableIndex";
@@ -55,7 +55,7 @@ class FunctionsBuilder with Builder<ir.Functions> {
ir.ImportedFunction import(String module, String name, ir.FunctionType type,
[String? functionName]) {
final function = ir.ImportedFunction(
module, name, ir.FinalizableIndex(), type, functionName);
_module, module, name, ir.FinalizableIndex(), type, functionName);
_importedFunctions.add(function);
_addName(functionName, function);
return function;
+4 -4
View File
@@ -8,11 +8,11 @@ part of 'globals.dart';
class GlobalBuilder extends ir.Global with IndexableBuilder<ir.DefinedGlobal> {
final InstructionsBuilder initializer;
GlobalBuilder(ModuleBuilder module, super.index, super.type,
GlobalBuilder(super.enclosingModule, super.index, super.type,
[super.globalName])
: initializer = InstructionsBuilder(module, [], [type.type]);
: initializer = InstructionsBuilder(enclosingModule, [], [type.type]);
@override
ir.DefinedGlobal forceBuild() =>
ir.DefinedGlobal(initializer.build(), finalizableIndex, type, globalName);
ir.DefinedGlobal forceBuild() => ir.DefinedGlobal(
enclosingModule, initializer.build(), finalizableIndex, type, globalName);
}
@@ -42,7 +42,8 @@ class GlobalsBuilder with Builder<ir.Globals> {
/// Imports a global variable into this module.
ir.ImportedGlobal import(String module, String name, ir.GlobalType type) {
final global = ir.ImportedGlobal(module, name, ir.FinalizableIndex(), type);
final global =
ir.ImportedGlobal(_module, module, name, ir.FinalizableIndex(), type);
_importedGlobals.add(global);
return global;
}
@@ -635,6 +635,7 @@ class InstructionsBuilder with Builder<ir.Instructions> {
void call(ir.BaseFunction function) {
assert(_verifyTypes(function.type.inputs, function.type.outputs,
trace: ['call', function]));
assert(function.enclosingModule == module);
_add(ir.Call(function));
}
+5 -2
View File
@@ -10,7 +10,7 @@ import 'builder.dart';
class ModuleBuilder with Builder<ir.Module> {
final Uri? sourceMapUrl;
final List<int> watchPoints;
late final types = TypesBuilder(this);
late final TypesBuilder types;
late final functions = FunctionsBuilder(this);
final tables = TablesBuilder();
final memories = MemoriesBuilder();
@@ -25,7 +25,10 @@ class ModuleBuilder with Builder<ir.Module> {
/// bytes to watch. When the module is serialized, the stack traces leading
/// to the production of all watched bytes are printed. This can be used to
/// debug runtime errors happening at specific offsets within the module.
ModuleBuilder(this.sourceMapUrl, {this.watchPoints = const []});
ModuleBuilder(this.sourceMapUrl,
{ModuleBuilder? parent, this.watchPoints = const []}) {
types = TypesBuilder(this, parent: parent?.types);
}
@override
ir.Module forceBuild() {
+1 -1
View File
@@ -12,7 +12,7 @@ class TableBuilder extends ir.Table with IndexableBuilder<ir.DefinedTable> {
: elements = List.filled(minSize, null, growable: true);
void setElement(int index, ir.BaseFunction function) {
assert(type == ir.RefType.func(nullable: true),
assert(type.isSubtypeOf(ir.RefType.func(nullable: true)),
"Elements are only supported for funcref tables");
assert(maxSize == null || index < maxSize!,
'Index $index greater than max table size $maxSize');
@@ -36,4 +36,15 @@ class TablesBuilder with Builder<ir.Tables> {
_importedTables, _tableBuilders);
return ir.Tables(_importedTables, built);
}
void collectUsedTypes(Set<ir.DefType> types) {
for (final table in _tableBuilders) {
final defType = table.type.containedDefType;
if (defType != null) types.add(defType);
}
for (final table in _importedTables) {
final defType = table.type.containedDefType;
if (defType != null) types.add(defType);
}
}
}
+5 -3
View File
@@ -235,10 +235,11 @@ class _RecGroupBuilder {
class TypesBuilder with Builder<ir.Types> {
final ModuleBuilder _module;
late final Map<_FunctionTypeKey, ir.FunctionType> _functionTypeMap = {};
late final _RecGroupBuilder _recGroupBuilder = _RecGroupBuilder();
final Map<_FunctionTypeKey, ir.FunctionType> _functionTypeMap = {};
final _RecGroupBuilder _recGroupBuilder;
TypesBuilder(this._module);
TypesBuilder(this._module, {TypesBuilder? parent})
: _recGroupBuilder = parent?._recGroupBuilder ?? _RecGroupBuilder();
/// Add a new function type to the module.
///
@@ -287,6 +288,7 @@ class TypesBuilder with Builder<ir.Types> {
Set<ir.DefType> _collectUsedTypes() {
final usedTypes = <ir.DefType>{};
_module.tables.collectUsedTypes(usedTypes);
_module.functions.collectUsedTypes(usedTypes);
_module.globals.collectUsedTypes(usedTypes);
_module.tags.collectUsedTypes(usedTypes);
+7 -3
View File
@@ -21,9 +21,11 @@ abstract class BaseFunction with Indexable implements Exportable {
final FinalizableIndex finalizableIndex;
final FunctionType type;
final String? functionName;
final ModuleBuilder enclosingModule;
String? exportedName;
BaseFunction(this.finalizableIndex, this.type, this.functionName);
BaseFunction(this.enclosingModule, this.finalizableIndex, this.type,
this.functionName);
@override
String get name => functionName ?? super.name;
@@ -44,7 +46,8 @@ class DefinedFunction extends BaseFunction implements Serializable {
/// All local variables defined in the function, including its inputs.
List<Local> get locals => body.locals;
DefinedFunction(this.body, super.finalizableIndex, super.type,
DefinedFunction(
super.enclosingModule, this.body, super.finalizableIndex, super.type,
[super.functionName]);
@override
@@ -85,7 +88,8 @@ class ImportedFunction extends BaseFunction implements Import {
@override
final String name;
ImportedFunction(this.module, this.name, super.finalizableIndex, super.type,
ImportedFunction(super.enclosingModule, this.module, this.name,
super.finalizableIndex, super.type,
[super.functionName]);
@override
@@ -2,6 +2,7 @@
// 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 '../builder/module.dart';
import '../serialize/serialize.dart';
import 'ir.dart';
+7 -3
View File
@@ -9,11 +9,13 @@ abstract class Global with Indexable implements Exportable {
@override
final FinalizableIndex finalizableIndex;
final GlobalType type;
final ModuleBuilder enclosingModule;
/// Name of the global in the names section.
final String? globalName;
Global(this.finalizableIndex, this.type, this.globalName);
Global(
this.enclosingModule, this.finalizableIndex, this.type, this.globalName);
@override
String toString() => globalName ?? "$finalizableIndex";
@@ -26,7 +28,8 @@ abstract class Global with Indexable implements Exportable {
class DefinedGlobal extends Global implements Serializable {
final Instructions initializer;
DefinedGlobal(this.initializer, super.finalizableIndex, super.type,
DefinedGlobal(super.enclosingModule, this.initializer, super.finalizableIndex,
super.type,
[super.globalName]);
@override
@@ -44,7 +47,8 @@ class ImportedGlobal extends Global implements Import {
@override
final String name;
ImportedGlobal(this.module, this.name, super.finalizableIndex, super.type,
ImportedGlobal(super.enclosingModule, this.module, this.name,
super.finalizableIndex, super.type,
[super.globalName]);
@override
+1
View File
@@ -2,6 +2,7 @@
// 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 '../builder/module.dart';
import '../serialize/serialize.dart';
import 'ir.dart';
+3 -1
View File
@@ -9,7 +9,9 @@ class Table with Indexable implements Exportable, Serializable {
@override
final FinalizableIndex finalizableIndex;
final RefType type;
final int minSize;
// Mutable so that a table's size does not need to be known prior to the table
// being instantiated.
int minSize;
final int? maxSize;
Table(this.finalizableIndex, this.type, this.minSize, this.maxSize);
+12
View File
@@ -697,6 +697,18 @@ class FunctionType extends DefType {
return true;
}
bool isStructurallyEqualTo(FunctionType other) {
if (inputs.length != other.inputs.length) return false;
if (outputs.length != other.outputs.length) return false;
for (int i = 0; i < inputs.length; i++) {
if (inputs[i] != other.inputs[i]) return false;
}
for (int i = 0; i < outputs.length; i++) {
if (outputs[i] != other.outputs[i]) return false;
}
return true;
}
@override
void serializeDefinitionInner(Serializer s) {
s.writeByte(0x60); // -0x20
@@ -226,7 +226,7 @@ class _Element implements Serializable {
@override
void serialize(Serializer s) {
if (table.index != 0) {
s.writeByte(0x02);
s.writeByte(0x06);
s.writeUnsigned(table.index);
} else {
s.writeByte(0x00);
@@ -235,11 +235,17 @@ class _Element implements Serializable {
s.writeSigned(startIndex);
s.writeByte(0x0B); // end
if (table.index != 0) {
s.writeByte(0x00); // elemkind
s.write(table.type);
}
s.writeUnsigned(entries.length);
for (var entry in entries) {
s.writeUnsigned(entry.index);
if (table.index == 0) {
s.writeUnsigned(entry.index);
} else {
s.writeByte(0xD2); // ref.func
s.writeSigned(entry.index);
s.writeByte(0x0B); // end
}
}
}
}