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.
diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart
new file mode 100644
index 0000000..6b48ea6
--- /dev/null
+++ b/packages/flutter_tools/lib/src/base/build.dart
@@ -0,0 +1,44 @@
+// 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;
+}
diff --git a/packages/flutter_tools/lib/src/flx.dart b/packages/flutter_tools/lib/src/flx.dart
index d9ebce1..996b302 100644
--- a/packages/flutter_tools/lib/src/flx.dart
+++ b/packages/flutter_tools/lib/src/flx.dart
@@ -8,6 +8,7 @@
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 @@
@required String snapshotPath,
@required String depfilePath,
@required String packages
-}) {
+}) async {
assert(mainPath != null);
assert(snapshotPath != null);
assert(depfilePath != null);
@@ -53,8 +54,64 @@
'--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({
diff --git a/packages/flutter_tools/test/base/build_test.dart b/packages/flutter_tools/test/base/build_test.dart
new file mode 100644
index 0000000..ae1682a
--- /dev/null
+++ b/packages/flutter_tools/test/base/build_test.dart
@@ -0,0 +1,67 @@
+// 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);
+ });
+ });
+ });
+}