Commit ec0f8800 authored by John McCutchan's avatar John McCutchan Committed by GitHub

Hot reload: Only sync Dart sources necessary for startup on first launch (#5333)

- [x] Update engine to bring in new snapshotter.
- [x] Use the new snapshotter to quickly determine the minimal set of files necessary to run.
- [x] On first DevFS sync, only sync files necessary to run the application.
- [x] Fix a DevFS unit test failure.
- [x] Include DevFS tests in all.dart.
parent 996a59aa
9f6294c4f389633b9bfb9bea324337ab5ac49558 607d379c23379ab29948d116ed0e431ef6b7d86a
...@@ -286,13 +286,16 @@ class DevFS { ...@@ -286,13 +286,16 @@ class DevFS {
Future<dynamic> update({ DevFSProgressReporter progressReporter, Future<dynamic> update({ DevFSProgressReporter progressReporter,
AssetBundle bundle, AssetBundle bundle,
bool bundleDirty: false }) async { bool bundleDirty: false,
Set<String> fileFilter}) async {
_reset(); _reset();
printTrace('DevFS: Starting sync from $rootDirectory'); printTrace('DevFS: Starting sync from $rootDirectory');
Status status; Status status;
status = logger.startProgress('Scanning project files...'); status = logger.startProgress('Scanning project files...');
Directory directory = rootDirectory; Directory directory = rootDirectory;
await _scanDirectory(directory, recursive: true); await _scanDirectory(directory,
recursive: true,
fileFilter: fileFilter);
status.stop(showElapsedTime: true); status.stop(showElapsedTime: true);
status = logger.startProgress('Scanning package files...'); status = logger.startProgress('Scanning package files...');
...@@ -310,7 +313,8 @@ class DevFS { ...@@ -310,7 +313,8 @@ class DevFS {
bool packageExists = bool packageExists =
await _scanDirectory(directory, await _scanDirectory(directory,
directoryName: 'packages/$packageName', directoryName: 'packages/$packageName',
recursive: true); recursive: true,
fileFilter: fileFilter);
if (packageExists) { if (packageExists) {
sb ??= new StringBuffer(); sb ??= new StringBuffer();
sb.writeln('$packageName:packages/$packageName'); sb.writeln('$packageName:packages/$packageName');
...@@ -440,7 +444,7 @@ class DevFS { ...@@ -440,7 +444,7 @@ class DevFS {
List<String> ignoredPrefixes = <String>['android/', List<String> ignoredPrefixes = <String>['android/',
'build/', 'build/',
'ios/', 'ios/',
'packages/analyzer']; '.pub/'];
for (String ignoredPrefix in ignoredPrefixes) { for (String ignoredPrefix in ignoredPrefixes) {
if (devicePath.startsWith(ignoredPrefix)) if (devicePath.startsWith(ignoredPrefix))
return true; return true;
...@@ -451,7 +455,8 @@ class DevFS { ...@@ -451,7 +455,8 @@ class DevFS {
Future<bool> _scanDirectory(Directory directory, Future<bool> _scanDirectory(Directory directory,
{String directoryName, {String directoryName,
bool recursive: false, bool recursive: false,
bool ignoreDotFiles: true}) async { bool ignoreDotFiles: true,
Set<String> fileFilter}) async {
String prefix = directoryName; String prefix = directoryName;
if (prefix == null) { if (prefix == null) {
prefix = path.relative(directory.path, from: rootDirectory.path); prefix = path.relative(directory.path, from: rootDirectory.path);
...@@ -472,6 +477,15 @@ class DevFS { ...@@ -472,6 +477,15 @@ class DevFS {
} }
final String devicePath = final String devicePath =
path.join(prefix, path.relative(file.path, from: directory.path)); path.join(prefix, path.relative(file.path, from: directory.path));
if ((fileFilter != null) &&
!fileFilter.contains(devicePath)) {
// Skip files that are not included in the filter.
continue;
}
if (ignoreDotFiles && devicePath.startsWith('.')) {
// Skip directories that start with a dot.
continue;
}
if (!_shouldIgnore(devicePath)) if (!_shouldIgnore(devicePath))
_scanFile(devicePath, file); _scanFile(devicePath, file);
} }
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
...@@ -10,15 +11,18 @@ import 'package:path/path.dart' as path; ...@@ -10,15 +11,18 @@ import 'package:path/path.dart' as path;
import 'application_package.dart'; import 'application_package.dart';
import 'asset.dart'; import 'asset.dart';
import 'base/logger.dart'; import 'base/logger.dart';
import 'base/process.dart';
import 'base/utils.dart'; import 'base/utils.dart';
import 'cache.dart'; import 'cache.dart';
import 'commands/build_apk.dart'; import 'commands/build_apk.dart';
import 'commands/install.dart'; import 'commands/install.dart';
import 'dart/package_map.dart';
import 'device.dart'; import 'device.dart';
import 'globals.dart'; import 'globals.dart';
import 'devfs.dart'; import 'devfs.dart';
import 'observatory.dart'; import 'observatory.dart';
import 'resident_runner.dart'; import 'resident_runner.dart';
import 'toolchain.dart';
String getDevFSLoaderScript() { String getDevFSLoaderScript() {
return path.absolute(path.join(Cache.flutterRoot, return path.absolute(path.join(Cache.flutterRoot,
...@@ -29,6 +33,51 @@ String getDevFSLoaderScript() { ...@@ -29,6 +33,51 @@ String getDevFSLoaderScript() {
'loader_app.dart')); 'loader_app.dart'));
} }
class StartupDependencySetBuilder {
StartupDependencySetBuilder(this.mainScriptPath,
this.projectRootPath);
final String mainScriptPath;
final String projectRootPath;
Set<String> build() {
final String skySnapshotPath =
ToolConfiguration.instance.getHostToolPath(HostTool.SkySnapshot);
final List<String> args = <String>[
skySnapshotPath,
'--packages=${path.absolute(PackageMap.globalPackagesPath)}',
'--print-deps',
mainScriptPath
];
String output;
try {
output = runCheckedSync(args);
} catch (e) {
return null;
}
final List<String> lines = LineSplitter.split(output).toList();
final Set<String> minimalDependencies = new Set<String>();
for (String line in lines) {
// We need to convert the uris so that they are relative to the project
// root and tweak package: uris so that they reflect their devFS location.
if (line.startsWith('package:')) {
// Swap out package: for packages/ because we place all package sources
// under packages/.
line = line.replaceFirst('package:', 'packages/');
} else {
// Ensure paths are relative to the project root.
line = path.relative(line, from: projectRootPath);
}
minimalDependencies.add(line);
}
return minimalDependencies;
}
}
class FirstFrameTimer { class FirstFrameTimer {
FirstFrameTimer(this.serviceProtocol); FirstFrameTimer(this.serviceProtocol);
...@@ -81,6 +130,7 @@ class HotRunner extends ResidentRunner { ...@@ -81,6 +130,7 @@ class HotRunner extends ResidentRunner {
ApplicationPackage _package; ApplicationPackage _package;
String _mainPath; String _mainPath;
String _projectRootPath; String _projectRootPath;
Set<String> _startupDependencies;
final AssetBundle bundle = new AssetBundle(); final AssetBundle bundle = new AssetBundle();
final File pipe; final File pipe;
...@@ -194,7 +244,8 @@ class HotRunner extends ResidentRunner { ...@@ -194,7 +244,8 @@ class HotRunner extends ResidentRunner {
printStatus('Launching ${getDisplayPath(_mainPath)} on ${device.name}...'); printStatus('Launching ${getDisplayPath(_mainPath)} on ${device.name}...');
} }
LaunchResult result = await device.startApp( // Start the loader.
Future<LaunchResult> futureResult = device.startApp(
_package, _package,
debuggingOptions.buildMode, debuggingOptions.buildMode,
mainPath: device.needsDevFS ? getDevFSLoaderScript() : _mainPath, mainPath: device.needsDevFS ? getDevFSLoaderScript() : _mainPath,
...@@ -203,6 +254,17 @@ class HotRunner extends ResidentRunner { ...@@ -203,6 +254,17 @@ class HotRunner extends ResidentRunner {
route: route route: route
); );
// In parallel, compute the minimal dependency set.
StartupDependencySetBuilder startupDependencySetBuilder =
new StartupDependencySetBuilder(_mainPath, _projectRootPath);
_startupDependencies = startupDependencySetBuilder.build();
if (_startupDependencies == null) {
printError('Error determining the set of Dart sources necessary to start '
'the application. Initial file upload may take a long time.');
}
LaunchResult result = await futureResult;
if (!result.started) { if (!result.started) {
if (device.needsDevFS) { if (device.needsDevFS) {
printError('Error launching DevFS loader on ${device.name}.'); printError('Error launching DevFS loader on ${device.name}.');
...@@ -243,6 +305,10 @@ class HotRunner extends ResidentRunner { ...@@ -243,6 +305,10 @@ class HotRunner extends ResidentRunner {
registerSignalHandlers(); registerSignalHandlers();
printStatus('Finishing file synchronization...');
// Finish the file sync now.
await _updateDevFS();
return await waitForAppToFinish(); return await waitForAppToFinish();
} }
...@@ -298,8 +364,11 @@ class HotRunner extends ResidentRunner { ...@@ -298,8 +364,11 @@ class HotRunner extends ResidentRunner {
Status devFSStatus = logger.startProgress('Syncing files to device...'); Status devFSStatus = logger.startProgress('Syncing files to device...');
await _devFS.update(progressReporter: progressReporter, await _devFS.update(progressReporter: progressReporter,
bundle: bundle, bundle: bundle,
bundleDirty: rebuildBundle); bundleDirty: rebuildBundle,
fileFilter: _startupDependencies);
devFSStatus.stop(showElapsedTime: true); devFSStatus.stop(showElapsedTime: true);
// Clear the minimal set after the first sync.
_startupDependencies = null;
if (progressReporter != null) if (progressReporter != null)
printStatus('Synced ${getSizeAsMB(_devFS.bytes)}.'); printStatus('Synced ${getSizeAsMB(_devFS.bytes)}.');
else else
......
...@@ -21,6 +21,7 @@ import 'config_test.dart' as config_test; ...@@ -21,6 +21,7 @@ import 'config_test.dart' as config_test;
import 'context_test.dart' as context_test; import 'context_test.dart' as context_test;
import 'create_test.dart' as create_test; import 'create_test.dart' as create_test;
import 'daemon_test.dart' as daemon_test; import 'daemon_test.dart' as daemon_test;
import 'devfs_test.dart' as devfs_test;
import 'device_test.dart' as device_test; import 'device_test.dart' as device_test;
// import 'devices_test.dart' as devices_test; // import 'devices_test.dart' as devices_test;
import 'drive_test.dart' as drive_test; import 'drive_test.dart' as drive_test;
...@@ -51,6 +52,7 @@ void main() { ...@@ -51,6 +52,7 @@ void main() {
context_test.main(); context_test.main();
create_test.main(); create_test.main();
daemon_test.main(); daemon_test.main();
devfs_test.main();
device_test.main(); device_test.main();
// devices_test.main(); // https://github.com/flutter/flutter/issues/4480 // devices_test.main(); // https://github.com/flutter/flutter/issues/4480
drive_test.main(); drive_test.main();
......
...@@ -58,17 +58,17 @@ void main() { ...@@ -58,17 +58,17 @@ void main() {
expect(devFSOperations.contains('deleteFile test bar/foo.txt'), isTrue); expect(devFSOperations.contains('deleteFile test bar/foo.txt'), isTrue);
}); });
testUsingContext('add file in an asset bundle', () async { testUsingContext('add file in an asset bundle', () async {
await devFS.update(bundle: assetBundle); await devFS.update(bundle: assetBundle, bundleDirty: true);
expect(devFSOperations.contains('writeFile test build/flx/a.txt'), isTrue); expect(devFSOperations.contains('writeFile test build/flx/a.txt'), isTrue);
}); });
testUsingContext('add a file to the asset bundle', () async { testUsingContext('add a file to the asset bundle', () async {
assetBundle.entries.add(new AssetBundleEntry.fromString('b.txt', '')); assetBundle.entries.add(new AssetBundleEntry.fromString('b.txt', ''));
await devFS.update(bundle: assetBundle); await devFS.update(bundle: assetBundle, bundleDirty: true);
expect(devFSOperations.contains('writeFile test build/flx/b.txt'), isTrue); expect(devFSOperations.contains('writeFile test build/flx/b.txt'), isTrue);
}); });
testUsingContext('delete a file from the asset bundle', () async { testUsingContext('delete a file from the asset bundle', () async {
assetBundle.entries.clear(); assetBundle.entries.clear();
await devFS.update(bundle: assetBundle); await devFS.update(bundle: assetBundle, bundleDirty: true);
expect(devFSOperations.contains('deleteFile test build/flx/b.txt'), isTrue); expect(devFSOperations.contains('deleteFile test build/flx/b.txt'), isTrue);
}); });
testUsingContext('delete dev file system', () async { testUsingContext('delete dev file system', () async {
......
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