Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add AssetGraph/AssetNode #42

Merged
merged 1 commit into from
Feb 10, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions lib/src/asset_graph/exceptions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) 2016, 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.
import 'node.dart';

class DuplicateAssetNodeException implements Exception {
final AssetNode assetNode;

DuplicateAssetNodeException(this.assetNode);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

up to you whether to change, but this could be const

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't since AssetNode isn't const :(


@override
String toString() => 'DuplicateAssetNodeError: $assetNode';
}
41 changes: 41 additions & 0 deletions lib/src/asset_graph/graph.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2016, 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.
import '../asset/id.dart';
import 'exceptions.dart';
import 'node.dart';

/// Represents all the [Asset]s in the system, and all of their dependencies.
class AssetGraph {
/// All the [AssetNode]s in the system, indexed by [AssetId].
final _nodesById = <AssetId, AssetNode>{};

AssetGraph();

/// Checks if [id] exists in the graph.
bool contains(AssetId id) => _nodesById.containsKey(id);

/// Gets the [AssetNode] for [id], if one exists.
AssetNode get(AssetId id) => _nodesById[id];

/// Adds [node] to the graph.
void add(AssetNode node) {
if (_nodesById.containsKey(node.id)) {
throw new DuplicateAssetNodeException(node);
}
_nodesById[node.id] = node;
}

/// Adds the node returned by [ifAbsent] to the graph, if [id] doesn't
/// already exist.
///
/// Returns either the existing value or the one just added.
AssetNode addIfAbsent(AssetId id, AssetNode ifAbsent()) =>
_nodesById.putIfAbsent(id, ifAbsent);

/// Removes [node] from the graph.
AssetNode remove(AssetId id) => _nodesById.remove(id);

@override
toString() => _nodesById.values.toList().toString();
}
44 changes: 44 additions & 0 deletions lib/src/asset_graph/node.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) 2016, 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.
import '../asset/id.dart';
import '../builder/builder.dart';

/// A node in the asset graph.
///
/// This class specifically represents normal (ie: non-generated) assets.
class AssetNode {
/// The asset this node represents.
final AssetId id;

/// The [AssetId]s of all generated assets which depend on this node.
final outputs = new Set<AssetId>();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems fine for now, but I want to be careful about conflating information about the graph structure with details of the build system.

It feels like we should be able to swap out the graph implementation relatively easily without changing much about the rest of the implementation.

Not sure we need to do anything about this now but I wanted to bring it up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will shake out in the coming cls as this thing actually gets used.


AssetNode(this.id);

@override
String toString() => 'AssetNode: $id';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@override, here & in GeneratedAssetNode

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}

