Add time to frame tracking to hot run (#5316)

diff --git a/packages/flutter_tools/lib/src/base/utils.dart b/packages/flutter_tools/lib/src/base/utils.dart
index 0248ff3..7207d88 100644
--- a/packages/flutter_tools/lib/src/base/utils.dart
+++ b/packages/flutter_tools/lib/src/base/utils.dart
@@ -78,6 +78,15 @@
   return '${(bytesLength / (1024 * 1024)).toStringAsFixed(1)}MB';
 }
 
+String getElapsedAsSeconds(Duration duration) {
+  double seconds = duration.inMilliseconds / Duration.MILLISECONDS_PER_SECOND;
+  return '${seconds.toStringAsFixed(2)} seconds';
+}
+
+String getElapsedAsMilliseconds(Duration duration) {
+  return '${duration.inMilliseconds} ms';
+}
+
 /// Return a relative path if [fullPath] is contained by the cwd, else return an
 /// absolute path.
 String getDisplayPath(String fullPath) {
diff --git a/packages/flutter_tools/lib/src/hot.dart b/packages/flutter_tools/lib/src/hot.dart
index 8dbdb59..f69de8f 100644
--- a/packages/flutter_tools/lib/src/hot.dart
+++ b/packages/flutter_tools/lib/src/hot.dart
@@ -29,6 +29,41 @@
                                  'loader_app.dart'));
 }
 
+class FirstFrameTimer {
+  FirstFrameTimer(this.serviceProtocol);
+
+  void start() {
+    stopwatch.reset();
+    stopwatch.start();
+    _subscription = serviceProtocol.onExtensionEvent.listen(_onExtensionEvent);
+  }
+
+  /// Returns a Future which completes after the first frame event is received.
+  Future<Null> firstFrame() => _completer.future;
+
+  void _onExtensionEvent(Event event) {
+    if (event.extensionKind == 'Flutter.FirstFrame')
+      _stop();
+  }
+
+  void _stop() {
+    _subscription?.cancel();
+    _subscription = null;
+    stopwatch.stop();
+    _completer.complete(null);
+  }
+
+  Duration get elapsed {
+    assert(!stopwatch.isRunning);
+    return stopwatch.elapsed;
+  }
+
+  final Observatory serviceProtocol;
+  final Stopwatch stopwatch = new Stopwatch();
+  final Completer<Null> _completer = new Completer<Null>();
+  StreamSubscription<Event> _subscription;
+}
+
 class HotRunner extends ResidentRunner {
   HotRunner(
     Device device, {
@@ -347,16 +382,21 @@
   }
 
   Future<Null> _restartFromSources() async {
+    FirstFrameTimer firstFrameTimer = new FirstFrameTimer(serviceProtocol);
+    firstFrameTimer.start();
     if (_devFS == null) {
-      Status restartStatus = logger.startProgress('Restarting application...');
       await _launchFromDisk(_package, _mainPath);
-      restartStatus.stop(showElapsedTime: true);
     } else {
       await _updateDevFS();
-      Status restartStatus = logger.startProgress('Restarting application...');
       await _launchFromDevFS(_package, _mainPath);
-      restartStatus.stop(showElapsedTime: true);
     }
+    Status restartStatus =
+        logger.startProgress('Waiting for application to start...');
+    // Wait for the first frame to be rendered.
+    await firstFrameTimer.firstFrame();
+    restartStatus.stop(showElapsedTime: true);
+    printStatus('Restart time: '
+                '${getElapsedAsMilliseconds(firstFrameTimer.elapsed)}');
     flutterUsage.sendEvent('hot', 'restart');
   }
 
@@ -380,6 +420,8 @@
   Future<bool> _reloadSources() async {
     if (serviceProtocol.firstIsolateId == null)
       throw 'Application isolate not found';
+    FirstFrameTimer firstFrameTimer = new FirstFrameTimer(serviceProtocol);
+    firstFrameTimer.start();
     if (_devFS != null)
       await _updateDevFS();
     Status reloadStatus = logger.startProgress('Performing hot reload...');
@@ -410,6 +452,9 @@
       return false;
     }
     reassembleStatus.stop(showElapsedTime: true);
+    await firstFrameTimer.firstFrame();
+    printStatus('Hot reload time: '
+                '${getElapsedAsMilliseconds(firstFrameTimer.elapsed)}');
     return true;
   }