Chromium Code Reviews| 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 |