Improve hot reload performance (#28152)

diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart
index 7940e8b..407a51b 100644
--- a/packages/flutter_tools/lib/src/devfs.dart
+++ b/packages/flutter_tools/lib/src/devfs.dart
@@ -30,8 +30,6 @@
 
 /// Common superclass for content copied to the device.
 abstract class DevFSContent {
-  bool _exists = true;
-
   /// Return true if this is the first time this method is called
   /// or if the entry has been modified since this method was last called.
   bool get isModified;
@@ -59,13 +57,6 @@
 class DevFSFileContent extends DevFSContent {
   DevFSFileContent(this.file);
 
-  static DevFSFileContent clone(DevFSFileContent fsFileContent) {
-    final DevFSFileContent newFsFileContent = DevFSFileContent(fsFileContent.file);
-    newFsFileContent._linkTarget = fsFileContent._linkTarget;
-    newFsFileContent._fileStat = fsFileContent._fileStat;
-    return newFsFileContent;
-  }
-
   final FileSystemEntity file;
   FileSystemEntity _linkTarget;
   FileStat _fileStat;
@@ -218,7 +209,6 @@
   Future<Uri> create(String fsName);
   Future<dynamic> destroy(String fsName);
   Future<dynamic> writeFile(String fsName, Uri deviceUri, DevFSContent content);
-  Future<dynamic> deleteFile(String fsName, Uri deviceUri);
 }
 
 /// An implementation of [DevFSOperations] that speaks to the
@@ -261,11 +251,6 @@
       printTrace('DevFS: Failed to write $deviceUri: $error');
     }
   }
-
-  @override
-  Future<dynamic> deleteFile(String fsName, Uri deviceUri) async {
-    // TODO(johnmccutchan): Add file deletion to the devFS protocol.
-  }
 }
 
 class DevFSException implements Exception {
@@ -288,16 +273,14 @@
   int _inFlight = 0;
   Map<Uri, DevFSContent> _outstanding;
   Completer<void> _completer;
-  HttpClient _client;
+  final HttpClient _client = HttpClient();
 
   Future<void> write(Map<Uri, DevFSContent> entries) async {
-    _client = HttpClient();
     _client.maxConnectionsPerHost = kMaxInFlight;
     _completer = Completer<void>();
     _outstanding = Map<Uri, DevFSContent>.from(entries);
     _scheduleWrites();
     await _completer.future;
-    _client.close();
   }
 
   void _scheduleWrites() {
@@ -405,9 +388,6 @@
   final Map<Uri, DevFSContent> _entries = <Uri, DevFSContent>{};
   final Set<String> assetPathsToEvict = <String>{};
 
-  final List<Future<Map<String, dynamic>>> _pendingOperations =
-      <Future<Map<String, dynamic>>>[];
-
   Uri _baseUri;
   Uri get baseUri => _baseUri;
 
@@ -453,66 +433,33 @@
     DateTime firstBuildTime,
     bool bundleFirstUpload = false,
     bool bundleDirty = false,
-    Set<String> fileFilter,
     @required ResidentCompiler generator,
     String dillOutputPath,
     @required bool trackWidgetCreation,
     bool fullRestart = false,
     String projectRootPath,
     @required String pathToReload,
+    @required List<String> invalidatedFiles,
   }) async {
     assert(trackWidgetCreation != null);
     assert(generator != null);
-    // Mark all entries as possibly deleted.
-    for (DevFSContent content in _entries.values) {
-      content._exists = false;
-    }
 
-    // Scan workspace, packages, and assets
-    printTrace('DevFS: Starting sync from $rootDirectory');
-    logger.printTrace('Scanning project files');
-    await _scanDirectory(rootDirectory,
-                         recursive: true,
-                         fileFilter: fileFilter);
-    if (fs.isFileSync(_packagesFilePath)) {
-      printTrace('Scanning package files');
-      await _scanPackages(fileFilter);
-    }
     if (bundle != null) {
       printTrace('Scanning asset files');
+      // We write the assets into the AssetBundle working dir so that they
+      // are in the same location in DevFS and the iOS simulator.
+      final String assetDirectory = getAssetBuildDirectory();
       bundle.entries.forEach((String archivePath, DevFSContent content) {
-        _scanBundleEntry(archivePath, content);
+        final Uri deviceUri = fs.path.toUri(fs.path.join(assetDirectory, archivePath));
+        _entries[deviceUri] = content;
       });
     }
 
-    // Handle deletions.
-    printTrace('Scanning for deleted files');
-    final String assetBuildDirPrefix = _asUriPath(getAssetBuildDirectory());
-    final List<Uri> toRemove = <Uri>[];
-    _entries.forEach((Uri deviceUri, DevFSContent content) {
-      if (!content._exists) {
-        final Future<Map<String, dynamic>> operation =
-            _operations.deleteFile(fsName, deviceUri)
-            .then<Map<String, dynamic>>((dynamic v) => v?.cast<String,dynamic>());
-        if (operation != null)
-          _pendingOperations.add(operation);
-        toRemove.add(deviceUri);
-        if (deviceUri.path.startsWith(assetBuildDirPrefix)) {
-          final String archivePath = deviceUri.path.substring(assetBuildDirPrefix.length);
-          assetPathsToEvict.add(archivePath);
-        }
-      }
-    });
-    if (toRemove.isNotEmpty) {
-      printTrace('Removing deleted files');
-      toRemove.forEach(_entries.remove);
-      await Future.wait<Map<String, dynamic>>(_pendingOperations);
-      _pendingOperations.clear();
-    }
-
     // Update modified files
-    int syncedBytes = 0;
+    final String assetBuildDirPrefix = _asUriPath(getAssetBuildDirectory());
     final Map<Uri, DevFSContent> dirtyEntries = <Uri, DevFSContent>{};
+
+    int syncedBytes = 0;
     _entries.forEach((Uri deviceUri, DevFSContent content) {
       String archivePath;
       if (deviceUri.path.startsWith(assetBuildDirPrefix))
@@ -527,30 +474,10 @@
           assetPathsToEvict.add(archivePath);
       }
     });
