Chromium Code Reviews| Index: pkg/barback/lib/src/phase.dart |
| diff --git a/pkg/barback/lib/src/phase.dart b/pkg/barback/lib/src/phase.dart |
| index 263741de931d6c3c3d0997b3b39b8a1d43c0a9e0..710317294cec2ca9da326b718dc941d95d8989c4 100644 |
| --- a/pkg/barback/lib/src/phase.dart |
| +++ b/pkg/barback/lib/src/phase.dart |
| @@ -7,13 +7,12 @@ library barback.phase; |
| import 'dart:async'; |
| import 'dart:collection'; |
| -import 'asset.dart'; |
| import 'asset_cascade.dart'; |
| import 'asset_id.dart'; |
| import 'asset_node.dart'; |
| import 'errors.dart'; |
| +import 'phase_input.dart'; |
| import 'stream_pool.dart'; |
| -import 'transform_node.dart'; |
| import 'transformer.dart'; |
| import 'utils.dart'; |
| @@ -38,39 +37,11 @@ class Phase { |
| /// Their outputs will be available to the next phase. |
| final Set<Transformer> _transformers; |
| - /// The inputs that are available for transforms in this phase to consume. |
| + /// The inputs for this phase. |
| /// |
| /// For the first phase, these will be the source assets. For all other |
| /// phases, they will be the outputs from the previous phase. |
| - final _inputs = new Map<AssetId, AssetNode>(); |
| - |
| - /// The transforms currently applicable to assets in [inputs], indexed by |
| - /// the ids of their primary inputs. |
| - /// |
| - /// These are the transforms that have been "wired up": they represent a |
| - /// repeatable transformation of a single concrete set of inputs. "dart2js" |
| - /// is a transformer. "dart2js on web/main.dart" is a transform. |
| - final _transforms = new Map<AssetId, Set<TransformNode>>(); |
| - |
| - /// Controllers for assets that aren't consumed by transforms in this phase. |
| - /// |
| - /// These assets are passed to the next phase unmodified. They need |
| - /// intervening controllers to ensure that the outputs can be marked dirty |
| - /// when determining whether transforms apply, and removed if they do. |
| - final _passThroughControllers = new Map<AssetId, AssetNodeController>(); |
| - |
| - /// Futures that will complete once the transformers that can consume a given |
| - /// asset are determined. |
| - /// |
| - /// Whenever an asset is added or modified, we need to asynchronously |
| - /// determine which transformers can use it as their primary input. We can't |
| - /// start processing until we know which transformers to run, and this allows |
| - /// us to wait until we do. |
| - var _adjustTransformersFutures = new Map<AssetId, Future>(); |
| - |
| - /// New asset nodes that were added while [_adjustTransformers] was still |
| - /// being run on an old version of that asset. |
| - var _pendingNewInputs = new Map<AssetId, AssetNode>(); |
| + final _inputs = new Map<AssetId, PhaseInput>(); |
| /// A map of output ids to the asset node outputs for those ids and the |
| /// transforms that produced those asset nodes. |
| @@ -93,10 +64,7 @@ class Phase { |
| /// A controller whose stream feeds into [_onDirtyPool]. |
| /// |
| - /// This is used whenever an input is added, changed, or removed. It's |
| - /// sometimes redundant with the events collected from [_transforms], but this |
| - /// stream is necessary for new and removed inputs, and the transform stream |
| - /// is necessary for modified secondary inputs. |
| + /// This is used whenever an input is added or transforms are changed. |
| final _onDirtyController = new StreamController.broadcast(sync: true); |
| /// The phase after this one. |
| @@ -129,51 +97,12 @@ class Phase { |
| /// removed and re-created. The phase will automatically handle updated assets |
| /// using the [AssetNode.onStateChange] stream. |
| void addInput(AssetNode node) { |
| - // We remove [node.id] from [inputs] as soon as the node is removed rather |
| - // than at the same time [node.id] is removed from [_transforms] so we don't |
| - // have to wait on [_adjustTransformers]. It's important that [inputs] is |
| - // always up-to-date so that the [AssetCascade] can look there for available |
| - // assets. |
| - _inputs[node.id] = node; |
| - node.whenRemoved.then((_) => _inputs.remove(node.id)); |
| + if (_inputs.containsKey(node.id)) _inputs[node.id].remove(); |
| - if (!_adjustTransformersFutures.containsKey(node.id)) { |
| - _transforms[node.id] = new Set<TransformNode>(); |
| - _adjustTransformers(node); |
| - return; |
| - } |
| - |
| - // If an input is added while the same input is still being processed, |
| - // that means that the asset was removed and recreated while |
| - // [_adjustTransformers] was being run on the old value. We have to wait |
| - // until that finishes, then run it again on whatever the newest version |
| - // of that asset is. |
| - |
| - // We may already be waiting for the existing [_adjustTransformers] call to |
| - // finish. If so, all we need to do is change the node that will be loaded |
| - // after it completes. |
| - var containedKey = _pendingNewInputs.containsKey(node.id); |
| - _pendingNewInputs[node.id] = node; |
| - if (containedKey) return; |
| - |
| - // If we aren't already waiting, start doing so. |
| - _adjustTransformersFutures[node.id].then((_) { |
| - assert(!_adjustTransformersFutures.containsKey(node.id)); |
| - assert(_pendingNewInputs.containsKey(node.id)); |
| - _transforms[node.id] = new Set<TransformNode>(); |
| - _adjustTransformers(_pendingNewInputs.remove(node.id)); |
| - }, onError: (_) { |
| - // If there was a programmatic error while processing the old input, |
| - // we don't want to just ignore it; it may have left the system in an |
| - // inconsistent state. We also don't want to top-level it, so we |
| - // ignore it here but don't start processing the new input. That way |
| - // when [process] is called, the error will be piped through its |
| - // return value. |
| - }).catchError((e) { |
| - // If our code above has a programmatic error, ensure it will be piped |
| - // through [process] by putting it into [_adjustTransformersFutures]. |
| - _adjustTransformersFutures[node.id] = new Future.error(e); |
| - }); |
| + _inputs[node.id] = new PhaseInput(this, node, _transformers); |
| + _inputs[node.id].input.whenRemoved.then((_) => _inputs.remove(node.id)); |
| + _onDirtyPool.add(_inputs[node.id].onDirty); |
|
Bob Nystrom
2013/08/21 19:47:38
Nit, but how about putting the new PhaseInput in a
nweiz
2013/08/21 20:33:51
Done.
|
| + _onDirtyController.add(null); |
| } |
| /// Gets the asset node for an input [id]. |
| @@ -181,8 +110,9 @@ class Phase { |
| /// If an input with that ID cannot be found, returns null. |
| Future<AssetNode> getInput(AssetId id) { |
| return newFuture(() { |
| - if (id.package == cascade.package) return _inputs[id]; |
| - return cascade.graph.getAssetNode(id); |
| + if (id.package != cascade.package) return cascade.graph.getAssetNode(id); |
| + if (_inputs.containsKey(id)) return _inputs[id].input; |
| + return null; |
| }); |
| } |
| @@ -200,43 +130,11 @@ class Phase { |
| /// Set this phase's transformers to [transformers]. |
| void updateTransformers(Iterable<Transformer> transformers) { |
| _onDirtyController.add(null); |
| - |
| - var newTransformers = transformers.toSet(); |
| - var oldTransformers = _transformers.toSet(); |
| - for (var removedTransformer in |
| - oldTransformers.difference(newTransformers)) { |
| - _transformers.remove(removedTransformer); |
| - |
| - // Remove old transforms for which [removedTransformer] was a transformer. |
| - for (var id in _inputs.keys) { |
| - // If the transformers are being adjusted for [id], it will |
| - // automatically pick up on [removedTransformer] being gone. |
| - if (_adjustTransformersFutures.containsKey(id)) continue; |
| - |
| - _transforms[id].removeWhere((transform) { |
| - if (transform.transformer != removedTransformer) return false; |
| - transform.remove(); |
| - return true; |
| - }); |
| - |
| - if (!_transforms[id].isEmpty) continue; |
| - _passThroughControllers.putIfAbsent(id, () { |
| - return new AssetNodeController.available( |
| - _inputs[id].asset, _inputs[id].transform); |
| - }); |
| - } |
| + _transformers.clear(); |
| + _transformers.addAll(transformers); |
| + for (var input in _inputs.values) { |
| + input.updateTransformers(transformers); |
| } |
| - |
| - var brandNewTransformers = newTransformers.difference(oldTransformers); |
| - if (brandNewTransformers.isEmpty) return; |
| - brandNewTransformers.forEach(_transformers.add); |
| - |
| - // If there are any new transformers, start re-adjusting the transforms for |
| - // all inputs so we pick up which inputs the new transformers apply to. |
| - _inputs.forEach((id, node) { |
| - if (_adjustTransformersFutures.containsKey(id)) return; |
| - _adjustTransformers(node); |
| - }); |
| } |
| /// Add a new phase after this one with [transformers]. |
| @@ -251,199 +149,21 @@ class Phase { |
| return _next; |
| } |
| - /// Asynchronously determines which transformers can consume [node] as a |
| - /// primary input and creates transforms for them. |
| - /// |
| - /// This ensures that if [node] is modified or removed during or after the |
| - /// time it takes to adjust its transformers, they're appropriately |
| - /// re-adjusted. Its progress can be tracked in [_adjustTransformersFutures]. |
| - void _adjustTransformers(AssetNode node) { |
| - // Mark the phase as dirty. This may not actually end up creating any new |
| - // transforms, but we want adding or removing a source asset to consistently |
| - // kick off a build, even if that build does nothing. |
| - _onDirtyController.add(null); |
| - |
| - // If there's a pass-through for this node, mark it dirty while we figure |
| - // out whether we need to add any transforms for it. |
| - var controller = _passThroughControllers[node.id]; |
| - if (controller != null) controller.setDirty(); |
| - |
| - // Once the input is available, hook up transformers for it. If it changes |
| - // while that's happening, try again. |
| - _adjustTransformersFutures[node.id] = _tryUntilStable(node, |
| - (asset, transformers) { |
| - var oldTransformers = _transforms[node.id] |
| - .map((transform) => transform.transformer).toSet(); |
| - |
| - return _removeStaleTransforms(asset, transformers).then((_) => |
| - _addFreshTransforms(node, transformers, oldTransformers)); |
| - }).then((_) { |
| - _adjustPassThrough(node); |
| - |
| - // Now all the transforms are set up correctly and the asset is available |
| - // for the time being. Set up handlers for when the asset changes in the |
| - // future. |
| - node.onStateChange.first.then((state) { |
| - if (state.isRemoved) { |
| - _onDirtyController.add(null); |
| - _transforms.remove(node.id); |
| - var passThrough = _passThroughControllers.remove(node.id); |
| - if (passThrough != null) passThrough.setRemoved(); |
| - } else { |
| - _adjustTransformers(node); |
| - } |
| - }).catchError((e) { |
| - _adjustTransformersFutures[node.id] = new Future.error(e); |
| - }); |
| - }).catchError((error) { |
| - if (error is! AssetNotFoundException || error.id != node.id) throw error; |
| - |
| - // If the asset is removed, [tryUntilStable] will throw an |
| - // [AssetNotFoundException]. In that case, just remove all transforms for |
| - // the node, and its pass-through. |
| - _transforms.remove(node.id); |
| - var passThrough = _passThroughControllers.remove(node.id); |
| - if (passThrough != null) passThrough.setRemoved(); |
| - }).whenComplete(() { |
| - _adjustTransformersFutures.remove(node.id); |
| - }); |
| - |
| - // Don't top-level errors coming from the input processing. Any errors will |
| - // eventually be piped through [process]'s returned Future. |
| - _adjustTransformersFutures[node.id].catchError((_) {}); |
| - } |
| - |
| - // Remove any old transforms that used to have [asset] as a primary asset but |
| - // no longer apply to its new contents. |
| - Future _removeStaleTransforms(Asset asset, Set<Transformer> transformers) { |
| - return Future.wait(_transforms[asset.id].map((transform) { |
| - return newFuture(() { |
| - if (!transformers.contains(transform.transformer)) return false; |
| - |
| - // TODO(rnystrom): Catch all errors from isPrimary() and redirect to |
| - // results. |
| - return transform.transformer.isPrimary(asset); |
| - }).then((isPrimary) { |
| - if (isPrimary) return; |
| - _transforms[asset.id].remove(transform); |
| - _onDirtyPool.remove(transform.onDirty); |
| - transform.remove(); |
| - }); |
| - })); |
| - } |
| - |
| - // Add new transforms for transformers that consider [node]'s asset to be a |
| - // primary input. |
| - // |
| - // [oldTransformers] is the set of transformers for which there were |
| - // transforms that had [node] as a primary input prior to this. They don't |
| - // need to be checked, since their transforms were removed or preserved in |
| - // [_removeStaleTransforms]. |
| - Future _addFreshTransforms(AssetNode node, Set<Transformer> transformers, |
| - Set<Transformer> oldTransformers) { |
| - return Future.wait(transformers.map((transformer) { |
| - if (oldTransformers.contains(transformer)) return new Future.value(); |
| - |
| - // If the asset is unavailable, the results of this [_adjustTransformers] |
| - // run will be discarded, so we can just short-circuit. |
| - if (node.asset == null) return new Future.value(); |
| - |
| - // We can safely access [node.asset] here even though it might have |
| - // changed since (as above) if it has, [_adjustTransformers] will just be |
| - // re-run. |
| - // TODO(rnystrom): Catch all errors from isPrimary() and redirect to |
| - // results. |
| - return transformer.isPrimary(node.asset).then((isPrimary) { |
| - if (!isPrimary) return; |
| - var transform = new TransformNode(this, transformer, node); |
| - _transforms[node.id].add(transform); |
| - _onDirtyPool.add(transform.onDirty); |
| - }); |
| - })); |
| - } |
| - |
| - /// Adjust whether [node] is passed through the phase unmodified, based on |
| - /// whether it's consumed by other transforms in this phase. |
| - /// |
| - /// If [node] was already passed-through, this will update the passed-through |
| - /// value. |
| - void _adjustPassThrough(AssetNode node) { |
| - assert(node.state.isAvailable); |
| - |
| - if (_transforms[node.id].isEmpty) { |
| - var controller = _passThroughControllers[node.id]; |
| - if (controller != null) { |
| - controller.setAvailable(node.asset); |
| - } else { |
| - _passThroughControllers[node.id] = |
| - new AssetNodeController.available(node.asset, node.transform); |
| - } |
| - } else { |
| - var controller = _passThroughControllers.remove(node.id); |
| - if (controller != null) controller.setRemoved(); |
| - } |
| - } |
| - |
| - /// Like [AssetNode.tryUntilStable], but also re-runs [callback] if this |
| - /// phase's transformers are modified. |
| - Future _tryUntilStable(AssetNode node, |
| - Future callback(Asset asset, Set<Transformer> transformers)) { |
| - var oldTransformers; |
| - return node.tryUntilStable((asset) { |
| - oldTransformers = _transformers.toSet(); |
| - return callback(asset, _transformers); |
| - }).then((result) { |
| - if (setEquals(oldTransformers, _transformers)) return result; |
| - return _tryUntilStable(node, callback); |
| - }); |
| - } |
| - |
| /// Processes this phase. |
| /// |
| /// Returns a future that completes when processing is done. If there is |
| /// nothing to process, returns `null`. |
| Future process() { |
| - if (_adjustTransformersFutures.isEmpty) return _processTransforms(); |
| - return _waitForInputs().then((_) => _processTransforms()); |
| - } |
| - |
| - Future _waitForInputs() { |
| - if (_adjustTransformersFutures.isEmpty) return new Future.value(); |
| - return Future.wait(_adjustTransformersFutures.values) |
| - .then((_) => _waitForInputs()); |
| - } |
| - |
| - /// Applies all currently wired up and dirty transforms. |
| - Future _processTransforms() { |
| - var newPassThroughs = _passThroughControllers.values |
| - .map((controller) => controller.node) |
| - .where((output) { |
| - return !_outputs.containsKey(output.id) || |
| - !_outputs[output.id].contains(output); |
| - }).toSet(); |
| - |
| - // Convert this to a list so we can safely modify _transforms while |
| - // iterating over it. |
| - var dirtyTransforms = |
| - flatten(_transforms.values.map((transforms) => transforms.toList())) |
| - .where((transform) => transform.isDirty).toList(); |
| - |
| - if (dirtyTransforms.isEmpty && newPassThroughs.isEmpty) return null; |
| - |
| - var collisions = new Set<AssetId>(); |
| - for (var output in newPassThroughs) { |
| - if (_addOutput(output)) collisions.add(output.id); |
| - } |
| + if (!_inputs.values.any((input) => input.isDirty)) return null; |
| - return Future.wait(dirtyTransforms.map((transform) { |
| - return transform.apply().then((outputs) { |
| - for (var output in outputs) { |
| - if (_addOutput(output)) collisions.add(output.id); |
| - } |
| + return Future.wait(_inputs.values.map((input) { |
| + if (!input.isDirty) return new Future.value(new Set()); |
| + return input.process().then((outputs) { |
| + return outputs.where(_addOutput).map((output) => output.id).toSet(); |
| }); |
| - })).then((_) { |
| + })).then((collisionsList) { |
| // Report collisions in a deterministic order. |
| - collisions = collisions.toList(); |
| + var collisions = unionAll(collisionsList).toList(); |
| collisions.sort((a, b) => a.compareTo(b)); |
| for (var collision in collisions) { |
| // Ensure that there's still a collision. It's possible it was resolved |