blob: a83ee4ffa0d3e2315bbaaee99c4205b64fb5a893 [file] [log] [blame] [view]
# Guide to Program Split Constraints in Dart2js
## Introduction
Deferred loading can be a very powerful tool for improving IPL in
Dart2js. However, if Dart2js generates too many part files, the user
experience can degrade significantly. This degradation occurs
because each part file has to be downloaded and initialized. While
the download overhead can be minimized via bundling, there is
currently no way to alleviate initialization overhead.
The Dart2js team has explored ways to reduce part file
initialization overhead, and we have ideas for further incremental
improvements. However, zero overhead is impossible as a
significant source of initialization overhead can be attributed to
parsing and compiling JS, and a naive restructuring of part files
for reduced overhead will significantly harm steady state
performance.
Though someday initializing part files may be as close to free as
possible, in the meantime clients want to ship more granular apps,
and thus there is an urgent need for addressing initialization
overhead. One obvious way to address initialization overhead is to
reduce the number of part files. Unfortunately, doing this
automatically turns out to be non-trivial. Dart2js simply does not
have enough information to intelligently reduce the number of part
files, and a naive reduction will do more harm than good, bloating
load lists with code that may be totally unrelated.
In order to reduce the number of part files efficiently, Dart2js
needs accurate load order information. Program split constraints are
a way for users of Dart2js to supply load order information to the
compiler. With accurate information about the relative ordering of
loadLibrary calls, Dart2js is able to constrain the deferred loading
graph, and reduce the number of part files all without bloating load
lists unnecessarily.
One final point, program split constraints are only useful if a
program is structured in a hierarchical manner, i.e. where
loadLibrary calls frequently dominate other loadLibrary calls. If
programs are structured hierarchically, and a complete load order
graph can be provided, then Dart2js can reduce the number of part
files, in many cases very significantly.
## Constraints
Practically, a program split constraints file is just a yaml list of
constraint nodes. There are different types of constraint nodes,
with different properties, but nearly all of them reduce the number
of part files. Writing constraint files requires a good
understanding of the ordering of loadLibrary calls in a program. It
is worth pointing out that constraints always affect performance,
and never program correctness. Thus, incorrect use of constraints
should never break a valid Dart program, but can negatively impact
performance.
### Reference Nodes
The most basic node type is a reference node. A reference node is
just a way to create a symbol which represents a deferred import.
For example:
**foo.dart:**
```dart
import '...' deferred as baz;
```
**constraints.yaml:**
```yaml
...
- type: reference
name: baz
import: /path/to/foo.dart#baz
...
```
Creates a reference baz which can be used in other nodes. We could
support references inline with the body of a node, but explicit
reference nodes do help keep constraints organized.
### Order Nodes
The most important constraint node is the order node. An order node
indicates that the predecessor temporally dominates a given
successor and thus Dart2js should ensure any code shared between
predecessor and successor loads with the predecessor.
Sequencing even just two nodes will reduce the total number of
output units if those nodes share code but because sequencing is a
transitive operation the real benefit comes from deep hierarchies.
For example:
**foo.dart:**
```dart
import '...' deferred as step1;
import '...' deferred as step2;
do() {
step1.loadLibrary().then((_) { step2.loadLibrary().then(...) } );
}
```
**constraints.yaml:**
```yaml
...
- type: relative_order
predecessor: step1
successor: step2
...
```
### Combiner Nodes
Order nodes support both fan in and fan out, that is multiple
predecessors mapping to the same successor and multiple successors
mapping to the same predecessor. In addition to fan in / out, other
ways of combining constraints are supported via explicit combiner
nodes.
The primary purpose of combiner nodes is to allow users to propagate
ordering information deeper into the deferred graph. However,
combiner nodes will also reduce the number of part files in many
cases.
For v0, combiner nodes must have only reference nodes as children,
though in the longer term we may relax this restriction and allow
combiners to nest under certain situations. A further limitation is
that we do not currently support cycles in the constraints graph,
and thus every reference to a node references the exact same node.
#### ‘And’ Combiner Node
And nodes are used for cases where multiple loadLibrary calls are
guaranteed to occur at a certain point in time, but the relative
ordering of those nodes may change.
In the case of an and node, because all of the nodes in an and
node are guaranteed to load at a given point in time, Dart2js can
merge certain part files. In the case of the below example, any code
shared between step1a and its successors can be merged into step1a.
And code shared between step1a and its predecessors can be merged
into its predecessors. The same optimizations will also apply to
step1b. However step1a and step1b themselves will not merge.
For example:
**foo.dart**:
```dart
import '...' deferred as step1a;
import '...' deferred as step1b;
do() {
if (...) {
step1a.loadLibrary().then((_) { step1b.loadLibrary().then(...) } );
} else {
step1b.loadLibrary().then((_) { step1a.loadLibrary().then(...) } );
}
}
```
**constraints.yaml**:
```yaml
...
- type: and
name: step1
nodes:
- step1a
- step1b
...
```
#### ‘Or’ Combiner Node
Or nodes are used for cases where at least one of multiple
loadLibrary calls will occur at a certain point in time. Nodes
within an or node need not be mutually exclusive.
Because at least one of the nodes within the or is guaranteed to
load at a certain point in time, Dart2js can perform optimizations
with the code shared between all of the nodes in the or.’
Specifically, code shared between successors of the or node and
all of the nodes in the or node can merge with the shared or
code, and code shared between all of the nodes in the or and
predecessors can merge with predecessors without bloating load
lists.
For example:
**foo.dart:**
```dart
import '...' deferred as step1a;
import '...' deferred as step1b;
do() {
if (...) {
step1a.loadLibrary().then((_) { ... });
} else {
step1b.loadLibrary().then((_) { ... } );
}
}
```
**constraints.yaml:**
```yaml
...
- type: or
name: step1
nodes:
- step1a
- step1b
...
```
#### Fuse Combiner Node
Fuse nodes combine multiple nodes into a strongly connected
component. This is very useful in cases where two loadLibrary calls
almost always happen together. Fuse nodes can greatly reduce the
number of part files.
For example:
**foo.dart:**
```dart
import '...' deferred as step1a;
import '...' deferred as step1b;
do() {
...
}
```
**constraints.yaml:**
```yaml
...
- type: fuse
name: step1
nodes:
- step1a
- step1b
...
```
## Examples
Below is a more complete example. The part files impact section is
based on a worst case scenario where code is shared between all
combinations of deferred imports.
**foo.dart**:
```dart
import '...' as deferred S1;
import '...' as deferred S2a;
import '...' as deferred S2b;
import '...' as deferred S3;
main() {
S1.loadLibrary().then((_) {
if (...) {
S2a.loadLibrary().then((_) {
S2b.loadLibrary().then((_) { S3.loadLibrary().then((_) {...}); });
});
} else {
S2b.loadLibrary().then((_) {
S2a.loadLibrary().then((_) { S3.loadLibrary().then((_) {...}); });
});
}
});
}
```
**constraints.yaml**:
```yaml
- type: reference
name: s1
import: /path/to/foo.dart#S1
- type: reference
name: s2a
import: /path/to/foo.dart#S2a
- type: reference
name: s2b
import: /path/to/foo.dart#S2b
- type: reference
name: s3
import: /path/to/foo.dart#S3
- type: $COMBINER_TYPE
name: s2
nodes: [ s2a, s2b ]
- type: relative_order
predecessor: s1
successor: s2
- type: relative_order
predecessor: s2
successor: s3
```
**part files impact**:
**unconstrained**
* {S1}
* {S2a}
* {S2b}
* {S3}
* {S1, S2a}
* {S1, S2b}
* {S1, S3}
* {S2a, S2b}
* {S2a, S3}
* {S2b, S3}
* {S1, S2a, S2b}
* {S1, S2a, S3}
* {S1, S2b, S3}
* {S2a, S2b, S3}
* {S1, S2a, S2b, S3}
**COMBINER\_TYPE = or**
* {S2a}
* {S2b}
* {S3}
* {S2a, S3}
* {S2b, S3}
* {S2a, S2b, S3}
* {S1, S2a, S2b, S3}
**COMBINER\_TYPE = and**
* {S3}
* {S2a, S3}
* {S2b, S3}
* {S2a, S2b, S3}
* {S1, S2a, S2b, S3}
**COMBINER\_TYPE = fuse**
* {S3}
* {S2a, S2b, S3}
* {S1, S2a, S2b, S3}
**load list impact(redundant loads are ~~crossed out~~):**
**unconstrained**
* S1 : {S1, S2a, S2b, S3}, {S1, S2a, S2b}, {S1, S2a, S3}, {S1, S2b, S3}, {S1, S2a}, {S1, S2b}, {S1, S3}, {S1}
* S2a: ~~{S1, S2a, S2b, S3}, {S1, S2a, S2b}, {S1, S2a, S3}~~, {S2a, S2b, S3}, ~~{S1, S2a}~~, {S2a, S2b}, {S2a, S3}, {S2a}
* S2b: ~~{S1, S2a, S2b, S3}, {S1, S2a, S2b}, {S1, S2b, S3}~~, {S2a, S2b, S3}, ~~{S1, S2b}~~, {S2a, S2b}, {S2b, S3}, {S2b}
* S3 : ~~{S1, S2a, S2b, S3}, {S2a, S2b, S3}, {S1, S2a, S3}, {S1, S2b, S3}, {S2a, S3}, {S2b, S3}, {S1, S3}~~, {S3}
**COMBINER_TYPE = or**
* S1 : {S1, S2a, S2b, S3}
* S2a: ~~{S1, S2a, S2b, S3}~~, {S2a, S2b, S3}, {S2a, S3}, {S2a}
* S2b: ~~{S1, S2a, S2b, S3}~~, {S2a, S2b, S3}, {S2b, S3}, {S2b}
* S3 : ~~{S1, S2a, S2b, S3}, {S2a, S2b, S3}, {S2a, S3}, {S2b, S3}~~, {S3}
**COMBINER_TYPE = and**
* S1: {S1, S2a, S2b, S3}
* S2a: ~~{S1, S2a, S2b, S3}~~, {S2a, S2b, S3}, {S2a, S3}
* S2b: ~~{S1, S2a, S2b, S3}~~, {S2a, S2b, S3}, {S2b, S3}
* S3 : ~~{S1, S2a, S2b, S3}, {S2a, S2b, S3}, {S2a, S3}, {S2b, S3}~~, {S3}
**COMBINER_TYPE = fuse**
* S1: {S1, S2a, S2b, S3}
* S2a: ~~{S1, S2a, S2b, S3}~~, {S2a, S2b, S3}
* S2b: ~~{S1, S2a, S2b, S3}~~, {S2a, S2b, S3}
* S3 : ~~{S1, S2a, S2b, S3}, {S2a, S2b, S3}~~, {S3}
## TODO
* Documentation for Inline references, glob support, groups, set
operations on groups, nested combiners
## Glossary
* deferred import: A method of asynchronously loading code. Deferred
imports are used to reduce the size of the main part file and thus
improve IPL.
* deferred load: The runtime implementation of a deferred import.
* load list: A list of part files which must be loaded before a
loadLibrary call completes.
* main part file: The part file representing the initial chunk of
code which must be downloaded and initialized before a given
program can run.
* part file: A chunk of JS code representing some subset of the
compiled output which results from compiling a Dart program to JS
via Dart2js.
* IPL: Stands for initial page load, i.e. the time it takes for a
given web page to complete its first load.