[vm/frontend] Change frontend server interface so compile response contains delta of sources files that were used by the compiler.

This is needed so that for incremental compilation client knows which files need to be tracked for changes, need to be reported as invalidated for next recompilation cycle(see https://github.com/flutter/flutter/pull/29004)

Change-Id: I8c9e7800b55335497a4cbecfd35cec7390528eb7
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/95920
Commit-Queue: Alexander Aprelev <aam@google.com>
Reviewed-by: Alexander Markov <alexmarkov@google.com>
diff --git a/pkg/vm/lib/frontend_server.dart b/pkg/vm/lib/frontend_server.dart
index 1aabfba..ecd0c8e 100644
--- a/pkg/vm/lib/frontend_server.dart
+++ b/pkg/vm/lib/frontend_server.dart
@@ -28,6 +28,7 @@
 import 'package:vm/incremental_compiler.dart' show IncrementalCompiler;
 import 'package:vm/kernel_front_end.dart'
     show
+        asFileUri,
         compileToKernel,
         parseCommandLineDefines,
         convertFileOrUriArgumentToUri,
@@ -240,6 +241,8 @@
   String _kernelBinaryFilenameFull;
   String _initializeFromDill;
 
+  Set<Uri> previouslyReportedDependencies = Set<Uri>();
+
   final ProgramTransformer transformer;
 
   final List<String> errors = new List<String>();
@@ -355,6 +358,8 @@
       await writeDillFile(component, _kernelBinaryFilename,
           filterExternal: importDill != null);
 
+      _outputStream.writeln(boundaryKey);
+      await _outputDependenciesDelta(component);
       _outputStream
           .writeln('$boundaryKey $_kernelBinaryFilename ${errors.length}');
       final String depfile = options['depfile'];
@@ -369,6 +374,27 @@
     return errors.isEmpty;
   }
 
+  void _outputDependenciesDelta(Component component) async {
+    Set<Uri> uris = new Set<Uri>();
+    for (Uri uri in component.uriToSource.keys) {
+      // Skip empty or corelib dependencies.
+      if (uri == null || uri.scheme == 'org-dartlang-sdk') continue;
+      uris.add(uri);
+    }
+    for (Uri uri in uris) {
+      if (previouslyReportedDependencies.contains(uri)) {
+        continue;
+      }
+      _outputStream.writeln('+${await asFileUri(_fileSystem, uri)}');
+    }
+    for (Uri uri in previouslyReportedDependencies) {
+      if (!uris.contains(uri)) {
+        _outputStream.writeln('-${await asFileUri(_fileSystem, uri)}');
+      }
+    }
+    previouslyReportedDependencies = uris;
+  }
+
   writeDillFile(Component component, String filename,
       {bool filterExternal: false}) async {
     final IOSink sink = new File(filename).openWrite();
@@ -468,6 +494,8 @@
       transformer.transform(deltaProgram);
     }
     await writeDillFile(deltaProgram, _kernelBinaryFilename);
+    _outputStream.writeln(boundaryKey);
+    await _outputDependenciesDelta(deltaProgram);
     _outputStream
         .writeln('$boundaryKey $_kernelBinaryFilename ${errors.length}');
     _kernelBinaryFilename = _kernelBinaryFilenameIncremental;
diff --git a/pkg/vm/test/frontend_server_test.dart b/pkg/vm/test/frontend_server_test.dart
index 6881882..bf7bf47 100644
--- a/pkg/vm/test/frontend_server_test.dart
+++ b/pkg/vm/test/frontend_server_test.dart
@@ -426,41 +426,25 @@
       final StreamController<List<int>> stdoutStreamController =
           new StreamController<List<int>>();
       final IOSink ioSink = new IOSink(stdoutStreamController.sink);
-      StreamController<String> receivedResults = new StreamController<String>();
-
-      String boundaryKey;
+      StreamController<Result> receivedResults = new StreamController<Result>();
+      final outputParser = new OutputParser(receivedResults);
       stdoutStreamController.stream
           .transform(utf8.decoder)
           .transform(const LineSplitter())
