Files
sdk/pkg/dart2wasm/lib/modules.dart
T
Martin Kustermann e3cf529f87 [dart2wasm] More compact encoding of deferred load lists
Measured on size of e main module (baseline is we don't embed
it in application code):

* embedding before: +16.5% uncompressed / +9.1% compressed
* embedding with this CL: +4% uncompressed / +4.3% compressed

When embeddeding deferred load list information into the app
(as opposed to a separate json file) we now use a more compact
encoding.

Specifically: Instead of encoding it as an array of an array of
strings (which are module names), we encode it as an array of an
array of module ids and construct the module name from the id.

To make the array of module ids more compact we utilize the fact
that we can sort them and encode in delta encoding (i.e. instead
of absolute module ids, encode the diff between previous module
id in the list).

We put the encoded module id lists in a data section and create
`WasmArray<WasmI8>`s from them at startup. When we trigger a load
we then decode them into the list of module names.

There's more opportunity to optimize it, but it's good to do
this as a first step.

Change-Id: I293fb8879d992fc370786f6c9b258ccd27e1559b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/508980
Reviewed-by: Srujan Gaddam <srujzs@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
2026-06-05 00:50:46 -07:00

294 lines
8.8 KiB
Dart

// 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 'package:kernel/ast.dart';
import 'package:kernel/core_types.dart';
import 'compiler_options.dart';
import 'reference_extensions.dart';
import 'target.dart';
import 'util.dart';
Library? _enclosingLibraryForReference(Reference reference) {
TreeNode? current = reference.node;
// References generated for constants will not have a node attached.
if (reference.node == null) return null;
while (current != null) {
if (current is Library) return current;
current = current.parent;
}
throw ArgumentError('Could not find enclosing library for ${reference.node}');
}
class ModuleMetadataBuilder {
int _counter = WasmCompilerOptions.mainModuleId;
final WasmCompilerOptions options;
ModuleMetadataBuilder(this.options);
ModuleMetadata buildModuleMetadata({
bool emitAsMain = false,
bool skipEmit = false,
}) {
final id = _counter++;
final moduleImportName = options.translatorOptions.minify
? intToMinString(id)
: 'module$id';
return ModuleMetadata._(
moduleImportName,
WasmCompilerOptions.moduleNameForId(
options.outputFile,
id,
emitAsMain: emitAsMain,
),
skipEmit: skipEmit,
isMain: id == WasmCompilerOptions.mainModuleId,
);
}
}
/// Deferred loading metadata for a single dart2wasm output module.
///
/// Each [ModuleMetadata] will map to a single wasm module emitted by the
/// compiler. The separation of modules is guided by the deferred imports
/// defined in the source code.
///
/// A module may contain code at any level of granularity. Code may be grouped
/// by library, by class or neither. [containsReference] should be used to
/// determine if a module contains a given class/member reference.
class ModuleMetadata {
final bool isMain;
/// The name used to import and export this module.
final String moduleImportName;
/// The name added to the wasm output file for this module.
final String moduleName;
/// Whether or not a wasm file should be emitted for this module.
final bool skipEmit;
ModuleMetadata._(
this.moduleImportName,
this.moduleName, {
this.skipEmit = false,
this.isMain = false,
});
@override
String toString() => moduleImportName;
}
/// Data needed to create deferred modules.
class ModuleOutputData {
/// All [ModuleMetadata]s generated for the program.
final List<ModuleMetadata> modules;
/// Maps the [Reference] to the corresponding [ModuleMetadata].
final Map<Reference, ModuleMetadata>? referenceToModuleMetadata;
/// Maps the [Constant] to the corresponding [ModuleMetadata].
final Map<Constant, ModuleMetadata>? constantToModuleMetadata;
/// Maps the [Library] to the corresponding [ModuleMetadata].
final Map<Library, ModuleMetadata>? libraryToModuleMetadata;
/// Module for any unassigned reference.
final ModuleMetadata? defaultModule;
ModuleOutputData.fineGrainedSplit(
this.modules,
this.referenceToModuleMetadata,
this.constantToModuleMetadata,
this.defaultModule,
) : libraryToModuleMetadata = null,
assert(modules[0].isMain);
ModuleOutputData.librarySplit(
this.modules,
this.libraryToModuleMetadata,
this.defaultModule,
) : referenceToModuleMetadata = null,
constantToModuleMetadata = null,
assert(modules[0].isMain);
ModuleOutputData.monolithic(ModuleMetadata module)
: modules = [module],
libraryToModuleMetadata = null,
referenceToModuleMetadata = null,
constantToModuleMetadata = null,
defaultModule = module,
assert(module.isMain);
ModuleMetadata get mainModule => modules[0];
Iterable<ModuleMetadata> get deferredModules => modules.skip(1);
bool get hasMultipleModules => modules.length > 1;
/// Returns the module that contains [reference].
ModuleMetadata moduleForReference(Reference reference) {
// Turn artificial [Reference]s used in dart2wasm to the normal Kernel AST
// [Reference]s.
final node = reference.node;
if (node is Field) {
if (reference.isGetter) {
reference = node.getterReference;
} else if (reference.isSetter) {
reference = node.setterReference!;
} else if (reference.isStaticFieldInitializer) {
assert(node.isStatic);
reference = node.getterReference;
} else {
assert(reference == node.fieldReference);
}
} else if (node is Constructor) {
if (reference.isInitializerReference ||
reference.isConstructorBodyReference) {
reference = node.reference;
} else {
assert(reference == node.reference);
}
} else {
node as Procedure;
if (reference.isCheckedEntryReference ||
reference.isUncheckedEntryReference ||
reference.isBodyReference ||
reference.isTearOffReference) {
reference = reference.asMember.reference;
} else {
assert(reference == reference.asMember.reference);
}
}
// We may have fine-grained partitioning of the application.
if (referenceToModuleMetadata != null) {
return referenceToModuleMetadata![reference] ?? defaultModule!;
}
// We may have coarse-grained library-based partitioning of the application.
if (libraryToModuleMetadata != null) {
final library = _enclosingLibraryForReference(reference);
return libraryToModuleMetadata![library] ?? defaultModule!;
}
// We put the entire application into the same wasm module.
return defaultModule!;
}
ModuleMetadata? moduleForConstant(Constant constant) {
return constantToModuleMetadata?[constant];
}
}
/// Module strategy that puts all libraries into a single module.
class DefaultModuleStrategy extends ModuleStrategy {
final CoreTypes coreTypes;
final Component component;
final WasmCompilerOptions options;
DefaultModuleStrategy(this.coreTypes, this.component, this.options);
@override
ModuleOutputData buildModuleOutputData() {
// If deferred loading is not enabled then put every library in the main
// module.
final builder = ModuleMetadataBuilder(options);
final mainModule = builder.buildModuleMetadata(emitAsMain: true);
return ModuleOutputData.monolithic(mainModule);
}
@override
void addEntryPoints() {}
@override
void prepareComponent() {}
@override
Future<void> processComponentAfterTfa(
DeferredModuleLoadingMap loadingMap,
) async {}
}
bool containsWasmExport(CoreTypes coreTypes, Library lib) {
if (lib.members.any((m) => hasWasmExportPragma(coreTypes, m))) {
return true;
}
return lib.classes.any(
(c) => c.members.any((m) => hasWasmExportPragma(coreTypes, m)),
);
}
abstract class ModuleStrategy {
void addEntryPoints();
void prepareComponent();
Future<void> processComponentAfterTfa(DeferredModuleLoadingMap loadingMap);
ModuleOutputData buildModuleOutputData();
}
Set<Library> getReachableLibraries(
Library entryPoint,
CoreTypes coreTypes,
WasmTarget kernelTarget,
) {
final List<Library> queue = [entryPoint];
final Set<Library> reachable = {entryPoint};
while (queue.isNotEmpty) {
final current = queue.removeLast();
for (final dep in current.dependencies) {
final importedLib = dep.targetLibrary;
if (reachable.add(importedLib)) {
queue.add(importedLib);
}
}
}
return reachable;
}
class DeferredModuleLoadingMap {
// Maps each (library, deferred import) to a unique id.
final Map<(Library, String), int> loadIds;
// Maps the unique load id to the deferred import.
final List<LibraryDependency> loadIdToDeferredImport;
// Maps (library, import-name)-id to list of needed modules.
//
// NOTE: The load lists are mutable and may get pruned by the compiler after
// code generation to avoid emitting & loading empty modules.
final List<List<ModuleMetadata>> moduleMap;
DeferredModuleLoadingMap._(
this.loadIds,
this.moduleMap,
this.loadIdToDeferredImport,
);
factory DeferredModuleLoadingMap.fromComponent(Component c) {
int nextLoadId = 0;
final loadIds = <(Library, String), int>{};
final loadIdToDeferredImport = <LibraryDependency>[];
final moduleMap = <List<ModuleMetadata>>[];
for (final library in c.libraries) {
for (final dep in library.dependencies) {
if (!dep.isDeferred) continue;
final name = dep.name!;
loadIds[(library, name)] = nextLoadId++;
loadIdToDeferredImport.add(dep);
moduleMap.add([]);
}
}
return DeferredModuleLoadingMap._(
loadIds,
moduleMap,
loadIdToDeferredImport,
);
}
void addModuleToLibraryImport(
Library lib,
String importName,
List<ModuleMetadata> modules,
) {
moduleMap[loadIds[(lib, importName)]!].addAll(modules);
}
}