Chromium Code Reviews| Index: pkg/barback/lib/src/asset_graph.dart |
| diff --git a/pkg/barback/lib/src/asset_graph.dart b/pkg/barback/lib/src/asset_graph.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..61a5bc769013d827ceed3a598798f56c87d0aebc |
| --- /dev/null |
| +++ b/pkg/barback/lib/src/asset_graph.dart |
| @@ -0,0 +1,404 @@ |
| +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
|
nweiz
2013/06/14 00:57:57
This should probably be exported somewhere, right?
Bob Nystrom
2013/06/17 23:35:05
The Barback class (when it exists) will wrap it an
|
| +// 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.asset_graph; |
| + |
| +import 'dart:async'; |
| +import 'dart:collection'; |
| + |
| +import '../barback.dart'; |
| +import '../transformer.dart'; |
| + |
| +/// The main build dependency manager. For any given input file, it can tell |
| +/// which output files are affected by it, and vice versa. |
| +class AssetGraph { |
| + final AssetProvider _provider; |
| + |
| + final _phases = <_Phase>[]; |
| + |
| + // TODO(rnystrom): Have a value type for results. |
| + Stream<ProcessResult> get results => _resultsController.stream; |
| + final _resultsController = new StreamController<ProcessResult>(); |
|
nweiz
2013/06/14 00:57:57
This should be a broadcast controller.
Bob Nystrom
2013/06/17 23:35:05
Done.
|
| + |
| + /// This holds a future that completes when the work queue is complete if the |
| + /// /work queue is currently being processed. Otherwise, it is `null`. |
|
nweiz
2013/06/14 00:57:57
"/work" -> "work"
Bob Nystrom
2013/06/17 23:35:05
Done.
|
| + Future _processDone; |
| + |
| + _ChangeBatch _sourceChanges; |
| + |
| + /// Creates a new [AssetGraph]. |
| + /// |
| + /// It loads source assets using [provider] and then uses [transformerPhases] |
| + /// to generate output files from them. |
| + //TODO(rnystrom): Better way of specifying transformers and their ordering. |
| + AssetGraph(this._provider, Iterable<Iterable<Transformer>> transformerPhases) { |
|
nweiz
2013/06/14 00:57:57
Long line.
Bob Nystrom
2013/06/17 23:35:05
Done.
|
| + // Add phases for each transformer stage. |
|
nweiz
2013/06/14 00:57:57
I'm convinced we're going to end up needing semant
Bob Nystrom
2013/06/17 23:35:05
I'm not convinced either way, but so far I haven't
nweiz
2013/06/18 23:14:45
The biggest driver won't come when you're doing th
Bob Nystrom
2013/06/20 00:23:59
Sure, but my rough thinking is that that can happe
nweiz
2013/06/20 23:06:08
Keep in mind that we may eventually want to suppor
Bob Nystrom
2013/06/21 00:13:20
Agreed. I'm making the simplifying assumption for
|
| + for (var transformers in transformerPhases) { |
| + var phase = new _Phase(this, _phases.length, transformers.toList()); |
| + _phases.add(phase); |
| + } |
| + |
| + // Add a phase for the final outputs. |
| + _phases.add(new _Phase(this, _phases.length, [])); |
|
nweiz
2013/06/14 00:57:57
This is confusing. What does a phase with no trans
Bob Nystrom
2013/06/17 23:35:05
Done.
|
| + |
| + // Chain them together. |
| + for (var i = 0; i < _phases.length - 1; i++){ |
| + _phases[i].next = _phases[i + 1]; |
| + } |
| + } |
| + |
| + Future<Asset> getAssetById(AssetId id) { |
| + return _waitForProcess().then((_) { |
|
nweiz
2013/06/14 00:57:57
Add a TODO to be smarter about only waiting until
Bob Nystrom
2013/06/17 23:35:05
At least right now, this is as smart as it can be.
nweiz
2013/06/18 23:14:45
There are several ways this could be smarter:
* [
Bob Nystrom
2013/06/20 00:23:59
All good points. Added a big TODO with all of this
|
| + // Find the latest phase that output this asset. |
| + for (var i = _phases.length - 1; i >= 0; i--) { |
| + var node = _phases[i].inputs[id]; |
|
nweiz
2013/06/14 00:57:57
Why are we looking in the phase inputs for the ass
Bob Nystrom
2013/06/17 23:35:05
Generated assets are stored in each phase. If you
nweiz
2013/06/18 23:14:45
This would be clearer if the comment said "Find th
Bob Nystrom
2013/06/20 00:23:59
Done.
|
| + if (node != null) { |
| + // By the time we get here, the asset should have been built. |
| + assert(node.asset != null); |
| + return node.asset; |
| + } |
| + } |
| + |
| + // Couldn't find it. |
| + _throwError(new AssetNotFoundException(id)); |
| + }); |
| + } |
| + |
| + /// Adds [sources] to the graph's known set of source assets. Will begin |
|
nweiz
2013/06/14 00:57:57
"Will begin" -> "Begins"
Bob Nystrom
2013/06/17 23:35:05
Done.
|
| + /// applying any transforms that can consume any of the sources. If a given |
| + /// source has already been added, it is considered modified and all |
|
nweiz
2013/06/14 00:57:57
"has already been added" -> "is already known"
Bob Nystrom
2013/06/17 23:35:05
Done.
|
| + /// transforms that use it will be re-applied. |
| + void updateSources(Iterable<AssetId> sources) { |
|
nweiz
2013/06/14 00:57:57
Right now I think you can postpone handling a chan
Bob Nystrom
2013/06/17 23:35:05
Added a test. I think it's working correctly here,
nweiz
2013/06/18 23:14:45
I was misreading somewhat, but I think there's sti
Bob Nystrom
2013/06/20 00:23:59
Wow, good catch. One line fix, but took me a good
|
| + if (_sourceChanges == null) _sourceChanges = new _ChangeBatch(); |
| + _sourceChanges.update(sources); |
| + |
| + _waitForProcess(); |
| + } |
| + |
| + /// Removes [removed] from the graph's known set of source assets. |
| + void removeSources(Iterable<AssetId> removed) { |
| + if (_sourceChanges == null) _sourceChanges = new _ChangeBatch(); |
| + _sourceChanges.remove(removed); |
| + |
| + _waitForProcess(); |
| + } |
| + |
| + /// Returns a future that completes with the background processing is done. |
|
nweiz
2013/06/14 00:57:57
It's not clear from the name or from the documenta
Bob Nystrom
2013/06/17 23:35:05
Clarified documentation.
nweiz
2013/06/18 23:14:45
I still worry that unintended errors from e.g. [_p
Bob Nystrom
2013/06/20 00:23:59
My intent (and there are TODOs for this) is to cat
nweiz
2013/06/20 23:06:08
Taking down barback as a component and taking down
Bob Nystrom
2013/06/21 00:13:20
Good call. Done. Added a long comment explaining i
|
| + Future _waitForProcess() { |
| + if (_processDone != null) return _processDone; |
| + // TODO(rnystrom): Handle errors. |
| + return _processDone = _process().then((_) { |
| + _processDone = null; |
| + }); |
| + } |
| + |
| + /// Starts the background processing. Returns a future that completes when |
| + /// all assets have been processed. |
| + Future _process() { |
| + return _processSourceChanges().then((_) { |
| + // Find the first phase that has work to do and do it. |
| + var future; |
| + for (var phase in _phases) { |
| + future = phase.process(); |
| + if (future != null) break; |
| + } |
| + |
| + // If all phases are done, so are we. |
| + if (future == null) return; |
| + |
| + // Process that phase and then loop onto the next. |
| + return future.then((_) => _process()); |
| + }); |
| + } |
| + |
| + /// Processes the current batch of changes to source assets. |
| + Future _processSourceChanges() { |
| + if (_sourceChanges == null) return new Future.value(); |
| + |
| + // Always pump the event loop. This ensures a bunch of synchronous source |
| + // changes are processed in a single batch even when the first one starts |
| + // the build process. |
|
nweiz
2013/06/14 00:57:57
I don't think this works right now. You'll end up
Bob Nystrom
2013/06/17 23:35:05
It's very unclear, but the behavior is correct. It
|
| + return new Future(() { |
| + var changes = _sourceChanges; |
| + _sourceChanges = null; |
| + |
| + var updated = new Map<AssetId, Asset>(); |
| + var futures = []; |
| + for (var id in changes.updated) { |
| + futures.add(_provider.loadAsset(id).then((asset) { |
| + updated[id] = asset; |
| + })); |
| + } |
| + |
| + return Future.wait(futures).then((_) { |
| + _phases.first.updateInputs(updated, changes.removed); |
| + }); |
| + }); |
| + } |
| + |
| + /// Reports a process result with the given error then throws it. |
| + void _throwError(Exception error) { |
|
nweiz
2013/06/14 00:57:57
It feels like it might be cleaner to just throw ex
Bob Nystrom
2013/06/17 23:35:05
Removed this.
|
| + _resultsController.add(new ProcessResult(error)); |
| + throw error; |
| + } |
| +} |
| + |
| +/// The build process runs asynchronously in the background. It reports back to |
| +/// the user be emitting a [Stream] of these objects. Currently, it only emits |
| +/// errors. |
| +class ProcessResult { |
| + /// The error that occurred. |
| + final error; |
| + |
| + ProcessResult(this.error); |
| +} |
| + |
| +/// Represents a batch of source asset changes: additions, removals and |
| +/// modifications. |
| +class _ChangeBatch { |
|
nweiz
2013/06/14 00:57:57
It would be nice to split out this and the followi
Bob Nystrom
2013/06/17 23:35:05
Done. For some reason, I thought they should be to
|
| + /// The assets that have been added or modified in this batch. |
| + final updated = new Set<AssetId>(); |
| + |
| + /// The assets that have been removed in this batch. |
| + final removed = new Set<AssetId>(); |
| + |
| + /// Adds the updated [assets] to this batch. |
| + void update(Iterable<AssetId> assets) { |
| + updated.addAll(assets); |
| + |
| + // If they were previously removed, they are back now. |
| + removed.removeAll(assets); |
| + } |
| + |
| + /// Removes [assets] from this batch. |
| + void remove(Iterable<AssetId> assets) { |
| + removed.addAll(assets); |
| + |
| + // If they were previously updated, they are gone now. |
| + updated.removeAll(assets); |
| + } |
| +} |
| + |
| +/// 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 |
| +/// which starts the background process. Sometime during phase 2 (which is |
| +/// 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; |
| + |
| + /// The transformers that can use [assets] as inputs. Their outputs will be |
|
nweiz
2013/06/14 00:57:57
There's no field named "assets". Did you mean "inp
Bob Nystrom
2013/06/17 23:35:05
Done.
|
| + /// 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 on assets in [inputs]. These are the |
|
nweiz
2013/06/14 00:57:57
"applicable to"
Bob Nystrom
2013/06/17 23:35:05
Done.
|
| + /// 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 the graph since the last time [process] was |
| + /// called. When we process, we'll check these to see if we can hang new |
| + /// transforms off them. |
|
nweiz
2013/06/14 00:57:57
This isn't all new nodes in the whole AssetGraph,
Bob Nystrom
2013/06/17 23:35:05
Yes, fixed.
|
| + final newInputs = new Set<_AssetNode>(); |
| + |
| + /// The phase after this one. Outputs from this phase will be passed to it. |
| + _Phase next; |
| + |
| + _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) { |
|
nweiz
2013/06/14 00:57:57
It's weird that this takes [updated] as a map. It
Bob Nystrom
2013/06/17 23:35:05
They did at first. It made some things cleaner to
nweiz
2013/06/18 23:14:45
Let's postpone the decision until we have a better
Bob Nystrom
2013/06/20 00:23:59
SGTM.
|
| + // Remove any nodes that are no longer being output. |
| + 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(); |
|
nweiz
2013/06/14 00:57:57
It'd be cleaner to just do "future = new Future.va
Bob Nystrom
2013/06/17 23:35:05
I hate how awkard this code is, but it's the best
|
| + } |
| + |
| + return future.then((_) { |
| + return _processTransforms(); |
|
nweiz
2013/06/14 00:57:57
Style nit: =>
Bob Nystrom
2013/06/17 23:35:05
Done.
|
| + }); |
| + } |
| + |
| + /// 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; |
| + |
| + var updated = new Map<AssetId, Asset>(); |
| + var removed = new Set<AssetId>(); |
| + |
| + return Future.wait(dirtyTransforms.map((node) { |
|
nweiz
2013/06/14 00:57:57
"node" -> "transform", to avoid it being confused
Bob Nystrom
2013/06/17 23:35:05
Done.
|
| + return node.apply(updated, removed); |
| + })).then((_) { |
| + // Pass the outputs to the next phase. |
| + next.updateInputs(updated, removed); |
|
nweiz
2013/06/14 00:57:57
What about the transform's output? Isn't it also u
Bob Nystrom
2013/06/17 23:35:05
The input/output terminology is a bit confusing he
|
| + }); |
| + } |
| +} |
| + |
| +/// Represents an asset within the build dependency graph. It tracks its ID, |
|
nweiz
2013/06/14 00:57:57
"Represents an asset" is confusing. The Asset clas
Bob Nystrom
2013/06/17 23:35:05
Rewrote.
|
| +/// the currently generated actual asset for it, and any transforms that use |
|
nweiz
2013/06/14 00:57:57
You're using "it" to refer to two different things
Bob Nystrom
2013/06/17 23:35:05
Done.
|
| +/// that asset as an input. |
| +class _AssetNode { |
| + final AssetId id; |
| + Asset asset; |
| + |
| + /// The [_TransformNode]s in this node's phase that consume this asset as an |
|
nweiz
2013/06/14 00:57:57
"this asset" -> "this node's asset".
Bob Nystrom
2013/06/17 23:35:05
Done.
|
| + /// input. |
| + final consumers = new Set<_TransformNode>(); |
| + |
| + _AssetNode(this.id); |
| + |
| + /// Updates this nodes's generated asset value and marks all transforms that |
|
nweiz
2013/06/14 00:57:57
"node's"
Bob Nystrom
2013/06/17 23:35:05
Done.
|
| + /// use this as dirty. |
| + void updateAsset(Asset asset) { |
| + this.asset = asset; |
| + consumers.forEach((consumer) => consumer.isDirty = true); |
| + } |
| +} |
| + |
| +/// Represents a transform step within the build dependency graph. |
| +class _TransformNode { |
|
nweiz
2013/06/14 00:57:57
It's pretty confusing right now what the distincti
Bob Nystrom
2013/06/17 23:35:05
Done.
|
| + final _Phase phase; |
| + final Transformer transformer; |
| + final _AssetNode primary; |
| + var isDirty = true; |
| + |
| + /// The outputs created by this transform the last time it was run. Used to |
| + /// tell if an output was removed in a later run. |
| + var outputs = new Set<AssetId>(); |
|
nweiz
2013/06/14 00:57:57
What about the non-primary inputs?
Bob Nystrom
2013/06/17 23:35:05
Done. Good catch. Added a test.
|
| + |
| + _TransformNode(this.phase, this.transformer, this.primary); |
| + |
| + /// Applies this transform. Outputs will be added to [updated]. Outputs that |
| + /// were generated the last time this was applied but were not generated |
| + /// this time will be added to [removed]. |
|
nweiz
2013/06/14 00:57:57
This signature is very confusing. The docstring im
Bob Nystrom
2013/06/20 00:23:59
Done.
|
| + Future apply(Map<AssetId, Asset> updated, Set<AssetId> removed) { |
| + var transform = new _Transform(this); |
| + return transformer.apply(transform).then((_) { |
| + isDirty = false; |
| + |
| + // Collect the outputs. |
| + transform._outputs.forEach((id, asset) { |
| + if (updated.containsKey(id)) { |
| + // Report a collision. |
| + phase.graph._resultsController.add(new ProcessResult( |
| + new AssetCollisionException(id))); |
| + // TODO(rnystrom): Define what happens after a collision occurs. |
|
nweiz
2013/06/14 00:57:57
We should probably have some notion of a node and
Bob Nystrom
2013/06/17 23:35:05
My current rough thoughts are that it would be lef
nweiz
2013/06/18 23:14:45
I agree; errors shouldn't bring everything down. M
Bob Nystrom
2013/06/20 00:23:59
Done.
|
| + } else { |
| + updated[id] = asset; |
| + } |
| + }); |
| + |
| + // See which outputs are missing from the last run. |
| + var outputIds = transform._outputs.keys.toSet(); |
| + var removedOutputs = outputs.difference(outputIds); |
| + outputs = outputIds; |
| + removed.addAll(removedOutputs); |
|
nweiz
2013/06/14 00:57:57
We should be sure we test the case where one trans
Bob Nystrom
2013/06/17 23:35:05
Done. It was doing the right thing, but it wasn't
|
| + }); |
| + } |
| +} |
| + |
| +/// A concrete implementation of [Transform]. |
| +class _Transform implements Transform { |
|
nweiz
2013/06/14 00:57:57
I really hate the pattern of an interface with a s
Bob Nystrom
2013/06/17 23:35:05
It isn't that simple. It needs to have both a priv
nweiz
2013/06/18 23:14:45
"lib/barback.dart" should only contain exports and
Bob Nystrom
2013/06/20 00:23:59
Yup, a later patch I'm working on does that.
|
| + final _TransformNode _node; |
| + |
| + final _inputs = new Set<_AssetNode>(); |
| + final _outputs = new Map<AssetId, Asset>(); |
| + |
| + AssetId get primaryId => _node.primary.id; |
| + Future<Asset> get primaryInput => getInput(primaryId); |
| + |
| + _Transform(this._node); |
| + |
| + Future<Asset> getInput(AssetId id) { |
| + return new Future(() { |
| + var node = _node.phase.inputs[id]; |
| + // TODO(rnystrom): Need to handle passthrough where an asset from a |
| + // previous phase can be found. |
| + if (node == null) { |
| + _node.phase.graph._throwError(new MissingInputException(id)); |
| + } |
| + |
| + // Keep track of which assets this transform depends on. |
| + _inputs.add(node); |
| + node.consumers.add(_node); |
| + return node.asset; |
| + }); |
| + } |
| + |
| + void addOutput(AssetId id, Asset output) { |
| + _outputs[id] = output; |
| + } |
| +} |