[L] Change in dart/sdk[main]: [dart2wasm] Prune library imports after TFA for better deferred modul...

0 views
Skip to first unread message

Ömer Ağacan (Gerrit)

unread,
Dec 1, 2025, 6:30:18 AM (yesterday) Dec 1
to Martin Kustermann, Commit Queue, dart2wasm-t...@google.com, rev...@dartlang.org
Attention needed from Martin Kustermann

Ömer Ağacan voted and added 17 comments

Votes added by Ömer Ağacan

Code-Review+1

17 comments

Patchset-level comments
File-level comment, Patchset 8 (Latest):
Ömer Ağacan . resolved

I think the overall idea makes sense. It's probably too much work to maintain a minimal set of improts as you do transformations in the front-end or TFA (there may be many small transformations that need to update import lists all the time, and accurately).

Instead of that having one pass after all the transformations are done is easier to implement, maintain, keep correct etc. as there will be one place where you deal with the imports (instead of many in each transformation).

Commit Message
Line 18, Patchset 5:the ladder "statically". Other uses of `Reference`s from other
Ömer Ağacan . unresolved

ladder --> latter

File pkg/dart2wasm/lib/deferred_loading.dart
Line 144, Patchset 8 (Latest): // procedure. This can have a varity of reasons:
Ömer Ağacan . unresolved

How did those libraries without a module work before?

Line 150, Patchset 5: // but unused).
Ömer Ağacan . unresolved

What is RTA?

"less precised" --> "less precise"

File pkg/dart2wasm/lib/library_dependencies_pruner.dart
Line 1, Patchset 8 (Latest):// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
Ömer Ağacan . unresolved

Everything in this library can be made private other than `unusedDeferredLibraryPrefix` and `pruneLibraryDependencies`.

Makes it easier to see what's the public interface and what are implementation details.

Line 20, Patchset 5:// -> The main wasm module has all RTT information atm
Ömer Ağacan . unresolved

By RTT do you mean the Wasm types? (structs, rec groups etc.)

Or is this some Dart-level thing that the main module includes?

Line 21, Patchset 8 (Latest):// -> No need to import a library to use classes from it in types.
Ömer Ağacan . unresolved

This is because the structs etc. need to be added to the using module anyway, because we can't import types?

