Report devfs stats (#25586)

* Collect devfs stats for better analytics

* Fix fields initialization

* Fix lints
diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart
index b7c78cb..07adbba 100644
--- a/packages/flutter_tools/lib/src/devfs.dart
+++ b/packages/flutter_tools/lib/src/devfs.dart
@@ -350,6 +350,32 @@
   }
 }
 
+// Basic statistics for DevFS update operation.
+class UpdateFSReport {
+  UpdateFSReport({bool success = false,
+    int invalidatedSourcesCount = 0, int syncedBytes = 0}) {
+    _success = success;
+    _invalidatedSourcesCount = invalidatedSourcesCount;
+    _syncedBytes = syncedBytes;
+  }
+
+  bool get success => _success;
+  int get invalidatedSourcesCount => _invalidatedSourcesCount;
+  int get syncedBytes => _syncedBytes;
+
+  void incorporateResults(UpdateFSReport report) {
+    if (!report._success) {
+      _success = false;
+    }
+    _invalidatedSourcesCount += report._invalidatedSourcesCount;
+    _syncedBytes += report._syncedBytes;
+  }
+
+  bool _success;
+  int _invalidatedSourcesCount;
+  int _syncedBytes;
+}
+
 class DevFS {
   /// Create a [DevFS] named [fsName] for the local files in [rootDirectory].
   DevFS(VMService serviceProtocol,
@@ -422,7 +448,7 @@
   /// Updates files on the device.
   ///
   /// Returns the number of bytes synced.
-  Future<int> update({
+  Future<UpdateFSReport> update({
     @required String mainPath,
     String target,
     AssetBundle bundle,
@@ -487,7 +513,7 @@
     }
 
     // Update modified files
-    int numBytes = 0;
+    int syncedBytes = 0;
     final Map<Uri, DevFSContent> dirtyEntries = <Uri, DevFSContent>{};
     _entries.forEach((Uri deviceUri, DevFSContent content) {
       String archivePath;
@@ -498,7 +524,7 @@
       // files to incremental compiler next time user does hot reload.
       if (content.isModified || ((bundleDirty || bundleFirstUpload) && archivePath != null)) {
         dirtyEntries[deviceUri] = content;
-        numBytes += content.size;
+        syncedBytes += content.size;
         if (archivePath != null && (!bundleFirstUpload || content.isModifiedAfter(firstBuildTime)))
           assetPathsToEvict.add(archivePath);
       }
@@ -516,7 +542,7 @@
         if (content is DevFSFileContent) {
           filesUris.add(uri);
           invalidatedFiles.add(content.file.uri.toString());
-          numBytes -= content.size;
+          syncedBytes -= content.size;
         }
       }
     }
@@ -545,7 +571,7 @@
         if (!dirtyEntries.containsKey(entryUri)) {
           final DevFSFileContent content = DevFSFileContent(fs.file(compiledBinary));
           dirtyEntries[entryUri] = content;
-          numBytes += content.size;
+          syncedBytes += content.size;
         }
       }
     }
@@ -576,7 +602,8 @@
     }
 
     printTrace('DevFS: Sync finished');
-    return numBytes;
+    return UpdateFSReport(success: true, syncedBytes: syncedBytes,
+        invalidatedSourcesCount: invalidatedFiles.length);
   }
 
   void _scanFile(Uri deviceUri, FileSystemEntity file) {
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index f18f145..0f3ce7d 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -368,7 +368,7 @@
     return 0;
   }
 
-  Future<bool> updateDevFS({
+  Future<UpdateFSReport> updateDevFS({
     String mainPath,
     String target,
     AssetBundle bundle,
@@ -384,9 +384,9 @@
       'Syncing files to device ${device.name}...',
       expectSlowOperation: true,
     );
-    int bytes = 0;
+    UpdateFSReport report;
     try {
-      bytes = await devFS.update(
+      report = await devFS.update(
         mainPath: mainPath,
         target: target,
         bundle: bundle,
@@ -403,11 +403,11 @@
       );
     } on DevFSException {
       devFSStatus.cancel();
-      return false;
+      return UpdateFSReport(success: false);
     }
     devFSStatus.stop();
-    printTrace('Synced ${getSizeAsMB(bytes)}.');
-    return true;
+    printTrace('Synced ${getSizeAsMB(report.syncedBytes)}.');
+    return report;
   }
 
   void updateReloadStatus(bool wasReloadSuccessful) {
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index d03e02f..f257517 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -19,6 +19,7 @@
 import 'compile.dart';
 import 'dart/dependencies.dart';
 import 'dart/pub.dart';
+import 'devfs.dart';
 import 'device.dart';
 import 'globals.dart';
 import 'resident_runner.dart';
@@ -193,12 +194,12 @@
       return 3;
     }
     final Stopwatch initialUpdateDevFSsTimer = Stopwatch()..start();
-    final bool devfsResult = await _updateDevFS(fullRestart: true);
+    final UpdateFSReport devfsResult = await _updateDevFS(fullRestart: true);
     _addBenchmarkData(
       'hotReloadInitialDevFSSyncMilliseconds',
       initialUpdateDevFSsTimer.elapsed.inMilliseconds,
     );
-    if (!devfsResult)
+    if (!devfsResult.success)
       return 3;
 
     await refreshViews();
@@ -329,10 +330,10 @@
     return devFSUris;
   }
 
-  Future<bool> _updateDevFS({ bool fullRestart = false }) async {
+  Future<UpdateFSReport> _updateDevFS({ bool fullRestart = false }) async {
     if (!await _refreshDartDependencies()) {
       // Did not update DevFS because of a Dart source error.
-      return false;
+      return UpdateFSReport(success: false);
     }
     final bool isFirstUpload = assetBundle.wasBuiltOnce() == false;
     final bool rebuildBundle = assetBundle.needsBuild();
@@ -340,12 +341,12 @@
       printTrace('Updating assets');
       final int result = await assetBundle.build();
       if (result != 0)
-        return false;
+        return UpdateFSReport(success: false);
     }
 
-    final List<bool> results = <bool>[];
+    final UpdateFSReport results = UpdateFSReport(success: true);
     for (FlutterDevice device in flutterDevices) {
-      results.add(await device.updateDevFS(
+      results.incorporateResults(await device.updateDevFS(
         mainPath: mainPath,
         target: target,
         bundle: assetBundle,
@@ -358,16 +359,15 @@
         pathToReload: getReloadPath(fullRestart: fullRestart),
       ));
     }
-    // If there any failures reported, bail out.
-    if (results.any((bool result) => !result)) {
-      return false;
+    if (!results.success) {
+      return results;
     }
 
     if (!hotRunnerConfig.stableDartDependencies) {
       // Clear the set after the sync so they are recomputed next time.
       _dartDependencies = null;
     }
-    return true;
+    return results;
   }
 
   Future<void> _evictDirtyAssets() {
@@ -460,8 +460,8 @@
     final Stopwatch restartTimer = Stopwatch()..start();
     // TODO(aam): Add generator reset logic once we switch to using incremental
     // compiler for full application recompilation on restart.
-    final bool updatedDevFS = await _updateDevFS(fullRestart: true);
-    if (!updatedDevFS) {
+    final UpdateFSReport updatedDevFS = await _updateDevFS(fullRestart: true);
+    if (!updatedDevFS.success) {
       for (FlutterDevice device in flutterDevices) {
         if (device.generator != null)
           device.generator.reject();
@@ -618,11 +618,11 @@
     final Stopwatch reloadTimer = Stopwatch()..start();
 
     final Stopwatch devFSTimer = Stopwatch()..start();
-    final bool updatedDevFS = await _updateDevFS();
+    final UpdateFSReport updatedDevFS = await _updateDevFS();
     // Record time it took to synchronize to DevFS.
     _addBenchmarkData('hotReloadDevFSSyncMilliseconds',
         devFSTimer.elapsed.inMilliseconds);
-    if (!updatedDevFS)
+    if (!updatedDevFS.success)
       return OperationResult(1, 'DevFS synchronization failed');
     String reloadMessage;
     final Stopwatch vmReloadTimer = Stopwatch()..start();