Reapply "When parts of the program are changed in a hot reload, but not executed during the reassemble, warn that a restart may be needed." (#12490)
This reverts commit 5e7bcbacf80875730b2002973b5d82cf5331c2a3.
`flutter run --benchmark` was triggering a different quick bailout path in the VM than `flutter run`. The failure has been fixed upstream.
diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart
index 9cd850c..860a63c 100644
--- a/packages/flutter_tools/lib/src/devfs.dart
+++ b/packages/flutter_tools/lib/src/devfs.dart
@@ -340,6 +340,16 @@
Uri _baseUri;
Uri get baseUri => _baseUri;
+ Uri deviceUriToHostUri(Uri deviceUri) {
+ final String deviceUriString = deviceUri.toString();
+ final String baseUriString = baseUri.toString();
+ if (deviceUriString.startsWith(baseUriString)) {
+ final String deviceUriSuffix = deviceUriString.substring(baseUriString.length);
+ return rootDirectory.uri.resolve(deviceUriSuffix);
+ }
+ return deviceUri;
+ }
+
Future<Uri> create() async {
printTrace('DevFS: Creating new filesystem on the device ($_baseUri)');
try {
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index 1d0cece..f59eeef 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -139,6 +139,24 @@
return reports;
}
+ // Lists program elements changed in the most recent reload that have not
+ // since executed.
+ Future<List<ProgramElement>> unusedChangesInLastReload() async {
+ final List<Future<List<ProgramElement>>> reports =
+ <Future<List<ProgramElement>>>[];
+ for (FlutterView view in views)
+ reports.add(view.uiIsolate.getUnusedChangesInLastReload());
+ final List<ProgramElement> elements = <ProgramElement>[];
+ for (Future<List<ProgramElement>> report in reports) {
+ for (ProgramElement element in await report)
+ elements.add(new ProgramElement(element.qualifiedName,
+ devFS.deviceUriToHostUri(element.uri),
+ element.line,
+ element.column));
+ }
+ return elements;
+ }
+
Future<Null> debugDumpApp() async {
for (FlutterView view in views)
await view.uiIsolate.flutterDebugDumpApp();
@@ -807,14 +825,15 @@
}
class OperationResult {
- static final OperationResult ok = new OperationResult(0, '');
-
- OperationResult(this.code, this.message);
+ OperationResult(this.code, this.message, [this.hint]);
final int code;
final String message;
+ final String hint;
bool get isOk => code == 0;
+
+ static final OperationResult ok = new OperationResult(0, '');
}
/// Given the value of the --target option, return the path of the Dart file
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index 7173fed..2921fa7 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -430,6 +430,8 @@
status.cancel();
if (result.isOk)
printStatus("${result.message} in ${getElapsedAsMilliseconds(timer.elapsed)}.");
+ if (result.hint != null)
+ printStatus(result.hint);
return result;
} catch (error) {
status.cancel();
@@ -438,6 +440,14 @@
}
}
+ String _uriToRelativePath(Uri uri) {
+ final String path = uri.toString();
+ final String base = new Uri.file(projectRootPath).toString();
+ if (path.startsWith(base))
+ return path.substring(base.length + 1);
+ return path;
+ }
+
Future<OperationResult> _reloadSources({ bool pause: false }) async {
for (FlutterDevice device in flutterDevices) {
for (FlutterView view in device.views) {
@@ -606,9 +616,40 @@
!reassembleTimedOut &&
shouldReportReloadTime)
flutterUsage.sendTiming('hot', 'reload', reloadTimer.elapsed);
+
+ String unusedElementMessage;
+ if (!reassembleAndScheduleErrors && !reassembleTimedOut) {
+ final List<Future<List<ProgramElement>>> unusedReports =
+ <Future<List<ProgramElement>>>[];
+ for (FlutterDevice device in flutterDevices)
+ unusedReports.add(device.unusedChangesInLastReload());
+ final List<ProgramElement> unusedElements = <ProgramElement>[];
+ for (Future<List<ProgramElement>> unusedReport in unusedReports)
+ unusedElements.addAll(await unusedReport);
+
+ if (unusedElements.isNotEmpty) {
+ unusedElementMessage =
+ '\nThe following program elements were changed by the reload, '
+ 'but did not run when the view was reassembled. If this code '
+ 'only runs at start-up, you will need to restart ("R") for '
+ 'the changes to have an effect.';
+ for (ProgramElement unusedElement in unusedElements) {
+ final String name = unusedElement.qualifiedName;
+ final String path = _uriToRelativePath(unusedElement.uri);
+ final int line = unusedElement.line;
+ String elementDescription;
+ if (line == null)
+ elementDescription = '$name ($path)';
+ else
+ elementDescription = '$name ($path:$line)';
+ unusedElementMessage += '\n - $elementDescription';
+ }
+ }
+ }
+
return new OperationResult(
reassembleAndScheduleErrors ? 1 : OperationResult.ok.code,
- reloadMessage
+ reloadMessage, unusedElementMessage
);
}
diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart
index ef81678..40db37d 100644
--- a/packages/flutter_tools/lib/src/vmservice.dart
+++ b/packages/flutter_tools/lib/src/vmservice.dart
@@ -897,6 +897,24 @@
}
}
+// A function, field or class along with its source location.
+class ProgramElement {
+ ProgramElement(this.qualifiedName, this.uri, this.line, this.column);
+
+ final String qualifiedName;
+ final Uri uri;
+ final int line;
+ final int column;
+
+ @override
+ String toString() {
+ if (line == null)
+ return '$qualifiedName ($uri)';
+ else
+ return '$qualifiedName ($uri:$line)';
+ }
+}
+
/// An isolate running inside the VM. Instances of the Isolate class are always
/// canonicalized.
class Isolate extends ServiceObjectOwner {
@@ -1029,6 +1047,58 @@
}
}
+ Future<Map<String, dynamic>> getObject(Map<String, dynamic> objectRef) {
+ return invokeRpcRaw('getObject',
+ params: <String, dynamic>{'objectId': objectRef['id']});
+ }
+
+ Future<ProgramElement> _describeElement(Map<String, dynamic> elementRef) async {
+ String name = elementRef['name'];
+ Map<String, dynamic> owner = elementRef['owner'];
+ while (owner != null) {
+ final String ownerType = owner['type'];
+ if (ownerType == 'Library' || ownerType == '@Library')
+ break;
+ final String ownerName = owner['name'];
+ name = "$ownerName.$name";
+ owner = owner['owner'];
+ }
+
+ final Map<String, dynamic> fullElement = await getObject(elementRef);
+ final Map<String, dynamic> location = fullElement['location'];
+ final int tokenPos = location['tokenPos'];
+ final Map<String, dynamic> script = await getObject(location['script']);
+
+ // The engine's tag handler doesn't seem to create proper URIs.
+ Uri uri = Uri.parse(script['uri']);
+ if (uri.scheme == '')
+ uri = uri.replace(scheme: 'file');
+
+ // See https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md
+ for (List<int> lineTuple in script['tokenPosTable']) {
+ final int line = lineTuple[0];
+ for (int i = 1; i < lineTuple.length; i += 2) {
+ if (lineTuple[i] == tokenPos) {
+ final int column = lineTuple[i + 1];
+ return new ProgramElement(name, uri, line, column);
+ }
+ }
+ }
+ return new ProgramElement(name, uri, null, null);
+ }
+
+ // Lists program elements changed in the most recent reload that have not
+ // since executed.
+ Future<List<ProgramElement>> getUnusedChangesInLastReload() async {
+ final Map<String, dynamic> response =
+ await invokeRpcRaw('_getUnusedChangesInLastReload');
+ final List<Future<ProgramElement>> unusedElements =
+ <Future<ProgramElement>>[];
+ for (Map<String, dynamic> element in response['unused'])
+ unusedElements.add(_describeElement(element));
+ return Future.wait(unusedElements);
+ }
+
/// Resumes the isolate.
Future<Map<String, dynamic>> resume() {
return invokeRpcRaw('resume');