Remove the widget cache experiment.

Bug: https://github.com/flutter/flutter/issues/132157
Change-Id: Ie87ba74b8ed99477cc5032824286adb07f1157d6
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/319871
Reviewed-by: Jens Johansen <jensj@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
Commit-Queue: Johnni Winther <johnniwinther@google.com>
This commit is contained in:
Ian Hickson 2023-11-29 08:47:59 +00:00 committed by Commit Queue
parent 963068bd4e
commit 86231e2d70
4 changed files with 1 additions and 464 deletions

View file

@ -1,188 +0,0 @@
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:kernel/class_hierarchy.dart';
import 'package:kernel/kernel.dart';
/// Provides support for "single widget reloads" in Flutter, by determining if
/// a partial component contains single change to the class body of a
/// StatelessWidget, StatefulWidget, or State subtype.
class WidgetCache {
/// Create a [WidgetCache] from a [Component] containing the flutter
/// framework.
WidgetCache(Component fullComponent) {
Library? frameworkLibrary;
for (Library library in fullComponent.libraries) {
if (library.importUri == _frameworkLibrary) {
frameworkLibrary = library;
break;
}
}
if (frameworkLibrary == null) {
return;
}
_locatedClassDeclarations(frameworkLibrary);
_frameworkTypesLocated =
_statefulWidget != null && _state != null && _statelessWidget != null;
}
static const String _stateClassName = 'State';
static const String _statefulWidgetClassName = 'StatefulWidget';
static const String _statelessWidgetClassName = 'StatelessWidget';
Class? _statelessWidget;
Class? _state;
Class? _statefulWidget;
bool _frameworkTypesLocated = false;
static final Uri _frameworkLibrary =
Uri.parse('package:flutter/src/widgets/framework.dart');
/// Mark [uri] as invalidated.
void invalidate(Uri uri) {
_invalidatedLibraries.add(uri);
}
/// Reset the invalidated libraries.
void reset() {
_invalidatedLibraries.clear();
}
final List<Uri> _invalidatedLibraries = <Uri>[];
/// Determine if any changes to [partialComponent] were located entirely
/// within the class body of a single `StatefulWidget`, `StatelessWidget` or
/// `State` subtype.
///
/// Returns the class name if located, otherwise `null`.
String? checkSingleWidgetTypeModified(
Component? lastGoodComponent,
Component partialComponent,
ClassHierarchy? classHierarchy,
) {
if (!_frameworkTypesLocated ||
lastGoodComponent == null ||
_invalidatedLibraries.length != 1) {
return null;
}
Uri importUri = _invalidatedLibraries[0];
Library? library;
for (Library candidateLibrary in partialComponent.libraries) {
if (candidateLibrary.importUri == importUri) {
library = candidateLibrary;
break;
}
}
if (library == null) {
return null;
}
List<int> oldSource =
lastGoodComponent.uriToSource[library.fileUri]!.source;
List<int> newSource = partialComponent.uriToSource[library.fileUri]!.source;
int newStartIndex = 0;
int newEndIndex = newSource.length - 1;
int oldStartIndex = 0;
int oldEndIndex = oldSource.length - 1;
while (newStartIndex < newEndIndex && oldStartIndex < oldEndIndex) {
if (newSource[newStartIndex] != oldSource[oldStartIndex]) {
break;
}
newStartIndex += 1;
oldStartIndex += 1;
}
while (newEndIndex > newStartIndex && oldEndIndex > oldStartIndex) {
if (newSource[newEndIndex] != oldSource[oldEndIndex]) {
break;
}
newEndIndex -= 1;
oldEndIndex -= 1;
}
Class? newClass =
_locateContainingClass(library, newStartIndex, newEndIndex);
if (newClass == null) {
return null;
}
Library oldLibrary =
lastGoodComponent.libraries.firstWhere((Library library) {
return library.importUri == importUri;
});
Class? oldClass =
_locateContainingClass(oldLibrary, oldStartIndex, oldEndIndex);
if (oldClass == null || oldClass.name != newClass.name) {
return null;
}
// Update the class references to stateless, stateful, and state classes.
for (Library library in classHierarchy!.knownLibraries) {
if (library.importUri == _frameworkLibrary) {
_locatedClassDeclarations(library);
}
}
if (classHierarchy.isSubclassOf(newClass, _statelessWidget!) ||
classHierarchy.isSubclassOf(newClass, _statefulWidget!)) {
if (classHierarchy.isExtended(newClass) ||
classHierarchy.isUsedAsMixin(newClass)) {
return null;
}
return newClass.name;
}
// For changes to State classes, locate the name of the corresponding
// StatefulWidget that is provided as a type parameter. If the bounds are
// StatefulWidget itself, fail as that indicates the type was not
// specified.
Supertype? stateSuperType =
classHierarchy.getClassAsInstanceOf(newClass, _state!);
if (stateSuperType != null) {
if (stateSuperType.typeArguments.length != 1) {
return null;
}
DartType widgetType = stateSuperType.typeArguments[0];
if (widgetType is InterfaceType) {
Class statefulWidgetType = widgetType.classNode;
if (statefulWidgetType.name == _statefulWidgetClassName) {
return null;
}
if (classHierarchy.isExtended(statefulWidgetType) ||
classHierarchy.isUsedAsMixin(statefulWidgetType)) {
return null;
}
return statefulWidgetType.name;
}
}
return null;
}
// Locate the that fully contains the edit range, or null.
Class? _locateContainingClass(
Library library, int startOffset, int endOffset) {
for (Class classDeclaration in library.classes) {
if (classDeclaration.startFileOffset <= startOffset &&
classDeclaration.fileEndOffset >= endOffset) {
return classDeclaration;
}
}
return null;
}
void _locatedClassDeclarations(Library library) {
for (Class classDeclaration in library.classes) {
if (classDeclaration.name == _statelessWidgetClassName) {
_statelessWidget = classDeclaration;
} else if (classDeclaration.name == _statefulWidgetClassName) {
_statefulWidget = classDeclaration;
} else if (classDeclaration.name == _stateClassName) {
_state = classDeclaration;
}
}
}
}

