Unverified Commit 07caa0fb authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[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.
parent aa4b4d35
......@@ -851,7 +851,7 @@ class WebDevFS implements DevFS {
success: true,
syncedBytes: codeFile.lengthSync(),
invalidatedSourcesCount: invalidatedFiles.length,
)..invalidatedModules = modules;
);
}
@visibleForTesting
......
......@@ -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<FlutterDevice> flutterDevices = <FlutterDevice>[flutterDevice];
......
......@@ -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;
......
......@@ -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
......
......@@ -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<String> 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 {
......
......@@ -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<String> 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<Uri> 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<bool> _attemptFastReassembleCheck(List<Uri> invalidatedFiles, PackageConfig packageConfig) async {
if (invalidatedFiles.length != 1 || widgetCache == null) {
return false;
}
final List<FlutterView> 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<UpdateFSReport> 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(<Future<void>>[
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);
......
......@@ -154,62 +154,15 @@ class HotRunner extends ResidentRunner {
@override
Future<OperationResult> reloadMethod({ String libraryId, String classId }) async {
final Stopwatch stopwatch = Stopwatch()..start();
final UpdateFSReport results = UpdateFSReport(success: true);
final List<Uri> invalidated = <Uri>[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<Future<vm_service.ReloadReport>> reportFutures = await device.reloadSources(
entryPath, pause: false,
);
final List<vm_service.ReloadReport> 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<FlutterView> 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<void> 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));
......
......@@ -628,15 +628,13 @@ extension FlutterVmService on vm_service.VmService {
);
}
Future<Map<String, dynamic>> flutterFastReassemble(String classId, {
Future<Map<String, dynamic>> flutterFastReassemble({
@required String isolateId,
}) {
return invokeFlutterExtensionRpcRaw(
'ext.flutter.fastReassemble',
isolateId: isolateId,
args: <String, Object>{
'class': classId,
},
args: <String, Object>{},
);
}
......
// 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<String> validateLibrary(Uri libraryUri) async {
if (!_featureFlags.isSingleWidgetReloadEnabled) {
return null;
}
return null;
}
}
......@@ -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(),
......
......@@ -139,7 +139,7 @@ void main() {
trackWidgetCreation: anyNamed('trackWidgetCreation'),
packageConfig: anyNamed('packageConfig'),
)).thenAnswer((Invocation _) async {
return UpdateFSReport(success: true, syncedBytes: 0)..invalidatedModules = <String>[];
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 = <String>[];
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 = <String>['example'];
return UpdateFSReport(success: true);
});
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
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 = <String>['example'];
return UpdateFSReport(success: true);
});
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(
......@@ -727,8 +725,7 @@ void main() {
invalidatedFiles: anyNamed('invalidatedFiles'),
packageConfig: anyNamed('packageConfig'),
)).thenAnswer((Invocation invocation) async {
return UpdateFSReport(success: true)
..invalidatedModules = <String>['example'];
return UpdateFSReport(success: true);
});
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
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 = <String>[];
return UpdateFSReport(success: false, syncedBytes: 0);
});
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
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 = <String>[];
return UpdateFSReport(success: false, syncedBytes: 0);
});
final OperationResult result = await residentWebRunner.restart(fullRestart: true);
......
// 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);
});
}
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