-    // We run generator even if [dirtyEntries] was empty because we want to
-    // keep logic of accepting/rejecting generator's output simple: we must
-    // accept/reject generator's output after every [update] call.  Incremental
-    // run with no changes is supposed to be fast (considering that it is
-    // initiated by user key press).
-    final List<String> invalidatedFiles = <String>[];
-    final Set<Uri> filesUris = <Uri>{};
-    for (Uri uri in dirtyEntries.keys.toList()) {
-      if (!uri.path.startsWith(assetBuildDirPrefix)) {
-        final DevFSContent content = dirtyEntries[uri];
-        if (content is DevFSFileContent) {
-          filesUris.add(uri);
-          invalidatedFiles.add(content.file.uri.toString());
-          syncedBytes -= content.size;
-        }
-      }
-    }
-    // No need to send source files because all compilation is done on the
-    // host and result of compilation is single kernel file.
-    filesUris.forEach(dirtyEntries.remove);
-    printTrace('Compiling dart to kernel with ${invalidatedFiles.length} updated files');
     if (fullRestart) {
       generator.reset();
     }
+    printTrace('Compiling dart to kernel with ${invalidatedFiles.length} updated files');
     final CompilerOutput compilerOutput = await generator.recompile(
       mainPath,
       invalidatedFiles,
@@ -566,228 +493,28 @@
           ? fs.path.relative(pathToReload, from: projectRootPath)
           : pathToReload,
         );
-        if (!dirtyEntries.containsKey(entryUri)) {
-          final DevFSFileContent content = DevFSFileContent(fs.file(compiledBinary));
-          dirtyEntries[entryUri] = content;
-          syncedBytes += content.size;
-        }
+        final DevFSFileContent content = DevFSFileContent(fs.file(compiledBinary));
+        syncedBytes += content.size;
+        dirtyEntries[entryUri] = content;
       }
     }
