Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(283)

Side by Side Diff: pkg/barback/lib/src/asset_graph.dart

Issue 16854005: First pass at build dependency graph for barback. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
nweiz 2013/06/14 00:57:57 This should probably be exported somewhere, right?
Bob Nystrom 2013/06/17 23:35:05 The Barback class (when it exists) will wrap it an
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4
5 library barback.asset_graph;
6
7 import 'dart:async';
8 import 'dart:collection';
9
10 import '../barback.dart';
11 import '../transformer.dart';
12
13 /// The main build dependency manager. For any given input file, it can tell
14 /// which output files are affected by it, and vice versa.
15 class AssetGraph {
16 final AssetProvider _provider;
17
18 final _phases = <_Phase>[];
19
20 // TODO(rnystrom): Have a value type for results.
21 Stream<ProcessResult> get results => _resultsController.stream;
22 final _resultsController = new StreamController<ProcessResult>();
nweiz 2013/06/14 00:57:57 This should be a broadcast controller.
Bob Nystrom 2013/06/17 23:35:05 Done.
23
24 /// This holds a future that completes when the work queue is complete if the
25 /// /work queue is currently being processed. Otherwise, it is `null`.
nweiz 2013/06/14 00:57:57 "/work" -> "work"
Bob Nystrom 2013/06/17 23:35:05 Done.
26 Future _processDone;
27
28 _ChangeBatch _sourceChanges;
29
30 /// Creates a new [AssetGraph].
31 ///
32 /// It loads source assets using [provider] and then uses [transformerPhases]
33 /// to generate output files from them.
34 //TODO(rnystrom): Better way of specifying transformers and their ordering.
35 AssetGraph(this._provider, Iterable<Iterable<Transformer>> transformerPhases) {
nweiz 2013/06/14 00:57:57 Long line.
Bob Nystrom 2013/06/17 23:35:05 Done.
36 // Add phases for each transformer stage.
nweiz 2013/06/14 00:57:57 I'm convinced we're going to end up needing semant
Bob Nystrom 2013/06/17 23:35:05 I'm not convinced either way, but so far I haven't
nweiz 2013/06/18 23:14:45 The biggest driver won't come when you're doing th
Bob Nystrom 2013/06/20 00:23:59 Sure, but my rough thinking is that that can happe
nweiz 2013/06/20 23:06:08 Keep in mind that we may eventually want to suppor
Bob Nystrom 2013/06/21 00:13:20 Agreed. I'm making the simplifying assumption for
37 for (var transformers in transformerPhases) {
38 var phase = new _Phase(this, _phases.length, transformers.toList());
39 _phases.add(phase);
40 }
41
42 // Add a phase for the final outputs.
43 _phases.add(new _Phase(this, _phases.length, []));
nweiz 2013/06/14 00:57:57 This is confusing. What does a phase with no trans
Bob Nystrom 2013/06/17 23:35:05 Done.
44
45 // Chain them together.
46 for (var i = 0; i < _phases.length - 1; i++){
47 _phases[i].next = _phases[i + 1];
48 }
49 }
50
51 Future<Asset> getAssetById(AssetId id) {
52 return _waitForProcess().then((_) {
nweiz 2013/06/14 00:57:57 Add a TODO to be smarter about only waiting until
Bob Nystrom 2013/06/17 23:35:05 At least right now, this is as smart as it can be.
nweiz 2013/06/18 23:14:45 There are several ways this could be smarter: * [
Bob Nystrom 2013/06/20 00:23:59 All good points. Added a big TODO with all of this
53 // Find the latest phase that output this asset.
54 for (var i = _phases.length - 1; i >= 0; i--) {
55 var node = _phases[i].inputs[id];
nweiz 2013/06/14 00:57:57 Why are we looking in the phase inputs for the ass
Bob Nystrom 2013/06/17 23:35:05 Generated assets are stored in each phase. If you
nweiz 2013/06/18 23:14:45 This would be clearer if the comment said "Find th
Bob Nystrom 2013/06/20 00:23:59 Done.
56 if (node != null) {
57 // By the time we get here, the asset should have been built.
58 assert(node.asset != null);
59 return node.asset;
60 }
61 }
62
63 // Couldn't find it.
64 _throwError(new AssetNotFoundException(id));
65 });
66 }
67
68 /// Adds [sources] to the graph's known set of source assets. Will begin
nweiz 2013/06/14 00:57:57 "Will begin" -> "Begins"
Bob Nystrom 2013/06/17 23:35:05 Done.
69 /// applying any transforms that can consume any of the sources. If a given
70 /// source has already been added, it is considered modified and all
nweiz 2013/06/14 00:57:57 "has already been added" -> "is already known"
Bob Nystrom 2013/06/17 23:35:05 Done.
71 /// transforms that use it will be re-applied.
72 void updateSources(Iterable<AssetId> sources) {
nweiz 2013/06/14 00:57:57 Right now I think you can postpone handling a chan
Bob Nystrom 2013/06/17 23:35:05 Added a test. I think it's working correctly here,
nweiz 2013/06/18 23:14:45 I was misreading somewhat, but I think there's sti
Bob Nystrom 2013/06/20 00:23:59 Wow, good catch. One line fix, but took me a good
73 if (_sourceChanges == null) _sourceChanges = new _ChangeBatch();
74 _sourceChanges.update(sources);
75
76 _waitForProcess();
77 }
78
79 /// Removes [removed] from the graph's known set of source assets.
80 void removeSources(Iterable<AssetId> removed) {
81 if (_sourceChanges == null) _sourceChanges = new _ChangeBatch();
82 _sourceChanges.remove(removed);
83
84 _waitForProcess();
85 }
86
87 /// Returns a future that completes with the background processing is done.
nweiz 2013/06/14 00:57:57 It's not clear from the name or from the documenta
Bob Nystrom 2013/06/17 23:35:05 Clarified documentation.
nweiz 2013/06/18 23:14:45 I still worry that unintended errors from e.g. [_p
Bob Nystrom 2013/06/20 00:23:59 My intent (and there are TODOs for this) is to cat
nweiz 2013/06/20 23:06:08 Taking down barback as a component and taking down
Bob Nystrom 2013/06/21 00:13:20 Good call. Done. Added a long comment explaining i
88 Future _waitForProcess() {
89 if (_processDone != null) return _processDone;
90 // TODO(rnystrom): Handle errors.
91 return _processDone = _process().then((_) {
92 _processDone = null;
93 });
94 }
95
96 /// Starts the background processing. Returns a future that completes when
97 /// all assets have been processed.
98 Future _process() {
99 return _processSourceChanges().then((_) {
100 // Find the first phase that has work to do and do it.
101 var future;
102 for (var phase in _phases) {
103 future = phase.process();
104 if (future != null) break;
105 }
106
107 // If all phases are done, so are we.
108 if (future == null) return;
109
110 // Process that phase and then loop onto the next.
111 return future.then((_) => _process());
112 });
113 }
114
115 /// Processes the current batch of changes to source assets.
116 Future _processSourceChanges() {
117 if (_sourceChanges == null) return new Future.value();
118
119 // Always pump the event loop. This ensures a bunch of synchronous source
120 // changes are processed in a single batch even when the first one starts
121 // the build process.
nweiz 2013/06/14 00:57:57 I don't think this works right now. You'll end up
Bob Nystrom 2013/06/17 23:35:05 It's very unclear, but the behavior is correct. It
122 return new Future(() {
123 var changes = _sourceChanges;
124 _sourceChanges = null;
125
126 var updated = new Map<AssetId, Asset>();
127 var futures = [];
128 for (var id in changes.updated) {
129 futures.add(_provider.loadAsset(id).then((asset) {
130 updated[id] = asset;
131 }));
132 }
133
134 return Future.wait(futures).then((_) {
135 _phases.first.updateInputs(updated, changes.removed);
136 });
137 });
138 }
139
140 /// Reports a process result with the given error then throws it.
141 void _throwError(Exception error) {
nweiz 2013/06/14 00:57:57 It feels like it might be cleaner to just throw ex
Bob Nystrom 2013/06/17 23:35:05 Removed this.
142 _resultsController.add(new ProcessResult(error));
143 throw error;
144 }
145 }
146
147 /// The build process runs asynchronously in the background. It reports back to
148 /// the user be emitting a [Stream] of these objects. Currently, it only emits
149 /// errors.
150 class ProcessResult {
151 /// The error that occurred.
152 final error;
153
154 ProcessResult(this.error);
155 }
156
157 /// Represents a batch of source asset changes: additions, removals and
158 /// modifications.
159 class _ChangeBatch {
nweiz 2013/06/14 00:57:57 It would be nice to split out this and the followi
Bob Nystrom 2013/06/17 23:35:05 Done. For some reason, I thought they should be to
160 /// The assets that have been added or modified in this batch.
161 final updated = new Set<AssetId>();
162
163 /// The assets that have been removed in this batch.
164 final removed = new Set<AssetId>();
165
166 /// Adds the updated [assets] to this batch.
167 void update(Iterable<AssetId> assets) {
168 updated.addAll(assets);
169
170 // If they were previously removed, they are back now.
171 removed.removeAll(assets);
172 }
173
174 /// Removes [assets] from this batch.
175 void remove(Iterable<AssetId> assets) {
176 removed.addAll(assets);
177
178 // If they were previously updated, they are gone now.
179 updated.removeAll(assets);
180 }
181 }
182
183 /// The transforms in a processing graph are organized into a series of phases.
184 /// Each phase can access outputs from previous phases and can in turn pass
185 /// outputs to later phases.
186 ///
187 /// Phases are processed strictly serially. All transforms in a phase will be
188 /// complete before moving on to the next phase. Within a single phase, all
189 /// transforms will be run in parallel.
190 ///
191 /// Building can be interrupted between phases. For example, an source is added
192 /// which starts the background process. Sometime during phase 2 (which is
193 /// running asynchronously) that source is modified. When the process queue
194 /// goes to advance to phase 3, it will see that modification and start the
195 /// waterfall from the beginning again.
196 class _Phase {
197 /// The graph that owns this phase.
198 final AssetGraph graph;
199
200 /// This phase's position relative to the other phases. Zero-based.
201 final int index;
202
203 /// The transformers that can use [assets] as inputs. Their outputs will be
nweiz 2013/06/14 00:57:57 There's no field named "assets". Did you mean "inp
Bob Nystrom 2013/06/17 23:35:05 Done.
204 /// available to the next phase.
205 final List<Transformer> transformers;
206
207 /// The inputs that are available for transforms in this phase to consume.
208 /// For the first phase, these will be the source assets. For all other
209 /// phases, they will be the outputs from the previous phase.
210 final inputs = new Map<AssetId, _AssetNode>();
211
212 /// The transforms currently applicable on assets in [inputs]. These are the
nweiz 2013/06/14 00:57:57 "applicable to"
Bob Nystrom 2013/06/17 23:35:05 Done.
213 /// transforms that have been "wired up": they represent a repeatable
214 /// transformation of a single concrete set of inputs. "dart2js" is a
215 /// transformer. "dart2js on web/main.dart" is a transform.
216 final transforms = new Set<_TransformNode>();
217
218 /// The nodes that are new in the graph since the last time [process] was
219 /// called. When we process, we'll check these to see if we can hang new
220 /// transforms off them.
nweiz 2013/06/14 00:57:57 This isn't all new nodes in the whole AssetGraph,
Bob Nystrom 2013/06/17 23:35:05 Yes, fixed.
221 final newInputs = new Set<_AssetNode>();
222
223 /// The phase after this one. Outputs from this phase will be passed to it.
224 _Phase next;
225
226 _Phase(this.graph, this.index, this.transformers);
227
228 /// Updates the phase's inputs with [updated] and removes [removed]. This
229 /// marks any affected [transforms] as dirty or discards them if their inputs
230 /// are removed.
231 void updateInputs(Map<AssetId, Asset> updated, Set<AssetId> removed) {
nweiz 2013/06/14 00:57:57 It's weird that this takes [updated] as a map. It
Bob Nystrom 2013/06/17 23:35:05 They did at first. It made some things cleaner to
nweiz 2013/06/18 23:14:45 Let's postpone the decision until we have a better
Bob Nystrom 2013/06/20 00:23:59 SGTM.
232 // Remove any nodes that are no longer being output.
233 for (var id in removed) {
234 var node = inputs.remove(id);
235
236 // Every transform that was using it is dirty now.
237 if (node != null) {
238 node.consumers.forEach((consumer) => consumer.isDirty = true);
239 }
240 }
241
242 // Update and new or modified assets.
243 updated.forEach((id, asset) {
244 var node = inputs.putIfAbsent(id, () => new _AssetNode(id));
245
246 // If it's a new node, remember that so we can see if any new transforms
247 // will consume it.
248 if (node.asset == null) newInputs.add(node);
249
250 node.updateAsset(asset);
251 });
252 }
253
254 /// Processes this phase. For all new inputs, it tries to see if there are
255 /// transformers that can consume them. Then all applicable transforms are
256 /// applied.
257 ///
258 /// Returns a future that completes when processing is done. If there is
259 /// nothing to process, returns `null`.
260 Future process() {
261 var future = _processNewInputs();
262 if (future == null) {
263 return _processTransforms();
nweiz 2013/06/14 00:57:57 It'd be cleaner to just do "future = new Future.va
Bob Nystrom 2013/06/17 23:35:05 I hate how awkard this code is, but it's the best
264 }
265
266 return future.then((_) {
267 return _processTransforms();
nweiz 2013/06/14 00:57:57 Style nit: =>
Bob Nystrom 2013/06/17 23:35:05 Done.
268 });
269 }
270
271 /// Creates new transforms for any new inputs that are applicable.
272 Future _processNewInputs() {
273 if (newInputs.isEmpty) return null;
274
275 var futures = [];
276 for (var node in newInputs) {
277 for (var transformer in transformers) {
278 futures.add(transformer.isPrimary(node.id).then((isPrimary) {
279 if (!isPrimary) return;
280 var transform = new _TransformNode(this, transformer, node);
281 node.consumers.add(transform);
282 transforms.add(transform);
283 }));
284 }
285 }
286
287 newInputs.clear();
288
289 return Future.wait(futures);
290 }
291
292 /// Applies all currently wired up and dirty transforms. Passes their outputs
293 /// to the next phase.
294 Future _processTransforms() {
295 var dirtyTransforms = transforms.where((transform) => transform.isDirty);
296 if (dirtyTransforms.isEmpty) return null;
297
298 var updated = new Map<AssetId, Asset>();
299 var removed = new Set<AssetId>();
300
301 return Future.wait(dirtyTransforms.map((node) {
nweiz 2013/06/14 00:57:57 "node" -> "transform", to avoid it being confused
Bob Nystrom 2013/06/17 23:35:05 Done.
302 return node.apply(updated, removed);
303 })).then((_) {
304 // Pass the outputs to the next phase.
305 next.updateInputs(updated, removed);
nweiz 2013/06/14 00:57:57 What about the transform's output? Isn't it also u
Bob Nystrom 2013/06/17 23:35:05 The input/output terminology is a bit confusing he
306 });
307 }
308 }
309
310 /// Represents an asset within the build dependency graph. It tracks its ID,
nweiz 2013/06/14 00:57:57 "Represents an asset" is confusing. The Asset clas
Bob Nystrom 2013/06/17 23:35:05 Rewrote.
311 /// the currently generated actual asset for it, and any transforms that use
nweiz 2013/06/14 00:57:57 You're using "it" to refer to two different things
Bob Nystrom 2013/06/17 23:35:05 Done.
312 /// that asset as an input.
313 class _AssetNode {
314 final AssetId id;
315 Asset asset;
316
317 /// The [_TransformNode]s in this node's phase that consume this asset as an
nweiz 2013/06/14 00:57:57 "this asset" -> "this node's asset".
Bob Nystrom 2013/06/17 23:35:05 Done.
318 /// input.
319 final consumers = new Set<_TransformNode>();
320
321 _AssetNode(this.id);
322
323 /// Updates this nodes's generated asset value and marks all transforms that
nweiz 2013/06/14 00:57:57 "node's"
Bob Nystrom 2013/06/17 23:35:05 Done.
324 /// use this as dirty.
325 void updateAsset(Asset asset) {
326 this.asset = asset;
327 consumers.forEach((consumer) => consumer.isDirty = true);
328 }
329 }
330
331 /// Represents a transform step within the build dependency graph.
332 class _TransformNode {
nweiz 2013/06/14 00:57:57 It's pretty confusing right now what the distincti
Bob Nystrom 2013/06/17 23:35:05 Done.
333 final _Phase phase;
334 final Transformer transformer;
335 final _AssetNode primary;
336 var isDirty = true;
337
338 /// The outputs created by this transform the last time it was run. Used to
339 /// tell if an output was removed in a later run.
340 var outputs = new Set<AssetId>();
nweiz 2013/06/14 00:57:57 What about the non-primary inputs?
Bob Nystrom 2013/06/17 23:35:05 Done. Good catch. Added a test.
341
342 _TransformNode(this.phase, this.transformer, this.primary);
343
344 /// Applies this transform. Outputs will be added to [updated]. Outputs that
345 /// were generated the last time this was applied but were not generated
346 /// this time will be added to [removed].
nweiz 2013/06/14 00:57:57 This signature is very confusing. The docstring im
Bob Nystrom 2013/06/20 00:23:59 Done.
347 Future apply(Map<AssetId, Asset> updated, Set<AssetId> removed) {
348 var transform = new _Transform(this);
349 return transformer.apply(transform).then((_) {
350 isDirty = false;
351
352 // Collect the outputs.
353 transform._outputs.forEach((id, asset) {
354 if (updated.containsKey(id)) {
355 // Report a collision.
356 phase.graph._resultsController.add(new ProcessResult(
357 new AssetCollisionException(id)));
358 // TODO(rnystrom): Define what happens after a collision occurs.
nweiz 2013/06/14 00:57:57 We should probably have some notion of a node and
Bob Nystrom 2013/06/17 23:35:05 My current rough thoughts are that it would be lef
nweiz 2013/06/18 23:14:45 I agree; errors shouldn't bring everything down. M
Bob Nystrom 2013/06/20 00:23:59 Done.
359 } else {
360 updated[id] = asset;
361 }
362 });
363
364 // See which outputs are missing from the last run.
365 var outputIds = transform._outputs.keys.toSet();
366 var removedOutputs = outputs.difference(outputIds);
367 outputs = outputIds;
368 removed.addAll(removedOutputs);
nweiz 2013/06/14 00:57:57 We should be sure we test the case where one trans
Bob Nystrom 2013/06/17 23:35:05 Done. It was doing the right thing, but it wasn't
369 });
370 }
371 }
372
373 /// A concrete implementation of [Transform].
374 class _Transform implements Transform {
nweiz 2013/06/14 00:57:57 I really hate the pattern of an interface with a s
Bob Nystrom 2013/06/17 23:35:05 It isn't that simple. It needs to have both a priv
nweiz 2013/06/18 23:14:45 "lib/barback.dart" should only contain exports and
Bob Nystrom 2013/06/20 00:23:59 Yup, a later patch I'm working on does that.
375 final _TransformNode _node;
376
377 final _inputs = new Set<_AssetNode>();
378 final _outputs = new Map<AssetId, Asset>();
379
380 AssetId get primaryId => _node.primary.id;
381 Future<Asset> get primaryInput => getInput(primaryId);
382
383 _Transform(this._node);
384
385 Future<Asset> getInput(AssetId id) {
386 return new Future(() {
387 var node = _node.phase.inputs[id];
388 // TODO(rnystrom): Need to handle passthrough where an asset from a
389 // previous phase can be found.
390 if (node == null) {
391 _node.phase.graph._throwError(new MissingInputException(id));
392 }
393
394 // Keep track of which assets this transform depends on.
395 _inputs.add(node);
396 node.consumers.add(_node);
397 return node.asset;
398 });
399 }
400
401 void addOutput(AssetId id, Asset output) {
402 _outputs[id] = output;
403 }
404 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698