OLD | NEW |
---|---|
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library barback.asset_cascade; | 5 library barback.asset_cascade; |
6 | 6 |
7 import 'dart:async'; | 7 import 'dart:async'; |
8 import 'dart:collection'; | 8 import 'dart:collection'; |
9 | 9 |
10 import 'package:stack_trace/stack_trace.dart'; | 10 import 'package:stack_trace/stack_trace.dart'; |
11 | 11 |
12 import 'asset.dart'; | 12 import 'asset.dart'; |
13 import 'asset_id.dart'; | 13 import 'asset_id.dart'; |
14 import 'asset_set.dart'; | 14 import 'asset_node.dart'; |
15 import 'cancelable_future.dart'; | |
15 import 'errors.dart'; | 16 import 'errors.dart'; |
16 import 'change_batch.dart'; | 17 import 'change_batch.dart'; |
17 import 'package_graph.dart'; | 18 import 'package_graph.dart'; |
18 import 'phase.dart'; | 19 import 'phase.dart'; |
19 import 'transformer.dart'; | 20 import 'transformer.dart'; |
20 import 'utils.dart'; | 21 import 'utils.dart'; |
21 | 22 |
22 /// The asset cascade for an individual package. | 23 /// The asset cascade for an individual package. |
23 /// | 24 /// |
24 /// This keeps track of which [Transformer]s are applied to which assets, and | 25 /// This keeps track of which [Transformer]s are applied to which assets, and |
25 /// re-runs those transformers when their dependencies change. The transformed | 26 /// re-runs those transformers when their dependencies change. The transformed |
26 /// assets are accessible via [getAssetById]. | 27 /// assets are accessible via [getAssetById]. |
27 /// | 28 /// |
28 /// A cascade consists of one or more [Phases], each of which has one or more | 29 /// A cascade consists of one or more [Phases], each of which has one or more |
29 /// [Transformer]s that run in parallel, potentially on the same inputs. The | 30 /// [Transformer]s that run in parallel, potentially on the same inputs. The |
30 /// inputs of the first phase are the source assets for this cascade's package. | 31 /// inputs of the first phase are the source assets for this cascade's package. |
31 /// The inputs of each successive phase are the outputs of the previous phase, | 32 /// The inputs of each successive phase are the outputs of the previous phase, |
32 /// as well as any assets that haven't yet been transformed. | 33 /// as well as any assets that haven't yet been transformed. |
33 class AssetCascade { | 34 class AssetCascade { |
34 /// The name of the package whose assets are managed. | 35 /// The name of the package whose assets are managed. |
35 final String package; | 36 final String package; |
36 | 37 |
37 /// The [PackageGraph] that tracks all [AssetCascade]s for all dependencies of | 38 /// The [PackageGraph] that tracks all [AssetCascade]s for all dependencies of |
38 /// the current app. | 39 /// the current app. |
39 final PackageGraph _graph; | 40 final PackageGraph _graph; |
40 | 41 |
42 /// The controllers for the [AssetNode]s that provide information about this | |
43 /// cascade's package's source assets, indexed by id. | |
Bob Nystrom
2013/07/31 20:05:41
Remove "indexed by id".
nweiz
2013/07/31 22:47:53
Done.
| |
44 final _sourceControllerMap = new Map<AssetId, AssetNodeController>(); | |
45 | |
46 /// Futures for source assets that are currently being loaded, indexed by id. | |
47 /// | |
48 /// These futures are cancelable so that if an asset is updated after a load | |
Bob Nystrom
2013/07/31 20:05:41
This implies that a PackageProvider's implementati
nweiz
2013/07/31 22:47:53
Done.
| |
49 /// has been kicked off, the previous load can be ignored in favor of a new | |
50 /// one. | |
51 final _loadingSources = new Map<AssetId, CancelableFuture<Asset>>(); | |
52 | |
41 final _phases = <Phase>[]; | 53 final _phases = <Phase>[]; |
42 | 54 |
43 /// A stream that emits a [BuildResult] each time the build is completed, | 55 /// A stream that emits a [BuildResult] each time the build is completed, |
44 /// whether or not it succeeded. | 56 /// whether or not it succeeded. |
45 /// | 57 /// |
46 /// If an unexpected error in barback itself occurs, it will be emitted | 58 /// If an unexpected error in barback itself occurs, it will be emitted |
47 /// through this stream's error channel. | 59 /// through this stream's error channel. |
48 Stream<BuildResult> get results => _resultsController.stream; | 60 Stream<BuildResult> get results => _resultsController.stream; |
49 final _resultsController = new StreamController<BuildResult>.broadcast(); | 61 final _resultsController = new StreamController<BuildResult>.broadcast(); |
50 | 62 |
(...skipping 10 matching lines...) Expand all Loading... | |
61 /// The errors that have occurred since the current build started. | 73 /// The errors that have occurred since the current build started. |
62 /// | 74 /// |
63 /// This will be empty if no build is occurring. | 75 /// This will be empty if no build is occurring. |
64 Queue _accumulatedErrors; | 76 Queue _accumulatedErrors; |
65 | 77 |
66 /// A future that completes when the currently running build process finishes. | 78 /// A future that completes when the currently running build process finishes. |
67 /// | 79 /// |
68 /// If no build it in progress, is `null`. | 80 /// If no build it in progress, is `null`. |
69 Future _processDone; | 81 Future _processDone; |
70 | 82 |
71 ChangeBatch _sourceChanges; | 83 /// Whether any source assets have been updated or removed since processing |
84 /// last began. | |
85 var _newChanges = false; | |
72 | 86 |
73 /// Creates a new [AssetCascade]. | 87 /// Creates a new [AssetCascade]. |
74 /// | 88 /// |
75 /// It loads source assets within [package] using [provider] and then uses | 89 /// It loads source assets within [package] using [provider] and then uses |
76 /// [transformerPhases] to generate output files from them. | 90 /// [transformerPhases] to generate output files from them. |
77 //TODO(rnystrom): Better way of specifying transformers and their ordering. | 91 //TODO(rnystrom): Better way of specifying transformers and their ordering. |
78 AssetCascade(this._graph, this.package, | 92 AssetCascade(this._graph, this.package, |
79 Iterable<Iterable<Transformer>> transformerPhases) { | 93 Iterable<Iterable<Transformer>> transformerPhases) { |
80 // Flatten the phases to a list so we can traverse backwards to wire up | 94 // Flatten the phases to a list so we can traverse backwards to wire up |
81 // each phase to its next. | 95 // each phase to its next. |
(...skipping 17 matching lines...) Expand all Loading... | |
99 /// it has been created and return it. If the asset cannot be found, throws | 113 /// it has been created and return it. If the asset cannot be found, throws |
100 /// [AssetNotFoundException]. | 114 /// [AssetNotFoundException]. |
101 Future<Asset> getAssetById(AssetId id) { | 115 Future<Asset> getAssetById(AssetId id) { |
102 assert(id.package == package); | 116 assert(id.package == package); |
103 | 117 |
104 // TODO(rnystrom): Waiting for the entire build to complete is unnecessary | 118 // TODO(rnystrom): Waiting for the entire build to complete is unnecessary |
105 // in some cases. Should optimize: | 119 // in some cases. Should optimize: |
106 // * [id] may be generated before the compilation is finished. We should | 120 // * [id] may be generated before the compilation is finished. We should |
107 // be able to quickly check whether there are any more in-place | 121 // be able to quickly check whether there are any more in-place |
108 // transformations that can be run on it. If not, we can return it early. | 122 // transformations that can be run on it. If not, we can return it early. |
109 // * If everything is compiled, something that didn't output [id] is | |
110 // dirtied, and then [id] is requested, we can return it immediately, | |
111 // since anything overwriting it at that point is an error. | |
112 // * If [id] has never been generated and all active transformers provide | 123 // * If [id] has never been generated and all active transformers provide |
113 // metadata about the file names of assets it can emit, we can prove that | 124 // metadata about the file names of assets it can emit, we can prove that |
114 // none of them can emit [id] and fail early. | 125 // none of them can emit [id] and fail early. |
115 return (_processDone == null ? new Future.value() : _processDone).then((_) { | 126 return newFuture(() { |
116 // Each phase's inputs are the outputs of the previous phase. Find the | 127 var node = _getAssetNode(id); |
117 // last phase that contains the asset. Since the last phase has no | |
118 // transformers, this will find the latest output for that id. | |
119 | 128 |
120 // TODO(rnystrom): Currently does not omit assets that are actually used | 129 // If the requested asset is available, we can just return it. |
121 // as inputs for transformers. This means you can request and get an | 130 if (node != null) return node.asset; |
122 // asset that should be "consumed" because it's used to generate the | 131 |
123 // real asset you care about. Need to figure out how we want to handle | 132 // If there's a build running, that build might generate the asset, so we |
124 // that and what use cases there are related to it. | 133 // wait for it to complete and then try again. |
125 for (var i = _phases.length - 1; i >= 0; i--) { | 134 if (_processDone != null) { |
126 var node = _phases[i].inputs[id]; | 135 return _processDone.then((_) => getAssetById(id)); |
127 if (node != null) { | |
128 // By the time we get here, the asset should have been built. | |
129 assert(node.asset != null); | |
130 return node.asset; | |
131 } | |
132 } | 136 } |
133 | 137 |
134 // Couldn't find it. | 138 // If the asset hasn't been built and nothing is building now, the asset |
139 // won't be generated, so we throw an error. | |
135 throw new AssetNotFoundException(id); | 140 throw new AssetNotFoundException(id); |
136 }); | 141 }); |
137 } | 142 } |
138 | 143 |
144 // Returns the post-transformation asset node for [id], if one is available. | |
145 // | |
146 // This will only return a node that has an asset available, and only if that | |
147 // node is guaranteed not to be consumed by any transforms. If the phase is | |
148 // still working to figure out if a node will be consumed by a transformer, | |
149 // that node won't be returned. | |
150 AssetNode _getAssetNode(AssetId id) { | |
151 // Each phase's inputs are the outputs of the previous phase. Find the last | |
152 // phase that contains the asset. Since the last phase has no transformers, | |
153 // this will find the latest output for that id. | |
154 for (var i = _phases.length - 1; i >= 0; i--) { | |
155 var node = _phases[i].getUnconsumedInput(id); | |
156 if (node != null) return node; | |
157 } | |
158 | |
159 return null; | |
160 } | |
161 | |
139 /// Adds [sources] to the graph's known set of source assets. | 162 /// Adds [sources] to the graph's known set of source assets. |
140 /// | 163 /// |
141 /// Begins applying any transforms that can consume any of the sources. If a | 164 /// Begins applying any transforms that can consume any of the sources. If a |
142 /// given source is already known, it is considered modified and all | 165 /// given source is already known, it is considered modified and all |
143 /// transforms that use it will be re-applied. | 166 /// transforms that use it will be re-applied. |
144 void updateSources(Iterable<AssetId> sources) { | 167 void updateSources(Iterable<AssetId> sources) { |
145 if (_sourceChanges == null) _sourceChanges = new ChangeBatch(); | 168 _newChanges = true; |
146 assert(sources.every((id) => id.package == package)); | 169 |
147 _sourceChanges.update(sources); | 170 sources.forEach((id) { |
Bob Nystrom
2013/07/31 20:05:41
Nit, but since this is imperative code, I think a
nweiz
2013/07/31 22:47:53
Done. Not sure why I wrote [forEach]. Too much Rub
| |
171 var controller = _sourceControllerMap[id]; | |
172 if (controller != null) { | |
173 controller.setDirty(); | |
174 } else { | |
175 _sourceControllerMap[id] = new AssetNodeController(id); | |
176 _phases.first.addInput(_sourceControllerMap[id].node); | |
177 } | |
178 | |
179 // If this source was already loading, cancel the old load, since it may | |
180 // return out-of-date contents for the asset. | |
181 if (_loadingSources.containsKey(id)) _loadingSources[id].cancel(); | |
182 | |
183 _loadingSources[id] = | |
184 new CancelableFuture<Asset>(_graph.provider.getAsset(id)); | |
185 _loadingSources[id].whenComplete(() { | |
186 _loadingSources.remove(id); | |
187 }).then((asset) { | |
188 var controller = _sourceControllerMap[id].setAvailable(asset); | |
189 }).catchError((error) { | |
190 reportError(error); | |
191 | |
192 // TODO(nweiz): propagate error information through asset nodes. | |
193 _sourceControllerMap.remove(id).setRemoved(); | |
194 }); | |
195 }); | |
148 | 196 |
149 _waitForProcess(); | 197 _waitForProcess(); |
150 } | 198 } |
151 | 199 |
152 /// Removes [removed] from the graph's known set of source assets. | 200 /// Removes [removed] from the graph's known set of source assets. |
153 void removeSources(Iterable<AssetId> removed) { | 201 void removeSources(Iterable<AssetId> removed) { |
154 if (_sourceChanges == null) _sourceChanges = new ChangeBatch(); | 202 _newChanges = true; |
155 assert(removed.every((id) => id.package == package)); | 203 |
156 _sourceChanges.remove(removed); | 204 removed.forEach((id) { |
205 // If the source was being loaded, cancel that load. | |
206 if (_loadingSources.containsKey(id)) _loadingSources.remove(id).cancel(); | |
207 | |
208 var controller = _sourceControllerMap.remove(id); | |
209 // Don't choke if an id is double-removed for some reason. | |
210 if (controller != null) controller.setRemoved(); | |
211 }); | |
157 | 212 |
158 _waitForProcess(); | 213 _waitForProcess(); |
159 } | 214 } |
160 | 215 |
161 void reportError(error) { | 216 void reportError(error) { |
162 _accumulatedErrors.add(error); | 217 _accumulatedErrors.add(error); |
163 _errorsController.add(error); | 218 _errorsController.add(error); |
164 } | 219 } |
165 | 220 |
166 /// Starts the build process asynchronously if there is work to be done. | 221 /// Starts the build process asynchronously if there is work to be done. |
(...skipping 24 matching lines...) Expand all Loading... | |
191 }).whenComplete(() { | 246 }).whenComplete(() { |
192 _processDone = null; | 247 _processDone = null; |
193 _accumulatedErrors = null; | 248 _accumulatedErrors = null; |
194 }); | 249 }); |
195 } | 250 } |
196 | 251 |
197 /// Starts the background processing. | 252 /// Starts the background processing. |
198 /// | 253 /// |
199 /// Returns a future that completes when all assets have been processed. | 254 /// Returns a future that completes when all assets have been processed. |
200 Future _process() { | 255 Future _process() { |
201 return _processSourceChanges().then((_) { | 256 _newChanges = false; |
257 return newFuture(() { | |
202 // Find the first phase that has work to do and do it. | 258 // Find the first phase that has work to do and do it. |
203 var future; | 259 var future; |
204 for (var phase in _phases) { | 260 for (var phase in _phases) { |
205 future = phase.process(); | 261 future = phase.process(); |
206 if (future != null) break; | 262 if (future != null) break; |
207 } | 263 } |
208 | 264 |
209 // If all phases are done and no new updates have come in, we're done. | 265 // If all phases are done and no new updates have come in, we're done. |
210 if (future == null) { | 266 if (future == null) { |
211 // If changes have come in, start over. | 267 // If changes have come in, start over. |
212 if (_sourceChanges != null) return _process(); | 268 if (_newChanges) return _process(); |
213 | 269 |
214 // Otherwise, everything is done. | 270 // Otherwise, everything is done. |
215 return; | 271 return; |
216 } | 272 } |
217 | 273 |
218 // Process that phase and then loop onto the next. | 274 // Process that phase and then loop onto the next. |
219 return future.then((_) => _process()); | 275 return future.then((_) => _process()); |
220 }); | 276 }); |
221 } | 277 } |
222 | |
223 /// Processes the current batch of changes to source assets. | |
224 Future _processSourceChanges() { | |
225 // Always pump the event loop. This ensures a bunch of synchronous source | |
226 // changes are processed in a single batch even when the first one starts | |
227 // the build process. | |
228 return newFuture(() { | |
229 if (_sourceChanges == null) return null; | |
230 | |
231 // Take the current batch to ensure it doesn't get added to while we're | |
232 // processing it. | |
233 var changes = _sourceChanges; | |
234 _sourceChanges = null; | |
235 | |
236 var updated = new AssetSet(); | |
237 var futures = []; | |
238 for (var id in changes.updated) { | |
239 // TODO(rnystrom): Catch all errors from provider and route to results. | |
240 futures.add(_graph.provider.getAsset(id).then((asset) { | |
241 updated.add(asset); | |
242 }).catchError((error) { | |
243 if (error is AssetNotFoundException) { | |
244 // Handle missing asset errors like regular missing assets. | |
245 reportError(error); | |
246 } else { | |
247 // It's an unexpected error, so rethrow it. | |
248 throw error; | |
249 } | |
250 })); | |
251 } | |
252 | |
253 return Future.wait(futures).then((_) { | |
254 _phases.first.updateInputs(updated, changes.removed); | |
255 }); | |
256 }); | |
257 } | |
258 } | 278 } |
259 | 279 |
260 /// An event indicating that the cascade has finished building all assets. | 280 /// An event indicating that the cascade has finished building all assets. |
261 /// | 281 /// |
262 /// A build can end either in success or failure. If there were no errors during | 282 /// A build can end either in success or failure. If there were no errors during |
263 /// the build, it's considered to be a success; any errors render it a failure, | 283 /// the build, it's considered to be a success; any errors render it a failure, |
264 /// although individual assets may still have built successfully. | 284 /// although individual assets may still have built successfully. |
265 class BuildResult { | 285 class BuildResult { |
266 /// All errors that occurred during the build. | 286 /// All errors that occurred during the build. |
267 final List errors; | 287 final List errors; |
(...skipping 21 matching lines...) Expand all Loading... | |
289 msg.write(prefixLines(error.toString())); | 309 msg.write(prefixLines(error.toString())); |
290 if (stackTrace != null) { | 310 if (stackTrace != null) { |
291 msg.write("\n\n"); | 311 msg.write("\n\n"); |
292 msg.write("Stack trace:\n"); | 312 msg.write("Stack trace:\n"); |
293 msg.write(prefixLines(stackTrace.toString())); | 313 msg.write(prefixLines(stackTrace.toString())); |
294 } | 314 } |
295 return msg.toString(); | 315 return msg.toString(); |
296 }).join("\n\n"); | 316 }).join("\n\n"); |
297 } | 317 } |
298 } | 318 } |
OLD | NEW |