| 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 'asset.dart'; | 10 import 'asset.dart'; |
| 11 import 'asset_id.dart'; | 11 import 'asset_id.dart'; |
| 12 import 'asset_node.dart'; | 12 import 'asset_node.dart'; |
| 13 import 'asset_set.dart'; | 13 import 'asset_set.dart'; |
| 14 import 'log.dart'; | 14 import 'log.dart'; |
| 15 import 'build_result.dart'; | |
| 16 import 'cancelable_future.dart'; | 15 import 'cancelable_future.dart'; |
| 17 import 'errors.dart'; | 16 import 'errors.dart'; |
| 18 import 'package_graph.dart'; | 17 import 'package_graph.dart'; |
| 19 import 'phase.dart'; | 18 import 'phase.dart'; |
| 20 import 'stream_pool.dart'; | 19 import 'stream_pool.dart'; |
| 21 import 'transformer.dart'; | 20 import 'transformer.dart'; |
| 22 import 'utils.dart'; | |
| 23 | 21 |
| 24 /// The asset cascade for an individual package. | 22 /// The asset cascade for an individual package. |
| 25 /// | 23 /// |
| 26 /// This keeps track of which [Transformer]s are applied to which assets, and | 24 /// This keeps track of which [Transformer]s are applied to which assets, and |
| 27 /// re-runs those transformers when their dependencies change. The transformed | 25 /// re-runs those transformers when their dependencies change. The transformed |
| 28 /// asset nodes are accessible via [getAssetNode]. | 26 /// asset nodes are accessible via [getAssetNode]. |
| 29 /// | 27 /// |
| 30 /// A cascade consists of one or more [Phases], each of which has one or more | 28 /// A cascade consists of one or more [Phases], each of which has one or more |
| 31 /// [Transformer]s that run in parallel, potentially on the same inputs. The | 29 /// [Transformer]s that run in parallel, potentially on the same inputs. The |
| 32 /// inputs of the first phase are the source assets for this cascade's package. | 30 /// inputs of the first phase are the source assets for this cascade's package. |
| (...skipping 13 matching lines...) Expand all Loading... |
| 46 | 44 |
| 47 /// Futures for source assets that are currently being loaded. | 45 /// Futures for source assets that are currently being loaded. |
| 48 /// | 46 /// |
| 49 /// These futures are cancelable so that if an asset is updated after a load | 47 /// These futures are cancelable so that if an asset is updated after a load |
| 50 /// has been kicked off, the previous load can be ignored in favor of a new | 48 /// has been kicked off, the previous load can be ignored in favor of a new |
| 51 /// one. | 49 /// one. |
| 52 final _loadingSources = new Map<AssetId, CancelableFuture<Asset>>(); | 50 final _loadingSources = new Map<AssetId, CancelableFuture<Asset>>(); |
| 53 | 51 |
| 54 final _phases = <Phase>[]; | 52 final _phases = <Phase>[]; |
| 55 | 53 |
| 56 /// A stream that emits a [BuildResult] each time the build is completed, | |
| 57 /// whether or not it succeeded. | |
| 58 /// | |
| 59 /// If an unexpected error in barback itself occurs, it will be emitted | |
| 60 /// through this stream's error channel. | |
| 61 Stream<BuildResult> get results => _resultsController.stream; | |
| 62 final _resultsController = new StreamController<BuildResult>.broadcast(); | |
| 63 | |
| 64 /// A stream that emits any errors from the cascade or the transformers. | 54 /// A stream that emits any errors from the cascade or the transformers. |
| 65 /// | 55 /// |
| 66 /// This emits errors as they're detected. If an error occurs in one part of | 56 /// This emits errors as they're detected. If an error occurs in one part of |
| 67 /// the cascade, unrelated parts will continue building. | 57 /// the cascade, unrelated parts will continue building. |
| 68 /// | |
| 69 /// This will not emit programming errors from barback itself. Those will be | |
| 70 /// emitted through the [results] stream's error channel. | |
| 71 Stream<BarbackException> get errors => _errorsController.stream; | 58 Stream<BarbackException> get errors => _errorsController.stream; |
| 72 final _errorsController = new StreamController<BarbackException>.broadcast(); | 59 final _errorsController = |
| 73 | 60 new StreamController<BarbackException>.broadcast(sync: true); |
| 74 /// A stream that emits an event whenever this cascade becomes dirty. | |
| 75 /// | |
| 76 /// After this stream emits an event, [results] will emit an event once the | |
| 77 /// cascade is no longer dirty. | |
| 78 /// | |
| 79 /// This may emit events when the cascade was already dirty. Events are | |
| 80 /// emitted synchronously to ensure that the dirty state is thoroughly | |
| 81 /// propagated as soon as any assets are changed. | |
| 82 Stream get onDirty => _onDirtyPool.stream; | |
| 83 final _onDirtyPool = new StreamPool.broadcast(); | |
| 84 | |
| 85 /// A controller whose stream feeds into [_onDirtyPool]. | |
| 86 final _onDirtyController = new StreamController.broadcast(sync: true); | |
| 87 | 61 |
| 88 /// A stream that emits an event whenever any transforms in this cascade logs | 62 /// A stream that emits an event whenever any transforms in this cascade logs |
| 89 /// an entry. | 63 /// an entry. |
| 90 Stream<LogEntry> get onLog => _onLogPool.stream; | 64 Stream<LogEntry> get onLog => _onLogPool.stream; |
| 91 final _onLogPool = new StreamPool<LogEntry>.broadcast(); | 65 final _onLogPool = new StreamPool<LogEntry>.broadcast(); |
| 92 | 66 |
| 93 /// The errors that have occurred since the current build started. | 67 /// Whether [this] is dirty and still has more processing to do. |
| 68 bool get isDirty => _phases.any((phase) => phase.isDirty); |
| 69 |
| 70 /// A stream that emits an event whenever [this] is no longer dirty. |
| 94 /// | 71 /// |
| 95 /// This will be empty if no build is occurring. | 72 /// This is synchronous in order to guarantee that it will emit an event as |
| 96 Queue<BarbackException> _accumulatedErrors; | 73 /// soon as [isDirty] flips from `true` to `false`. |
| 97 | 74 Stream get onDone => _onDoneController.stream; |
| 98 /// The number of errors that have been logged since the current build | 75 final _onDoneController = new StreamController.broadcast(sync: true); |
| 99 /// started. | |
| 100 int _numLogErrors; | |
| 101 | |
| 102 /// A future that completes when the currently running build process finishes. | |
| 103 /// | |
| 104 /// If no build it in progress, is `null`. | |
| 105 Future _processDone; | |
| 106 | |
| 107 /// Whether any source assets have been updated or removed since processing | |
| 108 /// last began. | |
| 109 var _newChanges = false; | |
| 110 | 76 |
| 111 /// Returns all currently-available output assets from this cascade. | 77 /// Returns all currently-available output assets from this cascade. |
| 112 AssetSet get availableOutputs => | 78 AssetSet get availableOutputs => |
| 113 new AssetSet.from(_phases.last.availableOutputs.map((node) => node.asset)); | 79 new AssetSet.from(_phases.last.availableOutputs.map((node) => node.asset)); |
| 114 | 80 |
| 81 /// A map of asset ids to completers for [getAssetNode] requests. |
| 82 /// |
| 83 /// If an asset node is requested before it's available, we put a completer in |
| 84 /// this map to wait for the asset to be generated. If it's not generated, the |
| 85 /// completer should complete to `null`. |
| 86 final _pendingAssetRequests = new Map<AssetId, Completer<AssetNode>>(); |
| 87 |
| 115 /// Creates a new [AssetCascade]. | 88 /// Creates a new [AssetCascade]. |
| 116 /// | 89 /// |
| 117 /// It loads source assets within [package] using [provider]. | 90 /// It loads source assets within [package] using [provider]. |
| 118 AssetCascade(this.graph, this.package) { | 91 AssetCascade(this.graph, this.package) { |
| 119 _onDirtyPool.add(_onDirtyController.stream); | 92 _addPhase(new Phase(this, package)); |
| 120 _addPhase(new Phase(this, [], package)); | |
| 121 | |
| 122 // Keep track of logged errors so we can know that the build failed. | |
| 123 onLog.listen((entry) { | |
| 124 if (entry.level == LogLevel.ERROR) { | |
| 125 // TODO(nweiz): keep track of stack chain. | |
| 126 _accumulatedErrors.add( | |
| 127 new TransformerException(entry.transform, entry.message, null)); | |
| 128 } | |
| 129 }); | |
| 130 } | 93 } |
| 131 | 94 |
| 132 /// Gets the asset identified by [id]. | 95 /// Gets the asset identified by [id]. |
| 133 /// | 96 /// |
| 134 /// If [id] is for a generated or transformed asset, this will wait until it | 97 /// If [id] is for a generated or transformed asset, this will wait until it |
| 135 /// has been created and return it. This means that the returned asset will | 98 /// has been created and return it. This means that the returned asset will |
| 136 /// always be [AssetState.AVAILABLE]. | 99 /// always be [AssetState.AVAILABLE]. |
| 137 /// | 100 /// |
| 138 /// If the asset cannot be found, returns null. | 101 /// If the asset cannot be found, returns null. |
| 139 Future<AssetNode> getAssetNode(AssetId id) { | 102 Future<AssetNode> getAssetNode(AssetId id) { |
| 140 assert(id.package == package); | 103 assert(id.package == package); |
| 141 | 104 |
| 142 // TODO(rnystrom): Waiting for the entire build to complete is unnecessary | 105 // TODO(rnystrom): Waiting for the entire build to complete is unnecessary |
| 143 // in some cases. Should optimize: | 106 // in some cases. Should optimize: |
| 144 // * [id] may be generated before the compilation is finished. We should | 107 // * [id] may be generated before the compilation is finished. We should |
| 145 // be able to quickly check whether there are any more in-place | 108 // be able to quickly check whether there are any more in-place |
| 146 // transformations that can be run on it. If not, we can return it early. | 109 // transformations that can be run on it. If not, we can return it early. |
| 147 // * If [id] has never been generated and all active transformers provide | 110 // * If [id] has never been generated and all active transformers provide |
| 148 // metadata about the file names of assets it can emit, we can prove that | 111 // metadata about the file names of assets it can emit, we can prove that |
| 149 // none of them can emit [id] and fail early. | 112 // none of them can emit [id] and fail early. |
| 150 return _phases.last.getOutput(id).then((node) { | 113 return _phases.last.getOutput(id).then((node) { |
| 151 // If the requested asset is available, we can just return it. | 114 if (node != null) { |
| 152 if (node != null && node.state.isAvailable) return node; | 115 // If the requested asset is available, we can just return it. |
| 116 if (node.state.isAvailable) return node; |
| 153 | 117 |
| 154 if (_processDone != null) { | 118 // If the requested asset exists but isn't yet available, wait to see if |
| 155 // If there's a build running, that build might generate the asset, so | 119 // it becomes available. If it's removed before becoming available, try |
| 156 // we wait for it to complete and then try again. | 120 // again, since it could be generated again. |
| 157 return _processDone.then((_) => getAssetNode(id)); | 121 node.force(); |
| 122 return node.whenAvailable((_) => node).catchError((error) { |
| 123 if (error is! AssetNotFoundException) throw error; |
| 124 return getAssetNode(id); |
| 125 }); |
| 158 } | 126 } |
| 159 | 127 |
| 160 // If the asset hasn't been built and nothing is building now, the asset | 128 // If the cascade isn't dirty, the phase won't generate the requested |
| 161 // won't be generated, so we return null. | 129 // asset in the future. |
| 162 return null; | 130 if (!isDirty) return null; |
| 131 |
| 132 // If the cascade is dirty, store a completer for the asset node. If it's |
| 133 // generated in the future, we'll complete this completer. |
| 134 var completer = _pendingAssetRequests.putIfAbsent(id, |
| 135 () => new Completer.sync()); |
| 136 return completer.future; |
| 163 }); | 137 }); |
| 164 } | 138 } |
| 165 | 139 |
| 166 /// Adds [sources] to the graph's known set of source assets. | 140 /// Adds [sources] to the graph's known set of source assets. |
| 167 /// | 141 /// |
| 168 /// Begins applying any transforms that can consume any of the sources. If a | 142 /// Begins applying any transforms that can consume any of the sources. If a |
| 169 /// given source is already known, it is considered modified and all | 143 /// given source is already known, it is considered modified and all |
| 170 /// transforms that use it will be re-applied. | 144 /// transforms that use it will be re-applied. |
| 171 void updateSources(Iterable<AssetId> sources) { | 145 void updateSources(Iterable<AssetId> sources) { |
| 172 for (var id in sources) { | 146 for (var id in sources) { |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 215 /// [Transformer]s or [TransformerGroup]s. | 189 /// [Transformer]s or [TransformerGroup]s. |
| 216 void updateTransformers(Iterable<Iterable> transformersIterable) { | 190 void updateTransformers(Iterable<Iterable> transformersIterable) { |
| 217 var transformers = transformersIterable.toList(); | 191 var transformers = transformersIterable.toList(); |
| 218 | 192 |
| 219 for (var i = 0; i < transformers.length; i++) { | 193 for (var i = 0; i < transformers.length; i++) { |
| 220 if (_phases.length > i) { | 194 if (_phases.length > i) { |
| 221 _phases[i].updateTransformers(transformers[i]); | 195 _phases[i].updateTransformers(transformers[i]); |
| 222 continue; | 196 continue; |
| 223 } | 197 } |
| 224 | 198 |
| 225 _addPhase(_phases.last.addPhase(transformers[i])); | 199 var phase = _phases.last.addPhase(); |
| 200 _addPhase(phase); |
| 201 phase.updateTransformers(transformers[i]); |
| 226 } | 202 } |
| 227 | 203 |
| 228 if (transformers.length == 0) { | 204 if (transformers.length == 0) { |
| 229 _phases.last.updateTransformers([]); | 205 _phases.last.updateTransformers([]); |
| 230 } else if (transformers.length < _phases.length) { | 206 } else if (transformers.length < _phases.length) { |
| 231 _phases[transformers.length - 1].removeFollowing(); | 207 _phases[transformers.length - 1].removeFollowing(); |
| 232 _phases.removeRange(transformers.length, _phases.length); | 208 _phases.removeRange(transformers.length, _phases.length); |
| 233 } | 209 } |
| 234 } | 210 } |
| 235 | 211 |
| 236 /// Force all [LazyTransformer]s' transforms in this cascade to begin | 212 /// Force all [LazyTransformer]s' transforms in this cascade to begin |
| 237 /// producing concrete assets. | 213 /// producing concrete assets. |
| 238 void forceAllTransforms() { | 214 void forceAllTransforms() { |
| 239 for (var phase in _phases) { | 215 for (var phase in _phases) { |
| 240 phase.forceAllTransforms(); | 216 phase.forceAllTransforms(); |
| 241 } | 217 } |
| 242 } | 218 } |
| 243 | 219 |
| 244 void reportError(BarbackException error) { | 220 void reportError(BarbackException error) { |
| 245 _accumulatedErrors.add(error); | |
| 246 _errorsController.add(error); | 221 _errorsController.add(error); |
| 247 } | 222 } |
| 248 | 223 |
| 249 /// Add [phase] to the end of [_phases] and watch its [onDirty] stream. | 224 /// Add [phase] to the end of [_phases] and watch its streams. |
| 250 void _addPhase(Phase phase) { | 225 void _addPhase(Phase phase) { |
| 251 _onDirtyPool.add(phase.onDirty); | |
| 252 _onLogPool.add(phase.onLog); | 226 _onLogPool.add(phase.onLog); |
| 253 phase.onDirty.listen((_) { | 227 phase.onAsset.listen(_providePendingAsset); |
| 254 _newChanges = true; | 228 |
| 255 _waitForProcess(); | 229 phase.onDone.listen((_) { |
| 230 if (isDirty) return; |
| 231 |
| 232 // This cascade has finished building. If anyone's still waiting for |
| 233 // assets, cut off the wait; we won't be generating them, at least until a |
| 234 // source asset changes. |
| 235 for (var completer in _pendingAssetRequests.values) { |
| 236 completer.complete(null); |
| 237 } |
| 238 _pendingAssetRequests.clear(); |
| 239 _onDoneController.add(null); |
| 256 }); | 240 }); |
| 241 |
| 257 _phases.add(phase); | 242 _phases.add(phase); |
| 258 } | 243 } |
| 259 | 244 |
| 260 /// Starts the build process asynchronously if there is work to be done. | 245 /// Provide an asset to a pending [getAssetNode] call. |
| 261 /// | 246 void _providePendingAsset(AssetNode asset) { |
| 262 /// Returns a future that completes with the background processing is done. | 247 // If anyone's waiting for this asset, provide it to them. |
| 263 /// If there is no work to do, returns a future that completes immediately. | 248 var request = _pendingAssetRequests.remove(asset.id); |
| 264 /// All errors that occur during processing will be caught (and routed to the | 249 if (request == null) return; |
| 265 /// [results] stream) before they get to the returned future, so it is safe | |
| 266 /// to discard it. | |
| 267 Future _waitForProcess() { | |
| 268 if (_processDone != null) return _processDone; | |
| 269 | 250 |
| 270 _accumulatedErrors = new Queue(); | 251 if (asset.state.isAvailable) { |
| 271 _numLogErrors = 0; | 252 request.complete(asset); |
| 272 return _processDone = _process().then((_) { | 253 return; |
| 273 // Report the build completion. | 254 } |
| 274 // TODO(rnystrom): Put some useful data in here. | |
| 275 _resultsController.add( | |
| 276 new BuildResult(_accumulatedErrors)); | |
| 277 _processDone = null; | |
| 278 _accumulatedErrors = null; | |
| 279 }); | |
| 280 } | |
| 281 | 255 |
| 282 /// Starts the background processing. | 256 // A lazy asset may be emitted while still dirty. If so, we wait until |
| 283 /// | 257 // it's either available or removed before trying again to access it. We |
| 284 /// Returns a future that completes when all assets have been processed. | 258 // retry the entire [getAsset] process because the state of the graph may |
| 285 Future _process() { | 259 // have changed dramatically by the time it's available. |
| 286 _newChanges = false; | 260 assert(asset.state.isDirty); |
| 287 return newFuture(() { | 261 asset.force(); |
| 288 // Find the first phase that has work to do and do it. | 262 asset.whenStateChanges() |
| 289 var future; | 263 .then((_) => getAssetNode(asset.id)) |
| 290 for (var phase in _phases) { | 264 .then(request.complete) |
| 291 future = phase.process(); | 265 .catchError(request.completeError); |
| 292 if (future != null) break; | |
| 293 } | |
| 294 | |
| 295 // If all phases are done and no new updates have come in, we're done. | |
| 296 if (future == null) { | |
| 297 // If changes have come in, start over. | |
| 298 if (_newChanges) return _process(); | |
| 299 | |
| 300 // Otherwise, everything is done. | |
| 301 return null; | |
| 302 } | |
| 303 | |
| 304 // Process that phase and then loop onto the next. | |
| 305 return future.then((_) => _process()); | |
| 306 }); | |
| 307 } | 266 } |
| 308 | 267 |
| 309 String toString() => "cascade for $package"; | 268 String toString() => "cascade for $package"; |
| 310 } | 269 } |
| OLD | NEW |