diff --git a/pkg/front_end/lib/widget_cache.dart b/pkg/front_end/lib/widget_cache.dart index 9ccfa385d22..158125b91bd 100644 --- a/pkg/front_end/lib/widget_cache.dart +++ b/pkg/front_end/lib/widget_cache.dart @@ -61,7 +61,7 @@ class WidgetCache { /// `State` subtype. /// /// Returns the class name if located, otherwise `null`. - String checkWidgetCache( + String checkSingleWidgetTypeModified( Component lastGoodComponent, Component partialComponent, ClassHierarchy classHierarchy, diff --git a/pkg/frontend_server/lib/frontend_server.dart b/pkg/frontend_server/lib/frontend_server.dart index af482511df6..c95b0a2ae8f 100644 --- a/pkg/frontend_server/lib/frontend_server.dart +++ b/pkg/frontend_server/lib/frontend_server.dart @@ -20,6 +20,7 @@ import 'package:dev_compiler/dev_compiler.dart' import 'package:front_end/src/api_prototype/compiler_options.dart' show CompilerOptions, parseExperimentalFlags; import 'package:front_end/src/api_unstable/vm.dart'; +import 'package:front_end/widget_cache.dart'; import 'package:kernel/ast.dart' show Library, Procedure, LibraryDependency; import 'package:kernel/binary/ast_to_binary.dart'; import 'package:kernel/kernel.dart' @@ -176,7 +177,10 @@ ArgParser argParser = ArgParser(allowTrailingOptions: true) defaultsTo: false) ..addOption('dartdevc-module-format', help: 'The module format to use on for the dartdevc compiler', - defaultsTo: 'amd'); + defaultsTo: 'amd') + ..addFlag('flutter-widget-cache', + help: 'Enable the widget cache to track changes to Widget subtypes', + defaultsTo: false); String usage = ''' Usage: server [options] [input.dart] @@ -345,6 +349,8 @@ class FrontendCompiler implements CompilerInterface { IncrementalCompiler _generator; JavaScriptBundler _bundler; + WidgetCache _widgetCache; + String _kernelBinaryFilename; String _kernelBinaryFilenameIncremental; String _kernelBinaryFilenameFull; @@ -530,6 +536,9 @@ class FrontendCompiler implements CompilerInterface { component.uriToSource.keys); incrementalSerializer = _generator.incrementalSerializer; + if (options['flutter-widget-cache']) { + _widgetCache = WidgetCache(component); + } } else { if (options['link-platform']) { // TODO(aam): Remove linkedDependencies once platform is directly embedded @@ -904,6 +913,7 @@ class FrontendCompiler implements CompilerInterface { await writeDillFile(results, _kernelBinaryFilename, incrementalSerializer: _generator.incrementalSerializer); } + _updateWidgetCache(deltaProgram); _outputStream.writeln(boundaryKey); await _outputDependenciesDelta(results.compiledSources); @@ -1095,6 +1105,7 @@ class FrontendCompiler implements CompilerInterface { @override void acceptLastDelta() { _generator.accept(); + _widgetCache?.reset(); } @override @@ -1108,11 +1119,13 @@ class FrontendCompiler implements CompilerInterface { @override void invalidate(Uri uri) { _generator.invalidate(uri); + _widgetCache?.invalidate(uri); } @override void resetIncrementalCompiler() { _generator.resetDeltaState(); + _widgetCache?.reset(); _kernelBinaryFilename = _kernelBinaryFilenameFull; } @@ -1122,6 +1135,29 @@ class FrontendCompiler implements CompilerInterface { incrementalSerialization: incrementalSerialization); } + /// If the flutter widget cache is enabled, check if a single class was modified. + /// + /// The resulting class name is written as a String to + /// `_kernelBinaryFilename`.widget_cache, or else the file is deleted + /// if it exists. + void _updateWidgetCache(Component partialComponent) { + if (_widgetCache == null) { + return; + } + final String singleModifiedClassName = + _widgetCache.checkSingleWidgetTypeModified( + _generator.lastKnownGoodComponent, + partialComponent, + _generator.getClassHierarchy(), + ); + final File outputFile = File('$_kernelBinaryFilename.widget_cache'); + if (singleModifiedClassName != null) { + outputFile.writeAsStringSync(singleModifiedClassName); + } else if (outputFile.existsSync()) { + outputFile.deleteSync(); + } + } + Uri _ensureFolderPath(String path) { String uriPath = Uri.file(path).toString(); if (!uriPath.endsWith('/')) { diff --git a/pkg/frontend_server/test/frontend_server_test.dart b/pkg/frontend_server/test/frontend_server_test.dart index d4b17b79667..90b4d5fb8d2 100644 --- a/pkg/frontend_server/test/frontend_server_test.dart +++ b/pkg/frontend_server/test/frontend_server_test.dart @@ -68,6 +68,24 @@ void main() async { expect(capturedArgs.single['sdk-root'], equals('sdkroot')); expect(capturedArgs.single['link-platform'], equals(true)); }); + + test('compile from command line with widget cache', () async { + final List args = [ + 'server.dart', + '--sdk-root', + 'sdkroot', + '--flutter-widget-cache', + ]; + await starter(args, compiler: compiler); + final List capturedArgs = verify(compiler.compile( + argThat(equals('server.dart')), + captureAny, + generator: anyNamed('generator'), + )).captured; + expect(capturedArgs.single['sdk-root'], equals('sdkroot')); + expect(capturedArgs.single['link-platform'], equals(true)); + expect(capturedArgs.single['flutter-widget-cache'], equals(true)); + }); }); group('interactive compile with mocked compiler', () { @@ -230,6 +248,34 @@ void main() async { inputStreamController.close(); }); + test('recompile one file with widget cache does not fail', () async { + // The component will not contain the flutter framework sources so + // this should no-op. + final StreamController> inputStreamController = + StreamController>(); + final ReceivePort recompileCalled = ReceivePort(); + + when(compiler.recompileDelta(entryPoint: null)) + .thenAnswer((Invocation invocation) async { + recompileCalled.sendPort.send(true); + }); + Future result = starter( + [...args, '--flutter-widget-cache'], + compiler: compiler, + input: inputStreamController.stream, + ); + inputStreamController.add('recompile abc\nfile1.dart\nabc\n'.codeUnits); + await recompileCalled.first; + + verifyInOrder([ + compiler.invalidate(Uri.base.resolve('file1.dart')), + await compiler.recompileDelta(entryPoint: null), + ]); + inputStreamController.add('quit\n'.codeUnits); + expect(await result, 0); + inputStreamController.close(); + }); + test('recompile few files with new entrypoint', () async { final StreamController> inputStreamController = StreamController>(); @@ -923,6 +969,218 @@ true inputStreamController.close(); }); + test( + 'recompile request with flutter widget cache outputs change in class name', + () async { + var frameworkDirectory = Directory('${tempDir.path}/flutter'); + var flutterFramework = + File('${frameworkDirectory.path}/lib/src/widgets/framework.dart') + ..createSync(recursive: true); + flutterFramework.writeAsStringSync(''' +abstract class Widget {} +class StatelessWidget extends Widget {} +class StatefulWidget extends Widget {} +class State {} +'''); + + var file = File('${tempDir.path}/foo.dart')..createSync(); + file.writeAsStringSync(""" +import "package:flutter/src/widgets/framework.dart"; + +void main() {} + +class FooWidget extends StatelessWidget {} + +class FizzWidget extends StatefulWidget {} + +class BarState extends State {} +"""); + var config = File('${tempDir.path}/package_config.json')..createSync(); + config.writeAsStringSync(''' +{ + "configVersion": 2, + "packages": [ + { + "name": "flutter", + "rootUri": "${frameworkDirectory.uri}", + "packageUri": "lib/", + "languageVersion": "2.2" + } + ] +} +'''); + + var dillFile = File('${tempDir.path}/app.dill'); + expect(dillFile.existsSync(), equals(false)); + final List args = [ + '--sdk-root=${sdkRoot.toFilePath()}', + '--incremental', + '--platform=${platformKernel.path}', + '--output-dill=${dillFile.path}', + '--flutter-widget-cache', + '--packages=${config.path}', + ]; + + final StreamController> inputStreamController = + StreamController>(); + final StreamController> stdoutStreamController = + StreamController>(); + final IOSink ioSink = IOSink(stdoutStreamController.sink); + StreamController receivedResults = StreamController(); + + final outputParser = OutputParser(receivedResults); + stdoutStreamController.stream + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen(outputParser.listener); + + Future result = + starter(args, input: inputStreamController.stream, output: ioSink); + inputStreamController.add('compile ${file.path}\n'.codeUnits); + int count = 0; + receivedResults.stream.listen((Result compiledResult) { + if (count == 0) { + // First request is to 'compile', which results in full kernel file. + expect(dillFile.existsSync(), equals(true)); + compiledResult.expectNoErrors(filename: dillFile.path); + count += 1; + inputStreamController.add('accept\n'.codeUnits); + file.writeAsStringSync(""" +import "package:flutter/src/widgets/framework.dart"; + +void main() {} + +class FooWidget extends StatelessWidget { + // Added. +} + +class FizzWidget extends StatefulWidget {} + +class BarState extends State {} +"""); + inputStreamController.add('recompile ${file.path} abc\n' + '${file.path}\n' + 'abc\n' + .codeUnits); + } else if (count == 1) { + expect(count, 1); + // Second request is to 'recompile', which results in incremental + // kernel file and invalidation of StatelessWidget. + var dillIncFile = File('${dillFile.path}.incremental.dill'); + var widgetCacheFile = + File('${dillFile.path}.incremental.dill.widget_cache'); + compiledResult.expectNoErrors(filename: dillIncFile.path); + expect(dillIncFile.existsSync(), equals(true)); + expect(widgetCacheFile.existsSync(), equals(true)); + expect(widgetCacheFile.readAsStringSync(), 'FooWidget'); + count += 1; + inputStreamController.add('accept\n'.codeUnits); + + file.writeAsStringSync(""" +import "package:flutter/src/widgets/framework.dart"; + +void main() {} + +class FooWidget extends StatelessWidget { + // Added. +} + +class FizzWidget extends StatefulWidget { + // Added. +} + +class BarState extends State {} +"""); + inputStreamController.add('recompile ${file.path} abc\n' + '${file.path}\n' + 'abc\n' + .codeUnits); + } else if (count == 2) { + // Second request is to 'recompile', which results in incremental + // kernel file and invalidation of StatelessWidget. + var dillIncFile = File('${dillFile.path}.incremental.dill'); + var widgetCacheFile = + File('${dillFile.path}.incremental.dill.widget_cache'); + compiledResult.expectNoErrors(filename: dillIncFile.path); + expect(dillIncFile.existsSync(), equals(true)); + expect(widgetCacheFile.existsSync(), equals(true)); + expect(widgetCacheFile.readAsStringSync(), 'FizzWidget'); + count += 1; + inputStreamController.add('accept\n'.codeUnits); + + file.writeAsStringSync(""" +import "package:flutter/src/widgets/framework.dart"; + +void main() {} + +class FooWidget extends StatelessWidget { + // Added. +} + +class FizzWidget extends StatefulWidget { + // Added. +} + +class BarState extends State { + // Added. +} +"""); + inputStreamController.add('recompile ${file.path} abc\n' + '${file.path}\n' + 'abc\n' + .codeUnits); + } else if (count == 3) { + // Third request is to 'recompile', which results in incremental + // kernel file and invalidation of State class. + var dillIncFile = File('${dillFile.path}.incremental.dill'); + var widgetCacheFile = + File('${dillFile.path}.incremental.dill.widget_cache'); + compiledResult.expectNoErrors(filename: dillIncFile.path); + expect(dillIncFile.existsSync(), equals(true)); + expect(widgetCacheFile.existsSync(), equals(true)); + expect(widgetCacheFile.readAsStringSync(), 'FizzWidget'); + count += 1; + inputStreamController.add('accept\n'.codeUnits); + + file.writeAsStringSync(""" +import "package:flutter/src/widgets/framework.dart"; + +void main() {} + +// Added + +class FooWidget extends StatelessWidget { + // Added. +} + +class FizzWidget extends StatefulWidget { + // Added. +} + +class BarState extends State { + // Added. +} +"""); + inputStreamController.add('recompile ${file.path} abc\n' + '${file.path}\n' + 'abc\n' + .codeUnits); + } else if (count == 4) { + // Fourth request is to 'recompile', which results in incremental + // kernel file and no widget cache + var dillIncFile = File('${dillFile.path}.incremental.dill'); + var widgetCacheFile = + File('${dillFile.path}.incremental.dill.widget_cache'); + compiledResult.expectNoErrors(filename: dillIncFile.path); + expect(dillIncFile.existsSync(), equals(true)); + expect(widgetCacheFile.existsSync(), equals(false)); + inputStreamController.add('quit\n'.codeUnits); + } + }); + expect(await result, 0); + inputStreamController.close(); + }); + test('unsafe-package-serialization', () async { // Package A. var file = File('${tempDir.path}/pkgA/a.dart')