/// A generated node in the asset graph.
class GeneratedAssetNode extends AssetNode {
/// The builder which generated this node.
final Builder builder;

/// The primary input which generated this node.
final AssetId primaryInput;

/// The phase group number which generates this asset.
final int generatingPhaseGroup;

/// Whether or not this asset needs to be updated.
bool needsUpdate;

GeneratedAssetNode(this.builder, this.primaryInput, this.generatingPhaseGroup,
this.needsUpdate, AssetId id)
: super(id);

@override
toString() => 'GeneratedAssetNode: $id generated for input $primaryInput to '
'$builder in phase group $generatingPhaseGroup.';
}
45 changes: 40 additions & 5 deletions lib/src/generate/build.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import '../asset/file_based.dart';
import '../asset/id.dart';
import '../asset/reader.dart';
import '../asset/writer.dart';
import '../asset_graph/graph.dart';
import '../asset_graph/node.dart';
import '../builder/builder.dart';
import '../builder/build_step_impl.dart';
import '../package_graph/package_graph.dart';
Expand Down Expand Up @@ -45,7 +47,6 @@ Future<BuildResult> build(List<List<Phase>> phaseGroups,
Logger.root.level = logLevel;
onLog ??= print;
var logListener = Logger.root.onRecord.listen(onLog);
// No need to create a package graph if we were supplied a reader/writer.
packageGraph ??= new PackageGraph.forThisPackage();
var cache = new AssetCache();
reader ??=
Expand Down Expand Up @@ -75,6 +76,7 @@ Future<BuildResult> build(List<List<Phase>> phaseGroups,
exception: e, stackTrace: s);
}
}, zoneValues: {
_assetGraphKey: new AssetGraph(),
_assetReaderKey: reader,
_assetWriterKey: writer,
_packageGraphKey: packageGraph,
Expand All @@ -86,11 +88,13 @@ Future<BuildResult> build(List<List<Phase>> phaseGroups,
}

/// Keys for reading zone local values.
Symbol _assetGraphKey = #buildAssetGraph;
Symbol _assetReaderKey = #buildAssetReader;
Symbol _assetWriterKey = #buildAssetWriter;
Symbol _packageGraphKey = #buildPackageGraph;

/// Getters for zone local values.
AssetGraph get _assetGraph => Zone.current[_assetGraphKey];
AssetReader get _reader => Zone.current[_assetReaderKey];
AssetWriter get _writer => Zone.current[_assetWriterKey];
PackageGraph get _packageGraph => Zone.current[_packageGraphKey];
Expand Down Expand Up @@ -123,6 +127,7 @@ Future _deletePreviousOutputs(List<List<Phase>> phaseGroups) async {
for (var input in inputs) {
for (var builder in phase.builders) {
var outputs = builder.declareOutputs(input);

groupOutputIds.addAll(outputs);
for (var output in outputs) {
if (allInputs[output.package]?.contains(output) == true) {
Expand Down Expand Up @@ -179,14 +184,16 @@ Future _deletePreviousOutputs(List<List<Phase>> phaseGroups) async {
Future<BuildResult> _runPhases(List<List<Phase>> phaseGroups) async {
final allInputs = await _allInputs(phaseGroups);
final outputs = <Asset>[];
int phaseGroupNum = 0;
for (var group in phaseGroups) {
final groupOutputs = <Asset>[];
for (var phase in group) {
var inputs = _matchingInputs(allInputs, phase.inputSets);
for (var builder in phase.builders) {
// TODO(jakemac): Optimize, we can run all the builders in a phase
// at the same time instead of sequentially.
await for (var output in _runBuilder(builder, inputs, allInputs)) {
await for (var output
in _runBuilder(builder, inputs, allInputs, phaseGroupNum)) {
groupOutputs.add(output);
outputs.add(output);
}
Expand All @@ -199,6 +206,7 @@ Future<BuildResult> _runPhases(List<List<Phase>> phaseGroups) async {
allInputs.putIfAbsent(output.id.package, () => new Set<AssetId>());
allInputs[output.id.package].add(output.id);
}
phaseGroupNum++;
}
return new BuildResult(BuildStatus.Success, BuildType.Full, outputs);
}
Expand Down Expand Up @@ -254,9 +262,10 @@ bool _isValidInput(AssetId input) {

/// Runs [builder] with [inputs] as inputs.
Stream<Asset> _runBuilder(Builder builder, Iterable<AssetId> primaryInputs,
Map<String, Set<AssetId>> allInputs) async* {
Map<String, Set<AssetId>> allInputs, int phaseGroupNum) async* {
for (var input in primaryInputs) {
var expectedOutputs = builder.declareOutputs(input);

/// Validate [expectedOutputs].
for (var output in expectedOutputs) {
if (output.package != _packageGraph.root.name) {
Expand All @@ -267,11 +276,37 @@ Stream<Asset> _runBuilder(Builder builder, Iterable<AssetId> primaryInputs,
}
}

/// Add nodes to the [AssetGraph] for [expectedOutputs] and [input].
var inputNode = _assetGraph.addIfAbsent(input, () => new AssetNode(input));
for (var output in expectedOutputs) {
inputNode.outputs.add(output);
_assetGraph.addIfAbsent(
output,
() => new GeneratedAssetNode(
builder, input, phaseGroupNum, true, output));
}

/// Skip the build step if none of the outputs need updating.
var skipBuild = !expectedOutputs.any((output) =>
(_assetGraph.get(output) as GeneratedAssetNode).needsUpdate);
if (skipBuild) continue;

var inputAsset = new Asset(input, await _reader.readAsString(input));
var buildStep = new BuildStepImpl(
inputAsset, expectedOutputs, _reader, _writer, _packageGraph.root.name);
var buildStep = new BuildStepImpl(inputAsset, expectedOutputs, _reader,
_writer, _packageGraph.root.name);
await builder.build(buildStep);
await buildStep.complete();

/// Update the asset graph based on the dependencies discovered.
for (var dependency in buildStep.dependencies) {
var dependencyNode =
_assetGraph.addIfAbsent(dependency, () => new AssetNode(dependency));

/// We care about all [expectedOutputs], not just real outputs.
dependencyNode.outputs.addAll(expectedOutputs);
}

/// Yield the outputs.
for (var output in buildStep.outputs) {
yield output;
}
Expand Down
80 changes: 80 additions & 0 deletions test/asset_graph/graph_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) 2016, 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.
import 'package:test/test.dart';

import 'package:build/src/asset_graph/graph.dart';
import 'package:build/src/asset_graph/node.dart';

import '../common/common.dart';

main() {
group('AssetGraph', () {
AssetGraph graph;

setUp(() {
graph = new AssetGraph();
});

void expectNodeDoesNotExist(AssetNode node) {
expect(graph.contains(node.id), isFalse);
expect(graph.get(node.id), isNull);
}

void expectNodeExists(AssetNode node) {
expect(graph.contains(node.id), isTrue);
expect(graph.get(node.id), node);
}

AssetNode testAddNode() {
var node = makeAssetNode();
expectNodeDoesNotExist(node);
graph.add(node);
expectNodeExists(node);
return node;
}

test('add, contains, get', () {
for (int i = 0; i < 5; i++) {
testAddNode();
}
});

test('addIfAbsent', () {
var node = makeAssetNode();
expect(graph.addIfAbsent(node.id, () => node), same(node));
expect(graph.contains(node.id), isTrue);

var otherNode = new AssetNode(node.id);
expect(graph.addIfAbsent(otherNode.id, () => otherNode), same(node));
expect(graph.contains(otherNode.id), isTrue);
});

test('duplicate adds throw DuplicateAssetNodeException', () {
var node = testAddNode();
expect(() => graph.add(node), throwsA(duplicateAssetNodeException));
});

test('remove', () {
var nodes = <AssetNode>[];
for (int i = 0; i < 5; i++) {
nodes.add(testAddNode());
}
graph.remove(nodes[1].id);
graph.remove(nodes[4].id);

expectNodeExists(nodes[0]);
expectNodeDoesNotExist(nodes[1]);
expectNodeExists(nodes[2]);
expectNodeDoesNotExist(nodes[4]);
expectNodeExists(nodes[3]);

// Doesn't throw.
graph.remove(nodes[1].id);

// Can be added back
graph.add(nodes[1]);
expectNodeExists(nodes[1]);
});
});
}
4 changes: 4 additions & 0 deletions test/common/assets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// 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.
import 'package:build/build.dart';
import 'package:build/src/asset_graph/node.dart';

import 'in_memory_writer.dart';

Expand Down Expand Up @@ -33,3 +34,6 @@ void addAssets(Iterable<Asset> assets, InMemoryAssetWriter writer) {
writer.assets[asset.id] = asset.stringContents;
}
}

AssetNode makeAssetNode([String assetIdString]) =>
new AssetNode(makeAssetId(assetIdString));
3 changes: 3 additions & 0 deletions test/common/matchers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
import 'package:test/test.dart';

import 'package:build/build.dart';
import 'package:build/src/asset_graph/exceptions.dart';

final assetNotFoundException = new isInstanceOf<AssetNotFoundException>();
final duplicateAssetNodeException =
new isInstanceOf<DuplicateAssetNodeException>();
final invalidInputException = new isInstanceOf<InvalidInputException>();
final invalidOutputException = new isInstanceOf<InvalidOutputException>();
final packageNotFoundException = new isInstanceOf<PackageNotFoundException>();
Expand Down