[frontend_server] connect widget cache to frontend_server

Connect the widget cache to the frontend server to be enabled when
--flutter-widget-cache is provided as a flag.

See also: go/fast-single-widget-reloads

Bug: https://github.com/flutter/flutter/issues/61407
Change-Id: I9d1f2c59dbb1dc56bb7e6ac7ebd05a01d1d47803
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/162943
Reviewed-by: Alexander Aprelev <aam@google.com>
Commit-Queue: Jonah Williams <jonahwilliams@google.com>
This commit is contained in:
jonahwilliams
2020-09-16 15:09:18 +00:00
committed by commit-bot@chromium.org
parent 623a9bda67
commit d0e095507c
3 changed files with 296 additions and 2 deletions
+1 -1
View File
@@ -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,
+37 -1
View File
@@ -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('/')) {
@@ -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<String> args = <String>[
'server.dart',
'--sdk-root',
'sdkroot',
'--flutter-widget-cache',
];
await starter(args, compiler: compiler);
final List<dynamic> 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<List<int>> inputStreamController =
StreamController<List<int>>();
final ReceivePort recompileCalled = ReceivePort();
when(compiler.recompileDelta(entryPoint: null))
.thenAnswer((Invocation invocation) async {
recompileCalled.sendPort.send(true);
});
Future<int> result = starter(
<String>[...args, '--flutter-widget-cache'],
compiler: compiler,
input: inputStreamController.stream,
);
inputStreamController.add('recompile abc\nfile1.dart\nabc\n'.codeUnits);
await recompileCalled.first;
verifyInOrder(<void>[
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<List<int>> inputStreamController =
StreamController<List<int>>();
@@ -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<T extends StatefulWidget> {}
''');
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<FizzWidget> {}
""");
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<String> args = <String>[
'--sdk-root=${sdkRoot.toFilePath()}',
'--incremental',
'--platform=${platformKernel.path}',
'--output-dill=${dillFile.path}',
'--flutter-widget-cache',
'--packages=${config.path}',
];
final StreamController<List<int>> inputStreamController =
StreamController<List<int>>();
final StreamController<List<int>> stdoutStreamController =
StreamController<List<int>>();
final IOSink ioSink = IOSink(stdoutStreamController.sink);
StreamController<Result> receivedResults = StreamController<Result>();
final outputParser = OutputParser(receivedResults);
stdoutStreamController.stream
.transform(utf8.decoder)
.transform(const LineSplitter())
.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((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<FizzWidget> {}
""");
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<FizzWidget> {}
""");
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<FizzWidget> {
// 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<FizzWidget> {
// 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')