Line 55, Patchset 8 (Latest):void pruneLibraryDependencies(LibraryIndex libraryIndex, Component component) {
Ömer Ağacan . unresolved

Does this modify the kernels other than the `Library.dependencies` members? It may be helpful to document what it's modifying.

Line 58, Patchset 8 (Latest): final collector = Collector(library, constantToLibrarySet);
Ömer Ağacan . unresolved

You could initialize the collector in `ImportPruner` to make it clear that it's per pruner and can't (or shouldn't) be shared with different pruners.

Actually it looks like you only need the `usedLibraries`, so I think it would be even better to just pass `collector.usedLibraries` to `ImportPruner`. Makes dependencies smaller (pruner depends on a set of libs instead of a `Collector` class with other things)

Line 60, Patchset 8 (Latest): }
Ömer Ağacan . unresolved

Does this need to visit all of the libraries? From the comment above "We import the library containing the definition of a [Reference] and not e.g. a library that may re-export it" it looks like we replace transitive imports with direct imports. So I think some of the libraries may end up being unused?

If that's the case, would it make sense to start from the main library and only process the libraries that are used?

Line 86, Patchset 8 (Latest): // The new (possibly smaller) set of imports.
Ömer Ağacan . unresolved

Can it ever be larger?

Line 89, Patchset 8 (Latest): if (!dep.isImport) {
Ömer Ağacan . unresolved

I checked the definition but I can't tell what `isImport` is.

I think this line checks for exported dependencies, but don't we replace transitive imports with direct imports, and doesn't that mean re-exports become unused?

Line 121, Patchset 8 (Latest): dep.name;
Ömer Ağacan . unresolved

Is this line missing some parts? If not it can be removed.

Line 122, Patchset 8 (Latest): removedDeps.add(dep);
Ömer Ağacan . unresolved

`removedDeps` seems to be unused? Should it be used?

Line 260, Patchset 8 (Latest): // Ignore
Ömer Ağacan . unresolved

Might be helpful to refer to your documentation above here. Something like "Ignore due to (a) above". (same below in `visitSupertype`)

Line 301, Patchset 8 (Latest): }
Ömer Ağacan . unresolved

It may be just me but I can't figure out what this is doing.

Couldn't you start with an empty set for every constant, and add the things added to the empty set to the parent call's set?

E.g something like:

```
final childrenLibrariesSet = ConstantToLibrarySet(); // also always initialize the `currentSet` as empty set
constant.visitChildren(childrenLibrariesSet); // collect libraries of child nodes
currentSet.addAll(childrenLibrariesSet.currentSet); // add libraries of child nodes to current set
map[constant] = Set.from(currentSet); // current constant's libraries = child libraries + current libraries
```

I also can't tell which sets are shared here and which are their own copies. There's

  • savedSet
  • transitiveSet
  • currentSet

and a bunch of assignments between them.

Feel free to ignore if it's clear to you.

Line 319, Patchset 8 (Latest): if (dep.isExport) {
Ömer Ağacan . unresolved

Similar to my comment above, if we replace all transitive imports with direct imports I think this condition should be gone? (or maybe I misunderstand what `isExport` is, I assume it's re-export of another library)

Open in Gerrit

Related details

Attention is currently required from:
  • Martin Kustermann
Submit Requirements:
  • requirement satisfiedCode-Owners
  • requirement satisfiedCode-Review
  • requirement satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: sdk
Gerrit-Branch: main
Gerrit-Change-Id: I9e486bb4de165e4b23644a84838ace06d2f35bce
Gerrit-Change-Number: 464901
Gerrit-PatchSet: 8
Gerrit-Owner: Martin Kustermann <kuste...@google.com>
Gerrit-Reviewer: Martin Kustermann <kuste...@google.com>
Gerrit-Reviewer: Ömer Ağacan <ome...@google.com>
Gerrit-Attention: Martin Kustermann <kuste...@google.com>
Gerrit-Comment-Date: Mon, 01 Dec 2025 11:30:12 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: Yes
satisfied_requirement
open
diffy

Martin Kustermann (Gerrit)

unread,
6:41 AM (5 hours ago) 6:41 AM
to Ömer Ağacan, Commit Queue, dart2wasm-t...@google.com, rev...@dartlang.org
Attention needed from Ömer Ağacan

Martin Kustermann added 17 comments

Patchset-level comments
File-level comment, Patchset 12 (Latest):
Martin Kustermann . resolved

Thanks, Omer!

I've tried to address all comments.

I've also added the test (see the removed code in main module after this CL in : https://dart-review.googlesource.com/c/sdk/+/464901/11..12/pkg/dart2wasm/test/ir_tests/deferred.constant.type_use.wat)

Commit Message
Line 18, Patchset 5:the ladder "statically". Other uses of `Reference`s from other
Ömer Ağacan . resolved

ladder --> latter

Martin Kustermann

Done

File pkg/dart2wasm/lib/deferred_loading.dart
Line 144, Patchset 8: // procedure. This can have a varity of reasons:
Ömer Ağacan . resolved

How did those libraries without a module work before?

Martin Kustermann

Let's say
```
// other.dart
class FooClass {}

// main.dart
import 'other.dart';

main() {
print(FooClass);
}
```

Before this CL, both libraries would be assigned to the same wasm module - due to the import statement.

But after this CL, we notice that `main.dart` doesn't actually use anything "statically" from `other.dart` (i.e. no constructor invocation, no static call, ...). It does use `FooClass` but only as a type literal.

Because of that we say that `main.dart` becomes a wasm module which leaves the `other.dart` library dangling. But the compiler wants each library to have an assigned wasm module, so we make this fake one here.

Line 150, Patchset 5: // but unused).
Ömer Ağacan . resolved

What is RTA?

"less precised" --> "less precise"

Martin Kustermann

The issue is the way TFA works: TFA starts with empty set of classes and empty set of selectors. Then it gradually analyzes code. If it hits a method invocation and there's only one possible receiver class, it devirtualizes it. But if it later sees another allocation, then it has to invalidate this earlier decision and re-analyze that (now incorrect) devirtualized code. These invalidations are costly and take compile-time.

So we run RTA before TFA. It will have a much faster, simpler algorithm to determine the set of live classes. It will then TFA immediately that those classes are alive, which will make TFA converge faster (less invalidations).

Overall it improves compile-time.

RTA == Rapid Type Analysis, https://github.com/dart-lang/sdk/blob/0f6928c311f394c2e33deddbcc6aa0a35e0883f0/pkg/vm/lib/transformations/type_flow/rta.dart#L196

File pkg/dart2wasm/lib/library_dependencies_pruner.dart
Line 1, Patchset 8:// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
Ömer Ağacan . resolved

Everything in this library can be made private other than `unusedDeferredLibraryPrefix` and `pruneLibraryDependencies`.

Makes it easier to see what's the public interface and what are implementation details.

Martin Kustermann

Done.

Made the top-level classes/functions that are only used in this file private.

Line 20, Patchset 5:// -> The main wasm module has all RTT information atm
Ömer Ağacan . resolved

By RTT do you mean the Wasm types? (structs, rec groups etc.)

Or is this some Dart-level thing that the main module includes?

Martin Kustermann

It means RunTimeType information (sometimes we call it also RTI). It's the kind of objects we use to represent Dart types in wasm and relationships between those dart types.

Rephrased.

Line 21, Patchset 8:// -> No need to import a library to use classes from it in types.
Ömer Ağacan . resolved

This is because the structs etc. need to be added to the using module anyway, because we can't import types?

Martin Kustermann

No, this isn't about wasm structs, it's about Dart types, Dart classes and their relationship.

For example if we compile `main() => print(foo<BarClass>() is BazClass);` then we need to create a runtime object for the `BarClass` type argument and we need the some lookup tables to see how `BarClass` is related to `BazClass`. But this information is already in the main module atm.

Line 55, Patchset 8:void pruneLibraryDependencies(LibraryIndex libraryIndex, Component component) {
Ömer Ağacan . resolved

Does this modify the kernels other than the `Library.dependencies` members? It may be helpful to document what it's modifying.

Martin Kustermann

Done

Line 58, Patchset 8: final collector = Collector(library, constantToLibrarySet);
Ömer Ağacan . resolved

You could initialize the collector in `ImportPruner` to make it clear that it's per pruner and can't (or shouldn't) be shared with different pruners.

Actually it looks like you only need the `usedLibraries`, so I think it would be even better to just pass `collector.usedLibraries` to `ImportPruner`. Makes dependencies smaller (pruner depends on a set of libs instead of a `Collector` class with other things)

Martin Kustermann

Done

Line 60, Patchset 8: }
Ömer Ağacan . resolved

Does this need to visit all of the libraries? From the comment above "We import the library containing the definition of a [Reference] and not e.g. a library that may re-export it" it looks like we replace transitive imports with direct imports. So I think some of the libraries may end up being unused?

If that's the case, would it make sense to start from the main library and only process the libraries that are used?

Martin Kustermann

We run this code after TFA has run - so most unused libraries should be already removed from the `Component`.

It's a good point, I could make this algorithm start from roots and only work towards direct/used dependencies. Though in reality I think this would be almost the same.

Doing it only from `main` wouldn't be enough, as we would also need to do that for each library that contains `@pragma('wasm:export')`s for example. Discovering that already requires traversing all libraries.

Furthermore IMHO it's nice that after running this transformation we have established an invariant on AST that library dependencies are precise (i.e. there iff they are needed).

So I'd prefer to keep it this way.

Line 86, Patchset 8: // The new (possibly smaller) set of imports.
Ömer Ağacan . resolved

Can it ever be larger?

Martin Kustermann

Yes, e.g. if modular transforms inserted static calls to functions without adding imports.

Updated the comment.

Line 89, Patchset 8: if (!dep.isImport) {
Ömer Ağacan . resolved

I checked the definition but I can't tell what `isImport` is.

I think this line checks for exported dependencies, but don't we replace transitive imports with direct imports, and doesn't that mean re-exports become unused?

Martin Kustermann

Changed `!dep.isImport` to `dep.isExport`.

This transformation needs the exports to determine whether an element may have come from a deferred library or a non-deferred. Imagine:
```
import 'def.dart' deferred as D;
import 'def.dart' as D2;

main() {
print(D.foo());
}
```

For the `StaticInvocation(foo, [])` node I don't know for sure whether it was called via `D` or `D2`. The CFE desugaring looses this information.

So for a case like the above I'm going to look whether any deferred imports (`D` here) happens to define or re-export the library containing `foo`. If it does, then I consider the deferred import `D` to be alive - which we need for semantics, as the above program has to throw (due to missing `await D.loadLibrary()`).

Since libraries can import each other in cycles, we need the exports while we prune the libraries of the component.

But yes, you're right that we don't need exports afterwards anymore - so I could remove them. Let me do this as a final pass.

Line 121, Patchset 8: dep.name;
Ömer Ağacan . resolved

Is this line missing some parts? If not it can be removed.

Martin Kustermann

Done

Line 122, Patchset 8: removedDeps.add(dep);
Ömer Ağacan . resolved

`removedDeps` seems to be unused? Should it be used?

Martin Kustermann

Done - it was from earlier iterations of this transformer (which I've re-written a few times before sending for review)

Line 260, Patchset 8: // Ignore
Ömer Ağacan . resolved

Might be helpful to refer to your documentation above here. Something like "Ignore due to (a) above". (same below in `visitSupertype`)

Martin Kustermann

Done

Line 301, Patchset 8: }
Ömer Ağacan . resolved

It may be just me but I can't figure out what this is doing.

Couldn't you start with an empty set for every constant, and add the things added to the empty set to the parent call's set?

E.g something like:

```
final childrenLibrariesSet = ConstantToLibrarySet(); // also always initialize the `currentSet` as empty set
constant.visitChildren(childrenLibrariesSet); // collect libraries of child nodes
currentSet.addAll(childrenLibrariesSet.currentSet); // add libraries of child nodes to current set
map[constant] = Set.from(currentSet); // current constant's libraries = child libraries + current libraries
```

I also can't tell which sets are shared here and which are their own copies. There's

  • savedSet
  • transitiveSet
  • currentSet

and a bunch of assignments between them.

Feel free to ignore if it's clear to you.

Martin Kustermann

So constants form a DAG (directed acyclic graph). We want to scan constants not as a tree because that could visit each node in the DAG many times, which would make this algorithm run in exponential time. So we want to handle one constant only once.

We compute it via DFS: A constant's libraries are it's childrens libraries plus enclosing library if constant is TearOff/Instance constant.

This collection of child constants happens via side-effect of setting `currentSet` to `{}` then `constant.visitChildren` then using `currentSet`.

Now since collecting the libraries of child constants may recursively collect libraries of other constants, we save the set & restore after.

Adding to the parent set is hacky as a) we'd need to maintain a stack of sets and add to all of them b) we won't add to all parents only to the current parent chain.

I've split it into two classes now, one that does the recursion and another that collects the shallow/direct child constants. It's a bit less efficient but probably easier to understand.

Line 319, Patchset 8: if (dep.isExport) {
Ömer Ağacan . resolved

Similar to my comment above, if we replace all transitive imports with direct imports I think this condition should be gone? (or maybe I misunderstand what `isExport` is, I assume it's re-export of another library)

Martin Kustermann

The information computed here is needed for this algorithm to do the right pruning. We don't use this after the pruning is done.

See my other comment. This is basically used to determine a boolean: For a use `U` (of class/function/field/... in library `H`) in library `M` we need to know whether the use `U` could possibly have come from any deferred import of `H` in `M`.(The used `U` may be available via deferred and non-deferred import - in which case we have to keep the deferred import for semantics pursposes)

Open in Gerrit

Related details

Attention is currently required from:
  • Ömer Ağacan
Submit Requirements:
  • requirement satisfiedCode-Owners
  • requirement is not satisfiedCode-Review
  • requirement is not satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: sdk
Gerrit-Branch: main
Gerrit-Change-Id: I9e486bb4de165e4b23644a84838ace06d2f35bce
Gerrit-Change-Number: 464901
Gerrit-PatchSet: 12
Gerrit-Owner: Martin Kustermann <kuste...@google.com>
Gerrit-Reviewer: Martin Kustermann <kuste...@google.com>
Gerrit-Reviewer: Ömer Ağacan <ome...@google.com>
Gerrit-Attention: Ömer Ağacan <ome...@google.com>
Gerrit-Comment-Date: Tue, 02 Dec 2025 11:41:50 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
Comment-In-Reply-To: Ömer Ağacan <ome...@google.com>
satisfied_requirement
unsatisfied_requirement
open
diffy

Ömer Ağacan (Gerrit)

unread,
7:12 AM (4 hours ago) 7:12 AM
to Martin Kustermann, Commit Queue, dart2wasm-t...@google.com, rev...@dartlang.org
Attention needed from Martin Kustermann

Ömer Ağacan voted Code-Review+1

Code-Review+1
Open in Gerrit

Related details

Attention is currently required from:
  • Martin Kustermann
Submit Requirements:
  • requirement satisfiedCode-Owners
  • requirement satisfiedCode-Review
  • requirement satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: sdk
Gerrit-Branch: main
Gerrit-Change-Id: I9e486bb4de165e4b23644a84838ace06d2f35bce
Gerrit-Change-Number: 464901
Gerrit-PatchSet: 12
Gerrit-Owner: Martin Kustermann <kuste...@google.com>
Gerrit-Reviewer: Martin Kustermann <kuste...@google.com>
Gerrit-Reviewer: Ömer Ağacan <ome...@google.com>
Gerrit-Attention: Martin Kustermann <kuste...@google.com>
Gerrit-Comment-Date: Tue, 02 Dec 2025 12:12:39 +0000
Gerrit-HasComments: No
Gerrit-Has-Labels: Yes
satisfied_requirement
open
diffy

Slava Egorov (Gerrit)

unread,
9:28 AM (2 hours ago) 9:28 AM
to Martin Kustermann, Ömer Ağacan, Commit Queue, dart2wasm-t...@google.com, rev...@dartlang.org
Attention needed from Martin Kustermann and Ömer Ağacan

Slava Egorov voted Code-Review+1

Code-Review+1
Open in Gerrit

Related details

Attention is currently required from:
  • Martin Kustermann
  • Ömer Ağacan
Submit Requirements:
  • requirement satisfiedCode-Owners
  • requirement satisfiedCode-Review
  • requirement satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: sdk
Gerrit-Branch: main
Gerrit-Change-Id: I9e486bb4de165e4b23644a84838ace06d2f35bce
Gerrit-Change-Number: 464901
Gerrit-PatchSet: 13
Gerrit-Owner: Martin Kustermann <kuste...@google.com>
Gerrit-Reviewer: Martin Kustermann <kuste...@google.com>
Gerrit-Reviewer: Slava Egorov <veg...@google.com>
Gerrit-Reviewer: Ömer Ağacan <ome...@google.com>
Gerrit-Attention: Ömer Ağacan <ome...@google.com>
Gerrit-Attention: Martin Kustermann <kuste...@google.com>
Gerrit-Comment-Date: Tue, 02 Dec 2025 14:28:24 +0000
Gerrit-HasComments: No
Gerrit-Has-Labels: Yes
satisfied_requirement
open
diffy

Martin Kustermann (Gerrit)

unread,
9:29 AM (2 hours ago) 9:29 AM
to Slava Egorov, Ömer Ağacan, Commit Queue, dart2wasm-t...@google.com, rev...@dartlang.org
Attention needed from Ömer Ağacan

Martin Kustermann voted Commit-Queue+2

Commit-Queue+2
Open in Gerrit

Related details

Attention is currently required from:
  • Ömer Ağacan
Submit Requirements:
  • requirement satisfiedCode-Owners
  • requirement satisfiedCode-Review
  • requirement satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: sdk
Gerrit-Branch: main
Gerrit-Change-Id: I9e486bb4de165e4b23644a84838ace06d2f35bce
Gerrit-Change-Number: 464901
Gerrit-PatchSet: 13
Gerrit-Owner: Martin Kustermann <kuste...@google.com>
Gerrit-Reviewer: Martin Kustermann <kuste...@google.com>
Gerrit-Reviewer: Slava Egorov <veg...@google.com>
Gerrit-Reviewer: Ömer Ağacan <ome...@google.com>
Gerrit-Attention: Ömer Ağacan <ome...@google.com>
Gerrit-Comment-Date: Tue, 02 Dec 2025 14:28:56 +0000
Gerrit-HasComments: No
Gerrit-Has-Labels: Yes
satisfied_requirement
open
diffy

Commit Queue (Gerrit)

unread,
10:05 AM (1 hour ago) 10:05 AM
to Martin Kustermann, Slava Egorov, Ömer Ağacan, dart2wasm-t...@google.com, rev...@dartlang.org

Commit Queue submitted the change

Change information

Commit message:
[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
Commit-Queue: Martin Kustermann <kuste...@google.com>
Reviewed-by: Slava Egorov <veg...@google.com>
Files:
  • M pkg/dart2wasm/lib/compile.dart
  • M pkg/dart2wasm/lib/deferred_loading.dart
  • A pkg/dart2wasm/lib/library_dependencies_pruner.dart
  • A pkg/dart2wasm/test/ir_tests/deferred.constant.multi_module_use_module4.wat
  • A pkg/dart2wasm/test/ir_tests/deferred.constant.type_use.dart
  • A pkg/dart2wasm/test/ir_tests/deferred.constant.type_use.h.0.dart
  • A pkg/dart2wasm/test/ir_tests/deferred.constant.type_use.h.1.dart
  • A pkg/dart2wasm/test/ir_tests/deferred.constant.type_use.wat
  • A pkg/dart2wasm/test/ir_tests/deferred.constant.type_use_module1.wat
  • A pkg/dart2wasm/test/ir_tests/deferred.constant.type_use_module2.wat
  • A pkg/dart2wasm/test/ir_tests/deferred.constant_module3.wat
  • A pkg/dart2wasm/test/ir_tests/import_name_module2.wat
Change size: L
Delta: 12 files changed, 560 insertions(+), 3 deletions(-)
Branch: refs/heads/main
Submit Requirements:
  • requirement satisfiedCode-Review: +1 by Slava Egorov
Open in Gerrit
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: merged
Gerrit-Project: sdk
Gerrit-Branch: main
Gerrit-Change-Id: I9e486bb4de165e4b23644a84838ace06d2f35bce
Gerrit-Change-Number: 464901
Gerrit-PatchSet: 14
Gerrit-Owner: Martin Kustermann <kuste...@google.com>
open
diffy
satisfied_requirement
Reply all
Reply to author
Forward
0 new messages