Unverified Commit a19f5bac authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] connect widget cache from frontend_server (#65951)

parent ae630b42
......@@ -433,21 +433,16 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
registerServiceExtension(
name: 'fastReassemble',
callback: (Map<String, Object> params) async {
final FastReassemblePredicate? fastReassemblePredicate = _debugFastReassembleMethod;
_debugFastReassembleMethod = null;
if (fastReassemblePredicate == null) {
throw FlutterError('debugFastReassembleMethod must be set to use fastReassemble.');
}
void markElementsDirty(Element? element) {
if (element == null) {
return;
}
if (fastReassemblePredicate(element.widget)) {
final String? className = params['className'] as String?;
void markElementsDirty(Element element) {
if (element.widget.runtimeType.toString() == className) {
element.markNeedsBuild();
}
element.visitChildElements(markElementsDirty);
}
markElementsDirty(renderViewElement);
if (renderViewElement != null) {
markElementsDirty(renderViewElement!);
}
await endOfFrame;
return <String, String>{'type': 'Success'};
},
......@@ -1061,36 +1056,6 @@ void runApp(Widget app) {
..scheduleWarmUpFrame();
}
/// A function that should validate that the provided object is assignable to a
/// given type.
typedef FastReassemblePredicate = bool Function(Object);
/// Debug-only functionality used to perform faster hot reloads.
///
/// This field is set by expression evaluation in the flutter tool and is
/// used to invalidate specific types of [Element]s. This setter
/// should not be referenced in user code and is only public so that expression
/// evaluation can be done in the context of an almost-arbitrary Dart library.
///
/// For example, expression evaluation might be performed with the following code:
///
/// ```dart
/// (debugFastReassembleMethod=(Object x) => x is Foo)()
/// ```
///
/// And then followed by a call to `ext.flutter.fastReassemble`. This will read
/// the provided predicate and use it to mark specific elements dirty wherever
/// [Element.widget] is a `Foo`. Afterwards, the internal field will be nulled
/// out.
FastReassemblePredicate? get debugFastReassembleMethod => _debugFastReassembleMethod;
set debugFastReassembleMethod(FastReassemblePredicate? fastReassemblePredicate) {
assert(() {
_debugFastReassembleMethod = fastReassemblePredicate;
return true;
}());
}
FastReassemblePredicate? _debugFastReassembleMethod;
/// Print a string representation of the currently running app.
void debugDumpApp() {
assert(WidgetsBinding.instance != null);
......
......@@ -740,8 +740,4 @@ void main() {
expect(brightnessValue, 'Brightness.light');
});
test('Service extensions - fastReassemble', () async {
expect(binding.testExtension('fastReassemble', <String, String>{}), throwsA(isA<FlutterError>()));
});
}
......@@ -588,6 +588,7 @@ class _ResidentWebRunner extends ResidentWebRunner {
reason: reason,
overallTimeInMs: timer.elapsed.inMilliseconds,
nullSafety: usageNullSafety,
fastReassemble: null,
).send();
}
return OperationResult.ok;
......
......@@ -15,7 +15,6 @@ 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';
......@@ -27,7 +26,6 @@ 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`.
......@@ -384,7 +382,6 @@ 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,7 +19,6 @@ 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';
......@@ -27,7 +26,6 @@ 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';
......@@ -472,7 +470,6 @@ class AppDomain extends Domain {
flutterProject: flutterProject,
target: target,
buildInfo: options.buildInfo,
widgetCache: WidgetCache(featureFlags: featureFlags),
);
ResidentRunner runner;
......
......@@ -23,7 +23,6 @@ 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 {
......@@ -532,7 +531,6 @@ 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,7 +302,7 @@ class UpdateFSReport {
bool success = false,
int invalidatedSourcesCount = 0,
int syncedBytes = 0,
this.fastReassemble,
this.fastReassembleClassName,
}) : _success = success,
_invalidatedSourcesCount = invalidatedSourcesCount,
_syncedBytes = syncedBytes;
......@@ -312,7 +312,7 @@ class UpdateFSReport {
int get syncedBytes => _syncedBytes;
bool _success;
bool fastReassemble;
String fastReassembleClassName;
int _invalidatedSourcesCount;
int _syncedBytes;
......@@ -320,11 +320,7 @@ class UpdateFSReport {
if (!report._success) {
_success = false;
}
if (report.fastReassemble != null && fastReassemble != null) {
fastReassemble &= report.fastReassemble;
} else if (report.fastReassemble != null) {
fastReassemble = report.fastReassemble;
}
fastReassembleClassName ??= report.fastReassembleClassName;
_invalidatedSourcesCount += report._invalidatedSourcesCount;
_syncedBytes += report._syncedBytes;
}
......@@ -365,6 +361,7 @@ class DevFS {
List<Uri> sources = <Uri>[];
DateTime lastCompiled;
PackageConfig lastPackageConfig;
File _widgetCacheOutputFile;
Uri _baseUri;
Uri get baseUri => _baseUri;
......@@ -404,6 +401,20 @@ class DevFS {
_logger.printTrace('DevFS: Deleted filesystem on the device ($_baseUri)');
}
/// 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`.
String _checkIfSingleWidgetReloadApplied() {
if (_widgetCacheOutputFile != null && _widgetCacheOutputFile.existsSync()) {
final String widget = _widgetCacheOutputFile.readAsStringSync().trim();
if (widget.isNotEmpty) {
return widget;
}
}
return null;
}
/// Updates files on the device.
///
/// Returns the number of bytes synced.
......@@ -427,6 +438,7 @@ class DevFS {
assert(generator != null);
final DateTime candidateCompileTime = DateTime.now();
lastPackageConfig = packageConfig;
_widgetCacheOutputFile = _fileSystem.file('$dillOutputPath.incremental.dill.widget_cache');
// Update modified files
final Map<Uri, DevFSContent> dirtyEntries = <Uri, DevFSContent>{};
......@@ -502,8 +514,12 @@ class DevFS {
}
}
_logger.printTrace('DevFS: Sync finished');
return UpdateFSReport(success: true, syncedBytes: syncedBytes,
invalidatedSourcesCount: invalidatedFiles.length);
return UpdateFSReport(
success: true,
syncedBytes: syncedBytes,
invalidatedSourcesCount: invalidatedFiles.length,
fastReassembleClassName: _checkIfSingleWidgetReloadApplied(),
);
}
/// Converts a platform-specific file path to a platform-independent URL path.
......
......@@ -239,6 +239,7 @@ const Feature flutterFuchsiaFeature = Feature(
const Feature singleWidgetReload = Feature(
name: 'Hot reload optimization for changes to class body of a single widget',
configSetting: 'single-widget-reload-optimization',
environmentOverride: 'FLUTTER_SINGLE_WIDGET_RELOAD',
master: FeatureChannelSetting(
available: true,
enabledByDefault: false,
......
......@@ -40,6 +40,7 @@ class HotEvent extends UsageEvent {
@required this.emulator,
@required this.fullRestart,
@required this.nullSafety,
@required this.fastReassemble,
this.reason,
this.finalLibraryCount,
this.syncedLibraryCount,
......@@ -57,6 +58,7 @@ class HotEvent extends UsageEvent {
final bool emulator;
final bool fullRestart;
final bool nullSafety;
final bool fastReassemble;
final int finalLibraryCount;
final int syncedLibraryCount;
final int syncedClassesCount;
......@@ -93,6 +95,8 @@ class HotEvent extends UsageEvent {
CustomDimensions.hotEventOverallTimeInMs: overallTimeInMs.toString(),
if (nullSafety != null)
CustomDimensions.nullSafety: nullSafety.toString(),
if (fastReassemble != null)
CustomDimensions.fastReassemble: fastReassemble.toString(),
});
flutterUsage.sendEvent(category, parameter, parameters: parameters);
}
......
......@@ -58,6 +58,7 @@ enum CustomDimensions {
commandRunAndroidEmbeddingVersion, // cd45
commandPackagesAndroidEmbeddingVersion, // cd46
nullSafety, // cd47
fastReassemble, // cd48
}
String cdKey(CustomDimensions cd) => 'cd${cd.index + 1}';
......
......@@ -34,7 +34,6 @@ import 'project.dart';
import 'run_cold.dart';
import 'run_hot.dart';
import 'vmservice.dart';
import 'widget_cache.dart';
class FlutterDevice {
FlutterDevice(
......@@ -46,7 +45,6 @@ class FlutterDevice {
TargetPlatform targetPlatform,
ResidentCompiler generator,
this.userIdentifier,
this.widgetCache,
}) : assert(buildInfo.trackWidgetCreation != null),
generator = generator ?? ResidentCompiler(
globals.artifacts.getArtifactPath(
......@@ -79,7 +77,6 @@ class FlutterDevice {
List<String> experimentalFlags,
ResidentCompiler generator,
String userIdentifier,
WidgetCache widgetCache,
}) async {
ResidentCompiler generator;
final TargetPlatform targetPlatform = await device.targetPlatform;
......@@ -134,6 +131,14 @@ class FlutterDevice {
logger: globals.logger,
);
} else {
// The flutter-widget-cache feature only applies to run mode.
List<String> extraFrontEndOptions = buildInfo.extraFrontEndOptions;
if (featureFlags.isSingleWidgetReloadEnabled) {
extraFrontEndOptions = <String>[
'--flutter-widget-cache',
...?extraFrontEndOptions,
];
}
generator = ResidentCompiler(
globals.artifacts.getArtifactPath(
Artifact.flutterPatchedSdkPath,
......@@ -146,11 +151,11 @@ class FlutterDevice {
fileSystemScheme: fileSystemScheme,
targetModel: targetModel,
dartDefines: buildInfo.dartDefines,
extraFrontEndOptions: buildInfo.extraFrontEndOptions,
extraFrontEndOptions: extraFrontEndOptions,
initializeFromDill: getDefaultCachedKernelPath(
trackWidgetCreation: buildInfo.trackWidgetCreation,
dartDefines: buildInfo.dartDefines,
extraFrontEndOptions: buildInfo.extraFrontEndOptions,
extraFrontEndOptions: extraFrontEndOptions,
),
packagesPath: buildInfo.packagesPath,
artifacts: globals.artifacts,
......@@ -168,7 +173,6 @@ class FlutterDevice {
generator: generator,
buildInfo: buildInfo,
userIdentifier: userIdentifier,
widgetCache: widgetCache,
);
}
......@@ -176,7 +180,6 @@ class FlutterDevice {
final ResidentCompiler generator;
final BuildInfo buildInfo;
final String userIdentifier;
final WidgetCache widgetCache;
Stream<Uri> observatoryUris;
vm_service.VmService vmService;
DevFS devFS;
......@@ -655,44 +658,6 @@ 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,
......@@ -712,34 +677,22 @@ class FlutterDevice {
timeout: timeoutConfiguration.fastOperation,
);
UpdateFSReport report;
bool fastReassemble = false;
try {
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;
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,
);
} on DevFSException {
devFSStatus.cancel();
return UpdateFSReport(success: false);
......
......@@ -21,6 +21,7 @@ import 'convert.dart';
import 'dart/package_map.dart';
import 'devfs.dart';
import 'device.dart';
import 'features.dart';
import 'globals.dart' as globals;
import 'reporting/reporting.dart';
import 'resident_runner.dart';
......@@ -744,7 +745,9 @@ class HotRunner extends ResidentRunner {
emulator: emulator,
fullRestart: true,
nullSafety: usageNullSafety,
reason: reason).send();
reason: reason,
fastReassemble: null,
).send();
status?.cancel();
}
return result;
......@@ -787,6 +790,7 @@ class HotRunner extends ResidentRunner {
fullRestart: false,
nullSafety: usageNullSafety,
reason: reason,
fastReassemble: null,
).send();
return OperationResult(1, 'hot reload failed to complete', fatal: true);
} finally {
......@@ -866,6 +870,7 @@ class HotRunner extends ResidentRunner {
fullRestart: false,
reason: reason,
nullSafety: usageNullSafety,
fastReassemble: null,
).send();
return OperationResult(1, 'Reload rejected');
}
......@@ -894,6 +899,7 @@ class HotRunner extends ResidentRunner {
fullRestart: false,
reason: reason,
nullSafety: usageNullSafety,
fastReassemble: null,
).send();
return OperationResult(errorCode, errorMessage);
}
......@@ -936,9 +942,10 @@ class HotRunner extends ResidentRunner {
// 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) {
if (updatedDevFS.fastReassembleClassName != null) {
reassembleWork = device.vmService.flutterFastReassemble(
isolateId: view.uiIsolate.id,
className: updatedDevFS.fastReassembleClassName,
);
} else {
reassembleWork = device.vmService.flutterReassemble(
......@@ -1030,6 +1037,9 @@ class HotRunner extends ResidentRunner {
invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount,
transferTimeInMs: devFSTimer.elapsed.inMilliseconds,
nullSafety: usageNullSafety,
fastReassemble: featureFlags.isSingleWidgetReloadEnabled
? updatedDevFS.fastReassembleClassName != null
: null,
).send();
if (shouldReportReloadTime) {
......
......@@ -628,11 +628,14 @@ extension FlutterVmService on vm_service.VmService {
Future<Map<String, dynamic>> flutterFastReassemble({
@required String isolateId,
@required String className,
}) {
return invokeFlutterExtensionRpcRaw(
'ext.flutter.fastReassemble',
isolateId: isolateId,
args: <String, Object>{},
args: <String, Object>{
'className': className,
},
);
}
......
// 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;
}
}
......@@ -6,7 +6,7 @@ import 'dart:async';
import 'package:flutter_tools/src/base/dds.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/widget_cache.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:meta/meta.dart';
import 'package:package_config/package_config.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
......@@ -714,22 +714,6 @@ void main() {
listViews,
listViews,
listViews,
FakeVmServiceRequest(
method: 'getIsolate',
args: <String, Object>{
'isolateId': '1',
},
jsonResponse: fakeUnpausedIsolate.toJson(),
),
const FakeVmServiceRequest(
method: 'evaluate',
args: <String, String>{
'isolateId': '1',
'targetId': '1',
'expression': '((){debugFastReassembleMethod=(Object _fastReassembleParam) => _fastReassembleParam is FakeWidget})()',
}
),
listViews,
const FakeVmServiceRequest(
method: '_flutter.setAssetBundlePath',
args: <String, Object>{
......@@ -769,13 +753,13 @@ void main() {
method: 'ext.flutter.fastReassemble',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'className': 'FOO',
},
),
]);
final FakeFlutterDevice flutterDevice = FakeFlutterDevice(
mockDevice,
BuildInfo.debug,
FakeWidgetCache(),
FakeResidentCompiler(),
mockDevFS,
)..vmService = fakeVmServiceHost.vmService;
......@@ -811,7 +795,7 @@ void main() {
invalidatedFiles: anyNamed('invalidatedFiles'),
packageConfig: anyNamed('packageConfig'),
)).thenAnswer((Invocation invocation) async {
return UpdateFSReport(success: true);
return UpdateFSReport(success: true, fastReassembleClassName: 'FOO');
});
final Completer<DebugConnectionInfo> onConnectionInfo = Completer<DebugConnectionInfo>.sync();
......@@ -822,142 +806,21 @@ void main() {
));
final OperationResult result = await residentRunner.restart(fullRestart: false);
expect(result.fatal, false);
expect(result.code, 0);
}, overrides: <Type, Generator>{
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: <VmServiceExpectation>[
listViews,
FakeVmServiceRequest(
method: 'getVM',
jsonResponse: fakeVM.toJson(),
),
listViews,
listViews,
listViews,
FakeVmServiceRequest(
method: 'getIsolate',
args: <String, Object>{
'isolateId': '1',
},
jsonResponse: fakeUnpausedIsolate.toJson(),
),
const FakeVmServiceRequest(
method: 'evaluate',
args: <String, String>{
'isolateId': '1',
'targetId': '1',
'expression': '((){debugFastReassembleMethod=(Object _fastReassembleParam) => _fastReassembleParam is FakeWidget})()',
},
errorCode: 500,
),
listViews,
const FakeVmServiceRequest(
method: '_flutter.setAssetBundlePath',
args: <String, Object>{
'viewId': 'a',
'assetDirectory': 'build/flutter_assets',
'isolateId': '1',
}
),
FakeVmServiceRequest(
method: 'getVM',
jsonResponse: fakeVM.toJson(),
),
const FakeVmServiceRequest(
method: 'reloadSources',
args: <String, Object>{
'isolateId': '1',
'pause': false,
'rootLibUri': 'lib/main.dart.incremental.dill',
},
jsonResponse: <String, Object>{
'type': 'ReloadReport',
'success': true,
'details': <String, Object>{
'loadedLibraryCount': 1,
},
},
),
listViews,
FakeVmServiceRequest(
method: 'getIsolate',
args: <String, Object>{
'isolateId': '1',
},
jsonResponse: fakeUnpausedIsolate.toJson(),
),
FakeVmServiceRequest(
method: 'ext.flutter.reassemble',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
},
),
]);
final FakeFlutterDevice flutterDevice = FakeFlutterDevice(
mockDevice,
BuildInfo.debug,
FakeWidgetCache(),
FakeResidentCompiler(),
mockDevFS,
)..vmService = fakeVmServiceHost.vmService;
residentRunner = HotRunner(
<FlutterDevice>[
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<DebugConnectionInfo> onConnectionInfo = Completer<DebugConnectionInfo>.sync();
final Completer<void> onAppStart = Completer<void>.sync();
unawaited(residentRunner.attach(
appStartedCompleter: onAppStart,
connectionInfoCompleter: onConnectionInfo,
));
final OperationResult result = await residentRunner.restart(fullRestart: false);
expect(result.fatal, false);
expect(result.code, 0);
verify(globals.flutterUsage.sendEvent('hot', 'reload', parameters: argThat(
containsPair('cd48', 'true'),
named: 'parameters',
))).called(1);
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem.test(),
Platform: () => FakePlatform(operatingSystem: 'linux'),
ProjectFileInvalidator: () => FakeProjectFileInvalidator(),
Usage: () => MockUsage(),
FeatureFlags: () => TestFeatureFlags(isSingleWidgetReloadEnabled: true),
}));
testUsingContext('ResidentRunner can send target platform to analytics from full restart', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
listViews,
......@@ -2251,6 +2114,34 @@ void main() {
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('FlutterDevice passes flutter-widget-cache flag when feature is enabled', () async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
final MockDevice mockDevice = MockDevice();
when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async {
return TargetPlatform.android_arm;
});
final DefaultResidentCompiler residentCompiler = (await FlutterDevice.create(
mockDevice,
buildInfo: const BuildInfo(
BuildMode.debug,
'',
treeShakeIcons: false,
extraFrontEndOptions: <String>[],
),
flutterProject: FlutterProject.current(),
target: null,
)).generator as DefaultResidentCompiler;
expect(residentCompiler.extraFrontEndOptions,
contains('--flutter-widget-cache'));
}, overrides: <Type, Generator>{
Artifacts: () => Artifacts.test(),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
FeatureFlags: () => TestFeatureFlags(isSingleWidgetReloadEnabled: true)
});
testUsingContext('connect sets up log reader', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
final MockDevice mockDevice = MockDevice();
......@@ -2325,21 +2216,13 @@ class ThrowingForwardingFileSystem extends ForwardingFileSystem {
}
}
class FakeWidgetCache implements WidgetCache {
@override
Future<String> 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);
) : super(device, buildInfo: buildInfo, generator: residentCompiler);
@override
Future<void> connect({
......
// 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);
});
}
......@@ -7,7 +7,6 @@ import 'dart:async';
import 'package:file/file.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service_io.dart';
import '../src/common.dart';
import 'test_data/hot_reload_project.dart';
......@@ -82,73 +81,6 @@ void main() {
}
});
testWithoutContext('fastReassemble behavior triggers hot reload behavior with evaluation of expression', () async {
final Completer<void> tick1 = Completer<void>();
final Completer<void> tick2 = Completer<void>();
final Completer<void> tick3 = Completer<void>();
final StreamSubscription<String> subscription = flutter.stdout.listen((String line) {
if (line.contains('TICK 1')) {
tick1.complete();
}
if (line.contains('TICK 2')) {
tick2.complete();
}
if (line.contains('TICK 3')) {
tick3.complete();
}
});
await flutter.run(withDebugger: true);
final int port = flutter.vmServicePort;
final VmService vmService = await vmServiceConnectUri('ws://localhost:$port/ws');
await tick1.future;
try {
// Since the single-widget reload feature is not yet implemented, manually
// evaluate the expression for the reload.
final Isolate isolate = await waitForExtension(vmService);
final LibraryRef targetRef = isolate.libraries.firstWhere((LibraryRef libraryRef) {
return libraryRef.uri == 'package:test/main.dart';
});
await vmService.evaluate(
isolate.id,
targetRef.id,
'((){debugFastReassembleMethod=(Object x) => x is MyApp})()',
);
final Response fastReassemble1 = await vmService
.callServiceExtension('ext.flutter.fastReassemble', isolateId: isolate.id);
// _extensionType indicates success.
expect(fastReassemble1.type, '_extensionType');
await tick2.future;
// verify evaluation did not produce invalidat type by checking with dart:core
// type.
await vmService.evaluate(
isolate.id,
targetRef.id,
'((){debugFastReassembleMethod=(Object x) => x is bool})()',
);
final Response fastReassemble2 = await vmService
.callServiceExtension('ext.flutter.fastReassemble', isolateId: isolate.id);
// _extensionType indicates success.
expect(fastReassemble2.type, '_extensionType');
unawaited(tick3.future.whenComplete(() {
fail('Should not complete');
}));
// Invocation without evaluation leads to runtime error.
expect(vmService
.callServiceExtension('ext.flutter.fastReassemble', isolateId: isolate.id),
throwsA(isA<Exception>())
);
} finally {
await subscription.cancel();
}
});
testWithoutContext('hot restart works without error', () async {
await flutter.run();
await flutter.hotRestart();
......
// 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 'dart:async';
import 'package:flutter_tools/src/base/file_system.dart';
import '../src/common.dart';
import 'test_data/single_widget_reload_project.dart';
import 'test_driver.dart';
import 'test_utils.dart';
void main() {
Directory tempDir;
final SingleWidgetReloadProject project = SingleWidgetReloadProject();
FlutterRunTestDriver flutter;
setUp(() async {
tempDir = createResolvedTempDirectorySync('hot_reload_test.');
await project.setUpIn(tempDir);
flutter = FlutterRunTestDriver(tempDir);
});
tearDown(() async {
await flutter?.stop();
tryToDelete(tempDir);
});
testWithoutContext('newly added code executes during hot reload with single widget reloads, but only invalidated widget', () async {
final StringBuffer stdout = StringBuffer();
final StreamSubscription<String> subscription = flutter.stdout.listen(stdout.writeln);
await flutter.run(singleWidgetReloads: true);
project.uncommentHotReloadPrint();
try {
await flutter.hotReload();
expect(stdout.toString(), allOf(
contains('(((TICK 1)))'),
contains('(((((RELOAD WORKED)))))'),
// Does not invalidate parent widget, so second tick is not output.
isNot(contains('(((TICK 2)))'),
)));
} finally {
await subscription.cancel();
}
});
testWithoutContext('changes outside of the class body triggers a full reload', () async {
final StringBuffer stdout = StringBuffer();
final StreamSubscription<String> subscription = flutter.stdout.listen(stdout.writeln);
await flutter.run(singleWidgetReloads: true);
project.modifyFunction();
try {
await flutter.hotReload();
expect(stdout.toString(), allOf(
contains('(((TICK 1)))'),
contains('(((TICK 2)))'),
));
} finally {
await subscription.cancel();
}
});
}
// 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 '../test_utils.dart';
import 'project.dart';
class SingleWidgetReloadProject extends Project {
@override
final String pubspec = '''
name: test
environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
''';
@override
final String main = r'''
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final ByteData message = const StringCodec().encodeMessage('AppLifecycleState.resumed');
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/lifecycle', message, (_) { });
runApp(MyApp());
}
int count = 1;
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// PARENT WIDGET
print('((((TICK $count))))');
count += 1;
return MaterialApp(
title: 'Flutter Demo',
home: SecondWidget(),
);
}
}
class SecondWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Do not remove the next line, it's uncommented by a test to verify that
// hot reloading worked:
// printHotReloadWorked();
return Container();
}
}
void printHotReloadWorked() {
// The call to this function is uncommented by a test to verify that hot
// reloading worked.
print('(((((RELOAD WORKED)))))');
}
''';
Uri get parentWidgetUri => mainDart;
int get parentWidgetLine => lineContaining(main, '// PARENT WIDGET');
void uncommentHotReloadPrint() {
final String newMainContents = main.replaceAll(
'// printHotReloadWorked();',
'printHotReloadWorked();',
);
writeFile(fileSystem.path.join(dir.path, 'lib', 'main.dart'), newMainContents);
}
void modifyFunction() {
final String newMainContents = main.replaceAll(
'(((((RELOAD WORKED)))))',
'(((((RELOAD WORKED 2)))))',
);
writeFile(fileSystem.path.join(dir.path, 'lib', 'main.dart'), newMainContents);
}
}
......@@ -84,6 +84,7 @@ abstract class FlutterTestDriver {
String script,
bool withDebugger = false,
File pidFile,
bool singleWidgetReloads = false,
}) async {
final String flutterBin = fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter');
if (withDebugger) {
......@@ -107,7 +108,12 @@ abstract class FlutterTestDriver {
.toList(),
workingDirectory: _projectFolder.path,
// The web environment variable has the same effect as `flutter config --enable-web`.
environment: <String, String>{'FLUTTER_TEST': 'true', 'FLUTTER_WEB': 'true'},
environment: <String, String>{
'FLUTTER_TEST': 'true',
'FLUTTER_WEB': 'true',
if (singleWidgetReloads)
'FLUTTER_SINGLE_WIDGET_RELOAD': 'true',
},
);
// This class doesn't use the result of the future. It's made available
......@@ -442,6 +448,7 @@ class FlutterRunTestDriver extends FlutterTestDriver {
bool expressionEvaluation = true,
bool structuredErrors = false,
bool machine = true,
bool singleWidgetReloads = false,
File pidFile,
String script,
}) async {
......@@ -470,6 +477,7 @@ class FlutterRunTestDriver extends FlutterTestDriver {
pauseOnExceptions: pauseOnExceptions,
pidFile: pidFile,
script: script,
singleWidgetReloads: singleWidgetReloads,
);
}
......@@ -479,6 +487,7 @@ class FlutterRunTestDriver extends FlutterTestDriver {
bool startPaused = false,
bool pauseOnExceptions = false,
File pidFile,
bool singleWidgetReloads = false,
}) async {
await _setupProcess(
<String>[
......@@ -496,6 +505,7 @@ class FlutterRunTestDriver extends FlutterTestDriver {
startPaused: startPaused,
pauseOnExceptions: pauseOnExceptions,
pidFile: pidFile,
singleWidgetReloads: singleWidgetReloads,
);
}
......@@ -506,6 +516,7 @@ class FlutterRunTestDriver extends FlutterTestDriver {
bool withDebugger = false,
bool startPaused = false,
bool pauseOnExceptions = false,
bool singleWidgetReloads = false,
File pidFile,
}) async {
assert(!startPaused || withDebugger);
......@@ -514,6 +525,7 @@ class FlutterRunTestDriver extends FlutterTestDriver {
script: script,
withDebugger: withDebugger,
pidFile: pidFile,
singleWidgetReloads: singleWidgetReloads,
);
final Completer<void> prematureExitGuard = Completer<void>();
......@@ -723,12 +735,14 @@ class FlutterTestTestDriver extends FlutterTestDriver {
bool pauseOnExceptions = false,
File pidFile,
Future<void> Function() beforeStart,
bool singleWidgetReloads = false,
}) async {
await super._setupProcess(
args,
script: script,
withDebugger: withDebugger,
pidFile: pidFile,
singleWidgetReloads: singleWidgetReloads,
);
// Stash the PID so that we can terminate the VM more reliably than using
......
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