Reland change that speeds up multiple devices hot-reload (#23695)

* Revert "Revert "Run reload asynchronously so that multiple devices can reload in parallel. (#22693)" (#23598)"

This reverts commit 0b68068d6a33f35cec568ae6a9b394ff2a9fdb76.

* Fix refreshViews so it sends app-wide(rather than per-isolate) service request.

Sending per-isolate request caused dead-lock in the engine in case of more-than-one ui isolate.
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..d248d3f 100644
--- a/packages/flutter_tools/lib/src/vmservice.dart
+++ b/packages/flutter_tools/lib/src/vmservice.dart
@@ -949,15 +949,12 @@
     return invokeRpcRaw('_getVMTimeline', timeout: kLongRequestTimeout);
   }
 
-  Future<void> refreshViews() async {
+  Future<void> refreshViews() {
     if (!isFlutterEngine)
-      return;
+      return null;
     _viewCache.clear();
-    for (Isolate isolate in isolates.toList()) {
-      await vmService.vm.invokeRpc<ServiceObject>('_flutter.listViews',
-          timeout: kLongRequestTimeout,
-          params: <String, dynamic> {'isolateId': isolate.id});
-    }
+    // Send one per-application request that refreshes all views in the app.
+    return vmService.vm.invokeRpc<ServiceObject>('_flutter.listViews', timeout: kLongRequestTimeout);
   }
 
   Iterable<FlutterView> get views => _viewCache.values;
@@ -1226,15 +1223,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 +1285,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 +1306,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,