| # 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 user’s 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. | |