Unverified Commit 5fc6b871 authored by Alexander Aprelev's avatar Alexander Aprelev Committed by GitHub

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 0b68068d.

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