+    printTrace('Updating files');
     if (dirtyEntries.isNotEmpty) {
-      printTrace('Updating files');
-      if (_httpWriter != null) {
-        try {
-          await _httpWriter.write(dirtyEntries);
-        } on SocketException catch (socketException, stackTrace) {
-          printTrace('DevFS sync failed. Lost connection to device: $socketException');
-          throw DevFSException('Lost connection to device.', socketException, stackTrace);
-        } catch (exception, stackTrace) {
-          printError('Could not update files on device: $exception');
-          throw DevFSException('Sync failed', exception, stackTrace);
-        }
-      } else {
-        // Make service protocol requests for each.
-        dirtyEntries.forEach((Uri deviceUri, DevFSContent content) {
-          final Future<Map<String, dynamic>> operation =
-              _operations.writeFile(fsName, deviceUri, content)
-                  .then<Map<String, dynamic>>((dynamic v) => v?.cast<String, dynamic>());
-          if (operation != null)
-            _pendingOperations.add(operation);
-        });
-        await Future.wait<Map<String, dynamic>>(_pendingOperations, eagerError: true);
-        _pendingOperations.clear();
+      try {
+        await _httpWriter.write(dirtyEntries);
+      } on SocketException catch (socketException, stackTrace) {
+        printTrace('DevFS sync failed. Lost connection to device: $socketException');
+        throw DevFSException('Lost connection to device.', socketException, stackTrace);
+      } catch (exception, stackTrace) {
+        printError('Could not update files on device: $exception');
+        throw DevFSException('Sync failed', exception, stackTrace);
       }
     }
-
     printTrace('DevFS: Sync finished');
     return UpdateFSReport(success: true, syncedBytes: syncedBytes,
-        invalidatedSourcesCount: invalidatedFiles.length);
-  }
-
-  void _scanFile(Uri deviceUri, FileSystemEntity file) {
-    final DevFSContent content = _entries.putIfAbsent(deviceUri, () => DevFSFileContent(file));
-    content._exists = true;
-  }
-
-  void _scanBundleEntry(String archivePath, DevFSContent content) {
-    // We write the assets into the AssetBundle working dir so that they
-    // are in the same location in DevFS and the iOS simulator.
-    final Uri deviceUri = fs.path.toUri(fs.path.join(getAssetBuildDirectory(), archivePath));
-
-    _entries[deviceUri] = content;
-    content._exists = true;
-  }
-
-  bool _shouldIgnore(Uri deviceUri) {
-    final List<String> ignoredUriPrefixes = <String>['android/',
-                                               _asUriPath(getBuildDirectory()),
-                                               'ios/',
-                                               '.pub/'];
-    for (String ignoredUriPrefix in ignoredUriPrefixes) {
-      if (deviceUri.path.startsWith(ignoredUriPrefix))
-        return true;
-    }
-    return false;
-  }
-
-  bool _shouldSkip(
-    FileSystemEntity file,
-    String relativePath,
-    Uri directoryUriOnDevice, {
-    bool ignoreDotFiles = true,
-  }) {
-    if (file is Directory) {
-      // Skip non-files.
-      return true;
-    }
-    assert((file is Link) || (file is File));
-    final String basename = fs.path.basename(file.path);
-    if (ignoreDotFiles && basename.startsWith('.')) {
-      // Skip dot files, but not the '.packages' file (even though in dart1
-      // mode devfs['.packages'] will be overwritten with synthesized string content).
-      return basename != '.packages';
-    }
-    return false;
-  }
-
-  Uri _directoryUriOnDevice(Uri directoryUriOnDevice, Directory directory) {
-    if (directoryUriOnDevice == null) {
-      final String relativeRootPath = fs.path.relative(directory.path, from: rootDirectory.path);
-      if (relativeRootPath == '.') {
-        directoryUriOnDevice = Uri();
-      } else {
-        directoryUriOnDevice = fs.path.toUri(relativeRootPath);
-      }
-    }
-    return directoryUriOnDevice;
-  }
-
-  /// Scan all files from the [fileFilter] that are contained in [directory] and
-  /// pass various filters (e.g. ignoreDotFiles).
-  Future<bool> _scanFilteredDirectory(
-    Set<String> fileFilter,
-    Directory directory, {
-    Uri directoryUriOnDevice,
-    bool ignoreDotFiles = true,
-  }) async {
-    directoryUriOnDevice =
-        _directoryUriOnDevice(directoryUriOnDevice, directory);
-    try {
-      final String absoluteDirectoryPath = canonicalizePath(directory.path);
-      // For each file in the file filter.
-      for (String filePath in fileFilter) {
-        if (!filePath.startsWith(absoluteDirectoryPath)) {
-          // File is not in this directory. Skip.
-          continue;
-        }
-        final String relativePath =
-          fs.path.relative(filePath, from: directory.path);
-        final FileSystemEntity file = fs.file(filePath);
-        if (_shouldSkip(file, relativePath, directoryUriOnDevice, ignoreDotFiles: ignoreDotFiles)) {
-          continue;
-        }
-        final Uri deviceUri = directoryUriOnDevice.resolveUri(fs.path.toUri(relativePath));
-        if (!_shouldIgnore(deviceUri))
-          _scanFile(deviceUri, file);
-      }
-    } on FileSystemException catch (e) {
-      _printScanDirectoryError(directory.path, e);
-      return false;
-    }
-    return true;
-  }
-
-  /// Scan all files in [directory] that pass various filters (e.g. ignoreDotFiles).
-  Future<bool> _scanDirectory(
-    Directory directory, {
-    Uri directoryUriOnDevice,
-    bool recursive = false,
-    bool ignoreDotFiles = true,
-    Set<String> fileFilter,
-  }) async {
-    directoryUriOnDevice = _directoryUriOnDevice(directoryUriOnDevice, directory);
-    if ((fileFilter != null) && fileFilter.isNotEmpty) {
-      // When the fileFilter isn't empty, we can skip crawling the directory
-      // tree and instead use the fileFilter as the source of potential files.
-      return _scanFilteredDirectory(fileFilter,
-                                    directory,
-                                    directoryUriOnDevice: directoryUriOnDevice,
-                                    ignoreDotFiles: ignoreDotFiles);
-    }
-    try {
-      final Stream<FileSystemEntity> files =
-          directory.list(recursive: recursive, followLinks: false);
-      await for (FileSystemEntity file in files) {
-        if (!devFSConfig.noDirectorySymlinks && (file is Link)) {
-          // Check if this is a symlink to a directory and skip it.
-          try {
-            final FileSystemEntityType linkType =
-                fs.statSync(file.resolveSymbolicLinksSync()).type;
-            if (linkType == FileSystemEntityType.directory)
-              continue;
-          } on FileSystemException catch (e) {
-            _printScanDirectoryError(file.path, e);
-            continue;
-          }
-        }
-        final String relativePath =
-          fs.path.relative(file.path, from: directory.path);
-        if (_shouldSkip(file, relativePath, directoryUriOnDevice, ignoreDotFiles: ignoreDotFiles)) {
-          continue;
-        }
-        final Uri deviceUri = directoryUriOnDevice.resolveUri(fs.path.toUri(relativePath));
-        if (!_shouldIgnore(deviceUri))
-          _scanFile(deviceUri, file);
-      }
-    } on FileSystemException catch (e) {
-      _printScanDirectoryError(directory.path, e);
-      return false;
-    }
-    return true;
-  }
-
-  void _printScanDirectoryError(String path, Exception e) {
-    printError(
-        'Error while scanning $path.\n'
-        'Hot Reload might not work until the following error is resolved:\n'
-        '$e\n'
-    );
-  }
-
-  Future<void> _scanPackages(Set<String> fileFilter) async {
-    StringBuffer sb;
-    final PackageMap packageMap = PackageMap(_packagesFilePath);
-
-    for (String packageName in packageMap.map.keys) {
-      final Uri packageUri = packageMap.map[packageName];
-      final String packagePath = fs.path.fromUri(packageUri);
-      final Directory packageDirectory = fs.directory(packageUri);
-      Uri directoryUriOnDevice = fs.path.toUri(fs.path.join('packages', packageName) + fs.path.separator);
-      bool packageExists = packageDirectory.existsSync();
-
-      if (!packageExists) {
-        // If the package directory doesn't exist at all, we ignore it.
-        continue;
-      }
-
-      if (fs.path.isWithin(rootDirectory.path, packagePath)) {
-        // We already scanned everything under the root directory.
-        directoryUriOnDevice = fs.path.toUri(
-            fs.path.relative(packagePath, from: rootDirectory.path) + fs.path.separator
-        );
-      } else {
-        packageExists =
-            await _scanDirectory(packageDirectory,
-                                 directoryUriOnDevice: directoryUriOnDevice,
-                                 recursive: true,
-                                 fileFilter: fileFilter);
-      }
-      if (packageExists) {
-        sb ??= StringBuffer();
-        sb.writeln('$packageName:$directoryUriOnDevice');
-      }
-    }
+         invalidatedSourcesCount: invalidatedFiles.length);
   }
 }
+
 /// Converts a platform-specific file path to a platform-independent Uri path.
 String _asUriPath(String filePath) => fs.path.toUri(filePath).path + '/';