Skip to content

Commit dd22a35

Browse files
sigmundchCommit Queue
authored andcommitted
[dart2js] Adding some documentation on split constraints.
We haven't worked on this feature in a couple years, but we have some documentation that may be useful to existing developers. This was initially written by Joshua a long time ago, but we never added it to our repo. Change-Id: Icf0dbc40978ed8529c2918304b712d0f9d84811d Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/408342 Commit-Queue: Sigmund Cherem <[email protected]> Reviewed-by: Nate Biggs <[email protected]>
1 parent e962350 commit dd22a35

File tree

1 file changed

+385
-0
lines changed

1 file changed

+385
-0
lines changed
Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
1+
# Guide to Program Split Constraints in Dart2js
2+
3+
## Introduction
4+
Deferred loading can be a very powerful tool for improving IPL in
5+
Dart2js. However, if Dart2js generates too many part files, the user
6+
experience can degrade significantly. This degradation occurs
7+
because each part file has to be downloaded and initialized. While
8+
the download overhead can be minimized via bundling, there is
9+
currently no way to alleviate initialization overhead.
10+
11+
The Dart2js team has explored ways to reduce part file
12+
initialization overhead, and we have ideas for further incremental
13+
improvements. However, ‘zero’ overhead is impossible as a
14+
significant source of initialization overhead can be attributed to
15+
parsing and compiling JS, and a naive restructuring of part files
16+
for reduced overhead will significantly harm steady state
17+
performance.
18+
19+
Though someday initializing part files may be as close to ‘free’ as
20+
possible, in the meantime clients want to ship more granular apps,
21+
and thus there is an urgent need for addressing initialization
22+
overhead. One obvious way to address initialization overhead is to
23+
reduce the number of part files. Unfortunately, doing this
24+
automatically turns out to be non-trivial. Dart2js simply does not
25+
have enough information to intelligently reduce the number of part
26+
files, and a naive reduction will do more harm than good, bloating
27+
load lists with code that may be totally unrelated.
28+
29+
In order to reduce the number of part files efficiently, Dart2js
30+
needs accurate load order information. Program split constraints are
31+
a way for user’s of Dart2js to supply load order information to the
32+
compiler. With accurate information about the relative ordering of
33+
loadLibrary calls, Dart2js is able to constrain the deferred loading
34+
graph, and reduce the number of part files all without bloating load
35+
lists unnecessarily.
36+
37+
One final point, program split constraints are only useful if a
38+
program is structured in a hierarchical manner, i.e. where
39+
loadLibrary calls frequently dominate other loadLibrary calls. If
40+
programs are structured hierarchically, and a complete load order
41+
graph can be provided, then Dart2js can reduce the number of part
42+
files, in many cases very significantly.
43+
44+
45+
## Constraints
46+
Practically, a program split constraints file is just a yaml list of
47+
constraint nodes. There are different types of constraint nodes,
48+
with different properties, but nearly all of them reduce the number
49+
of part files. Writing constraint files requires a good
50+
understanding of the ordering of loadLibrary calls in a program. It
51+
is worth pointing out that constraints always affect performance,
52+
and never program correctness. Thus, incorrect use of constraints
53+
should never break a valid Dart program, but can negatively impact
54+
performance.
55+
56+
### Reference Nodes
57+
The most basic node type is a reference node. A reference node is
58+
just a way to create a symbol which represents a deferred import.
59+
60+
For example:
61+
**foo.dart:**
62+
63+
```dart
64+
import ‘...’ deferred as baz;
65+
```
66+
67+
**constraints.yaml:**
68+
69+
```yaml
70+
...
71+
- type: reference
72+
name: baz
73+
import: /path/to/foo.dart#baz
74+
...
75+
```
76+
77+
Creates a reference ‘baz’ which can be used in other nodes. We could
78+
support references inline with the body of a node, but explicit
79+
reference nodes do help keep constraints organized.
80+
81+
### Order Nodes
82+
The most important constraint node is the order node. An order node
83+
indicates that the ‘predecessor’ temporally dominates a given
84+
‘successor’ and thus Dart2js should ensure any code shared between
85+
predecessor and successor loads with the predecessor.
86+
87+
Sequencing even just two nodes will reduce the total number of
88+
output units if those nodes share code but because sequencing is a
89+
transitive operation the real benefit comes from deep hierarchies.
90+
91+
For example:
92+
93+
**foo.dart:**
94+
95+
```dart
96+
import ‘...’ deferred as step1;
97+
import ‘...’ deferred as step2;
98+
99+
do() {
100+
step1.loadLibrary().then((_) { step2.loadLibrary().then(...) } );
101+
}
102+
```
103+
104+
**constraints.yaml:**
105+
106+
```yaml
107+
...
108+
- type: order
109+
predecessor: step1
110+
successor: step2
111+
...
112+
```
113+
114+
### Combiner Nodes
115+
Order nodes support both fan in and fan out, that is multiple
116+
predecessors mapping to the same successor and multiple successors
117+
mapping to the same predecessor. In addition to fan in / out, other
118+
ways of combining constraints are supported via explicit combiner
119+
nodes.
120+
121+
The primary purpose of combiner nodes is to allow users to propagate
122+
ordering information deeper into the deferred graph. However,
123+
combiner nodes will also reduce the number of part files in many
124+
cases.
125+
126+
For v0, combiner nodes must have only reference nodes as children,
127+
though in the longer term we may relax this restriction and allow
128+
combiners to nest under certain situations. A further limitation is
129+
that we do not currently support cycles in the constraints graph,
130+
and thus every reference to a node references the exact same node.
131+
132+
#### ‘And’ Combiner Node
133+
‘And’ nodes are used for cases where multiple loadLibrary calls are
134+
guaranteed to occur at a certain point in time, but the relative
135+
ordering of those nodes may change.
136+
137+
In the case of an ‘and’ node, because all of the nodes in an ‘and’
138+
node are guaranteed to load at a given point in time, Dart2js can
139+
merge certain part files. In the case of the below example, any code
140+
shared between step1a and its successors can be merged into step1a.
141+
And code shared between step1a and its predecessors can be merged
142+
into its predecessors. The same optimizations will also apply to
143+
step1b. However step1a and step1b themselves will not merge.
144+
145+
For example:
146+
147+
**foo.dart**:
148+
149+
```dart
150+
import ‘...’ deferred as step1a;
151+
import ‘...’ deferred as step1b;
152+
153+
do() {
154+
if (...) {
155+
step1a.loadLibrary().then((_) { step1b.loadLibrary().then(...) } );
156+
} else {
157+
step1b.loadLibrary().then((_) { step1a.loadLibrary().then(...) } );
158+
}
159+
}
160+
```
161+
162+
**constraints.yaml**:
163+
164+
```yaml
165+
...
166+
- type: and
167+
name: step1
168+
nodes:
169+
- step1a
170+
- step1b
171+
...
172+
```
173+
174+
#### ‘Or’ Combiner Node
175+
‘Or’ nodes are used for cases where at least one of multiple
176+
loadLibrary calls will occur at a certain point in time. Nodes
177+
within an ‘or’ node need not be mutually exclusive.
178+
179+
Because at least one of the nodes within the ‘or’ is guaranteed to
180+
load at a certain point in time, Dart2js can perform optimizations
181+
with the code shared between all of the nodes in the ‘or.’
182+
Specifically, code shared between successors of the ‘or’ node and
183+
all of the nodes in the ‘or’ node can merge with the shared ‘or’
184+
code, and code shared between all of the nodes in the ‘or’ and
185+
predecessors can merge with predecessors without bloating load
186+
lists.
187+
188+
For example:
189+
190+
**foo.dart:**
191+
192+
```dart`
193+
import ‘...’ deferred as step1a;
194+
import ‘...’ deferred as step1b;
195+
196+
do() {
197+
if (...) {
198+
step1a.loadLibrary().then((_) { ... });
199+
} else {
200+
step1b.loadLibrary().then((_) { ... } );
201+
}
202+
}
203+
```
204+
205+
**constraints.yaml:**
206+
207+
```yaml
208+
...
209+
- type: or
210+
name: step1
211+
nodes:
212+
- step1a
213+
- step1b
214+
...
215+
```
216+
217+
#### Fuse Combiner Node
218+
Fuse nodes combine multiple nodes into a strongly connected
219+
component. This is very useful in cases where two loadLibrary calls
220+
almost always happen together. Fuse nodes can greatly reduce the
221+
number of part files.
222+
223+
For example:
224+
225+
**foo.dart:**
226+
```dart
227+
import ‘...’ deferred as step1a;
228+
import ‘...’ deferred as step1b;
229+
230+
do() {
231+
...
232+
}
233+
```
234+
235+
**constraints.yaml:**
236+
```yaml
237+
...
238+
- type: fuse
239+
name: step1
240+
nodes:
241+
- step1a
242+
- step1b
243+
...
244+
```
245+
246+
## Examples
247+
Below is a more complete example. The part files impact section is
248+
based on a worst case scenario where code is shared between all
249+
combinations of deferred imports.
250+
251+
**foo.dart**:
252+
253+
```dart
254+
import ‘...’ as deferred S1;
255+
import ‘...’ as deferred S2a;
256+
import ‘...’ as deferred S2b;
257+
import ‘...’ as deferred S3;
258+
259+
main() {
260+
S1.loadLibrary().then((_) {
261+
if (...) {
262+
S2a.loadLibrary().then((_) {
263+
S2b.loadLibrary().then((_) { S3.loadLibrary().then((_) {...}); });
264+
});
265+
} else {
266+
S2b.loadLibrary().then((_) {
267+
S2a.loadLibrary().then((_) { S3.loadLibrary().then((_) {...}); });
268+
});
269+
}
270+
});
271+
}
272+
```
273+
274+
**constraints.yaml**:
275+
276+
```yaml
277+
- type: reference
278+
name: s1
279+
import: /path/to/foo.dart#S1
280+
- type: reference
281+
name: s2a
282+
import: /path/to/foo.dart#S2a
283+
- type: reference
284+
name: s2b
285+
import: /path/to/foo.dart#S2b
286+
- type: reference
287+
name: s3
288+
import: /path/to/foo.dart#S3
289+
- type: $COMBINER_TYPE
290+
name: s2
291+
nodes: [ s2a, s2b ]
292+
- type: order
293+
predecessor: s1
294+
successor: s2
295+
- type: order
296+
predecessor: s2
297+
successor: s3
298+
```
299+
300+
**part files impact**:
301+
302+
**unconstrained**
303+
* {S1}
304+
* {S2a}
305+
* {S2b}
306+
* {S3}
307+
* {S1, S2a}
308+
* {S1, S2b}
309+
* {S1, S3}
310+
* {S2a, S2b}
311+
* {S2a, S3}
312+
* {S2b, S3}
313+
* {S1, S2a, S2b}
314+
* {S1, S2a, S3}
315+
* {S1, S2b, S3}
316+
* {S2a, S2b, S3}
317+
* {S1, S2a, S2b, S3}
318+
319+
**COMBINER\_TYPE = or**
320+
* {S2a}
321+
* {S2b}
322+
* {S3}
323+
* {S2a, S3}
324+
* {S2b, S3}
325+
* {S2a, S2b, S3}
326+
* {S1, S2a, S2b, S3}
327+
328+
**COMBINER\_TYPE = and**
329+
* {S3}
330+
* {S2a, S3}
331+
* {S2b, S3}
332+
* {S2a, S2b, S3}
333+
* {S1, S2a, S2b, S3}
334+
335+
**COMBINER\_TYPE = fuse**
336+
* {S3}
337+
* {S2a, S2b, S3}
338+
* {S1, S2a, S2b, S3}
339+
340+
**load list impact(redundant loads are ~~crossed out~~):**
341+
342+
**unconstrained**
343+
* S1 : {S1, S2a, S2b, S3}, {S1, S2a, S2b}, {S1, S2a, S3}, {S1, S2b, S3}, {S1, S2a}, {S1, S2b}, {S1, S3}, {S1}
344+
* S2a: ~~{S1, S2a, S2b, S3}, {S1, S2a, S2b}, {S1, S2a, S3}~~, {S2a, S2b, S3}, ~~{S1, S2a}~~, {S2a, S2b}, {S2a, S3}, {S2a}
345+
* S2b: ~~{S1, S2a, S2b, S3}, {S1, S2a, S2b}, {S1, S2b, S3}~~, {S2a, S2b, S3}, ~~{S1, S2b}~~, {S2a, S2b}, {S2b, S3}, {S2b}
346+
* S3 : ~~{S1, S2a, S2b, S3}, {S2a, S2b, S3}, {S1, S2a, S3}, {S1, S2b, S3}, {S2a, S3}, {S2b, S3}, {S1, S3}~~, {S3}
347+
348+
**COMBINER_TYPE = or**
349+
* S1 : {S1, S2a, S2b, S3}
350+
* S2a: ~~{S1, S2a, S2b, S3}~~, {S2a, S2b, S3}, {S2a, S3}, {S2a}
351+
* S2b: ~~{S1, S2a, S2b, S3}~~, {S2a, S2b, S3}, {S2b, S3}, {S2b}
352+
* S3 : ~~{S1, S2a, S2b, S3}, {S2a, S2b, S3}, {S2a, S3}, {S2b, S3}~~, {S3}
353+
354+
**COMBINER_TYPE = and**
355+
* S1: {S1, S2a, S2b, S3}
356+
* S2a: ~~{S1, S2a, S2b, S3}~~, {S2a, S2b, S3}, {S2a, S3}
357+
* S2b: ~~{S1, S2a, S2b, S3}~~, {S2a, S2b, S3}, {S2b, S3}
358+
* S3 : ~~{S1, S2a, S2b, S3}, {S2a, S2b, S3}, {S2a, S3}, {S2b, S3}~~, {S3}
359+
360+
**COMBINER_TYPE = fuse**
361+
* S1: {S1, S2a, S2b, S3}
362+
* S2a: ~~{S1, S2a, S2b, S3}~~, {S2a, S2b, S3}
363+
* S2b: ~~{S1, S2a, S2b, S3}~~, {S2a, S2b, S3}
364+
* S3 : ~~{S1, S2a, S2b, S3}, {S2a, S2b, S3}~~, {S3}
365+
366+
## TODO
367+
* Documentation for Inline references, glob support, groups, set
368+
operations on groups, nested combiners
369+
370+
## Glossary
371+
* deferred import: A method of asynchronously loading code. Deferred
372+
imports are used to reduce the size of the main part file and thus
373+
improve IPL.
374+
* deferred load: The runtime implementation of a deferred import.
375+
* load list: A list of part files which must be loaded before a
376+
loadLibrary call completes.
377+
* main part file: The part file representing the initial chunk of
378+
code which must be downloaded and initialized before a given
379+
program can run.
380+
* part file: A chunk of JS code representing some subset of the
381+
compiled output which results from compiling a Dart program to JS
382+
via Dart2js.
383+
* IPL: Stands for initial page load, i.e. the time it takes for a
384+
given web page to complete its first load.
385+

0 commit comments

Comments
 (0)