diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index f13572722f2..21bf77c28b2 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -110,7 +110,6 @@ class HotRunner extends ResidentRunner { final Map> benchmarkData = >{}; DateTime firstBuildTime; - bool _shouldResetAssetDirectory = true; void _addBenchmarkData(String name, int value) { benchmarkData[name] ??= []; @@ -205,7 +204,7 @@ class HotRunner extends ResidentRunner { ), ); } - } on Exception catch (error) { + } on DevFSException catch (error) { globals.printError('Error initializing DevFS: $error'); return 3; } @@ -226,6 +225,14 @@ class HotRunner extends ResidentRunner { device.generator.accept(); } final List views = await device.vmService.getFlutterViews(); + final Uri deviceAssetsDirectoryUri = device.devFS.baseUri.resolveUri(globals.fs.path.toUri(getAssetBuildDirectory())); + await Future.wait(views.map>( + (FlutterView view) => device.vmService.setAssetDirectory( + assetsDirectory: deviceAssetsDirectoryUri, + uiIsolateId: view.uiIsolate.id, + viewId: view.id, + ) + )); for (final FlutterView view in views) { globals.printTrace('Connected to $view.'); } @@ -729,17 +736,36 @@ class HotRunner extends ResidentRunner { ); }, ); - } on vm_service.RPCError { - HotEvent('exception', - targetPlatform: targetPlatform, - sdkName: sdkName, - emulator: emulator, - fullRestart: false, - nullSafety: usageNullSafety, - reason: reason, - fastReassemble: null, - ).send(); - return OperationResult(1, 'hot reload failed to complete', fatal: true); + } on vm_service.RPCError catch (error) { + String errorMessage = 'hot reload failed to complete'; + int errorCode = 1; + if (error.code == kIsolateReloadBarred) { + errorCode = error.code; + errorMessage = 'Unable to hot reload application due to an unrecoverable error in ' + 'the source code. Please address the error and then use "R" to ' + 'restart the app.\n' + '${error.message} (error code: ${error.code})'; + HotEvent('reload-barred', + targetPlatform: targetPlatform, + sdkName: sdkName, + emulator: emulator, + fullRestart: false, + reason: reason, + nullSafety: usageNullSafety, + fastReassemble: null, + ).send(); + } else { + HotEvent('exception', + targetPlatform: targetPlatform, + sdkName: sdkName, + emulator: emulator, + fullRestart: false, + nullSafety: usageNullSafety, + reason: reason, + fastReassemble: null, + ).send(); + } + return OperationResult(errorCode, errorMessage, fatal: true); } finally { status.cancel(); } @@ -764,20 +790,6 @@ class HotRunner extends ResidentRunner { ]; } - Future _resetAssetDirectory(FlutterDevice device) async { - final Uri deviceAssetsDirectoryUri = device.devFS.baseUri.resolveUri( - globals.fs.path.toUri(getAssetBuildDirectory())); - assert(deviceAssetsDirectoryUri != null); - final List views = await device.vmService.getFlutterViews(); - await Future.wait(views.map>( - (FlutterView view) => device.vmService.setAssetDirectory( - assetsDirectory: deviceAssetsDirectoryUri, - uiIsolateId: view.uiIsolate.id, - viewId: view.id, - ) - )); - } - Future _reloadSources({ String targetPlatform, String sdkName, @@ -786,8 +798,10 @@ class HotRunner extends ResidentRunner { String reason, void Function(String message) onSlow, }) async { + final Map> viewCache = >{}; for (final FlutterDevice device in flutterDevices) { final List views = await device.vmService.getFlutterViews(); + viewCache[device] = views; for (final FlutterView view in views) { if (view.uiIsolate == null) { return OperationResult(2, 'Application isolate not found', fatal: true); @@ -806,74 +820,36 @@ class HotRunner extends ResidentRunner { } String reloadMessage; final Stopwatch vmReloadTimer = Stopwatch()..start(); - Map firstReloadDetails; - try { - const String entryPath = 'main.dart.incremental.dill'; - final List> allReportsFutures = >[]; - for (final FlutterDevice device in flutterDevices) { - if (_shouldResetAssetDirectory) { - // Asset directory has to be set only once when the engine switches from - // running from bundle to uploaded files. - await _resetAssetDirectory(device); - _shouldResetAssetDirectory = false; - } - final List> reportFutures = await _reloadDeviceSources( - device, - entryPath, pause: pause, - ); - allReportsFutures.add(Future.wait(reportFutures).then( - (List reports) async { - // TODO(aam): Investigate why we are validating only first reload report, - // which seems to be current behavior - final vm_service.ReloadReport firstReport = reports.first; - // Don't print errors because they will be printed further down when - // `validateReloadReport` is called again. - await device.updateReloadStatus( - validateReloadReport(firstReport, printErrors: false), - ); - return DeviceReloadReport(device, reports); - }, - )); - } - final List reports = await Future.wait(allReportsFutures); - for (final DeviceReloadReport report in reports) { - final vm_service.ReloadReport reloadReport = report.reports[0]; - if (!validateReloadReport(reloadReport)) { - // Reload failed. - HotEvent('reload-reject', - targetPlatform: targetPlatform, - sdkName: sdkName, - emulator: emulator, - fullRestart: false, - reason: reason, - nullSafety: usageNullSafety, - fastReassemble: null, - ).send(); - // Reset devFS lastCompileTime to ensure the file will still be marked - // as dirty on subsequent reloads. - _resetDevFSCompileTime(); - final ReloadReportContents contents = ReloadReportContents.fromReloadReport(reloadReport); - return OperationResult(1, 'Reload rejected: ${contents.notices.join("\n")}'); - } - // Collect stats only from the first device. If/when run -d all is - // refactored, we'll probably need to send one hot reload/restart event - // per device to analytics. - firstReloadDetails ??= castStringKeyedMap(reloadReport.json['details']); - final int loadedLibraryCount = reloadReport.json['details']['loadedLibraryCount'] as int; - final int finalLibraryCount = reloadReport.json['details']['finalLibraryCount'] as int; - globals.printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries'); - reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries'; - } - } on Map catch (error, stackTrace) { - globals.printTrace('Hot reload failed: $error\n$stackTrace'); - final int errorCode = error['code'] as int; - String errorMessage = error['message'] as String; - if (errorCode == kIsolateReloadBarred) { - errorMessage = 'Unable to hot reload application due to an unrecoverable error in ' - 'the source code. Please address the error and then use "R" to ' - 'restart the app.\n' - '$errorMessage (error code: $errorCode)'; - HotEvent('reload-barred', + Map firstReloadDetails; + const String entryPath = 'main.dart.incremental.dill'; + final List> allReportsFutures = >[]; + + for (final FlutterDevice device in flutterDevices) { + final List> reportFutures = await _reloadDeviceSources( + device, + entryPath, + pause: pause, + ); + allReportsFutures.add(Future.wait(reportFutures).then( + (List reports) async { + // TODO(aam): Investigate why we are validating only first reload report, + // which seems to be current behavior + final vm_service.ReloadReport firstReport = reports.first; + // Don't print errors because they will be printed further down when + // `validateReloadReport` is called again. + await device.updateReloadStatus( + validateReloadReport(firstReport, printErrors: false), + ); + return DeviceReloadReport(device, reports); + }, + )); + } + final List reports = await Future.wait(allReportsFutures); + for (final DeviceReloadReport report in reports) { + final vm_service.ReloadReport reloadReport = report.reports[0]; + if (!validateReloadReport(reloadReport)) { + // Reload failed. + HotEvent('reload-reject', targetPlatform: targetPlatform, sdkName: sdkName, emulator: emulator, @@ -882,27 +858,35 @@ class HotRunner extends ResidentRunner { nullSafety: usageNullSafety, fastReassemble: null, ).send(); - return OperationResult(errorCode, errorMessage); + // Reset devFS lastCompileTime to ensure the file will still be marked + // as dirty on subsequent reloads. + _resetDevFSCompileTime(); + final ReloadReportContents contents = ReloadReportContents.fromReloadReport(reloadReport); + return OperationResult(1, 'Reload rejected: ${contents.notices.join("\n")}'); } - return OperationResult(errorCode, '$errorMessage (error code: $errorCode)'); - } on Exception catch (error, stackTrace) { - globals.printTrace('Hot reload failed: $error\n$stackTrace'); - return OperationResult(1, '$error'); + // Collect stats only from the first device. If/when run -d all is + // refactored, we'll probably need to send one hot reload/restart event + // per device to analytics. + firstReloadDetails ??= castStringKeyedMap(reloadReport.json['details']); + final int loadedLibraryCount = reloadReport.json['details']['loadedLibraryCount'] as int; + final int finalLibraryCount = reloadReport.json['details']['finalLibraryCount'] as int; + globals.printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries'); + reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries'; } + // Record time it took for the VM to reload the sources. _addBenchmarkData('hotReloadVMReloadMilliseconds', vmReloadTimer.elapsed.inMilliseconds); final Stopwatch reassembleTimer = Stopwatch()..start(); await _evictDirtyAssets(); - // Check if any isolates are paused and reassemble those - // that aren't. + // Check if any isolates are paused and reassemble those that aren't. final Map reassembleViews = {}; final List> reassembleFutures = >[]; String serviceEventKind; int pausedIsolatesFound = 0; bool failedReassemble = false; for (final FlutterDevice device in flutterDevices) { - final List views = await device.vmService.getFlutterViews(); + final List views = viewCache[device]; for (final FlutterView view in views) { // Check if the isolate is paused, and if so, don't reassemble. Ignore the // PostPauseEvent event - the client requesting the pause will resume the app. diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart index e51dd65ed1e..d6ef19f4c9f 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart @@ -112,6 +112,15 @@ final FakeVmServiceRequest listViews = FakeVmServiceRequest( }, ); +const FakeVmServiceRequest setAssetBundlePath = FakeVmServiceRequest( + method: '_flutter.setAssetBundlePath', + args: { + 'viewId': 'a', + 'assetDirectory': 'build/flutter_assets', + 'isolateId': '1', + } +); + void main() { final Uri testUri = Uri.parse('foo://bar'); Testbed testbed; @@ -168,7 +177,7 @@ void main() { return UpdateFSReport( success: true, syncedBytes: 0, - invalidatedSourcesCount: 0, + invalidatedSourcesCount: 1, ); }); when(mockFlutterDevice.devFS).thenReturn(mockDevFS); @@ -194,6 +203,7 @@ void main() { fakeVmServiceHost = FakeVmServiceHost(requests: [ listViews, listViews, + setAssetBundlePath, ]); final Completer onConnectionInfo = Completer.sync(); final Completer onAppStart = Completer.sync(); @@ -219,6 +229,7 @@ void main() { fakeVmServiceHost = FakeVmServiceHost(requests: [ listViews, listViews, + setAssetBundlePath, ]); final MockResidentCompiler residentCompiler = MockResidentCompiler(); residentRunner = HotRunner( @@ -345,6 +356,7 @@ void main() { fakeVmServiceHost = FakeVmServiceHost(requests: [ listViews, listViews, + setAssetBundlePath, ]); final MockResidentCompiler residentCompiler = MockResidentCompiler(); residentRunner = HotRunner( @@ -387,6 +399,7 @@ void main() { fakeVmServiceHost = FakeVmServiceHost(requests: [ listViews, listViews, + setAssetBundlePath, listViews, FakeVmServiceRequest( method: 'getIsolate', @@ -465,6 +478,7 @@ void main() { fakeVmServiceHost = FakeVmServiceHost(requests: [ listViews, listViews, + setAssetBundlePath, listViews, ]); when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async { @@ -514,10 +528,66 @@ void main() { Usage: () => MockUsage(), })); + testUsingContext('ResidentRunner can handle an reload-barred exception from hot reload', () => testbed.run(() async { + fakeVmServiceHost = FakeVmServiceHost(requests: [ + listViews, + listViews, + setAssetBundlePath, + listViews, + ]); + when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async { + return 'Example'; + }); + when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async { + return TargetPlatform.android_arm; + }); + when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) async { + return false; + }); + final Completer onConnectionInfo = Completer.sync(); + final Completer onAppStart = Completer.sync(); + unawaited(residentRunner.attach( + appStartedCompleter: onAppStart, + connectionInfoCompleter: onConnectionInfo, + )); + await onAppStart.future; + when(mockFlutterDevice.updateDevFS( + mainUri: anyNamed('mainUri'), + target: anyNamed('target'), + bundle: anyNamed('bundle'), + firstBuildTime: anyNamed('firstBuildTime'), + bundleFirstUpload: anyNamed('bundleFirstUpload'), + bundleDirty: anyNamed('bundleDirty'), + fullRestart: anyNamed('fullRestart'), + projectRootPath: anyNamed('projectRootPath'), + pathToReload: anyNamed('pathToReload'), + invalidatedFiles: anyNamed('invalidatedFiles'), + dillOutputPath: anyNamed('dillOutputPath'), + packageConfig: anyNamed('packageConfig'), + )).thenThrow(vm_service.RPCError('something bad happened', kIsolateReloadBarred, '')); + + final OperationResult result = await residentRunner.restart(fullRestart: false); + expect(result.fatal, true); + expect(result.code, kIsolateReloadBarred); + expect(result.message, contains('Unable to hot reload application due to an unrecoverable error')); + verify(globals.flutterUsage.sendEvent('hot', 'reload-barred', parameters: { + cdKey(CustomDimensions.hotEventTargetPlatform): + getNameForTargetPlatform(TargetPlatform.android_arm), + cdKey(CustomDimensions.hotEventSdkName): 'Example', + cdKey(CustomDimensions.hotEventEmulator): 'false', + cdKey(CustomDimensions.hotEventFullRestart): 'false', + cdKey(CustomDimensions.nullSafety): 'false', + })).called(1); + expect(fakeVmServiceHost.hasRemainingExpectations, false); + }, overrides: { + Usage: () => MockUsage(), + })); + testUsingContext('ResidentRunner reports hot reload event with null safety analytics', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ listViews, listViews, + setAssetBundlePath, listViews, ]); residentRunner = HotRunner( @@ -582,16 +652,8 @@ void main() { fakeVmServiceHost = FakeVmServiceHost(requests: [ listViews, listViews, + setAssetBundlePath, listViews, - listViews, - const FakeVmServiceRequest( - method: '_flutter.setAssetBundlePath', - args: { - 'viewId': 'a', - 'assetDirectory': 'build/flutter_assets', - 'isolateId': '1', - } - ), FakeVmServiceRequest( method: 'getVM', jsonResponse: vm_service.VM.parse({ @@ -615,7 +677,6 @@ void main() { }, }, ), - listViews, FakeVmServiceRequest( method: 'getIsolate', args: { @@ -660,7 +721,7 @@ void main() { dillOutputPath: anyNamed('dillOutputPath'), packageConfig: anyNamed('packageConfig'), )).thenAnswer((Invocation invocation) async { - return UpdateFSReport(success: true); + return UpdateFSReport(success: true, invalidatedSourcesCount: 1); }); final OperationResult result = await residentRunner.restart(fullRestart: false); @@ -675,16 +736,8 @@ void main() { fakeVmServiceHost = FakeVmServiceHost(requests: [ listViews, listViews, + setAssetBundlePath, listViews, - listViews, - const FakeVmServiceRequest( - method: '_flutter.setAssetBundlePath', - args: { - 'viewId': 'a', - 'assetDirectory': 'build/flutter_assets', - 'isolateId': '1', - } - ), FakeVmServiceRequest( method: 'getVM', jsonResponse: vm_service.VM.parse({ @@ -756,7 +809,7 @@ void main() { dillOutputPath: anyNamed('dillOutputPath'), packageConfig: anyNamed('packageConfig'), )).thenAnswer((Invocation invocation) async { - return UpdateFSReport(success: true); + return UpdateFSReport(success: true, invalidatedSourcesCount: 1); }); final OperationResult result = await residentRunner.restart(fullRestart: false); @@ -772,15 +825,7 @@ void main() { listViews, listViews, listViews, - listViews, - const FakeVmServiceRequest( - method: '_flutter.setAssetBundlePath', - args: { - 'viewId': 'a', - 'assetDirectory': 'build/flutter_assets', - 'isolateId': '1', - } - ), + setAssetBundlePath, FakeVmServiceRequest( method: 'getVM', jsonResponse: vm_service.VM.parse({ @@ -804,7 +849,6 @@ void main() { }, }, ), - listViews, FakeVmServiceRequest( method: 'getIsolate', args: { @@ -856,19 +900,11 @@ void main() { ), listViews, listViews, - listViews, - const FakeVmServiceRequest( - method: '_flutter.setAssetBundlePath', - args: { - 'viewId': 'a', - 'assetDirectory': 'build/flutter_assets', - 'isolateId': '1', - } - ), FakeVmServiceRequest( method: 'getVM', jsonResponse: fakeVM.toJson(), ), + setAssetBundlePath, const FakeVmServiceRequest( method: 'reloadSources', args: { @@ -884,7 +920,6 @@ void main() { }, }, ), - listViews, FakeVmServiceRequest( method: 'getIsolate', args: { @@ -938,7 +973,11 @@ void main() { invalidatedFiles: anyNamed('invalidatedFiles'), packageConfig: anyNamed('packageConfig'), )).thenAnswer((Invocation invocation) async { - return UpdateFSReport(success: true, fastReassembleClassName: 'FOO'); + return UpdateFSReport( + success: true, + fastReassembleClassName: 'FOO', + invalidatedSourcesCount: 1, + ); }); final Completer onConnectionInfo = Completer.sync(); @@ -969,6 +1008,7 @@ void main() { listViews, listViews, listViews, + setAssetBundlePath, FakeVmServiceRequest( method: 'getIsolate', args: { @@ -1038,6 +1078,7 @@ void main() { listViews, listViews, listViews, + setAssetBundlePath, FakeVmServiceRequest( method: 'getIsolate', args: { @@ -1113,6 +1154,7 @@ void main() { listViews, listViews, listViews, + setAssetBundlePath, FakeVmServiceRequest( method: 'getIsolate', args: { @@ -1243,6 +1285,7 @@ void main() { fakeVmServiceHost = FakeVmServiceHost(requests: [ listViews, listViews, + setAssetBundlePath, ]); when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async { return 'Example'; @@ -2112,6 +2155,7 @@ void main() { fakeVmServiceHost = FakeVmServiceHost(requests: [ listViews, listViews, + setAssetBundlePath, ]); setWsAddress(testUri, fakeVmServiceHost.vmService); globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true); @@ -2137,6 +2181,7 @@ void main() { fakeVmServiceHost = FakeVmServiceHost(requests: [ listViews, listViews, + setAssetBundlePath, ]); setWsAddress(testUri, fakeVmServiceHost.vmService); globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true); @@ -2163,6 +2208,7 @@ void main() { fakeVmServiceHost = FakeVmServiceHost(requests: [ listViews, listViews, + setAssetBundlePath, ]); setWsAddress(testUri, fakeVmServiceHost.vmService); globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true); @@ -2197,6 +2243,7 @@ void main() { fakeVmServiceHost = FakeVmServiceHost(requests: [ listViews, listViews, + setAssetBundlePath, ]); setWsAddress(testUri, fakeVmServiceHost.vmService); globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true); @@ -2231,6 +2278,7 @@ void main() { fakeVmServiceHost = FakeVmServiceHost(requests: [ listViews, listViews, + setAssetBundlePath, ]); setWsAddress(testUri, fakeVmServiceHost.vmService); globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true); @@ -2258,6 +2306,7 @@ void main() { fakeVmServiceHost = FakeVmServiceHost(requests: [ listViews, listViews, + setAssetBundlePath, ]); setWsAddress(testUri, fakeVmServiceHost.vmService); globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true); @@ -2322,6 +2371,7 @@ void main() { fakeVmServiceHost = FakeVmServiceHost(requests: [ listViews, listViews, + setAssetBundlePath, ]); final MockDevicePortForwarder mockPortForwarder = MockDevicePortForwarder(); when(mockDevice.portForwarder).thenReturn(mockPortForwarder); @@ -2353,6 +2403,7 @@ void main() { fakeVmServiceHost = FakeVmServiceHost(requests: [ listViews, listViews, + setAssetBundlePath, ]); globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true); residentRunner = HotRunner(