Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(160)

Unified Diff: pkg/barback/lib/src/asset_graph.dart

Issue 16854005: First pass at build dependency graph for barback. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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;
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698