Run reload asynchronously so that multiple devices can reload in parallel. (#22693)
* Run reload asynchronously so that multiple devices can reload in parallel.
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index 160a9cf..9333feb 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -79,11 +79,15 @@
vmServices = localVmServices;
}
- Future<void> refreshViews() async {
+ Future<void> refreshViews() {
if (vmServices == null || vmServices.isEmpty)
- return;
+ return Future<void>.value(null);
+ final List<Future<void>> futures = <Future<void>>[];
for (VMService service in vmServices)
- await service.vm.refreshViews();
+ futures.add(service.vm.refreshViews());
+ final Completer<void> completer = Completer<void>();
+ Future.wait(futures).whenComplete(() => completer.complete(null)); // ignore: unawaited_futures
+ return completer.future;
}
List<FlutterView> get views {
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index 3ac58e9..5491808 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -41,6 +41,13 @@
const bool kHotReloadDefault = true;
+class DeviceReloadReport {
+ DeviceReloadReport(this.device, this.reports);
+
+ FlutterDevice device;
+ List<Map<String, dynamic>> reports; // List has one report per Flutter view.
+}
+
// TODO(flutter/flutter#23031): Test this.
class HotRunner extends ResidentRunner {
HotRunner(
@@ -320,8 +327,9 @@
return false;
}
+ final List<bool> results = <bool>[];
for (FlutterDevice device in flutterDevices) {
- final bool result = await device.updateDevFS(
+ results.add(await device.updateDevFS(
mainPath: mainPath,
target: target,
bundle: assetBundle,
@@ -332,9 +340,11 @@
fullRestart: fullRestart,
projectRootPath: projectRootPath,
pathToReload: getReloadPath(fullRestart: fullRestart),
- );
- if (!result)
- return false;
+ ));
+ }
+ // If there any failures reported, bail out.
+ if (results.any((bool result) => !result)) {
+ return false;
}
if (!hotRunnerConfig.stableDartDependencies) {
@@ -344,16 +354,20 @@
return true;
}
- Future<void> _evictDirtyAssets() async {
+ Future<void> _evictDirtyAssets() {
+ final List<Future<Map<String, dynamic>>> futures = <Future<Map<String, dynamic>>>[];
for (FlutterDevice device in flutterDevices) {
if (device.devFS.assetPathsToEvict.isEmpty)
- return;
- if (device.views.first.uiIsolate == null)
- throw 'Application isolate not found';
+ continue;
+ if (device.views.first.uiIsolate == null) {
+ printError('Application isolate not found for $device');
+ continue;
+ }
for (String assetPath in device.devFS.assetPathsToEvict)
- await device.views.first.uiIsolate.flutterEvictAsset(assetPath);
+ futures.add(device.views.first.uiIsolate.flutterEvictAsset(assetPath));
device.devFS.assetPathsToEvict.clear();
}
+ return Future.wait<Map<String, dynamic>>(futures);
}
void _resetDirtyAssets() {
@@ -361,46 +375,59 @@
device.devFS.assetPathsToEvict.clear();
}
- Future<void> _cleanupDevFS() async {
+ Future<void> _cleanupDevFS() {
+ final List<Future<void>> futures = <Future<void>>[];
for (FlutterDevice device in flutterDevices) {
if (device.devFS != null) {
// Cleanup the devFS; don't wait indefinitely, and ignore any errors.
- await device.devFS.destroy()
+ futures.add(device.devFS.destroy()
.timeout(const Duration(milliseconds: 250))
.catchError((dynamic error) {
printTrace('$error');
- });
+ }));
}
device.devFS = null;
}
+ final Completer<void> completer = Completer<void>();
+ Future.wait(futures).whenComplete(() { completer.complete(null); } ); // ignore: unawaited_futures
+ return completer.future;
}
Future<void> _launchInView(FlutterDevice device,
Uri entryUri,
Uri packagesUri,
- Uri assetsDirectoryUri) async {
+ Uri assetsDirectoryUri) {
+ final List<Future<void>> futures = <Future<void>>[];
for (FlutterView view in device.views)
- await view.runFromSource(entryUri, packagesUri, assetsDirectoryUri);
+ futures.add(view.runFromSource(entryUri, packagesUri, assetsDirectoryUri));
+ final Completer<void> completer = Completer<void>();
+ Future.wait(futures).whenComplete(() { completer.complete(null); }); // ignore: unawaited_futures
+ return completer.future;
}
Future<void> _launchFromDevFS(String mainScript) async {
final String entryUri = fs.path.relative(mainScript, from: projectRootPath);
+ final List<Future<void>> futures = <Future<void>>[];
for (FlutterDevice device in flutterDevices) {
final Uri deviceEntryUri = device.devFS.baseUri.resolveUri(
fs.path.toUri(entryUri));
final Uri devicePackagesUri = device.devFS.baseUri.resolve('.packages');
final Uri deviceAssetsDirectoryUri = device.devFS.baseUri.resolveUri(
fs.path.toUri(getAssetBuildDirectory()));
- await _launchInView(device,
+ futures.add(_launchInView(device,
deviceEntryUri,
devicePackagesUri,
- deviceAssetsDirectoryUri);
- if (benchmarkMode) {
- for (FlutterDevice device in flutterDevices)
- for (FlutterView view in device.views)
- await view.flushUIThreadTasks();
- }
+ deviceAssetsDirectoryUri));
}
+ await Future.wait(futures);
+ if (benchmarkMode) {
+ futures.clear();
+ for (FlutterDevice device in flutterDevices)
+ for (FlutterView view in device.views)
+ futures.add(view.flushUIThreadTasks());
+ await Future.wait(futures);
+ }
+
}
Future<OperationResult> _restartFromSources({ String reason }) async {
@@ -433,19 +460,24 @@
device.generator.accept();
}
// Check if the isolate is paused and resume it.
+ final List<Future<void>> futures = <Future<void>>[];
for (FlutterDevice device in flutterDevices) {
for (FlutterView view in device.views) {
if (view.uiIsolate != null) {
// Reload the isolate.
- await view.uiIsolate.reload();
- final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
- if ((pauseEvent != null) && pauseEvent.isPauseEvent) {
- // Resume the isolate so that it can be killed by the embedder.
- await view.uiIsolate.resume();
- }
+ final Completer<void> completer = Completer<void>();
+ futures.add(completer.future);
+ view.uiIsolate.reload().then((ServiceObject _) { // ignore: unawaited_futures
+ final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
+ if ((pauseEvent != null) && pauseEvent.isPauseEvent) {
+ // Resume the isolate so that it can be killed by the embedder.
+ return view.uiIsolate.resume();
+ }
+ }).whenComplete(() { completer.complete(null); });
}
}
}
+ await Future.wait(futures);
// We are now running from source.
_runningFromSnapshot = false;
await _launchFromDevFS(mainPath + '.dill');
@@ -580,9 +612,7 @@
getReloadPath(fullRestart: false),
from: projectRootPath,
);
- final Completer<Map<String, dynamic>> retrieveFirstReloadReport = Completer<Map<String, dynamic>>();
-
- int countExpectedReports = 0;
+ final List<Future<DeviceReloadReport>> allReportsFutures = <Future<DeviceReloadReport>>[];
for (FlutterDevice device in flutterDevices) {
if (_runningFromSnapshot) {
// Asset directory has to be set only once when we switch from
@@ -590,51 +620,36 @@
await device.resetAssetDirectory();
}
- // List has one report per Flutter view.
- final List<Future<Map<String, dynamic>>> reports = device.reloadSources(
- entryPath,
- pause: pause
+ final Completer<DeviceReloadReport> completer = Completer<DeviceReloadReport>();
+ allReportsFutures.add(completer.future);
+ final List<Future<Map<String, dynamic>>> reportFutures = device.reloadSources(
+ entryPath, pause: pause
);
- countExpectedReports += reports.length;
- await Future
- .wait<Map<String, dynamic>>(reports)
- .catchError((dynamic error) {
- return <Map<String, dynamic>>[error];
- })
- .then<void>(
- (List<Map<String, dynamic>> list) {
- // TODO(aam): Investigate why we are validating only first reload report,
- // which seems to be current behavior
- final Map<String, dynamic> firstReport = list.first;
- // Don't print errors because they will be printed further down when
- // `validateReloadReport` is called again.
- device.updateReloadStatus(
- validateReloadReport(firstReport, printErrors: false)
- );
- if (!retrieveFirstReloadReport.isCompleted)
- retrieveFirstReloadReport.complete(firstReport);
- },
- onError: (dynamic error, StackTrace stack) {
- retrieveFirstReloadReport.completeError(error, stack);
- },
- );
+ Future.wait(reportFutures).then((List<Map<String, dynamic>> reports) { // ignore: unawaited_futures
+ // TODO(aam): Investigate why we are validating only first reload report,
+ // which seems to be current behavior
+ final Map<String, dynamic> firstReport = reports.first;
+ // Don't print errors because they will be printed further down when
+ // `validateReloadReport` is called again.
+ device.updateReloadStatus(validateReloadReport(firstReport, printErrors: false));
+ completer.complete(DeviceReloadReport(device, reports));
+ });
}
- if (countExpectedReports == 0) {
- printError('Unable to hot reload. No instance of Flutter is currently running.');
- return OperationResult(1, 'No instances running');
- }
- final Map<String, dynamic> reloadReport = await retrieveFirstReloadReport.future;
- if (!validateReloadReport(reloadReport)) {
- // Reload failed.
- flutterUsage.sendEvent('hot', 'reload-reject');
- return OperationResult(1, 'Reload rejected');
- } else {
- flutterUsage.sendEvent('hot', 'reload', parameters: analyticsParameters);
- final int loadedLibraryCount = reloadReport['details']['loadedLibraryCount'];
- final int finalLibraryCount = reloadReport['details']['finalLibraryCount'];
- printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries');
- reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries';
+ final List<DeviceReloadReport> reports = await Future.wait(allReportsFutures);
+ for (DeviceReloadReport report in reports) {
+ final Map<String, dynamic> reloadReport = report.reports[0];
+ if (!validateReloadReport(reloadReport)) {
+ // Reload failed.
+ flutterUsage.sendEvent('hot', 'reload-reject');
+ return OperationResult(1, 'Reload rejected');
+ } else {
+ flutterUsage.sendEvent('hot', 'reload', parameters: analyticsParameters);
+ final int loadedLibraryCount = reloadReport['details']['loadedLibraryCount'];
+ final int finalLibraryCount = reloadReport['details']['finalLibraryCount'];
+ printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries');
+ reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries';
+ }
}
} on Map<String, dynamic> catch (error, st) {
printError('Hot reload failed: $error\n$st');
@@ -661,14 +676,22 @@
vmReloadTimer.elapsed.inMilliseconds);
final Stopwatch reassembleTimer = Stopwatch()..start();
// Reload the isolate.
+ final List<Future<void>> allDevices = <Future<void>>[];
for (FlutterDevice device in flutterDevices) {
printTrace('Sending reload events to ${device.device.name}');
+ final List<Future<ServiceObject>> futuresViews = <Future<ServiceObject>>[];
for (FlutterView view in device.views) {
printTrace('Sending reload event to "${view.uiIsolate.name}"');
- await view.uiIsolate.reload();
+ futuresViews.add(view.uiIsolate.reload());
}
- await device.refreshViews();
+ final Completer<void> deviceCompleter = Completer<void>();
+ Future.wait(futuresViews).whenComplete(() { // ignore: unawaited_futures
+ deviceCompleter.complete(device.refreshViews());
+ });
+ allDevices.add(deviceCompleter.future);
}
+
+ await Future.wait(allDevices);
// We are now running from source.
_runningFromSnapshot = false;
// Check if the isolate is paused.
@@ -694,27 +717,23 @@
printTrace('Reassembling application');
bool reassembleAndScheduleErrors = false;
bool reassembleTimedOut = false;
+ final List<Future<void>> futures = <Future<void>>[];
for (FlutterView view in reassembleViews) {
- try {
- await view.uiIsolate.flutterReassemble();
- } on TimeoutException {
- reassembleTimedOut = true;
- printTrace('Reassembling ${view.uiIsolate.name} took too long.');
- printStatus('Hot reloading ${view.uiIsolate.name} took too long; the reload may have failed.');
- continue;
- } catch (error) {
- reassembleAndScheduleErrors = true;
- printError('Reassembling ${view.uiIsolate.name} failed: $error');
- continue;
- }
- try {
- /* ensure that a frame is scheduled */
- await view.uiIsolate.uiWindowScheduleFrame();
- } catch (error) {
- reassembleAndScheduleErrors = true;
- printError('Scheduling a frame for ${view.uiIsolate.name} failed: $error');
- }
+ futures.add(view.uiIsolate.flutterReassemble().then((_) {
+ return view.uiIsolate.uiWindowScheduleFrame();
+ }).catchError((dynamic error) {
+ if (error is TimeoutException) {
+ reassembleTimedOut = true;
+ printTrace('Reassembling ${view.uiIsolate.name} took too long.');
+ printStatus('Hot reloading ${view.uiIsolate.name} took too long; the reload may have failed.');
+ } else {
+ reassembleAndScheduleErrors = true;
+ printError('Reassembling ${view.uiIsolate.name} failed: $error');
+ }
+ }));
}
+ await Future.wait(futures);
+
// Record time it took for Flutter to reassemble the application.
_addBenchmarkData('hotReloadFlutterReassembleMilliseconds',
reassembleTimer.elapsed.inMilliseconds);
diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart
index bc1586f..4b8c6c4 100644
--- a/packages/flutter_tools/lib/src/vmservice.dart
+++ b/packages/flutter_tools/lib/src/vmservice.dart
@@ -949,15 +949,19 @@
return invokeRpcRaw('_getVMTimeline', timeout: kLongRequestTimeout);
}
- Future<void> refreshViews() async {
+ Future<void> refreshViews() {
if (!isFlutterEngine)
- return;
+ return Future<void>.value(null);
_viewCache.clear();
+ final Completer<void> completer = Completer<void>();
+ final List<Future<ServiceObject>> futures = <Future<ServiceObject>>[];
for (Isolate isolate in isolates.toList()) {
- await vmService.vm.invokeRpc<ServiceObject>('_flutter.listViews',
+ futures.add(vmService.vm.invokeRpc<ServiceObject>('_flutter.listViews',
timeout: kLongRequestTimeout,
- params: <String, dynamic> {'isolateId': isolate.id});
+ params: <String, dynamic> {'isolateId': isolate.id}));
}
+ Future.wait(futures).whenComplete(() => completer.complete(null)); // ignore: unawaited_futures
+ return completer.future;
}
Iterable<FlutterView> get views => _viewCache.values;
@@ -1226,15 +1230,15 @@
Duration timeout,
bool timeoutFatal = true,
}
- ) async {
- try {
- return await invokeRpcRaw(method, params: params, timeout: timeout, timeoutFatal: timeoutFatal);
- } on rpc.RpcException catch (e) {
- // If an application is not using the framework
- if (e.code == rpc_error_code.METHOD_NOT_FOUND)
- return null;
- rethrow;
- }
+ ) {
+ return invokeRpcRaw(method, params: params, timeout: timeout,
+ timeoutFatal: timeoutFatal).catchError((dynamic error) {
+ if (error is rpc.RpcException) {
+ // If an application is not using the framework
+ if (error.code == rpc_error_code.METHOD_NOT_FOUND)
+ return null;
+ throw error;
+ }});
}
// Debug dump extension methods.
@@ -1288,20 +1292,20 @@
}
// Reload related extension methods.
- Future<Map<String, dynamic>> flutterReassemble() async {
- return await invokeFlutterExtensionRpcRaw(
+ Future<Map<String, dynamic>> flutterReassemble() {
+ return invokeFlutterExtensionRpcRaw(
'ext.flutter.reassemble',
timeout: kShortRequestTimeout,
timeoutFatal: true,
);
}
- Future<Map<String, dynamic>> uiWindowScheduleFrame() async {
- return await invokeFlutterExtensionRpcRaw('ext.ui.window.scheduleFrame');
+ Future<Map<String, dynamic>> uiWindowScheduleFrame() {
+ return invokeFlutterExtensionRpcRaw('ext.ui.window.scheduleFrame');
}
- Future<Map<String, dynamic>> flutterEvictAsset(String assetPath) async {
- return await invokeFlutterExtensionRpcRaw('ext.flutter.evict',
+ Future<Map<String, dynamic>> flutterEvictAsset(String assetPath) {
+ return invokeFlutterExtensionRpcRaw('ext.flutter.evict',
params: <String, dynamic>{
'value': assetPath,
}
@@ -1309,8 +1313,8 @@
}
// Application control extension methods.
- Future<Map<String, dynamic>> flutterExit() async {
- return await invokeFlutterExtensionRpcRaw(
+ Future<Map<String, dynamic>> flutterExit() {
+ return invokeFlutterExtensionRpcRaw(
'ext.flutter.exit',
timeout: const Duration(seconds: 2),
timeoutFatal: false,