-          .listen((String s) {
-        const String RESULT_OUTPUT_SPACE = 'result ';
-        if (boundaryKey == null) {
-          if (s.startsWith(RESULT_OUTPUT_SPACE)) {
-            boundaryKey = s.substring(RESULT_OUTPUT_SPACE.length);
-          }
-        } else {
-          if (s.startsWith(boundaryKey)) {
-            receivedResults.add(s.length > boundaryKey.length
-                ? s.substring(boundaryKey.length + 1)
-                : null);
-            boundaryKey = null;
-          }
-        }
-      });
+          .listen(outputParser.listener);
 
       Future<int> result =
           starter(args, input: streamController.stream, output: ioSink);
       streamController.add('compile ${file.path}\n'.codeUnits);
       int count = 0;
-      receivedResults.stream.listen((String outputFilenameAndErrorCount) {
+      receivedResults.stream.listen((Result compiledResult) {
+        CompilationResult result =
+            new CompilationResult.parse(compiledResult.status);
         if (count == 0) {
           // First request is to 'compile', which results in full kernel file.
-          CompilationResult result =
-              new CompilationResult.parse(outputFilenameAndErrorCount);
-
+          expect(result.errorsCount, equals(0));
           expect(dillFile.existsSync(), equals(true));
           expect(result.filename, dillFile.path);
-          expect(result.errorsCount, equals(0));
           streamController.add('accept\n'.codeUnits);
 
           // 'compile-expression <boundarykey>
@@ -474,26 +458,25 @@
           // <libraryUri: String>
           // <klass: String>
           // <isStatic: true|false>
+          outputParser.expectSources = false;
           streamController.add(
               'compile-expression abc\n2+2\nabc\nabc\n${file.uri}\n\n\n'
                   .codeUnits);
           count += 1;
         } else if (count == 1) {
+          expect(result.errorsCount, isNull);
           // Previous request should have failed because isStatic was blank
-          expect(outputFilenameAndErrorCount, isNull);
+          expect(compiledResult.status, isNull);
 
+          outputParser.expectSources = false;
           streamController.add(
               'compile-expression abc\n2+2\nabc\nabc\n${file.uri}\n\nfalse\n'
                   .codeUnits);
           count += 1;
         } else if (count == 2) {
+          expect(result.errorsCount, equals(0));
           // Second request is to 'compile-expression', which results in
           // kernel file with a function that wraps compiled expression.
-          expect(outputFilenameAndErrorCount, isNotNull);
-          CompilationResult result =
-              new CompilationResult.parse(outputFilenameAndErrorCount);
-
-          expect(result.errorsCount, equals(0));
           File outputFile = new File(result.filename);
           expect(outputFile.existsSync(), equals(true));
           expect(outputFile.lengthSync(), isPositive);
@@ -503,9 +486,6 @@
         } else {
           expect(count, 3);
           // Third request is to 'compile' non-existent file, that should fail.
-          expect(outputFilenameAndErrorCount, isNotNull);
-          CompilationResult result =
-              new CompilationResult.parse(outputFilenameAndErrorCount);
           expect(result.errorsCount, greaterThan(0));
 
           streamController.add('quit\n'.codeUnits);
@@ -515,6 +495,126 @@
       expect(await result, 0);
     });
 
+    test('compiler reports correct sources added', () async {
+      var libFile = new File('${tempDir.path}/lib.dart')
+        ..createSync(recursive: true)
+        ..writeAsStringSync("var foo = 42;");
+      var mainFile = new File('${tempDir.path}/main.dart')
+        ..createSync(recursive: true)
+        ..writeAsStringSync("main() => print('foo');\n");
+      var dillFile = new File('${tempDir.path}/app.dill');
+      expect(dillFile.existsSync(), equals(false));
+      final List<String> args = <String>[
+        '--sdk-root=${sdkRoot.toFilePath()}',
+        '--incremental',
+        '--platform=${platformKernel.path}',
+        '--output-dill=${dillFile.path}'
+      ];
+
+      final StreamController<List<int>> inputStreamController =
+          new StreamController<List<int>>();
+      final StreamController<List<int>> stdoutStreamController =
+          new StreamController<List<int>>();
+      final IOSink ioSink = new IOSink(stdoutStreamController.sink);
+      StreamController<Result> receivedResults = new StreamController<Result>();
+
+      final outputParser = new OutputParser(receivedResults);
+      stdoutStreamController.stream
+          .transform(utf8.decoder)
+          .transform(const LineSplitter())
+          .listen(outputParser.listener);
+
+      final Future<int> result =
+          starter(args, input: inputStreamController.stream, output: ioSink);
+      inputStreamController.add('compile ${mainFile.path}\n'.codeUnits);
+      int count = 0;
+      receivedResults.stream.listen((Result compiledResult) {
+        compiledResult.expectNoErrors();
+        if (count == 0) {
+          expect(compiledResult.sources.length, equals(1));
+          expect(compiledResult.sources, contains('+${mainFile.uri}'));
+
+          inputStreamController.add('accept\n'.codeUnits);
+          mainFile
+              .writeAsStringSync("import 'lib.dart';  main() => print(foo);\n");
+          inputStreamController.add('recompile ${mainFile.path} abc\n'
+              '${mainFile.uri}\n'
+              'abc\n'
+              .codeUnits);
+          count += 1;
+        } else if (count == 1) {
+          expect(compiledResult.sources.length, equals(1));
+          expect(compiledResult.sources, contains('+${libFile.uri}'));
+          inputStreamController.add('accept\n'.codeUnits);
+          inputStreamController.add('quit\n'.codeUnits);
+        }
+      });
+
+      expect(await result, 0);
+      inputStreamController.close();
+    }, timeout: Timeout.factor(100));
+
+    test('compiler reports correct sources removed', () async {
+      var libFile = new File('${tempDir.path}/lib.dart')
+        ..createSync(recursive: true)
+        ..writeAsStringSync("var foo = 42;");
+      var mainFile = new File('${tempDir.path}/main.dart')
+        ..createSync(recursive: true)
+        ..writeAsStringSync("import 'lib.dart'; main() => print(foo);\n");
+      var dillFile = new File('${tempDir.path}/app.dill');
+      expect(dillFile.existsSync(), equals(false));
+      final List<String> args = <String>[
+        '--sdk-root=${sdkRoot.toFilePath()}',
+        '--incremental',
+        '--platform=${platformKernel.path}',
+        '--output-dill=${dillFile.path}'
+      ];
+
+      final StreamController<List<int>> inputStreamController =
+          new StreamController<List<int>>();
+      final StreamController<List<int>> stdoutStreamController =
+          new StreamController<List<int>>();
+      final IOSink ioSink = new IOSink(stdoutStreamController.sink);
+      StreamController<Result> receivedResults = new StreamController<Result>();
+
+      final outputParser = new OutputParser(receivedResults);
+      stdoutStreamController.stream
+          .transform(utf8.decoder)
+          .transform(const LineSplitter())
+          .listen(outputParser.listener);
+
+      final Future<int> result =
+          starter(args, input: inputStreamController.stream, output: ioSink);
+      inputStreamController.add('compile ${mainFile.path}\n'.codeUnits);
+      int count = 0;
+      receivedResults.stream.listen((Result compiledResult) {
+        compiledResult.expectNoErrors();
+        if (count == 0) {
+          expect(compiledResult.sources.length, equals(2));
+          expect(compiledResult.sources,
+              allOf(contains('+${mainFile.uri}'), contains('+${libFile.uri}')));
+
+          inputStreamController.add('accept\n'.codeUnits);
+          mainFile.writeAsStringSync("main() => print('foo');\n");
+          inputStreamController.add('recompile ${mainFile.path} abc\n'
+              '${mainFile.uri}\n'
+              'abc\n'
+              .codeUnits);
+          count += 1;
+        } else if (count == 1) {
+          expect(compiledResult.sources.length, equals(1));
+          expect(compiledResult.sources, contains('-${libFile.uri}'));
+          inputStreamController.add('accept\n'.codeUnits);
+          inputStreamController.add('quit\n'.codeUnits);
+        }
+      });
+
+      expect(await result, 0);
+      inputStreamController.close();
+    },
+        timeout: Timeout.factor(100),
+        skip: true /* TODO(dartbug/36197): Unskip when compiler is fixed. */);
+
     test('compile expression when delta is rejected', () async {
       var fileLib = new File('${tempDir.path}/lib.dart')..createSync();
       fileLib.writeAsStringSync("foo() => 42;\n");
@@ -534,42 +634,26 @@
       final StreamController<List<int>> stdoutStreamController =
           new StreamController<List<int>>();
       final IOSink ioSink = new IOSink(stdoutStreamController.sink);
-      StreamController<String> receivedResults = new StreamController<String>();
+      StreamController<Result> receivedResults = new StreamController<Result>();
 
-      String boundaryKey;
+      final outputParser = new OutputParser(receivedResults);
       stdoutStreamController.stream
           .transform(utf8.decoder)
           .transform(const LineSplitter())
-          .listen((String s) {
-        print(s);
-        const String RESULT_OUTPUT_SPACE = 'result ';
-        if (boundaryKey == null) {
-          if (s.startsWith(RESULT_OUTPUT_SPACE)) {
-            boundaryKey = s.substring(RESULT_OUTPUT_SPACE.length);
-          }
-        } else {
-          if (s.startsWith(boundaryKey)) {
-            receivedResults.add(s.length > boundaryKey.length
-                ? s.substring(boundaryKey.length + 1)
-                : null);
-            boundaryKey = null;
-          }
-        }
-      });
+          .listen(outputParser.listener);
 
       final Future<int> result =
           starter(args, input: inputStreamController.stream, output: ioSink);
       inputStreamController.add('compile ${file.path}\n'.codeUnits);
       int count = 0;
-      receivedResults.stream.listen((String outputFilenameAndErrorCount) {
+      receivedResults.stream.listen((Result compiledResult) {
+        CompilationResult result =
+            new CompilationResult.parse(compiledResult.status);
         if (count == 0) {
           // First request was to 'compile', which resulted in full kernel file.
-          CompilationResult result =
-              new CompilationResult.parse(outputFilenameAndErrorCount);
-
+          expect(result.errorsCount, 0);
           expect(dillFile.existsSync(), equals(true));
           expect(result.filename, dillFile.path);
-          expect(result.errorsCount, equals(0));
           inputStreamController.add('accept\n'.codeUnits);
 
           // 'compile-expression <boundarykey>
@@ -583,6 +667,7 @@
           // <libraryUri: String>
           // <klass: String>
           // <isStatic: true|false>
+          outputParser.expectSources = false;
           inputStreamController.add('''
 compile-expression abc
 main1
@@ -597,12 +682,7 @@
         } else if (count == 1) {
           // Second request was to 'compile-expression', which resulted in
           // kernel file with a function that wraps compiled expression.
-          expect(outputFilenameAndErrorCount, isNotNull);
-          CompilationResult result =
-              new CompilationResult.parse(outputFilenameAndErrorCount);
-          print(outputFilenameAndErrorCount);
-
-          expect(result.errorsCount, equals(0));
+          expect(result.errorsCount, 0);
           File outputFile = new File(result.filename);
           expect(outputFile.existsSync(), equals(true));
           expect(outputFile.lengthSync(), isPositive);
@@ -616,15 +696,13 @@
           count += 1;
         } else if (count == 2) {
           // Third request was to recompile the script after renaming a function.
-          expect(outputFilenameAndErrorCount, isNotNull);
-          CompilationResult result =
-              new CompilationResult.parse(outputFilenameAndErrorCount);
-          expect(result.errorsCount, equals(0));
-
+          expect(result.errorsCount, 0);
+          outputParser.expectSources = false;
           inputStreamController.add('reject\n'.codeUnits);
           count += 1;
         } else if (count == 3) {
           // Fourth request was to reject the compilation results.
+          outputParser.expectSources = false;
           inputStreamController.add(
               'compile-expression abc\nmain1\nabc\nabc\n${file.uri}\n\ntrue\n'
                   .codeUnits);
@@ -633,10 +711,7 @@
           expect(count, 4);
           // Fifth request was to 'compile-expression' that references original
           // function, which should still be successful.
-          expect(outputFilenameAndErrorCount, isNotNull);
-          CompilationResult result =
-              new CompilationResult.parse(outputFilenameAndErrorCount);
-          expect(result.errorsCount, equals(0));
+          expect(result.errorsCount, 0);
           inputStreamController.add('quit\n'.codeUnits);
         }
       });
@@ -662,37 +737,23 @@
       final StreamController<List<int>> stdoutStreamController =
           new StreamController<List<int>>();
       final IOSink ioSink = new IOSink(stdoutStreamController.sink);
-      StreamController<String> receivedResults = new StreamController<String>();
+      StreamController<Result> receivedResults = new StreamController<Result>();
 
-      String boundaryKey;
+      final outputParser = new OutputParser(receivedResults);
       stdoutStreamController.stream
           .transform(utf8.decoder)
           .transform(const LineSplitter())
-          .listen((String s) {
-        const String RESULT_OUTPUT_SPACE = 'result ';
-        if (boundaryKey == null) {
-          if (s.startsWith(RESULT_OUTPUT_SPACE)) {
-            boundaryKey = s.substring(RESULT_OUTPUT_SPACE.length);
-          }
-        } else {
-          if (s.startsWith(boundaryKey)) {
-            receivedResults.add(s.substring(boundaryKey.length + 1));
-            boundaryKey = null;
-          }
-        }
-      });
+          .listen(outputParser.listener);
+
       Future<int> result =
           starter(args, input: inputStreamController.stream, output: ioSink);
       inputStreamController.add('compile ${file.path}\n'.codeUnits);
       int count = 0;
-      receivedResults.stream.listen((String outputFilenameAndErrorCount) {
-        CompilationResult result =
-            new CompilationResult.parse(outputFilenameAndErrorCount);
+      receivedResults.stream.listen((Result compiledResult) {
         if (count == 0) {
           // First request is to 'compile', which results in full kernel file.
           expect(dillFile.existsSync(), equals(true));
-          expect(result.filename, dillFile.path);
-          expect(result.errorsCount, 0);
+          compiledResult.expectNoErrors(filename: dillFile.path);
           count += 1;
           inputStreamController.add('accept\n'.codeUnits);
           var file2 = new File('${tempDir.path}/bar.dart')..createSync();
@@ -706,8 +767,7 @@
           // Second request is to 'recompile', which results in incremental
           // kernel file.
           var dillIncFile = new File('${dillFile.path}.incremental.dill');
-          expect(result.filename, dillIncFile.path);
-          expect(result.errorsCount, 0);
+          compiledResult.expectNoErrors(filename: dillIncFile.path);
           expect(dillIncFile.existsSync(), equals(true));
           inputStreamController.add('quit\n'.codeUnits);
         }
@@ -771,32 +831,20 @@
       final StreamController<List<int>> stdoutStreamController =
           new StreamController<List<int>>();
       final IOSink ioSink = new IOSink(stdoutStreamController.sink);
-      StreamController<String> receivedResults = new StreamController<String>();
-
-      String boundaryKey;
+      StreamController<Result> receivedResults = new StreamController<Result>();
+      final outputParser = new OutputParser(receivedResults);
       stdoutStreamController.stream
           .transform(utf8.decoder)
           .transform(const LineSplitter())
-          .listen((String s) {
-        const String RESULT_OUTPUT_SPACE = 'result ';
-        if (boundaryKey == null) {
-          if (s.startsWith(RESULT_OUTPUT_SPACE)) {
-            boundaryKey = s.substring(RESULT_OUTPUT_SPACE.length);
-          }
-        } else {
-          if (s.startsWith(boundaryKey)) {
-            receivedResults.add(s.substring(boundaryKey.length + 1));
-            boundaryKey = null;
-          }
-        }
-      });
+          .listen(outputParser.listener);
+
       Future<int> result =
           starter(args, input: inputStreamController.stream, output: ioSink);
       inputStreamController.add('compile ${file.path}\n'.codeUnits);
       int count = 0;
-      receivedResults.stream.listen((String outputFilenameAndErrorCount) {
+      receivedResults.stream.listen((Result compiledResult) {
         CompilationResult result =
-            new CompilationResult.parse(outputFilenameAndErrorCount);
+            new CompilationResult.parse(compiledResult.status);
         switch (count) {
           case 0:
             expect(dillFile.existsSync(), equals(true));
@@ -854,32 +902,21 @@
       final StreamController<List<int>> stdoutStreamController =
           new StreamController<List<int>>();
       final IOSink ioSink = new IOSink(stdoutStreamController.sink);
-      StreamController<String> receivedResults = new StreamController<String>();
+      StreamController<Result> receivedResults = new StreamController<Result>();
 
-      String boundaryKey;
+      final outputParser = new OutputParser(receivedResults);
       stdoutStreamController.stream
           .transform(utf8.decoder)
           .transform(const LineSplitter())
-          .listen((String s) {
-        const String RESULT_OUTPUT_SPACE = 'result ';
-        if (boundaryKey == null) {
-          if (s.startsWith(RESULT_OUTPUT_SPACE)) {
-            boundaryKey = s.substring(RESULT_OUTPUT_SPACE.length);
-          }
-        } else {
-          if (s.startsWith(boundaryKey)) {
-            receivedResults.add(s.substring(boundaryKey.length + 1));
-            boundaryKey = null;
-          }
-        }
-      });
+          .listen(outputParser.listener);
+
       Future<int> result =
           starter(args, input: inputStreamController.stream, output: ioSink);
       inputStreamController.add('compile ${file.uri}\n'.codeUnits);
       int count = 0;
-      receivedResults.stream.listen((String outputFilenameAndErrorCount) {
+      receivedResults.stream.listen((Result compiledResult) {
         CompilationResult result =
-            new CompilationResult.parse(outputFilenameAndErrorCount);
+            new CompilationResult.parse(compiledResult.status);
         switch (count) {
           case 0:
             expect(dillFile.existsSync(), equals(true));
@@ -1035,36 +1072,23 @@
         final StreamController<List<int>> stdoutStreamController =
             new StreamController<List<int>>();
         final IOSink ioSink = new IOSink(stdoutStreamController.sink);
-        StreamController<String> receivedResults =
-            new StreamController<String>();
+        StreamController<Result> receivedResults =
+            new StreamController<Result>();
 
-        String boundaryKey;
+        final outputParser = new OutputParser(receivedResults);
         stdoutStreamController.stream
             .transform(utf8.decoder)
             .transform(const LineSplitter())
-            .listen((String s) {
-          const String RESULT_OUTPUT_SPACE = 'result ';
-          if (boundaryKey == null) {
-            if (s.startsWith(RESULT_OUTPUT_SPACE)) {
-              boundaryKey = s.substring(RESULT_OUTPUT_SPACE.length);
-            }
-          } else {
-            if (s.startsWith(boundaryKey)) {
-              receivedResults.add(s.substring(boundaryKey.length + 1));
-              boundaryKey = null;
-            }
-          }
-        });
+            .listen(outputParser.listener);
 
         Future<int> result =
             starter(args, input: inputStreamController.stream, output: ioSink);
         inputStreamController.add('compile ${dart2js.path}\n'.codeUnits);
         int count = 0;
-        receivedResults.stream.listen((String outputFilenameAndErrorCount) {
-          int delim = outputFilenameAndErrorCount.lastIndexOf(' ');
-          expect(delim > 0, equals(true));
-          String outputFilename =
-              outputFilenameAndErrorCount.substring(0, delim);
+        receivedResults.stream.listen((Result compiledResult) {
+          CompilationResult result =
+              CompilationResult.parse(compiledResult.status);
+          String outputFilename = result.filename;
           print("$outputFilename -- count $count");
 
           // Ensure that kernel file produced when compiler was initialized
@@ -1205,9 +1229,75 @@
   int errorsCount;
 
   CompilationResult.parse(String filenameAndErrorCount) {
+    if (filenameAndErrorCount == null) {
+      return;
+    }
     int delim = filenameAndErrorCount.lastIndexOf(' ');
     expect(delim > 0, equals(true));
     filename = filenameAndErrorCount.substring(0, delim);
     errorsCount = int.parse(filenameAndErrorCount.substring(delim + 1).trim());
   }
 }
+
+class Result {
+  String status;
+  List<String> sources;
+
+  Result(this.status, this.sources);
+
+  void expectNoErrors({String filename}) {
+    var result = CompilationResult.parse(status);
+    expect(result.errorsCount, equals(0));
+    if (filename != null) {
+      expect(result.filename, equals(filename));
+    }
+  }
+}
+
+class OutputParser {
+  OutputParser(this._receivedResults);
+  bool expectSources = true;
+
+  StreamController<Result> _receivedResults;
+  List<String> _receivedSources;
+
+  String _boundaryKey;
+  bool _readingSources;
+
+  void listener(String s) {
+    if (_boundaryKey == null) {
+      const String RESULT_OUTPUT_SPACE = 'result ';
+      if (s.startsWith(RESULT_OUTPUT_SPACE)) {
+        _boundaryKey = s.substring(RESULT_OUTPUT_SPACE.length);
+      }
+      _readingSources = false;
+      _receivedSources?.clear();
+      return;
+    }
+
+    if (s.startsWith(_boundaryKey)) {
+      // First boundaryKey separates compiler output from list of sources
+      // (if we expect list of sources, which is indicated by receivedSources
+      // being not null)
+      if (expectSources && !_readingSources) {
+        _readingSources = true;
+        return;
+      }
+      // Second boundaryKey indicates end of frontend server response
+      expectSources = true;
+      _receivedResults.add(Result(
+          s.length > _boundaryKey.length
+              ? s.substring(_boundaryKey.length + 1)
+              : null,
+          _receivedSources));
+      _boundaryKey = null;
+    } else {
+      if (_readingSources) {
+        if (_receivedSources == null) {
+          _receivedSources = <String>[];
+        }
+        _receivedSources.add(s);
+      }
+    }
+  }
+}
diff --git a/tools/patches/flutter-flutter/8b1a299ed52d4ef9521ccd65c6c52d563129d8af.patch b/tools/patches/flutter-flutter/8b1a299ed52d4ef9521ccd65c6c52d563129d8af.patch
index 75981c4..18bc69e 100644
--- a/tools/patches/flutter-flutter/8b1a299ed52d4ef9521ccd65c6c52d563129d8af.patch
+++ b/tools/patches/flutter-flutter/8b1a299ed52d4ef9521ccd65c6c52d563129d8af.patch
@@ -1280,3 +1280,185 @@
 -    );
 -  }, skip: !Platform.isLinux); // Coretext uses different thicknesses for decoration
 -}
+diff --git a/packages/flutter_tools/lib/src/codegen.dart b/packages/flutter_tools/lib/src/codegen.dart
+index 7d521a4f2..60403279d 100644
+--- a/packages/flutter_tools/lib/src/codegen.dart
++++ b/packages/flutter_tools/lib/src/codegen.dart
+@@ -190,10 +190,10 @@ class CodeGeneratingKernelCompiler implements KernelCompiler {
+         await outputFile.create();
+       }
+       await outputFile.writeAsBytes(await buildResult.dillFile.readAsBytes());
+-      return CompilerOutput(outputFilePath, 0);
++      return CompilerOutput(outputFilePath, 0, /* sources= */ null);
+     } on Exception catch (err) {
+       printError('Compilation Failed: $err');
+-      return const CompilerOutput(null, 1);
++      return const CompilerOutput(null, 1, /* sources= */ null);
+     }
+   }
+ }
+diff --git a/packages/flutter_tools/lib/src/compile.dart b/packages/flutter_tools/lib/src/compile.dart
+index dd255f53a..4cd09eacb 100644
+--- a/packages/flutter_tools/lib/src/compile.dart
++++ b/packages/flutter_tools/lib/src/compile.dart
+@@ -57,12 +57,15 @@ class TargetModel {
+ }
+ 
+ class CompilerOutput {
+-  const CompilerOutput(this.outputFilename, this.errorCount);
++  const CompilerOutput(this.outputFilename, this.errorCount, this.sources);
+ 
+   final String outputFilename;
+   final int errorCount;
++  final List<Uri> sources;
+ }
+ 
++enum StdoutState { CollectDiagnostic, CollectDependencies }
++
+ /// Handles stdin/stdout communication with the frontend server.
+ class StdoutHandler {
+   StdoutHandler({this.consumer = printError}) {
+@@ -72,30 +75,54 @@ class StdoutHandler {
+   bool compilerMessageReceived = false;
+   final CompilerMessageConsumer consumer;
+   String boundaryKey;
++  StdoutState state = StdoutState.CollectDiagnostic;
+   Completer<CompilerOutput> compilerOutput;
++  final List<Uri> sources = <Uri>[];
+ 
+   bool _suppressCompilerMessages;
+ 
+   void handler(String message) {
++    printTrace('-> $message');
+     const String kResultPrefix = 'result ';
+     if (boundaryKey == null && message.startsWith(kResultPrefix)) {
+         boundaryKey = message.substring(kResultPrefix.length);
+     } else if (message.startsWith(boundaryKey)) {
+-      if (message.length <= boundaryKey.length) {
+-        compilerOutput.complete(null);
+-        return;
++      if (state == StdoutState.CollectDiagnostic) {
++        state = StdoutState.CollectDependencies;
++      } else {
++        if (message.length <= boundaryKey.length) {
++          compilerOutput.complete(null);
++          return;
++        }
++        final int spaceDelimiter = message.lastIndexOf(' ');
++        compilerOutput.complete(
++            CompilerOutput(
++                message.substring(boundaryKey.length + 1, spaceDelimiter),
++                int.parse(message.substring(spaceDelimiter + 1).trim()),
++                sources));
+       }
+-      final int spaceDelimiter = message.lastIndexOf(' ');
+-      compilerOutput.complete(
+-        CompilerOutput(
+-          message.substring(boundaryKey.length + 1, spaceDelimiter),
+-          int.parse(message.substring(spaceDelimiter + 1).trim())));
+-    } else if (!_suppressCompilerMessages) {
+-      if (compilerMessageReceived == false) {
+-        consumer('\nCompiler message:');
+-        compilerMessageReceived = true;
++    } else {
++      if (state == StdoutState.CollectDiagnostic) {
++        if (!_suppressCompilerMessages) {
++          if (compilerMessageReceived == false) {
++            consumer('\nCompiler message:');
++            compilerMessageReceived = true;
++          }
++          consumer(message);
++        }
++      } else {
++        assert(state == StdoutState.CollectDependencies);
++        switch (message[0]) {
++          case '+':
++            sources.add(Uri.parse(message.substring(1)));
++            break;
++          case '-':
++            sources.remove(Uri.parse(message.substring(1)));
++            break;
++          default:
++            printTrace('Unexpected prefix for $message uri - ignoring');
++        }
+       }
+-      consumer(message);
+     }
+   }
+ 
+@@ -106,6 +133,7 @@ class StdoutHandler {
+     compilerMessageReceived = false;
+     compilerOutput = Completer<CompilerOutput>();
+     _suppressCompilerMessages = suppressCompilerMessages;
++    state = StdoutState.CollectDiagnostic;
+   }
+ }
+ 
+@@ -199,7 +227,7 @@ class KernelCompiler {
+ 
+       if (await fingerprinter.doesFingerprintMatch()) {
+         printTrace('Skipping kernel compilation. Fingerprint match.');
+-        return CompilerOutput(outputFilePath, 0);
++        return CompilerOutput(outputFilePath, 0, /* sources */ null);
+       }
+     }
+ 
+@@ -449,10 +477,13 @@ class ResidentCompiler {
+         ? _mapFilename(request.mainPath, packageUriMapper) + ' '
+         : '';
+     _server.stdin.writeln('recompile $mainUri$inputKey');
++    printTrace('<- recompile $mainUri$inputKey');
+     for (String fileUri in request.invalidatedFiles) {
+       _server.stdin.writeln(_mapFileUri(fileUri, packageUriMapper));
++      printTrace('<- ${_mapFileUri(fileUri, packageUriMapper)}');
+     }
+     _server.stdin.writeln(inputKey);
++    printTrace('<- $inputKey');
+ 
+     return _stdoutHandler.compilerOutput.future;
+   }
+@@ -542,6 +573,7 @@ class ResidentCompiler {
+       .listen((String message) { printError(message); });
+ 
+     _server.stdin.writeln('compile $scriptUri');
++    printTrace('<- compile $scriptUri');
+ 
+     return _stdoutHandler.compilerOutput.future;
+   }
+@@ -594,6 +626,7 @@ class ResidentCompiler {
+   void accept() {
+     if (_compileRequestNeedsConfirmation) {
+       _server.stdin.writeln('accept');
++      printTrace('<- accept');
+     }
+     _compileRequestNeedsConfirmation = false;
+   }
+@@ -617,6 +650,7 @@ class ResidentCompiler {
+     }
+     _stdoutHandler.reset();
+     _server.stdin.writeln('reject');
++    printTrace('<- reject');
+     _compileRequestNeedsConfirmation = false;
+     return _stdoutHandler.compilerOutput.future;
+   }
+@@ -626,6 +660,7 @@ class ResidentCompiler {
+   /// kernel file.
+   void reset() {
+     _server?.stdin?.writeln('reset');
++    printTrace('<- reset');
+   }
+ 
+   String _mapFilename(String filename, PackageUriMapper packageUriMapper) {
+diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart
+index 7940e8bb0..9e8b9bd45 100644
+--- a/packages/flutter_tools/lib/src/devfs.dart
++++ b/packages/flutter_tools/lib/src/devfs.dart
+@@ -557,6 +557,8 @@ class DevFS {
+       outputPath:  dillOutputPath ?? getDefaultApplicationKernelPath(trackWidgetCreation: trackWidgetCreation),
+       packagesFilePath : _packagesFilePath,
+     );
++    // list of sources that needs to be monitored are in [compilerOutput.sources]
++    //
+     // Don't send full kernel file that would overwrite what VM already
+     // started loading from.
+     if (!bundleFirstUpload) {