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');