Commit 1438ae85 authored by Chris Bracken's avatar Chris Bracken Committed by GitHub

Skip snapshot build if inputs are unchanged (#11047)

Previously, the snapshot file was recomputed on every build. We now
record checksums for all snapshot inputs (which are catalogued in the
snapshot dependencies file output alongside the snapshot) and only
rebuild if the checksum for any input file has changed.
parent eb668c3f
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert' show JSON;
import 'package:crypto/crypto.dart' show md5;
import 'file_system.dart';
/// A collection of checksums for a set of input files.
///
/// This class can be used during build actions to compute a checksum of the
/// build action inputs, and if unchanged from the previous build, skip the
/// build step. This assumes that build outputs are strictly a product of the
/// input files.
class Checksum {
Checksum.fromFiles(Set<String> inputPaths) : _checksums = <String, String>{} {
final Iterable<File> files = inputPaths.map(fs.file);
final Iterable<File> missingInputs = files.where((File file) => !file.existsSync());
if (missingInputs.isNotEmpty)
throw new ArgumentError('Missing input files:\n' + missingInputs.join('\n'));
for (File file in files) {
final List<int> bytes = file.readAsBytesSync();
_checksums[file.path] = md5.convert(bytes).toString();
}
}
Checksum.fromJson(String json) : _checksums = JSON.decode(json);
final Map<String, String> _checksums;
String toJson() => JSON.encode(_checksums);
@override
bool operator==(dynamic other) {
return other is Checksum &&
_checksums.length == other._checksums.length &&
_checksums.keys.every((String key) => _checksums[key] == other._checksums[key]);
}
@override
int get hashCode => _checksums.hashCode;
}
......@@ -8,6 +8,7 @@ import 'package:meta/meta.dart' show required;
import 'artifacts.dart';
import 'asset.dart';
import 'base/build.dart';
import 'base/common.dart';
import 'base/file_system.dart';
import 'base/process.dart';
......@@ -34,7 +35,7 @@ Future<int> _createSnapshot({
@required String snapshotPath,
@required String depfilePath,
@required String packages
}) {
}) async {
assert(mainPath != null);
assert(snapshotPath != null);
assert(depfilePath != null);
......@@ -53,8 +54,64 @@ Future<int> _createSnapshot({
'--dependencies=$depfilePath',
mainPath,
];
// Write the depfile path to disk.
fs.file(depfilePath).parent.childFile('gen_snapshot.d').writeAsString('$depfilePath: $snapshotterPath\n');
return runCommandAndStreamOutput(args);
final File checksumFile = fs.file('$depfilePath.checksums');
final File snapshotFile = fs.file(snapshotPath);
final File depfile = fs.file(depfilePath);
if (snapshotFile.existsSync() && depfile.existsSync() && checksumFile.existsSync()) {
try {
final String json = await checksumFile.readAsString();
final Checksum oldChecksum = new Checksum.fromJson(json);
final Set<String> inputPaths = await _readDepfile(depfilePath);
inputPaths.add(snapshotPath);
final Checksum newChecksum = new Checksum.fromFiles(inputPaths);
if (oldChecksum == newChecksum) {
printTrace('Skipping snapshot build. Checksums match.');
return 0;
}
} catch (e, s) {
// Log exception and continue, this step is a performance improvement only.
printTrace('Error during snapshot checksum check: $e\n$s');
}
}
// Build the snapshot.
final int exitCode = await runCommandAndStreamOutput(args);
if (exitCode != 0)
return exitCode;
// Compute and record input file checksums.
try {
final Set<String> inputPaths = await _readDepfile(depfilePath);
inputPaths.add(snapshotPath);
final Checksum checksum = new Checksum.fromFiles(inputPaths);
await checksumFile.writeAsString(checksum.toJson());
} catch (e, s) {
// Log exception and continue, this step is a performance improvement only.
printTrace('Error during snapshot checksum output: $e\n$s');
}
return 0;
}
/// Parses a VM snapshot dependency file.
///
/// Snapshot dependency files are a single line mapping the output snapshot to a
/// space-separated list of input files used to generate that output. e.g,
///
/// outfile : file1.dart file2.dart file3.dart
Future<Set<String>> _readDepfile(String depfilePath) async {
// Depfile format:
// outfile : file1.dart file2.dart file3.dart
final String contents = await fs.file(depfilePath).readAsString();
final String dependencies = contents.split(': ')[1];
return dependencies
.split(' ')
.map((String path) => path.trim())
.where((String path) => path.isNotEmpty)
.toSet();
}
Future<Null> build({
......
// Copyright 2017 The Chromium Authors. 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:file/memory.dart';
import 'package:flutter_tools/src/base/build.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:test/test.dart';
import '../src/context.dart';
void main() {
group('Checksum', () {
group('fromFiles', () {
MemoryFileSystem fs;
setUp(() {
fs = new MemoryFileSystem();
});
testUsingContext('throws if any input file does not exist', () async {
await fs.file('a.dart').create();
expect(() => new Checksum.fromFiles(<String>['a.dart', 'b.dart'].toSet()), throwsA(anything));
}, overrides: <Type, Generator>{ FileSystem: () => fs});
testUsingContext('populates checksums for valid files', () async {
await fs.file('a.dart').writeAsString('This is a');
await fs.file('b.dart').writeAsString('This is b');
final Checksum checksum = new Checksum.fromFiles(<String>['a.dart', 'b.dart'].toSet());
final String json = checksum.toJson();
expect(json, '{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07c"}');
}, overrides: <Type, Generator>{ FileSystem: () => fs});
});
group('fromJson', () {
test('throws if JSON is invalid', () async {
expect(() => new Checksum.fromJson('<xml></xml>'), throwsA(anything));
});
test('populates checksums for valid JSON', () async {
final String json = '{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07c"}';
final Checksum checksum = new Checksum.fromJson(json);
expect(checksum.toJson(), '{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07c"}');
});
});
group('operator ==', () {
test('reports not equal if checksums do not match', () async {
final Checksum a = new Checksum.fromJson('{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07c"}');
final Checksum b = new Checksum.fromJson('{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07d"}');
expect(a == b, isFalse);
});
test('reports not equal if keys do not match', () async {
final Checksum a = new Checksum.fromJson('{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07c"}');
final Checksum b = new Checksum.fromJson('{"a.dart":"8a21a15fad560b799f6731d436c1b698","c.dart":"6f144e08b58cd0925328610fad7ac07c"}');
expect(a == b, isFalse);
});
test('reports equal if all checksums match', () async {
final Checksum a = new Checksum.fromJson('{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07c"}');
final Checksum b = new Checksum.fromJson('{"a.dart":"8a21a15fad560b799f6731d436c1b698","b.dart":"6f144e08b58cd0925328610fad7ac07c"}');
expect(a == b, isTrue);
});
});
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment