diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 160a9cfd472..9333febc73e 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -79,11 +79,15 @@ class FlutterDevice { vmServices = localVmServices; } - Future refreshViews() async { + Future refreshViews() { if (vmServices == null || vmServices.isEmpty) - return; + return Future.value(null); + final List> futures = >[]; for (VMService service in vmServices) - await service.vm.refreshViews(); + futures.add(service.vm.refreshViews()); + final Completer completer = Completer(); + Future.wait(futures).whenComplete(() => completer.complete(null)); // ignore: unawaited_futures + return completer.future; } List get views { diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index 3ac58e91d13..54918085e24 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -41,6 +41,13 @@ HotRunnerConfig get hotRunnerConfig => context[HotRunnerConfig]; const bool kHotReloadDefault = true; +class DeviceReloadReport { + DeviceReloadReport(this.device, this.reports); + + FlutterDevice device; + List> reports; // List has one report per Flutter view. +} + // TODO(flutter/flutter#23031): Test this. class HotRunner extends ResidentRunner { HotRunner( @@ -320,8 +327,9 @@ class HotRunner extends ResidentRunner { return false; } + final List results = []; 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 @@ class HotRunner extends ResidentRunner { 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 @@ class HotRunner extends ResidentRunner { return true; } - Future _evictDirtyAssets() async { + Future _evictDirtyAssets() { + final List>> futures = >>[]; 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>(futures); } void _resetDirtyAssets() { @@ -361,46 +375,59 @@ class HotRunner extends ResidentRunner { device.devFS.assetPathsToEvict.clear(); } - Future _cleanupDevFS() async { + Future _cleanupDevFS() { + final List> futures = >[]; 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 completer = Completer(); + Future.wait(futures).whenComplete(() { completer.complete(null); } ); // ignore: unawaited_futures + return completer.future; } Future _launchInView(FlutterDevice device, Uri entryUri, Uri packagesUri, - Uri assetsDirectoryUri) async { + Uri assetsDirectoryUri) { + final List> futures = >[]; for (FlutterView view in device.views) - await view.runFromSource(entryUri, packagesUri, assetsDirectoryUri); + futures.add(view.runFromSource(entryUri, packagesUri, assetsDirectoryUri)); + final Completer completer = Completer(); + Future.wait(futures).whenComplete(() { completer.complete(null); }); // ignore: unawaited_futures + return completer.future; } Future _launchFromDevFS(String mainScript) async { final String entryUri = fs.path.relative(mainScript, from: projectRootPath); + final List> futures = >[]; 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 _restartFromSources({ String reason }) async { @@ -433,19 +460,24 @@ class HotRunner extends ResidentRunner { device.generator.accept(); } // Check if the isolate is paused and resume it. + final List> futures = >[]; 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 completer = Completer(); + 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 @@ class HotRunner extends ResidentRunner { getReloadPath(fullRestart: false), from: projectRootPath, ); - final Completer> retrieveFirstReloadReport = Completer>(); - - int countExpectedReports = 0; + final List> allReportsFutures = >[]; for (FlutterDevice device in flutterDevices) { if (_runningFromSnapshot) { // Asset directory has to be set only once when we switch from @@ -590,51 +620,36 @@ class HotRunner extends ResidentRunner { await device.resetAssetDirectory(); } - // List has one report per Flutter view. - final List>> reports = device.reloadSources( - entryPath, - pause: pause + final Completer completer = Completer(); + allReportsFutures.add(completer.future); + final List>> reportFutures = device.reloadSources( + entryPath, pause: pause ); - countExpectedReports += reports.length; - await Future - .wait>(reports) - .catchError((dynamic error) { - return >[error]; - }) - .then( - (List> list) { - // TODO(aam): Investigate why we are validating only first reload report, - // which seems to be current behavior - final Map 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> reports) { // ignore: unawaited_futures + // TODO(aam): Investigate why we are validating only first reload report, + // which seems to be current behavior + final Map 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 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 reports = await Future.wait(allReportsFutures); + for (DeviceReloadReport report in reports) { + final Map 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 catch (error, st) { printError('Hot reload failed: $error\n$st'); @@ -661,14 +676,22 @@ class HotRunner extends ResidentRunner { vmReloadTimer.elapsed.inMilliseconds); final Stopwatch reassembleTimer = Stopwatch()..start(); // Reload the isolate. + final List> allDevices = >[]; for (FlutterDevice device in flutterDevices) { printTrace('Sending reload events to ${device.device.name}'); + final List> futuresViews = >[]; 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 deviceCompleter = Completer(); + 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 @@ class HotRunner extends ResidentRunner { printTrace('Reassembling application'); bool reassembleAndScheduleErrors = false; bool reassembleTimedOut = false; + final List> futures = >[]; 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 bc1586f724b..4b8c6c48e29 100644 --- a/packages/flutter_tools/lib/src/vmservice.dart +++ b/packages/flutter_tools/lib/src/vmservice.dart @@ -949,15 +949,19 @@ class VM extends ServiceObjectOwner { return invokeRpcRaw('_getVMTimeline', timeout: kLongRequestTimeout); } - Future refreshViews() async { + Future refreshViews() { if (!isFlutterEngine) - return; + return Future.value(null); _viewCache.clear(); + final Completer completer = Completer(); + final List> futures = >[]; for (Isolate isolate in isolates.toList()) { - await vmService.vm.invokeRpc('_flutter.listViews', + futures.add(vmService.vm.invokeRpc('_flutter.listViews', timeout: kLongRequestTimeout, - params: {'isolateId': isolate.id}); + params: {'isolateId': isolate.id})); } + Future.wait(futures).whenComplete(() => completer.complete(null)); // ignore: unawaited_futures + return completer.future; } Iterable get views => _viewCache.values; @@ -1226,15 +1230,15 @@ class Isolate extends ServiceObjectOwner { 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 @@ class Isolate extends ServiceObjectOwner { } // Reload related extension methods. - Future> flutterReassemble() async { - return await invokeFlutterExtensionRpcRaw( + Future> flutterReassemble() { + return invokeFlutterExtensionRpcRaw( 'ext.flutter.reassemble', timeout: kShortRequestTimeout, timeoutFatal: true, ); } - Future> uiWindowScheduleFrame() async { - return await invokeFlutterExtensionRpcRaw('ext.ui.window.scheduleFrame'); + Future> uiWindowScheduleFrame() { + return invokeFlutterExtensionRpcRaw('ext.ui.window.scheduleFrame'); } - Future> flutterEvictAsset(String assetPath) async { - return await invokeFlutterExtensionRpcRaw('ext.flutter.evict', + Future> flutterEvictAsset(String assetPath) { + return invokeFlutterExtensionRpcRaw('ext.flutter.evict', params: { 'value': assetPath, } @@ -1309,8 +1313,8 @@ class Isolate extends ServiceObjectOwner { } // Application control extension methods. - Future> flutterExit() async { - return await invokeFlutterExtensionRpcRaw( + Future> flutterExit() { + return invokeFlutterExtensionRpcRaw( 'ext.flutter.exit', timeout: const Duration(seconds: 2), timeoutFatal: false,