[dart2wasm] Prune library imports after TFA for better deferred module splits

This significantly prunes the imports in the library graph which in
return significantly reduces code size of main module.

 - gallery: -27% (-677 KB)
 - essentials: -45% (-8.7 MB)

The main insight here is that for the purpose of splitting up
Dart libraries into wasm modules we only have a dependency of
one dart library on another if the former uses something from
the latter "statically". Other uses of `Reference`s from other
libraries (e.g. interface targets, dart types, etc) don't
require an import.

The other insight is that a library A may import library B which
may re-export library C1, C2, C3. If A only uses things from C2
then we can remove import B and inject import C2 instead. That
means library A no longer needs B, C1, C2.

For dart types specifically: Compared to other backends,
dart2wasm stores all RTI (runtime type information) in the
main module - i.e. deferred modules don't bring extra RTI -
which is why we don't have to load deferred module before we
can access it's types.

Change-Id: I9e486bb4de165e4b23644a84838ace06d2f35bce
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/464901
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Slava Egorov <vegorov@google.com>
This commit is contained in:
Martin Kustermann
2025-12-02 07:05:40 -08:00
committed by Commit Queue
parent 57ea885496
commit ec83c5918a
12 changed files with 560 additions and 3 deletions
+25
View File
@@ -39,6 +39,7 @@ import 'dynamic_modules.dart';
import 'io_util.dart';
import 'js/method_collector.dart' show JSMethods;
import 'js/runtime_generator.dart' as js;
import 'library_dependencies_pruner.dart';
import 'modules.dart';
import 'record_class_generator.dart';
import 'records.dart';
@@ -548,6 +549,30 @@ Future<CompilationResult> _runTfaPhase(
libraryIndex = LibraryIndex(component, _librariesToIndex);
}
// NOTE: The [Library.dependencies] will be in a weird state after TFA:
//
// * a library may use members of other libraries without an import that
// provides the member
//
// * a library may have many unused imports
//
// -> See https://dartbug.com/62112 & https://dartbug.com/62111 for details.
//
// At this point dart2wasm uses library dependencies only for one purpose,
// namely for partitioning all libraries into deferred wasm modules. So we now
// perform a dart2wasm specific pruning of library imports.
//
// See [pruneLibraryDependencies] for more information.
//
// NOTE: In stress test mode the component is manually split into one wasm
// module per library without making imports of libraries deferred. To ensure
// all modules get loaded before main it injects dummy [LoadLibrary]
// expressions that would be optimized out by [pruneLibraryDependencies]. So
// we disable the pruning in this case.
if (!options.translatorOptions.enableMultiModuleStressTestMode) {
pruneLibraryDependencies(libraryIndex, component);
}
if (options.emitTfa) {
// Store metadata needed for codegen so that it can be serialized.
final recordClassesRepo = _RecordClassesRepository();
+32 -3
View File
@@ -14,6 +14,7 @@ import 'package:kernel/core_types.dart';
import 'await_transformer.dart' as await_transformer;
import 'compiler_options.dart';
import 'library_dependencies_pruner.dart';
import 'modules.dart';
import 'target.dart';
import 'util.dart' show addPragma;
@@ -139,8 +140,32 @@ class DeferredLoadingModuleStrategy extends ModuleStrategy {
});
});
moduleOutputData =
ModuleOutputData([mainModule, ...rootSetToModule.values]);
// Some libraries may not have gotten a module assigned in the above
// procedure. This can have a varity of reasons:
//
// - A class that's never really used but still in the program because TFA
// left it there (this happens occasionally because we enable RTA before
// TFA, RTA is less precised and may mark a class as allocated but TFA
// later on optimizes usages away which leave the class as non-abstract
// but unused).
// - A class is only used in type expressions
// - ...
//
// The code generator still requires every library to have a corresponding
// module, so we make an artificial one here.
final assignedLibraries = <Library>{
...mainModule.libraries,
for (final module in rootSetToModule.values) ...module.libraries,
};
final unassignedLibraries = component.libraries.toSet()
..removeAll(assignedLibraries);
moduleOutputData = ModuleOutputData([
mainModule,
...rootSetToModule.values,
if (unassignedLibraries.isNotEmpty)
builder.buildModuleMetadata()..libraries.addAll(unassignedLibraries)
]);
}
@override
@@ -195,6 +220,10 @@ class DeferredLoadingModuleStrategy extends ModuleStrategy {
for (final dependency in currentLibrary.dependencies) {
final targetLibrary = dependency.importedLibraryReference.asLibrary;
if (dependency.isDeferred) {
if (dependency.name!.startsWith(unusedDeferredLibraryPrefix)) {
continue;
}
newDeferredRoots.add(targetLibrary);
(importTargetMap[currentLibrary] ??= {})[dependency.name!] =
targetLibrary;
@@ -401,7 +430,7 @@ class DeferredLoadingLowering extends Transformer {
_checkLibraryIsLoadedFromLoadId, Arguments([IntLiteral(loadId)]));
}
static void addEntryPointPragma(CoreTypes coreTypes, Procedure node) {
static void addEntryPointPragma(CoreTypes coreTypes, Annotatable node) {
addPragma(node, 'wasm:entry-point', coreTypes, value: BoolConstant(true));
}
}
@@ -0,0 +1,365 @@
// 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/kernel.dart';
import 'package:kernel/library_index.dart';
/// If a deferred library import has this name prefix it isn't used to load
/// anything. It only serves to maintain that any [CheckLibraryIsLoaded] throws
/// if the [LoadLibrary] call was not called.
const unusedDeferredLibraryPrefix = 'unused-';
/// Prunes [Library.dependencies] to contain precisely those imports needed.
///
/// Dart2wasm only uses library dependencies for one purpose, namely for
/// computing deferred loading units. This computation is done based on the
/// import graph and the granularity is on a library level.
///
/// We make the following overvations:
///
/// a) Using a type from a import
/// -> The main wasm module has all Dart runtime type information atm
/// -> No need to import a library to use classes from it in types.
///
/// b) Using a constant from an import
/// -> If we use a [InstanceConstant] / [TearOffConstant] in a library then
/// we have to ensure the enclosing library of the
/// [InstanceConstant.classNode] / [TearOffConstant.target] is imported.
/// -> This ensures that the code for closure/method is available when
/// invoking it.
/// -> Any other constant doesn't require an import.
///
/// c) Using static elements from an import
/// -> If we invoke a constructor, a static method, static getter, super
/// constructor, etc we need to import the target's enclosing library.
/// -> This will guarantee we have the code for target loaded when we perform
/// the call.
///
/// d) Instance/Dynamic invocations, other uses of [Reference]s
/// -> Does not require an import of the [Reference]s enclosing library.
/// -> We have sound type system: If the call is executed we know that the
/// receiver was allocated (and whoever allocated it has ensured - via c)
/// above - that the code for methods of the receiver is loaded).
///
/// So we establish the following invariants
///
/// * We only have imports for [Reference]s which are used for "static"-like
/// calls
///
/// * We import the library containing the definition of a [Reference] and not
/// e.g. a library that may re-export it
///
/// * If a [Reference] was usable via a deferred import (possibly via
/// a deferred library that re-exported the [Reference]) we ensure the newly
/// inserted import will also be deferred.
///
/// The transform will
///
/// * prune [Library.dependencies] to be exact, i.e. have a import iff
/// something is used from the imported library
///
/// * will remove all exports
///
/// * may insert more precise [LoadLibrary]/[CheckLibraryIsLoaded] if we don't
/// use a deferred library directly but things it re-exported
///
void pruneLibraryDependencies(LibraryIndex libraryIndex, Component component) {
final constantToLibrarySet = _ConstantToLibrarySet();
for (final library in component.libraries) {
final usedLibraries =
_Collector(library, constantToLibrarySet).usedLibraries;
_ImportPruner(libraryIndex, usedLibraries, library);
}
for (final library in component.libraries) {
library.dependencies.removeWhere((dep) {
if (dep.isExport) {
dep.parent = null;
return true;
}
return false;
});
}
}
class _ImportPruner extends Transformer {
final LibraryIndex libraryIndex;
final Set<Library> usedLibraries;
final Library library;
final additionalDeferredImports =
<LibraryDependency, List<LibraryDependency>>{};
late final futureImmediate =
libraryIndex.getConstructor('dart:async', '_Future', 'immediate');
_ImportPruner(this.libraryIndex, this.usedLibraries, this.library) {
// Step 1) Prune existing library imports.
// The set of libraries that we need to import and are already covered via
// an existing library import.
final librariesOfExistingImports = <Library>{};
// Maps a library to all the deferred imports that made this library
// available.
final libraryToDeferredImports = <Library, List<LibraryDependency>>{};
// The new set of imports (possibly smaller - removing unused dependencies,
// possibly larger - adding used but not yet imported dependencies).
final prunedDependencies = <LibraryDependency>[];
for (final dep in library.dependencies) {
if (dep.isExport) {
prunedDependencies.add(dep);
continue;
}
if (usedLibraries.contains(dep.targetLibrary)) {
librariesOfExistingImports.add(dep.targetLibrary);
prunedDependencies.add(dep);
continue;
}
if (dep.isDeferred) {
// Although the deferred dependency isn't used, for making sure
// [CheckLibraryIsLoaded] nodes throw if no preceding
// [LoadLibrary] was called we have to maintain a dummy import. This
// will also ensure the exception mentions the right name.
prunedDependencies.add(dep);
dep.name = '$unusedDeferredLibraryPrefix${dep.name!}';
// Loop over all libraries available via the deferred import that are
// used. The transformer will then issue individual [LoadLibrary] calls
// to them.
for (final available in _transitiveLibrarySet(dep.targetLibrary)) {
if (usedLibraries.contains(available)) {
libraryToDeferredImports.putIfAbsent(available, () => []).add(dep);
}
}
continue;
}
// The [dep] isn't directly used, remove it.
assert(!dep.isDeferred);
dep.parent = null;
}
library.dependencies = prunedDependencies;
// Step 2) Add missing imports.
for (final used in usedLibraries) {
// Maybe we already import the [used] library.
if (librariesOfExistingImports.contains(used)) {
continue;
}
// Never emit a library import to `dart:core`, it's special.
if (used.importUri.scheme == 'dart' && used.importUri.path == 'core') {
continue;
}
// We need to inject a new import of the [used] library.
final oldDeferredImports = libraryToDeferredImports[used];
if (oldDeferredImports == null) {
// This library was not accessible via old deferred imports, so we emit
// a normal import.
library.addDependency(LibraryDependency.import(used));
continue;
}
// The library was accessible (via a re-export) from an deferred
// import. Let's make a new deferred import for that particular library.
final newDep = LibraryDependency.deferredImport(
used, 'PreciseDeferredDep-${used.dependencies.length}');
library.addDependency(newDep);
for (final oldImport in oldDeferredImports) {
// Any [LoadLibrary] or [CheckLibraryIsLoaded] node that operated on the
// old (unused) deferred import needs to cover the [newDep] (possibly in
// addition to the existing dep (if used) and others).
additionalDeferredImports.putIfAbsent(oldImport, () => []).add(newDep);
}
}
// We only have to transform the body of the library if any [LoadLibrary] or
// [CheckLibraryIsLoaded] has to be modified.
if (additionalDeferredImports.isNotEmpty) {
library.transformChildren(this);
}
}
@override
TreeNode visitLoadLibrary(LoadLibrary node) {
node = super.visitLoadLibrary(node) as LoadLibrary;
final additional = additionalDeferredImports[node.import];
if (additional == null) return node;
return BlockExpression(
Block([
// This may be a dummy/unused which we only omit for throwing correct
// errors if a access (e.g. of a type) is used before the load call.
ExpressionStatement(node),
for (final replacement in additional.skip(1))
ExpressionStatement(LoadLibrary(replacement)),
]),
LoadLibrary(additional.last));
}
@override
TreeNode visitCheckLibraryIsLoaded(CheckLibraryIsLoaded node) {
node = super.visitCheckLibraryIsLoaded(node) as CheckLibraryIsLoaded;
final additional = additionalDeferredImports[node.import];
if (additional == null) return node;
return BlockExpression(
Block([
// This may be a dummy/unused which we only omit for throwing correct
// errors if a access (e.g. of a type) is used before the load call.
ExpressionStatement(node),
for (final replacement in additional.skip(1))
ExpressionStatement(CheckLibraryIsLoaded(replacement)),
]),
CheckLibraryIsLoaded(additional.last));
}
}
/// Traverses the AST of a [Library] and collects the set of libraries we need
/// to import due to accessing elements "statically" (see
/// [pruneLibraryDependencies] for more information)
class _Collector extends RecursiveVisitor {
final Library library;
final _ConstantToLibrarySet constantToLibrarySet;
/// The libraries that need to be imported.
final Set<Library> usedLibraries = {};
_Collector(this.library, this.constantToLibrarySet) {
library.accept(this);
}
@override
void visitStaticGet(StaticGet node) {
super.visitStaticGet(node);
addLibrary(node.target.enclosingLibrary);
}
@override
void visitStaticSet(StaticSet node) {
super.visitStaticSet(node);
addLibrary(node.target.enclosingLibrary);
}
@override
void visitStaticInvocation(StaticInvocation node) {
super.visitStaticInvocation(node);
addLibrary(node.target.enclosingLibrary);
}
@override
void visitConstructorInvocation(ConstructorInvocation node) {
super.visitConstructorInvocation(node);
addLibrary(node.target.enclosingLibrary);
}
@override
void visitSuperInitializer(SuperInitializer node) {
super.visitSuperInitializer(node);
addLibrary(node.target.enclosingLibrary);
}
@override
void visitRedirectingInitializer(RedirectingInitializer node) {
super.visitRedirectingInitializer(node);
addLibrary(node.target.enclosingLibrary);
}
@override
void visitStaticTearOff(StaticTearOff node) {
super.visitStaticTearOff(node);
addLibrary(node.target.enclosingLibrary);
}
@override
void defaultDartType(DartType node) {
// Ignore due to compiler always able to construct runtime type objects when
// needed (see also [pruneLibraryDependencies]).
}
@override
void visitSupertype(Supertype node) {
// Ignore due to compiler always able to construct runtime type objects when
// needed (see also [pruneLibraryDependencies]).
}
@override
void visitConstantExpression(ConstantExpression node) {
usedLibraries.addAll(constantToLibrarySet.librariesFor(node.constant));
}
void addLibrary(Library used) {
if (used != library) {
usedLibraries.add(used);
}
}
}
class _ConstantToLibrarySet {
final _constantToTransitiveLibraries = <Constant, Set<Library>>{};
/// Collects the set of libraries one needs to import when accessing
/// [constant].
///
/// This include enclosing libraries of all [InstanceConstant]s
/// and [TearOffConstant]s of the transitive constant graph of [constant].
Set<Library> librariesFor(Constant constant) {
final existing = _constantToTransitiveLibraries[constant];
if (existing != null) return existing;
final transitiveLibraries = <Library>{
if (constant is InstanceConstant) constant.classNode.enclosingLibrary,
if (constant is TearOffConstant) constant.target.enclosingLibrary,
// Collect all transitive libraries for direct child constants.
for (final childConstant
in _ChildConstantCollector.directChildrenOf(constant))
...librariesFor(childConstant),
};
return _constantToTransitiveLibraries[constant] =
transitiveLibraries.isEmpty ? const <Library>{} : transitiveLibraries;
}
}
class _ChildConstantCollector extends RecursiveVisitor {
/// Returns the set of constants referred to by the (possibly composed)
/// [constant].
static Set<Constant> directChildrenOf(Constant constant) {
final children = <Constant>{};
constant.visitChildren(_ChildConstantCollector._(children));
return children;
}
final Set<Constant> _directChildren;
_ChildConstantCollector._(this._directChildren);
@override
void defaultConstantReference(Constant node) {
_directChildren.add(node);
}
}
/// Collects the set of libraries transitively imported via importing [library].
///
/// This includes [library] and any other library it transitively re-exports.
Set<Library> _transitiveLibrarySet(Library library) {
final transitiveLibrarySet = <Library>{library};
final worklist = <Library>[library];
while (worklist.isNotEmpty) {
final toBeExpanded = worklist.removeLast();
assert(transitiveLibrarySet.contains(toBeExpanded));
for (final dep in toBeExpanded.dependencies) {
if (dep.isExport) {
final reExportedLibrary = dep.targetLibrary;
if (transitiveLibrarySet.add(reExportedLibrary)) {
worklist.add(reExportedLibrary);
}
}
}
}
return transitiveLibrarySet;
}
@@ -0,0 +1,2 @@
(module $module4
)
@@ -0,0 +1,39 @@
// 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.
// functionFilter=useFoo
// tableFilter=static[0-9]+
// globalFilter=Foo
// typeFilter=Foo
// compilerOption=--enable-deferred-loading
// compilerOption=--no-minify
import 'deferred.constant.type_use.h.0.dart';
import 'deferred.constant.type_use.h.1.dart' deferred as h1;
void main() async {
// Ensure the deferred libraries are loaded.
await h1.loadLibrary();
useFoo();
}
@pragma('wasm:never-inline')
void useFoo() {
// We use `Foo` as type but that doesn't mean
// => This shouldn't require the code for `Foo` to be bundled with the main
// module.
useFooAsType();
// We use `Foo` code, but only via deferred library which uses `Foo`
// directly.
// => This should require the code for `Foo` to end up in the deferred
// library.
h1.useFooAsObject();
}
@pragma('wasm:never-inline')
void useFooAsType() {
print(Foo);
}
@@ -0,0 +1,10 @@
// 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.
class Foo {
int i = 0;
void printFoo() {
print('Foo called ${i++}');
}
}
@@ -0,0 +1,12 @@
// 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 'deferred.constant.type_use.h.0.dart';
@pragma('wasm:never-inline')
void useFooAsObject() {
final f = Foo();
f.printFoo();
f.printFoo();
}
@@ -0,0 +1,41 @@
(module $module0
(type $#Top <...>)
(type $DefaultSet <...>)
(type $DeferredLoadIdNotLoadedError <...>)
(type $_InterfaceType <...>)
(type $type0 <...>)
(table $static0-0 (export "static0-0") 1 (ref null $type0))
(global $"C414 _InterfaceType" (ref $_InterfaceType) <...>)
(global $_loaded (mut (ref null $DefaultSet)) <...>)
(func $_loaded implicit getter (result (ref $DefaultSet)) <...>)
(func $"useFoo <noInline>"
call $"useFooAsType <noInline>"
block $label0 (result (ref $DefaultSet))
global.get $_loaded
br_on_non_null $label0
call $"_loaded implicit getter"
end $label0
call $_DefaultSet&_HashFieldBase&SetMixin&_HashBase&_OperatorEqualsAndHashCode&_LinkedHashSetMixin.contains
i32.eqz
if
i32.const 49
i32.const 0
ref.null none
i64.const 0
struct.new $DeferredLoadIdNotLoadedError
call $Error._throwWithCurrentStackTrace
unreachable
end
i32.const 0
call_indirect $static0-0 (result (ref null $#Top))
drop
)
(func $"useFooAsType <noInline>"
global.get $"C414 _InterfaceType"
call $print
drop
)
(func $Error._throwWithCurrentStackTrace (param $var0 (ref $#Top)) <...>)
(func $_DefaultSet&_HashFieldBase&SetMixin&_HashBase&_OperatorEqualsAndHashCode&_LinkedHashSetMixin.contains (param $var0 (ref $DefaultSet)) (result i32) <...>)
(func $print (param $var0 (ref null $#Top)) (result (ref null $#Top)) <...>)
)
@@ -0,0 +1,28 @@
(module $module1
(type $#Top <...>)
(type $Foo (sub final $Object (struct
(field $field0 i32)
(field $field1 (mut i32))
(field $i (mut i64)))))
(type $JSStringImpl <...>)
(type $Object <...>)
(global $".Foo called " (import "" "Foo called ") (ref extern))
(global $"C462 \"Foo called \"" (ref $JSStringImpl)
(i32.const 4)
(i32.const 0)
(global.get $".Foo called ")
(struct.new $JSStringImpl))
(func $"useFooAsObject <noInline>" (result (ref null $#Top))
(local $var0 (ref $Foo))
i32.const 114
i32.const 0
i64.const 0
struct.new $Foo
local.tee $var0
call $Foo.printFoo
local.get $var0
call $Foo.printFoo
ref.null none
)
(func $Foo.printFoo (param $var0 (ref $Foo)) <...>)
)
@@ -0,0 +1,2 @@
(module $module2
)
@@ -0,0 +1,2 @@
(module $module3
)
@@ -0,0 +1,2 @@
(module $module2
)