Unverified Commit 485034ca authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] update fastReassemble method for single widget reloads (#61413)

For #61407 , we need to be able to find all widgets that of a given type. Previously I experimented with using the type name, but of course this does not handles subtypes. The actual check needs to be an is check.

Since there is no way to convert a String to a Type at runtime for use in this check, we can instead evaluate an expression which assigns a closure to a field. The idea for this was inspired by how the dart devtools adds debug functionality to older versions of flutter.

Since the reload feature is not complete yet, adds an integration test which simulates how it will eventually behave
parent d22d65c6
......@@ -415,24 +415,26 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
},
);
// Register the ability to quickly mark elements as dirty.
// The performance of this method may be improved with additional
// information from https://github.com/flutter/flutter/issues/46195.
registerServiceExtension(
name: 'fastReassemble',
callback: (Map<String, Object> params) async {
final String className = params['class'] as String;
final FastReassemblePredicate fastReassemblePredicate = _debugFastReassembleMethod;
_debugFastReassembleMethod = null;
if (fastReassemblePredicate == null) {
throw FlutterError('debugFastReassembleMethod must be set to use fastReassemble.');
}
void markElementsDirty(Element element) {
if (element == null) {
return;
}
if (element.widget?.runtimeType?.toString()?.startsWith(className) ?? false) {
if (fastReassemblePredicate(element.widget)) {
element.markNeedsBuild();
}
element.visitChildElements(markElementsDirty);
}
markElementsDirty(renderViewElement);
return <String, String>{'Success': 'true'};
await endOfFrame;
return <String, String>{'type': 'Success'};
},
);
......@@ -1027,6 +1029,35 @@ void runApp(Widget app) {
..scheduleWarmUpFrame();
}
/// A function that should validate that the provided object is assignable to a
/// given type.
typedef FastReassemblePredicate = bool Function(Object);
/// Debug-only functionality used to perform faster hot reloads.
///
/// This field is set by expression evaluation in the flutter tool and is
/// used to invalidate specific types of [Element]s. This setter
/// should not be referenced in user code and is only public so that expression
/// evaluation can be done in the context of an almost-arbitrary Dart library.
///
/// For example, expression evaluation might be performed with the following code:
///
/// ```dart
/// (debugFastReassembleMethod=(Object x) => x is Foo)()
/// ```
///
/// And then followed by a call to `ext.flutter.fastReassemble`. This will read
/// the provided predicate and use it to mark specific elements dirty wherever
/// [Element.widget] is a `Foo`. Afterwards, the internal field will be nulled
/// out.
set debugFastReassembleMethod(FastReassemblePredicate fastReassemblePredicate) {
assert(() {
_debugFastReassembleMethod = fastReassemblePredicate;
return true;
}());
}
FastReassemblePredicate _debugFastReassembleMethod;
/// Print a string representation of the currently running app.
void debugDumpApp() {
assert(WidgetsBinding.instance != null);
......
......@@ -742,9 +742,6 @@ void main() {
});
test('Service extensions - fastReassemble', () async {
Map<String, dynamic> result;
result = await binding.testExtension('fastReassemble', <String, String>{'class': 'Foo'});
expect(result, containsPair('Success', 'true'));
expect(binding.testExtension('fastReassemble', <String, String>{}), throwsA(isA<FlutterError>()));
});
}
......@@ -8,6 +8,7 @@ import 'package:file/file.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service_io.dart';
import '../src/common.dart';
import 'test_data/hot_reload_project.dart';
......@@ -16,65 +17,100 @@ import 'test_utils.dart';
void main() {
Directory tempDir;
final HotReloadProject _project = HotReloadProject();
FlutterRunTestDriver _flutter;
final HotReloadProject project = HotReloadProject();
FlutterRunTestDriver flutter;
setUp(() async {
tempDir = createResolvedTempDirectorySync('hot_reload_test.');
await _project.setUpIn(tempDir);
_flutter = FlutterRunTestDriver(tempDir);
await project.setUpIn(tempDir);
flutter = FlutterRunTestDriver(tempDir);
});
tearDown(() async {
await _flutter?.stop();
await flutter?.stop();
tryToDelete(tempDir);
});
test('hot reload works without error', () async {
await _flutter.run();
await _flutter.hotReload();
await flutter.run();
await flutter.hotReload();
});
test('newly added code executes during hot reload', () async {
final StringBuffer stdout = StringBuffer();
final StreamSubscription<String> subscription = _flutter.stdout.listen(stdout.writeln);
await _flutter.run();
_project.uncommentHotReloadPrint();
final StreamSubscription<String> subscription = flutter.stdout.listen(stdout.writeln);
await flutter.run();
project.uncommentHotReloadPrint();
try {
await _flutter.hotReload();
await flutter.hotReload();
expect(stdout.toString(), contains('(((((RELOAD WORKED)))))'));
} finally {
await subscription.cancel();
}
});
test('reloadMethod triggers hot reload behavior', () async {
test('fastReassemble behavior triggers hot reload behavior with evaluation of expression', () async {
final StringBuffer stdout = StringBuffer();
final StreamSubscription<String> subscription = _flutter.stdout.listen(stdout.writeln);
await _flutter.run();
_project.uncommentHotReloadPrint();
final StreamSubscription<String> subscription = flutter.stdout.listen(stdout.writeln);
await flutter.run(withDebugger: true);
final int port = flutter.vmServicePort;
final VmService vmService = await vmServiceConnectUri('ws://localhost:$port/ws');
try {
final String libraryId = _project.buildBreakpointUri.toString();
await _flutter.reloadMethod(libraryId: libraryId, classId: 'MyApp');
// reloadMethod does not wait for the next frame, to allow scheduling a new
// update while the previous update was pending.
await Future<void>.delayed(const Duration(seconds: 1));
expect(stdout.toString(), contains('(((((RELOAD WORKED)))))'));
// Since the single-widget reload feature is not yet implemented, manually
// evaluate the expression for the reload.
final Isolate isolate = await waitForExtension(vmService);
final LibraryRef targetRef = isolate.libraries.firstWhere((LibraryRef libraryRef) {
return libraryRef.uri == 'package:test/main.dart';
});
await vmService.evaluate(
isolate.id,
targetRef.id,
'((){debugFastReassembleMethod=(Object x) => x is MyApp})()',
);
final Response fastReassemble1 = await vmService
.callServiceExtension('ext.flutter.fastReassemble', isolateId: isolate.id);
// _extensionType indicates success.
expect(fastReassemble1.type, '_extensionType');
expect(stdout.toString(), contains('(((TICK 2))))'));
// verify evaluation did not produce invalidat type by checking with dart:core
// type.
await vmService.evaluate(
isolate.id,
targetRef.id,
'((){debugFastReassembleMethod=(Object x) => x is bool})()',
);
final Response fastReassemble2 = await vmService
.callServiceExtension('ext.flutter.fastReassemble', isolateId: isolate.id);
// _extensionType indicates success.
expect(fastReassemble2.type, '_extensionType');
expect(stdout.toString(), isNot(contains('(((TICK 3))))')));
// Invocation without evaluation leads to runtime error.
expect(vmService
.callServiceExtension('ext.flutter.fastReassemble', isolateId: isolate.id),
throwsA(isA<Exception>())
);
} finally {
await subscription.cancel();
}
});
test('hot restart works without error', () async {
await _flutter.run();
await _flutter.hotRestart();
await flutter.run();
await flutter.hotRestart();
});
test('breakpoints are hit after hot reload', () async {
Isolate isolate;
final Completer<void> sawTick1 = Completer<void>();
final Completer<void> sawDebuggerPausedMessage = Completer<void>();
final StreamSubscription<String> subscription = _flutter.stdout.listen(
final StreamSubscription<String> subscription = flutter.stdout.listen(
(String line) {
if (line.contains('((((TICK 1))))')) {
expect(sawTick1.isCompleted, isFalse);
......@@ -86,36 +122,36 @@ void main() {
}
},
);
await _flutter.run(withDebugger: true, startPaused: true);
await _flutter.resume(); // we start paused so we can set up our TICK 1 listener before the app starts
await flutter.run(withDebugger: true, startPaused: true);
await flutter.resume(); // we start paused so we can set up our TICK 1 listener before the app starts
unawaited(sawTick1.future.timeout(
const Duration(seconds: 5),
onTimeout: () { print('The test app is taking longer than expected to print its synchronization line...'); },
));
await sawTick1.future; // after this, app is in steady state
await _flutter.addBreakpoint(
_project.scheduledBreakpointUri,
_project.scheduledBreakpointLine,
await flutter.addBreakpoint(
project.scheduledBreakpointUri,
project.scheduledBreakpointLine,
);
await Future<void>.delayed(const Duration(seconds: 2));
await _flutter.hotReload(); // reload triggers code which eventually hits the breakpoint
isolate = await _flutter.waitForPause();
await flutter.hotReload(); // reload triggers code which eventually hits the breakpoint
isolate = await flutter.waitForPause();
expect(isolate.pauseEvent.kind, equals(EventKind.kPauseBreakpoint));
await _flutter.resume();
await _flutter.addBreakpoint(
_project.buildBreakpointUri,
_project.buildBreakpointLine,
await flutter.resume();
await flutter.addBreakpoint(
project.buildBreakpointUri,
project.buildBreakpointLine,
);
bool reloaded = false;
final Future<void> reloadFuture = _flutter.hotReload().then((void value) { reloaded = true; });
final Future<void> reloadFuture = flutter.hotReload().then((void value) { reloaded = true; });
print('waiting for pause...');
isolate = await _flutter.waitForPause();
isolate = await flutter.waitForPause();
expect(isolate.pauseEvent.kind, equals(EventKind.kPauseBreakpoint));
print('waiting for debugger message...');
await sawDebuggerPausedMessage.future;
expect(reloaded, isFalse);
print('waiting for resume...');
await _flutter.resume();
await flutter.resume();
print('waiting for reload future...');
await reloadFuture;
expect(reloaded, isTrue);
......@@ -128,7 +164,7 @@ void main() {
final Completer<void> sawTick1 = Completer<void>();
final Completer<void> sawDebuggerPausedMessage1 = Completer<void>();
final Completer<void> sawDebuggerPausedMessage2 = Completer<void>();
final StreamSubscription<String> subscription = _flutter.stdout.listen(
final StreamSubscription<String> subscription = flutter.stdout.listen(
(String line) {
print('[LOG]:"$line"');
if (line.contains('(((TICK 1)))')) {
......@@ -145,25 +181,25 @@ void main() {
}
},
);
await _flutter.run(withDebugger: true);
await flutter.run(withDebugger: true);
await Future<void>.delayed(const Duration(seconds: 1));
await sawTick1.future;
await _flutter.addBreakpoint(
_project.buildBreakpointUri,
_project.buildBreakpointLine,
await flutter.addBreakpoint(
project.buildBreakpointUri,
project.buildBreakpointLine,
);
bool reloaded = false;
await Future<void>.delayed(const Duration(seconds: 1));
final Future<void> reloadFuture = _flutter.hotReload().then((void value) { reloaded = true; });
final Isolate isolate = await _flutter.waitForPause();
final Future<void> reloadFuture = flutter.hotReload().then((void value) { reloaded = true; });
final Isolate isolate = await flutter.waitForPause();
expect(isolate.pauseEvent.kind, equals(EventKind.kPauseBreakpoint));
expect(reloaded, isFalse);
await sawDebuggerPausedMessage1.future; // this is the one where it say "uh, you broke into the debugger while reloading"
await reloadFuture; // this is the one where it times out because you're in the debugger
expect(reloaded, isTrue);
await _flutter.hotReload(); // now we're already paused
await flutter.hotReload(); // now we're already paused
await sawDebuggerPausedMessage2.future; // so we just get told that nothing is going to happen
await _flutter.resume();
await flutter.resume();
await subscription.cancel();
});
}
......@@ -806,3 +806,20 @@ class SourcePosition {
final int line;
final int column;
}
Future<Isolate> waitForExtension(VmService vmService) async {
final Completer<void> completer = Completer<void>();
await vmService.streamListen(EventStreams.kExtension);
vmService.onExtensionEvent.listen((Event event) {
if (event.json['extensionKind'] == 'Flutter.FrameworkInitialization') {
completer.complete();
}
});
final IsolateRef isolateRef = (await vmService.getVM()).isolates.first;
final Isolate isolate = await vmService.getIsolate(isolateRef.id);
if (isolate.extensionRPCs.contains('ext.flutter.brightnessOverride')) {
return isolate;
}
await completer.future;
return isolate;
}
......@@ -130,20 +130,3 @@ void main() {
// TODO(devoncarew): These tests fail on cirrus-ci windows.
}, skip: Platform.isWindows);
}
Future<Isolate> waitForExtension(VmService vmService) async {
final Completer<void> completer = Completer<void>();
await vmService.streamListen(EventStreams.kExtension);
vmService.onExtensionEvent.listen((Event event) {
if (event.json['extensionKind'] == 'Flutter.FrameworkInitialization') {
completer.complete();
}
});
final IsolateRef isolateRef = (await vmService.getVM()).isolates.first;
final Isolate isolate = await vmService.getIsolate(isolateRef.id);
if (isolate.extensionRPCs.contains('ext.flutter.brightnessOverride')) {
return isolate;
}
await completer.future;
return isolate;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment