mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Run reload asynchronously so that multiple devices can reload in parallel. (#22693)
* Run reload asynchronously so that multiple devices can reload in parallel.
This commit is contained in:
parent
53308465c7
commit
709f54f4bb
@ -79,11 +79,15 @@ class FlutterDevice {
|
||||
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 {
|
||||
|
||||
@ -41,6 +41,13 @@ HotRunnerConfig get hotRunnerConfig => context[HotRunnerConfig];
|
||||
|
||||
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 @@ class HotRunner extends ResidentRunner {
|
||||
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 @@ 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<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 @@ class HotRunner extends ResidentRunner {
|
||||
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 @@ class HotRunner extends ResidentRunner {
|
||||
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 @@ class HotRunner extends ResidentRunner {
|
||||
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 @@ class HotRunner extends ResidentRunner {
|
||||
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 @@ class HotRunner extends ResidentRunner {
|
||||
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 @@ class HotRunner extends ResidentRunner {
|
||||
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);
|
||||
|
||||
@ -949,15 +949,19 @@ class VM extends ServiceObjectOwner {
|
||||
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 @@ 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<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 @@ class Isolate extends ServiceObjectOwner {
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user