View file

@ -15,4 +15,4 @@ This API stability does not cover any of the source code APIs.
* The frontend_server kernel compilation and expression evaluation for kernel should be considered "stable".
* The frontend_server JavaScript compilation is semi-stable, but clients should anticipate coordinated breaking changes in the future.
* The frontend_server JavaScript expression evaluation is experimental and is expected to change significantly from Dec 2020 through the end of 2021.
* Specific flags like the --flutter-widget-cache may be added for experimentation and should not be considered stable.
* Specific flags may be added for experimentation and should not be considered stable.

View file

@ -26,7 +26,6 @@ import 'package:dev_compiler/dev_compiler.dart'
import 'package:front_end/src/api_unstable/ddc.dart' as ddc
show IncrementalCompiler;
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'
@ -211,9 +210,6 @@ ArgParser argParser = ArgParser(allowTrailingOptions: true)
defaultsTo: 'amd')
..addFlag('dartdevc-canary',
help: 'Enable canary features in dartdevc compiler', defaultsTo: false)
..addFlag('flutter-widget-cache',
help: 'Enable the widget cache to track changes to Widget subtypes',
defaultsTo: false)
..addFlag('print-incremental-dependencies',
help: 'Print list of sources added and removed from compilation',
defaultsTo: true)
@ -436,7 +432,6 @@ class FrontendCompiler implements CompilerInterface {
/// Nullable fields
final ProgramTransformer? transformer;
bool? unsafePackageSerialization;
WidgetCache? _widgetCache;
_onDiagnostic(DiagnosticMessage message) {
switch (message.severity) {
@ -623,9 +618,6 @@ class FrontendCompiler implements CompilerInterface {
);
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
@ -964,7 +956,6 @@ class FrontendCompiler implements CompilerInterface {
await writeDillFile(results, _kernelBinaryFilename,
incrementalSerializer: _generator.incrementalSerializer);
}
_updateWidgetCache(deltaProgram);
_outputStream.writeln(boundaryKey);
await _outputDependenciesDelta(results.compiledSources!);
@ -1188,7 +1179,6 @@ class FrontendCompiler implements CompilerInterface {
@override
void acceptLastDelta() {
_generator.accept();
_widgetCache?.reset();
}
@override
@ -1202,13 +1192,11 @@ class FrontendCompiler implements CompilerInterface {
@override
void invalidate(Uri uri) {
_generator.invalidate(uri);
_widgetCache?.invalidate(uri);
}
@override
void resetIncrementalCompiler() {
_generator.resetDeltaState();
_widgetCache?.reset();
_kernelBinaryFilename = _kernelBinaryFilenameFull;
}
@ -1219,31 +1207,6 @@ 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.
///
/// Should not run if a full component is requested.
void _updateWidgetCache(Component partialComponent) {
if (_widgetCache == null || _generator.fullComponent) {
return;
}
final String? singleModifiedClassName =
_widgetCache!.checkSingleWidgetTypeModified(
_generator.lastKnownGoodResult?.component,
partialComponent,
_generator.lastKnownGoodResult?.classHierarchy,
);
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('/')) {

View file

@ -156,24 +156,6 @@ void main() async {
];
await starter(args, compiler: compiler);
});
test('compile from command line with widget cache', () async {
verify(String entryPoint, ArgResults opts) {
expect(entryPoint, equals('server.dart'));
expect(opts['sdk-root'], equals('sdkroot'));
expect(opts['link-platform'], equals(true));
expect(opts['flutter-widget-cache'], equals(true));
}
final compiler = _MockedCompiler(verifyCompile: verify);
final List<String> args = <String>[
'server.dart',
'--sdk-root',
'sdkroot',
'--flutter-widget-cache',
];
await starter(args, compiler: compiler);
});
});
group('interactive compile with mocked compiler', () {
@ -326,39 +308,6 @@ void main() async {
await inputStreamController.close();
});
test('recompile one file with widget cache does not fail', () async {
final recompileDeltaCalled = ReceivePort();
bool invalidated = false;
verifyR(String? entryPoint) {
expect(invalidated, equals(true));
expect(entryPoint, equals(null));
recompileDeltaCalled.sendPort.send(true);
}
verifyI(Uri uri) {
invalidated = true;
expect(uri.path, contains('file1.dart'));
}
// The component will not contain the flutter framework sources so
// this should no-op.
final compiler = _MockedCompiler(
verifyRecompileDelta: verifyR, verifyInvalidate: verifyI);
final StreamController<List<int>> inputStreamController =
StreamController<List<int>>();
Future<int> result = starter(
<String>[...args, '--flutter-widget-cache'],
compiler: compiler,
input: inputStreamController.stream,
);
inputStreamController.add('recompile abc\nfile1.dart\nabc\n'.codeUnits);
await recompileDeltaCalled.first;
inputStreamController.add('quit\n'.codeUnits);
expect(await result, 0);
await inputStreamController.close();
});
test('recompile few files with new entrypoint', () async {
int invalidated = 0;
final recompileDeltaCalled = ReceivePort();
@ -929,193 +878,6 @@ void main() async {
frontendServer.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/"
}
]
}
''');
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 frontendServer = FrontendServer();
Future<int> result = frontendServer.open(args);
frontendServer.compile(file.path);
int count = 0;
frontendServer.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;
frontendServer.accept();
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> {}
""");
frontendServer.recompile(file.uri, entryPoint: file.path);
} 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;
frontendServer.accept();
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> {}
""");
frontendServer.recompile(file.uri, entryPoint: file.path);
} 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;
frontendServer.accept();
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.
}
""");
frontendServer.recompile(file.uri, entryPoint: file.path);
} 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;
frontendServer.accept();
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.
}
""");
frontendServer.recompile(file.uri, entryPoint: file.path);
} 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));
frontendServer.accept();
frontendServer.quit();
}
});
expect(await result, 0);
frontendServer.close();
});
test('unsafe-package-serialization', () async {
// Package A.
var file = File('${tempDir.path}/pkgA/a.dart')