Chromium Code Reviews| OLD | NEW |
|---|---|
| (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 } | |
| OLD | NEW |