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 |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..ea68439f355d5b13e654348306294b8c32590142 |
| --- /dev/null |
| +++ b/pkg/barback/lib/src/phase.dart |
| @@ -0,0 +1,171 @@ |
| +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| +// for details. All rights reserved. Use of this source code is governed by a |
| +// BSD-style license that can be found in the LICENSE file. |
| + |
| +library barback.phase; |
| + |
| +import 'dart:async'; |
| + |
| +import '../barback.dart'; |
| +import '../transformer.dart'; |
| +import 'asset_graph.dart'; |
| +import 'asset_node.dart'; |
| +import 'transform_node.dart'; |
| + |
| +/// The transforms in a processing graph are organized into a series of phases. |
| +/// Each phase can access outputs from previous phases and can in turn pass |
| +/// outputs to later phases. |
| +/// |
| +/// Phases are processed strictly serially. All transforms in a phase will be |
| +/// complete before moving on to the next phase. Within a single phase, all |
| +/// transforms will be run in parallel. |
| +/// |
| +/// Building can be interrupted between phases. For example, an source is added |
|
nweiz
2013/06/18 23:14:46
"a source"
Bob Nystrom
2013/06/20 00:23:59
Done.
|
| +/// which starts the background process. Sometime during phase 2 (which is |
|
nweiz
2013/06/18 23:14:46
It's not clear what "the background process" refer
Bob Nystrom
2013/06/20 00:23:59
Arbitrary number for the example.
|
| +/// running asynchronously) that source is modified. When the process queue |
| +/// goes to advance to phase 3, it will see that modification and start the |
| +/// waterfall from the beginning again. |
| +class Phase { |
| + /// The graph that owns this phase. |
| + final AssetGraph graph; |
| + |
| + /// This phase's position relative to the other phases. Zero-based. |
| + final int index; |
|
nweiz
2013/06/18 23:14:46
Is this actually used anywhere? It seems weird to
Bob Nystrom
2013/06/20 00:23:59
I end up using it for debug printing. I can remove
|
| + |
| + /// The transformers that can access [inputs]. Their outputs will be |
| + /// available to the next phase. |
| + final List<Transformer> transformers; |
| + |
| + /// The inputs that are available for transforms in this phase to consume. |
| + /// 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]. 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 Set<TransformNode>(); |
| + |
| + /// The nodes that are new in this phase since the last time [process] was |
| + /// called. When we process, we'll check these to see if we can hang new |
| + /// transforms off them. |
| + final newInputs = new Set<AssetNode>(); |
|
nweiz
2013/06/18 23:14:46
Seems like most of these fields should be private.
Bob Nystrom
2013/06/20 00:23:59
Done.
|
| + |
| + /// The phase after this one. Outputs from this phase will be passed to it. |
| + Phase next; |
|
nweiz
2013/06/18 23:14:46
It's weird that this is mutable. If you construct
Bob Nystrom
2013/06/20 00:23:59
Done.
|
| + |
| + Phase(this.graph, this.index, this.transformers); |
| + |
| + /// Updates the phase's inputs with [updated] and removes [removed]. This |
| + /// marks any affected [transforms] as dirty or discards them if their inputs |
| + /// are removed. |
| + void updateInputs(Map<AssetId, Asset> updated, Set<AssetId> removed) { |
| + // Remove any nodes that are no longer being output. Handle removals first |
| + // in case there are assets that were removed by one transform but updated |
| + // by another. In that case, the update should win. |
| + for (var id in removed) { |
| + var node = inputs.remove(id); |
| + |
| + // Every transform that was using it is dirty now. |
| + if (node != null) { |
| + node.consumers.forEach((consumer) => consumer.isDirty = true); |
| + } |
| + } |
| + |
| + // Update and new or modified assets. |
| + updated.forEach((id, asset) { |
| + var node = inputs.putIfAbsent(id, () => new AssetNode(id)); |
| + |
| + // If it's a new node, remember that so we can see if any new transforms |
| + // will consume it. |
| + if (node.asset == null) newInputs.add(node); |
| + |
| + node.updateAsset(asset); |
| + }); |
| + } |
| + |
| + /// Processes this phase. For all new inputs, it tries to see if there are |
| + /// transformers that can consume them. Then all applicable transforms are |
| + /// applied. |
| + /// |
| + /// Returns a future that completes when processing is done. If there is |
| + /// nothing to process, returns `null`. |
| + Future process() { |
| + var future = _processNewInputs(); |
| + if (future == null) { |
| + return _processTransforms(); |
| + } |
| + |
| + return future.then((_) => _processTransforms()); |
| + } |
| + |
| + /// Creates new transforms for any new inputs that are applicable. |
| + Future _processNewInputs() { |
| + if (newInputs.isEmpty) return null; |
| + |
| + var futures = []; |
| + for (var node in newInputs) { |
| + for (var transformer in transformers) { |
| + futures.add(transformer.isPrimary(node.id).then((isPrimary) { |
| + if (!isPrimary) return; |
| + var transform = new TransformNode(this, transformer, node); |
| + node.consumers.add(transform); |
| + transforms.add(transform); |
| + })); |
| + } |
| + } |
| + |
| + newInputs.clear(); |
| + |
| + return Future.wait(futures); |
| + } |
| + |
| + /// Applies all currently wired up and dirty transforms. Passes their outputs |
| + /// to the next phase. |
| + Future _processTransforms() { |
| + var dirtyTransforms = transforms.where((transform) => transform.isDirty); |
| + if (dirtyTransforms.isEmpty) return null; |
| + |
| + return Future.wait(dirtyTransforms.map( |
| + (transform) => transform.apply())).then((transformOutputs) { |
|
nweiz
2013/06/18 23:14:46
I think this would be a little cleaner formatted l
Bob Nystrom
2013/06/20 00:23:59
Done.
|
| + // Collect all of the outputs. Since the transforms are run in parallel, |
| + // we have to be careful here to ensure that the result is deterministic |
| + // and not influenced by the order that transforms complete. |
| + var updated = new Map<AssetId, Asset>(); |
| + var removed = new Set<AssetId>(); |
| + var collisions = new Set<AssetId>(); |
| + |
| + // Handle the generated outputs of all transforms first. |
| + for (var outputs in transformOutputs) { |
| + // Collect the outputs of all transformers together. |
| + outputs.updated.forEach((id, asset) { |
| + if (updated.containsKey(id)) { |
| + // Report a collision. |
| + collisions.add(id); |
| + } else { |
| + // TODO(rnystrom): In the case of a collision, the asset that |
| + // "wins" is chosen non-deterministically. Do something better. |
| + updated[id] = asset; |
| + } |
| + }); |
| + |
| + // Track any assets no longer output by this transform. We don't |
| + // handle the case where *another* transform generates the asset |
| + // no longer generated by this one. updateInputs() handles that. |
| + removed.addAll(outputs.removed); |
| + } |
| + |
| + // Report any collisions in deterministic order. |
| + collisions = collisions.toList(); |
| + collisions.sort((a, b) => a.toString().compareTo(b.toString())); |
| + for (var collision in collisions) { |
| + graph.reportError(new AssetCollisionException(collision)); |
| + // TODO(rnystrom): Define what happens after a collision occurs. |
| + } |
| + |
| + // Pass the outputs to the next phase. |
| + next.updateInputs(updated, removed); |
| + }); |
| + } |
| +} |