From 07caa0fbfe5186cc8a11902cc46377cebd592088 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Mon, 20 Jul 2020 14:03:44 -0700 Subject: [PATCH] [flutter_tools] Add plumbing for widget cache (#61766) To support #61407 , the tool needs to check if a single widget reload is feasible, and then conditionally perform a fast reassemble. To accomplish this, the FlutterDevice class will have a WidgetCache injected. This will eventually contain the logic for parsing the invalidated dart script. Concurrent with the devFS update, the widget cache will be updated/checked if a single widget reload is feasible. If so, an expression evaluation with the target type is performed and the success is communicated through the devFS result. An integration test which demonstrates that this works is already present in https://github.com/flutter/flutter/blob/master/packages/flutter_tools/test/integration.shard/hot_reload_test.dart#L86 Finally, when actually performing the reassemble the tool simply checks if this flag has been set and calls the alternative reassemble method. Cleanups: Remove modules, as this is unused now. --- .../lib/src/build_runner/devfs_web.dart | 2 +- .../lib/src/commands/attach.dart | 3 + .../lib/src/commands/daemon.dart | 3 + .../flutter_tools/lib/src/commands/run.dart | 2 + packages/flutter_tools/lib/src/devfs.dart | 28 +- .../lib/src/resident_runner.dart | 85 ++++- packages/flutter_tools/lib/src/run_hot.dart | 77 +--- packages/flutter_tools/lib/src/vmservice.dart | 6 +- .../flutter_tools/lib/src/widget_cache.dart | 28 ++ .../project_file_invalidator_test.dart | 4 +- .../general.shard/resident_runner_test.dart | 350 +++++++++++++++++- .../resident_web_runner_test.dart | 17 +- .../test/general.shard/widget_cache_test.dart | 22 ++ 13 files changed, 522 insertions(+), 105 deletions(-) create mode 100644 packages/flutter_tools/lib/src/widget_cache.dart create mode 100644 packages/flutter_tools/test/general.shard/widget_cache_test.dart diff --git a/packages/flutter_tools/lib/src/build_runner/devfs_web.dart b/packages/flutter_tools/lib/src/build_runner/devfs_web.dart index d80e67267d8..1dcaf2d7f29 100644 --- a/packages/flutter_tools/lib/src/build_runner/devfs_web.dart +++ b/packages/flutter_tools/lib/src/build_runner/devfs_web.dart @@ -851,7 +851,7 @@ class WebDevFS implements DevFS { success: true, syncedBytes: codeFile.lengthSync(), invalidatedSourcesCount: invalidatedFiles.length, - )..invalidatedModules = modules; + ); } @visibleForTesting diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart index d327867275e..00cfaac0c49 100644 --- a/packages/flutter_tools/lib/src/commands/attach.dart +++ b/packages/flutter_tools/lib/src/commands/attach.dart @@ -15,6 +15,7 @@ import '../base/io.dart'; import '../commands/daemon.dart'; import '../compile.dart'; import '../device.dart'; +import '../features.dart'; import '../fuchsia/fuchsia_device.dart'; import '../globals.dart' as globals; import '../ios/devices.dart'; @@ -26,6 +27,7 @@ import '../resident_runner.dart'; import '../run_cold.dart'; import '../run_hot.dart'; import '../runner/flutter_command.dart'; +import '../widget_cache.dart'; /// A Flutter-command that attaches to applications that have been launched /// without `flutter run`. @@ -369,6 +371,7 @@ class AttachCommand extends FlutterCommand { targetModel: TargetModel(stringArg('target-model')), buildInfo: getBuildInfo(), userIdentifier: userIdentifier, + widgetCache: WidgetCache(featureFlags: featureFlags), ); flutterDevice.observatoryUris = observatoryUris; final List flutterDevices = [flutterDevice]; diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index 987dba08da6..16fed434406 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -19,6 +19,7 @@ import '../build_info.dart'; import '../convert.dart'; import '../device.dart'; import '../emulator.dart'; +import '../features.dart'; import '../globals.dart' as globals; import '../project.dart'; import '../resident_runner.dart'; @@ -26,6 +27,7 @@ import '../run_cold.dart'; import '../run_hot.dart'; import '../runner/flutter_command.dart'; import '../web/web_runner.dart'; +import '../widget_cache.dart'; const String protocolVersion = '0.6.0'; @@ -468,6 +470,7 @@ class AppDomain extends Domain { viewFilter: isolateFilter, target: target, buildInfo: options.buildInfo, + widgetCache: WidgetCache(featureFlags: featureFlags), ); ResidentRunner runner; diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 7dec77488fc..5cc468b3d24 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -23,6 +23,7 @@ import '../run_hot.dart'; import '../runner/flutter_command.dart'; import '../tracing.dart'; import '../web/web_runner.dart'; +import '../widget_cache.dart'; import 'daemon.dart'; abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopmentArtifacts { @@ -519,6 +520,7 @@ class RunCommand extends RunCommandBase { target: stringArg('target'), buildInfo: getBuildInfo(), userIdentifier: userIdentifier, + widgetCache: WidgetCache(featureFlags: featureFlags), ), ]; // Only support "web mode" with a single web device due to resident runner diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart index 78f641339a7..a32b29b3a3d 100644 --- a/packages/flutter_tools/lib/src/devfs.dart +++ b/packages/flutter_tools/lib/src/devfs.dart @@ -302,34 +302,32 @@ class UpdateFSReport { bool success = false, int invalidatedSourcesCount = 0, int syncedBytes = 0, - }) { - _success = success; - _invalidatedSourcesCount = invalidatedSourcesCount; - _syncedBytes = syncedBytes; - } + this.fastReassemble, + }) : _success = success, + _invalidatedSourcesCount = invalidatedSourcesCount, + _syncedBytes = syncedBytes; bool get success => _success; int get invalidatedSourcesCount => _invalidatedSourcesCount; int get syncedBytes => _syncedBytes; - /// JavaScript modules produced by the incremental compiler in `dartdevc` - /// mode. - /// - /// Only used for JavaScript compilation. - List invalidatedModules; + bool _success; + bool fastReassemble; + int _invalidatedSourcesCount; + int _syncedBytes; void incorporateResults(UpdateFSReport report) { if (!report._success) { _success = false; } + if (report.fastReassemble != null && fastReassemble != null) { + fastReassemble &= report.fastReassemble; + } else if (report.fastReassemble != null) { + fastReassemble = report.fastReassemble; + } _invalidatedSourcesCount += report._invalidatedSourcesCount; _syncedBytes += report._syncedBytes; - invalidatedModules ??= report.invalidatedModules; } - - bool _success; - int _invalidatedSourcesCount; - int _syncedBytes; } class DevFS { diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 0e0294ab455..2c24cef852a 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -33,6 +33,7 @@ import 'project.dart'; import 'run_cold.dart'; import 'run_hot.dart'; import 'vmservice.dart'; +import 'widget_cache.dart'; class FlutterDevice { FlutterDevice( @@ -45,6 +46,7 @@ class FlutterDevice { TargetPlatform targetPlatform, ResidentCompiler generator, this.userIdentifier, + this.widgetCache, }) : assert(buildInfo.trackWidgetCreation != null), generator = generator ?? ResidentCompiler( globals.artifacts.getArtifactPath( @@ -78,6 +80,7 @@ class FlutterDevice { List experimentalFlags, ResidentCompiler generator, String userIdentifier, + WidgetCache widgetCache, }) async { ResidentCompiler generator; final TargetPlatform targetPlatform = await device.targetPlatform; @@ -167,6 +170,7 @@ class FlutterDevice { generator: generator, buildInfo: buildInfo, userIdentifier: userIdentifier, + widgetCache: widgetCache, ); } @@ -174,6 +178,7 @@ class FlutterDevice { final ResidentCompiler generator; final BuildInfo buildInfo; final String userIdentifier; + final WidgetCache widgetCache; Stream observatoryUris; vm_service.VmService vmService; DevFS devFS; @@ -641,6 +646,44 @@ class FlutterDevice { return 0; } + /// Validates whether this hot reload is a candidate for a fast reassemble. + Future _attemptFastReassembleCheck(List invalidatedFiles, PackageConfig packageConfig) async { + if (invalidatedFiles.length != 1 || widgetCache == null) { + return false; + } + final List views = await vmService.getFlutterViews(); + final String widgetName = await widgetCache?.validateLibrary(invalidatedFiles.single); + if (widgetName == null) { + return false; + } + final String packageUri = packageConfig.toPackageUri(invalidatedFiles.single)?.toString() + ?? invalidatedFiles.single.toString(); + for (final FlutterView view in views) { + final vm_service.Isolate isolate = await vmService.getIsolateOrNull(view.uiIsolate.id); + final vm_service.LibraryRef targetLibrary = isolate.libraries + .firstWhere( + (vm_service.LibraryRef libraryRef) => libraryRef.uri == packageUri, + orElse: () => null, + ); + if (targetLibrary == null) { + return false; + } + try { + // Evaluate an expression to allow type checking for that invalidated widget + // name. For more information, see `debugFastReassembleMethod` in flutter/src/widgets/binding.dart + await vmService.evaluate( + view.uiIsolate.id, + targetLibrary.id, + '((){debugFastReassembleMethod=(Object _fastReassembleParam) => _fastReassembleParam is $widgetName})()', + ); + } on Exception catch (err) { + globals.printTrace(err.toString()); + return false; + } + } + return true; + } + Future updateDevFS({ Uri mainUri, String target, @@ -660,22 +703,34 @@ class FlutterDevice { timeout: timeoutConfiguration.fastOperation, ); UpdateFSReport report; + bool fastReassemble = false; try { - report = await devFS.update( - mainUri: mainUri, - target: target, - bundle: bundle, - firstBuildTime: firstBuildTime, - bundleFirstUpload: bundleFirstUpload, - generator: generator, - fullRestart: fullRestart, - dillOutputPath: dillOutputPath, - trackWidgetCreation: buildInfo.trackWidgetCreation, - projectRootPath: projectRootPath, - pathToReload: pathToReload, - invalidatedFiles: invalidatedFiles, - packageConfig: packageConfig, - ); + await Future.wait(>[ + devFS.update( + mainUri: mainUri, + target: target, + bundle: bundle, + firstBuildTime: firstBuildTime, + bundleFirstUpload: bundleFirstUpload, + generator: generator, + fullRestart: fullRestart, + dillOutputPath: dillOutputPath, + trackWidgetCreation: buildInfo.trackWidgetCreation, + projectRootPath: projectRootPath, + pathToReload: pathToReload, + invalidatedFiles: invalidatedFiles, + packageConfig: packageConfig, + ).then((UpdateFSReport newReport) => report = newReport), + if (!fullRestart) + _attemptFastReassembleCheck( + invalidatedFiles, + packageConfig, + ).then((bool newFastReassemble) => fastReassemble = newFastReassemble) + ]); + if (fastReassemble) { + globals.logger.printTrace('Attempting fast reassemble.'); + } + report.fastReassemble = fastReassemble; } on DevFSException { devFSStatus.cancel(); return UpdateFSReport(success: false); diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index cc259c1dde6..d217e53d84e 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -154,62 +154,15 @@ class HotRunner extends ResidentRunner { @override Future reloadMethod({ String libraryId, String classId }) async { - final Stopwatch stopwatch = Stopwatch()..start(); - final UpdateFSReport results = UpdateFSReport(success: true); - final List invalidated = [Uri.parse(libraryId)]; - final PackageConfig packageConfig = await loadPackageConfigWithLogging( - globals.fs.file(debuggingOptions.buildInfo.packagesPath), - logger: globals.logger, - ); - for (final FlutterDevice device in flutterDevices) { - results.incorporateResults(await device.updateDevFS( - mainUri: globals.fs.file(mainPath).absolute.uri, - target: target, - bundle: assetBundle, - firstBuildTime: firstBuildTime, - bundleFirstUpload: false, - bundleDirty: false, - fullRestart: false, - projectRootPath: projectRootPath, - pathToReload: getReloadPath(fullRestart: false), - invalidatedFiles: invalidated, - packageConfig: packageConfig, - dillOutputPath: dillOutputPath, - )); - } - if (!results.success) { - return OperationResult(1, 'Failed to compile'); - } - try { - final String entryPath = globals.fs.path.relative( - getReloadPath(fullRestart: false), - from: projectRootPath, + final OperationResult result = await restart(pause: false); + if (!result.isOk) { + throw vm_service.RPCError( + 'Unable to reload sources', + RPCErrorCodes.kInternalError, + '', ); - for (final FlutterDevice device in flutterDevices) { - final List> reportFutures = await device.reloadSources( - entryPath, pause: false, - ); - final List reports = await Future.wait(reportFutures); - final vm_service.ReloadReport firstReport = reports.first; - await device.updateReloadStatus(validateReloadReport(firstReport.json, printErrors: false)); - } - } on Exception catch (error) { - return OperationResult(1, error.toString()); } - - for (final FlutterDevice device in flutterDevices) { - final List views = await device.vmService.getFlutterViews(); - for (final FlutterView view in views) { - await device.vmService.flutterFastReassemble( - classId, - isolateId: view.uiIsolate.id, - ); - } - } - - globals.printStatus('reloadMethod took ${stopwatch.elapsedMilliseconds}'); - globals.flutterUsage.sendTiming('hot', 'ui', stopwatch.elapsed); - return OperationResult.ok; + return result; } // Returns the exit code of the flutter tool process, like [run]. @@ -942,9 +895,19 @@ class HotRunner extends ResidentRunner { } } else { reassembleViews[view] = device.vmService; - reassembleFutures.add(device.vmService.flutterReassemble( - isolateId: view.uiIsolate.id, - ).catchError((dynamic error) { + // If the tool identified a change in a single widget, do a fast instead + // of a full reassemble. + Future reassembleWork; + if (updatedDevFS.fastReassemble == true) { + reassembleWork = device.vmService.flutterFastReassemble( + isolateId: view.uiIsolate.id, + ); + } else { + reassembleWork = device.vmService.flutterReassemble( + isolateId: view.uiIsolate.id, + ); + } + reassembleFutures.add(reassembleWork.catchError((dynamic error) { failedReassemble = true; globals.printError('Reassembling ${view.uiIsolate.name} failed: $error'); }, test: (dynamic error) => error is Exception)); diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart index 31a5ede978b..a1581f9bc8a 100644 --- a/packages/flutter_tools/lib/src/vmservice.dart +++ b/packages/flutter_tools/lib/src/vmservice.dart @@ -628,15 +628,13 @@ extension FlutterVmService on vm_service.VmService { ); } - Future> flutterFastReassemble(String classId, { + Future> flutterFastReassemble({ @required String isolateId, }) { return invokeFlutterExtensionRpcRaw( 'ext.flutter.fastReassemble', isolateId: isolateId, - args: { - 'class': classId, - }, + args: {}, ); } diff --git a/packages/flutter_tools/lib/src/widget_cache.dart b/packages/flutter_tools/lib/src/widget_cache.dart new file mode 100644 index 00000000000..a74ae9c42ea --- /dev/null +++ b/packages/flutter_tools/lib/src/widget_cache.dart @@ -0,0 +1,28 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart'; + +import 'features.dart'; + +/// The widget cache determines if the body of a single widget was modified since +/// the last scan of the token stream. +class WidgetCache { + WidgetCache({ + @required FeatureFlags featureFlags, + }) : _featureFlags = featureFlags; + + final FeatureFlags _featureFlags; + + /// If the build method of a single widget was modified, return the widget name. + /// + /// If any other changes were made, or there is an error scanning the file, + /// return `null`. + Future validateLibrary(Uri libraryUri) async { + if (!_featureFlags.isSingleWidgetReloadEnabled) { + return null; + } + return null; + } +} diff --git a/packages/flutter_tools/test/general.shard/project_file_invalidator_test.dart b/packages/flutter_tools/test/general.shard/project_file_invalidator_test.dart index 5d36d25405e..f5a9041438d 100644 --- a/packages/flutter_tools/test/general.shard/project_file_invalidator_test.dart +++ b/packages/flutter_tools/test/general.shard/project_file_invalidator_test.dart @@ -84,7 +84,7 @@ void main() { ', asyncScanning: $asyncScanning', () async { final DateTime past = DateTime.now().subtract(const Duration(seconds: 1)); final FileSystem fileSystem = MemoryFileSystem.test(); - final PackageConfig packageConfig = PackageConfig.empty; + const PackageConfig packageConfig = PackageConfig.empty; final ProjectFileInvalidator projectFileInvalidator = ProjectFileInvalidator( fileSystem: fileSystem, platform: FakePlatform(), @@ -126,7 +126,7 @@ void main() { testWithoutContext('Picks up changes to the .packages file and updates PackageConfig' ', asyncScanning: $asyncScanning', () async { final FileSystem fileSystem = MemoryFileSystem.test(); - final PackageConfig packageConfig = PackageConfig.empty; + const PackageConfig packageConfig = PackageConfig.empty; final ProjectFileInvalidator projectFileInvalidator = ProjectFileInvalidator( fileSystem: fileSystem, platform: FakePlatform(), 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 027de7c7577..c0dc95878d9 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart @@ -4,7 +4,11 @@ import 'dart:async'; +import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/widget_cache.dart'; +import 'package:meta/meta.dart'; +import 'package:package_config/package_config.dart'; import 'package:vm_service/vm_service.dart' as vm_service; import 'package:file/memory.dart'; import 'package:file_testing/file_testing.dart'; @@ -40,7 +44,13 @@ final vm_service.Isolate fakeUnpausedIsolate = vm_service.Isolate( ), breakpoints: [], exceptionPauseMode: null, - libraries: [], + libraries: [ + vm_service.LibraryRef( + id: '1', + uri: 'file:///hello_world/main.dart', + name: '', + ), + ], livePorts: 0, name: 'test', number: '1', @@ -66,6 +76,19 @@ final vm_service.Isolate fakePausedIsolate = vm_service.Isolate( startTime: 0, ); +final vm_service.VM fakeVM = vm_service.VM( + isolates: [fakeUnpausedIsolate], + pid: 1, + hostCPU: '', + isolateGroups: [], + targetCPU: '', + startTime: 0, + name: 'dart', + architectureBits: 64, + operatingSystem: '', + version: '', +); + final FlutterView fakeFlutterView = FlutterView( id: 'a', uiIsolate: fakeUnpausedIsolate, @@ -622,6 +645,260 @@ void main() { Usage: () => MockUsage(), })); + testUsingContext('ResidentRunner can perform fast reassemble', () => testbed.run(() async { + fakeVmServiceHost = FakeVmServiceHost(requests: [ + listViews, + FakeVmServiceRequest( + method: 'getVM', + jsonResponse: fakeVM.toJson(), + ), + listViews, + listViews, + listViews, + FakeVmServiceRequest( + method: 'getIsolate', + args: { + 'isolateId': '1', + }, + jsonResponse: fakeUnpausedIsolate.toJson(), + ), + const FakeVmServiceRequest( + method: 'evaluate', + args: { + 'isolateId': '1', + 'targetId': '1', + 'expression': '((){debugFastReassembleMethod=(Object _fastReassembleParam) => _fastReassembleParam is FakeWidget})()', + } + ), + listViews, + const FakeVmServiceRequest( + method: '_flutter.setAssetBundlePath', + args: { + 'viewId': 'a', + 'assetDirectory': 'build/flutter_assets', + 'isolateId': '1', + } + ), + FakeVmServiceRequest( + method: 'getVM', + jsonResponse: fakeVM.toJson(), + ), + const FakeVmServiceRequest( + method: 'reloadSources', + args: { + 'isolateId': '1', + 'pause': false, + 'rootLibUri': 'lib/main.dart.incremental.dill', + }, + jsonResponse: { + 'type': 'ReloadReport', + 'success': true, + 'details': { + 'loadedLibraryCount': 1, + }, + }, + ), + listViews, + FakeVmServiceRequest( + method: 'getIsolate', + args: { + 'isolateId': '1', + }, + jsonResponse: fakeUnpausedIsolate.toJson(), + ), + FakeVmServiceRequest( + method: 'ext.flutter.fastReassemble', + args: { + 'isolateId': fakeUnpausedIsolate.id, + }, + ), + ]); + final FakeFlutterDevice flutterDevice = FakeFlutterDevice( + mockDevice, + BuildInfo.debug, + FakeWidgetCache(), + FakeResidentCompiler(), + mockDevFS, + )..vmService = fakeVmServiceHost.vmService; + residentRunner = HotRunner( + [ + flutterDevice, + ], + stayResident: false, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + ); + 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; + }); + when(mockDevice.getLogReader(app: anyNamed('app'))).thenReturn(NoOpDeviceLogReader('test')); + when(mockDevFS.update( + mainUri: anyNamed('mainUri'), + target: anyNamed('target'), + bundle: anyNamed('bundle'), + firstBuildTime: anyNamed('firstBuildTime'), + bundleFirstUpload: anyNamed('bundleFirstUpload'), + generator: anyNamed('generator'), + fullRestart: anyNamed('fullRestart'), + dillOutputPath: anyNamed('dillOutputPath'), + trackWidgetCreation: anyNamed('trackWidgetCreation'), + projectRootPath: anyNamed('projectRootPath'), + pathToReload: anyNamed('pathToReload'), + invalidatedFiles: anyNamed('invalidatedFiles'), + packageConfig: anyNamed('packageConfig'), + )).thenAnswer((Invocation invocation) async { + return UpdateFSReport(success: true); + }); + + final Completer onConnectionInfo = Completer.sync(); + final Completer onAppStart = Completer.sync(); + unawaited(residentRunner.attach( + appStartedCompleter: onAppStart, + connectionInfoCompleter: onConnectionInfo, + )); + + final OperationResult result = await residentRunner.restart(fullRestart: false); + expect(result.fatal, false); + expect(result.code, 0); + }, overrides: { + FileSystem: () => MemoryFileSystem.test(), + Platform: () => FakePlatform(operatingSystem: 'linux'), + ProjectFileInvalidator: () => FakeProjectFileInvalidator(), + })); + + testUsingContext('ResidentRunner bails out of fast reassemble if evaluation fails', () => testbed.run(() async { + fakeVmServiceHost = FakeVmServiceHost(requests: [ + listViews, + FakeVmServiceRequest( + method: 'getVM', + jsonResponse: fakeVM.toJson(), + ), + listViews, + listViews, + listViews, + FakeVmServiceRequest( + method: 'getIsolate', + args: { + 'isolateId': '1', + }, + jsonResponse: fakeUnpausedIsolate.toJson(), + ), + const FakeVmServiceRequest( + method: 'evaluate', + args: { + 'isolateId': '1', + 'targetId': '1', + 'expression': '((){debugFastReassembleMethod=(Object _fastReassembleParam) => _fastReassembleParam is FakeWidget})()', + }, + errorCode: 500, + ), + listViews, + const FakeVmServiceRequest( + method: '_flutter.setAssetBundlePath', + args: { + 'viewId': 'a', + 'assetDirectory': 'build/flutter_assets', + 'isolateId': '1', + } + ), + FakeVmServiceRequest( + method: 'getVM', + jsonResponse: fakeVM.toJson(), + ), + const FakeVmServiceRequest( + method: 'reloadSources', + args: { + 'isolateId': '1', + 'pause': false, + 'rootLibUri': 'lib/main.dart.incremental.dill', + }, + jsonResponse: { + 'type': 'ReloadReport', + 'success': true, + 'details': { + 'loadedLibraryCount': 1, + }, + }, + ), + listViews, + FakeVmServiceRequest( + method: 'getIsolate', + args: { + 'isolateId': '1', + }, + jsonResponse: fakeUnpausedIsolate.toJson(), + ), + FakeVmServiceRequest( + method: 'ext.flutter.reassemble', + args: { + 'isolateId': fakeUnpausedIsolate.id, + }, + ), + ]); + final FakeFlutterDevice flutterDevice = FakeFlutterDevice( + mockDevice, + BuildInfo.debug, + FakeWidgetCache(), + FakeResidentCompiler(), + mockDevFS, + )..vmService = fakeVmServiceHost.vmService; + residentRunner = HotRunner( + [ + flutterDevice, + ], + stayResident: false, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + ); + 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; + }); + when(mockDevice.getLogReader(app: anyNamed('app'))).thenReturn(NoOpDeviceLogReader('test')); + when(mockDevFS.update( + mainUri: anyNamed('mainUri'), + target: anyNamed('target'), + bundle: anyNamed('bundle'), + firstBuildTime: anyNamed('firstBuildTime'), + bundleFirstUpload: anyNamed('bundleFirstUpload'), + generator: anyNamed('generator'), + fullRestart: anyNamed('fullRestart'), + dillOutputPath: anyNamed('dillOutputPath'), + trackWidgetCreation: anyNamed('trackWidgetCreation'), + projectRootPath: anyNamed('projectRootPath'), + pathToReload: anyNamed('pathToReload'), + invalidatedFiles: anyNamed('invalidatedFiles'), + packageConfig: anyNamed('packageConfig'), + )).thenAnswer((Invocation invocation) async { + return UpdateFSReport(success: true); + }); + + final Completer onConnectionInfo = Completer.sync(); + final Completer onAppStart = Completer.sync(); + unawaited(residentRunner.attach( + appStartedCompleter: onAppStart, + connectionInfoCompleter: onConnectionInfo, + )); + + final OperationResult result = await residentRunner.restart(fullRestart: false); + expect(result.fatal, false); + expect(result.code, 0); + }, overrides: { + FileSystem: () => MemoryFileSystem.test(), + Platform: () => FakePlatform(operatingSystem: 'linux'), + ProjectFileInvalidator: () => FakeProjectFileInvalidator(), + })); + + testUsingContext('ResidentRunner can send target platform to analytics from full restart', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: [ listViews, @@ -1740,3 +2017,74 @@ class ThrowingForwardingFileSystem extends ForwardingFileSystem { return delegate.file(path); } } + +class FakeWidgetCache implements WidgetCache { + @override + Future validateLibrary(Uri libraryUri) async { + return 'FakeWidget'; + } +} + +class FakeFlutterDevice extends FlutterDevice { + FakeFlutterDevice( + Device device, + BuildInfo buildInfo, + WidgetCache widgetCache, + ResidentCompiler residentCompiler, + this.fakeDevFS, + ) : super(device, buildInfo: buildInfo, widgetCache:widgetCache, generator: residentCompiler); + + @override + Future connect({ + ReloadSources reloadSources, + Restart restart, + CompileExpression compileExpression, + ReloadMethod reloadMethod, + GetSkSLMethod getSkSLMethod, + PrintStructuredErrorLogMethod printStructuredErrorLogMethod, + }) async { } + + + final DevFS fakeDevFS; + + @override + DevFS get devFS => fakeDevFS; + + @override + set devFS(DevFS value) {} +} + +class FakeResidentCompiler extends Fake implements ResidentCompiler { + @override + Future recompile( + Uri mainUri, + List invalidatedFiles, { + @required String outputPath, + @required PackageConfig packageConfig, + bool suppressErrors = false, + }) async { + return const CompilerOutput('foo.dill', 0, []); + } + + @override + void accept() { } + + @override + void reset() { } +} + +class FakeProjectFileInvalidator extends Fake implements ProjectFileInvalidator { + @override + Future findInvalidated({ + @required DateTime lastCompiled, + @required List urisToMonitor, + @required String packagesPath, + @required PackageConfig packageConfig, + bool asyncScanning = false, + }) async { + return InvalidationResult( + packageConfig: packageConfig ?? PackageConfig.empty, + uris: [Uri.parse('file:///hello_world/main.dart'), + ]); + } +} diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart index 9f2725cce24..78494a4183b 100644 --- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart @@ -139,7 +139,7 @@ void main() { trackWidgetCreation: anyNamed('trackWidgetCreation'), packageConfig: anyNamed('packageConfig'), )).thenAnswer((Invocation _) async { - return UpdateFSReport(success: true, syncedBytes: 0)..invalidatedModules = []; + return UpdateFSReport(success: true, syncedBytes: 0); }); when(mockDebugConnection.vmService).thenAnswer((Invocation invocation) { return fakeVmServiceHost.vmService; @@ -352,7 +352,7 @@ void main() { trackWidgetCreation: anyNamed('trackWidgetCreation'), packageConfig: anyNamed('packageConfig'), )).thenAnswer((Invocation _) async { - return UpdateFSReport(success: false, syncedBytes: 0)..invalidatedModules = []; + return UpdateFSReport(success: false, syncedBytes: 0); }); expect(await residentWebRunner.run(), 1); @@ -587,8 +587,7 @@ void main() { )).thenAnswer((Invocation invocation) async { // Generated entrypoint file in temp dir. expect(invocation.namedArguments[#mainUri].toString(), contains('entrypoint.dart')); - return UpdateFSReport(success: true) - ..invalidatedModules = ['example']; + return UpdateFSReport(success: true); }); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( @@ -668,8 +667,7 @@ void main() { packageConfig: anyNamed('packageConfig'), )).thenAnswer((Invocation invocation) async { entrypointFileUri = invocation.namedArguments[#mainUri] as Uri; - return UpdateFSReport(success: true) - ..invalidatedModules = ['example']; + return UpdateFSReport(success: true); }); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( @@ -727,8 +725,7 @@ void main() { invalidatedFiles: anyNamed('invalidatedFiles'), packageConfig: anyNamed('packageConfig'), )).thenAnswer((Invocation invocation) async { - return UpdateFSReport(success: true) - ..invalidatedModules = ['example']; + return UpdateFSReport(success: true); }); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( @@ -801,7 +798,7 @@ void main() { packageConfig: anyNamed('packageConfig'), trackWidgetCreation: anyNamed('trackWidgetCreation'), )).thenAnswer((Invocation _) async { - return UpdateFSReport(success: false, syncedBytes: 0)..invalidatedModules = []; + return UpdateFSReport(success: false, syncedBytes: 0); }); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( @@ -875,7 +872,7 @@ void main() { packageConfig: anyNamed('packageConfig'), trackWidgetCreation: anyNamed('trackWidgetCreation'), )).thenAnswer((Invocation _) async { - return UpdateFSReport(success: false, syncedBytes: 0)..invalidatedModules = []; + return UpdateFSReport(success: false, syncedBytes: 0); }); final OperationResult result = await residentWebRunner.restart(fullRestart: true); diff --git a/packages/flutter_tools/test/general.shard/widget_cache_test.dart b/packages/flutter_tools/test/general.shard/widget_cache_test.dart new file mode 100644 index 00000000000..80d1a4d10cb --- /dev/null +++ b/packages/flutter_tools/test/general.shard/widget_cache_test.dart @@ -0,0 +1,22 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_tools/src/widget_cache.dart'; + +import '../src/common.dart'; +import '../src/testbed.dart'; + +void main() { + testWithoutContext('widget cache returns null when experiment is disabled', () async { + final WidgetCache widgetCache = WidgetCache(featureFlags: TestFeatureFlags(isSingleWidgetReloadEnabled: false)); + + expect(await widgetCache.validateLibrary(Uri.parse('package:hello_world/main.dart')), null); + }); + + testWithoutContext('widget cache returns null because functionality is not complete', () async { + final WidgetCache widgetCache = WidgetCache(featureFlags: TestFeatureFlags(isSingleWidgetReloadEnabled: true)); + + expect(await widgetCache.validateLibrary(Uri.parse('package:hello_world/main.dart')), null); + }); +}