Commit 261923e5 authored by Ian Hickson's avatar Ian Hickson

Refactor service extensions (#3397)

Bindings now have a debugRegisterServiceExtensions() method that is
invoked in debug mode (only). (Once we have a profile mode, there'll be
a registerProfileServiceExtensions() method that gets called in that
mode only to register extensions that apply then.)

The BindingBase class provides convenience methods for registering
service extensions that do the equivalent of:

```dart
void extension() { ... }
bool extension([bool enabled]) { ... }
double extension([double extension])  { ... }
Map<String, String> extension([Map<String, String> parameters]) { ... }
```

The BindingBase class also itself registers ext.flutter.reassemble,
which it has call a function on the binding called
reassembleApplication().

The Scheduler binding now exposes the preexisting
ext.flutter.timeDilation.

The Renderer binding now exposes the preexisting ext.flutter.debugPaint.

The Renderer binding hooks reassembleApplication to trigger the
rendering tree to be reprocessed (in particular, to fix up the
optimisation closures).

All the logic from rendering/debug.dart about service extensions is
replaced by the above.

I moved basic_types to foundation.

The FlutterWidgets binding hooks reassembleApplication to trigger the
widget tree to be entirely rebuilt.

Flutter Driver now uses ext.flutter.driver instead of
ext.flutter_driver, and is hooked using the same binding mechanism.
Eventually we'll probably move the logic into the Flutter library so
that you just get it without having to invoke a special method first.
parent 8451b669
......@@ -10,5 +10,6 @@
library foundation;
export 'src/foundation/assertions.dart';
export 'src/foundation/basic_types.dart';
export 'src/foundation/binding.dart';
export 'src/foundation/print.dart';
......@@ -20,7 +20,6 @@
library rendering;
export 'src/rendering/auto_layout.dart';
export 'src/rendering/basic_types.dart';
export 'src/rendering/binding.dart';
export 'src/rendering/block.dart';
export 'src/rendering/box.dart';
......
......@@ -2,8 +2,27 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export 'dart:ui' show VoidCallback;
/// Signature for callbacks that report that an underlying value has changed.
///
/// See also [ValueSetter].
typedef void ValueChanged<T>(T value);
/// Signature for callbacks that report that a value has been set.
///
/// This is the same signature as [ValueChanged], but is used when the
/// callback is invoked even if the underlying value has not changed.
/// For example, service extensions use this callback because they
/// invoke the callback whenever the extension is invoked with a
/// value, regardless of whether the given value is new or not.
typedef void ValueSetter<T>(T value);
/// Signature for callbacks that are to report a value on demand.
///
/// See also [ValueSetter].
typedef T ValueGetter<T>();
/// A BitField over an enum (or other class whose values implement "index").
/// Only the first 63 values of the enum can be used as indices.
class BitField<T extends dynamic> {
......
......@@ -2,13 +2,31 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert' show JSON;
import 'dart:developer' as developer;
import 'package:meta/meta.dart';
import 'assertions.dart';
import 'basic_types.dart';
/// Signature for service extensions.
///
/// The returned map must not contain the keys "type" or "method", as
/// they will be replaced before the value is sent to the client. The
/// "type" key will be set to the string `_extensionType` to indicate
/// that this is a return value from a service extension, and the
/// "method" key will be set to the full name of the method.
typedef Future<Map<String, dynamic>> ServiceExtensionCallback(Map<String, String> parameters);
/// Base class for mixins that provide singleton services (also known as
/// "bindings").
///
/// To use this class in a mixin, inherit from it and implement
/// [initInstances()]. The mixin is guaranteed to only be constructed once in
/// the lifetime of the app (more precisely, it will assert if constructed twice
/// in checked mode).
/// [initInstances()]. The mixin is guaranteed to only be constructed
/// once in the lifetime of the app (more precisely, it will assert if
/// constructed twice in checked mode).
///
/// The top-most layer used to write the application will have a
/// concrete class that inherits from BindingBase and uses all the
......@@ -24,9 +42,14 @@ abstract class BindingBase {
assert(!_debugInitialized);
initInstances();
assert(_debugInitialized);
assert(!_debugServiceExtensionsRegistered);
initServiceExtensions();
assert(_debugServiceExtensionsRegistered);
}
static bool _debugInitialized = false;
static bool _debugServiceExtensionsRegistered = false;
/// The initialization method. Subclasses override this method to hook into
/// the platform and otherwise configure their services. Subclasses must call
......@@ -37,9 +60,180 @@ abstract class BindingBase {
/// `MixinClassName._instance`, a static field that is set by
/// `initInstances()`.
void initInstances() {
assert(!_debugInitialized);
assert(() { _debugInitialized = true; return true; });
}
/// Called when the binding is initialized, to register service
/// extensions.
///
/// Bindings that want to expose service extensions should overload
/// this method to register them using calls to
/// [registerSignalServiceExtension],
/// [registerBoolServiceExtension],
/// [registerNumericServiceExtension], and
/// [registerServiceExtension] (in increasing order of complexity).
///
/// Implementations of this method must call their superclass
/// implementation.
///
/// Service extensions are only exposed when the observatory is
/// included in the build, which should only happen in checked mode
/// and in profile mode.
///
/// See also:
///
/// * <https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#rpcs-requests-and-responses>
void initServiceExtensions() {
assert(!_debugServiceExtensionsRegistered);
registerSignalServiceExtension(
name: 'reassemble',
callback: reassembleApplication
);
assert(() { _debugServiceExtensionsRegistered = true; return true; });
}
/// Called when the ext.flutter.reassemble signal is sent by
/// development tools.
///
/// This is used by development tools when the application code has
/// changed, to cause the application to pick up any changed code.
/// Bindings are expected to use this method to reregister anything
/// that uses closures, so that they do not keep pointing to old
/// code, and to flush any caches of previously computed values, in
/// case the new code would compute them differently.
void reassembleApplication() { }
/// Registers a service extension method with the given name (full
/// name "ext.flutter.name"), which takes no arguments and returns
/// no value.
///
/// Invokes the `callback` callback when the service extension is
/// invoked.
void registerSignalServiceExtension({
@required String name,
@required VoidCallback callback
}) {
assert(name != null);
assert(callback != null);
registerServiceExtension(
name: name,
callback: (Map<String, String> parameters) async {
callback();
return <String, dynamic>{};
}
);
}
/// Registers a service extension method with the given name (full
/// name "ext.flutter.name"), which takes a single argument
/// "enabled" which can have the value "true" or the value "false"
/// or can be omitted to read the current value. (Any value other
/// than "true" is considered equivalent to "false". Other arguments
/// are ignored.)
///
/// Invokes the `getter` callback to obtain the value when
/// responding to the service extension method being invoked.
///
/// Invokes the `setter` callback with the new value when the
/// service extension method is invoked with a new value.
void registerBoolServiceExtension({
String name,
@required ValueGetter<bool> getter,
@required ValueSetter<bool> setter
}) {
assert(name != null);
assert(getter != null);
assert(setter != null);
registerServiceExtension(
name: name,
callback: (Map<String, String> parameters) async {
if (parameters.containsKey('enabled'))
setter(parameters['enabled'] == 'true');
return <String, dynamic>{ 'enabled': getter() };
}
);
}
/// Registers a service extension method with the given name (full
/// name "ext.flutter.name"), which takes a single argument with the
/// same name as the method which, if present, must have a value
/// that can be parsed by [double.parse], and can be omitted to read
/// the current value. (Other arguments are ignored.)
///
/// Invokes the `getter` callback to obtain the value when
/// responding to the service extension method being invoked.
///
/// Invokes the `setter` callback with the new value when the
/// service extension method is invoked with a new value.
void registerNumericServiceExtension({
@required String name,
@required ValueGetter<double> getter,
@required ValueSetter<double> setter
}) {
assert(name != null);
assert(getter != null);
assert(setter != null);
registerServiceExtension(
name: name,
callback: (Map<String, String> parameters) async {
if (parameters.containsKey(name))
setter(double.parse(parameters[name]));
return <String, dynamic>{ name: getter() };
}
);
}
/// Registers a service extension method with the given name (full
/// name "ext.flutter.name"). The given callback is invoked when the
/// extension method is called. The callback must return a [Future]
/// that either eventually completes to a return value in the form
/// of a name/value map where the values can all be converted to
/// JSON using [JSON.encode], or fails. In case of failure, the
/// failure is reported to the remote caller and is dumped to the
/// logs.
///
/// The returned map will be mutated.
void registerServiceExtension({
@required String name,
@required ServiceExtensionCallback callback
}) {
assert(name != null);
assert(callback != null);
final String methodName = 'ext.flutter.$name';
developer.registerExtension(methodName, (String method, Map<String, String> parameters) async {
assert(method == methodName);
dynamic caughtException;
StackTrace caughtStack;
Map<String, dynamic> result;
try {
result = await callback(parameters);
} catch (exception, stack) {
caughtException = exception;
caughtStack = stack;
}
if (caughtException == null) {
result['type'] = '_extensionType';
result['method'] = method;
return new developer.ServiceExtensionResponse.result(JSON.encode(result));
} else {
FlutterError.reportError(new FlutterErrorDetails(
exception: caughtException,
stack: caughtStack,
context: 'during a service extension callback for "$method"'
));
return new developer.ServiceExtensionResponse.error(
developer.ServiceExtensionResponse.extensionError,
JSON.encode({
'exception': caughtException.toString(),
'stack': caughtStack.toString(),
'method': method
})
);
}
});
}
@override
String toString() => '<$runtimeType>';
}
......@@ -31,15 +31,37 @@ abstract class Renderer extends Object with Scheduler, Services
initRenderView();
initSemantics();
assert(renderView != null);
addPersistentFrameCallback(_handlePersistentFrameCallback);
}
/// The current [Renderer], if one has been created.
static Renderer get instance => _instance;
static Renderer _instance;
@override
void initServiceExtensions() {
super.initServiceExtensions();
assert(() {
initServiceExtensions();
// this service extension only works in checked mode
registerBoolServiceExtension(
name: 'debugPaint',
getter: () => debugPaintSizeEnabled,
setter: (bool value) {
if (debugPaintSizeEnabled == value)
return;
debugPaintSizeEnabled = value;
RenderObjectVisitor visitor;
visitor = (RenderObject child) {
child.markNeedsPaint();
child.visitChildren(visitor);
};
instance?.renderView?.visitChildren(visitor);
}
);
return true;
});
addPersistentFrameCallback(_handlePersistentFrameCallback);
}
static Renderer _instance;
static Renderer get instance => _instance;
void initRenderView() {
if (renderView == null) {
......@@ -96,6 +118,12 @@ abstract class Renderer extends Object with Scheduler, Services
}
}
@override
void reassembleApplication() {
super.reassembleApplication();
pipelineOwner.reassemble(renderView);
}
@override
void hitTest(HitTestResult result, Point position) {
assert(renderView != null);
......
......@@ -2,13 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert' show JSON;
import 'dart:developer' as developer;
import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
export 'package:flutter/services.dart' show debugPrint;
......@@ -76,57 +71,3 @@ List<String> debugDescribeTransform(Matrix4 transform) {
matrix.removeLast();
return matrix;
}
bool _extensionsInitialized = false;
void initServiceExtensions() {
if (_extensionsInitialized)
return;
_extensionsInitialized = true;
assert(() {
developer.registerExtension('ext.flutter.debugPaint', _debugPaint);
developer.registerExtension('ext.flutter.timeDilation', _timeDilation);
return true;
});
}
/// Toggle the [debugPaintSizeEnabled] setting.
Future<developer.ServiceExtensionResponse> _debugPaint(String method, Map<String, String> parameters) {
if (parameters.containsKey('enabled')) {
debugPaintSizeEnabled = parameters['enabled'] == 'true';
// Redraw everything - mark the world as dirty.
RenderObjectVisitor visitor;
visitor = (RenderObject child) {
child.markNeedsPaint();
child.visitChildren(visitor);
};
Renderer.instance?.renderView?.visitChildren(visitor);
}
return new Future<developer.ServiceExtensionResponse>.value(
new developer.ServiceExtensionResponse.result(JSON.encode({
'type': '_extensionType',
'method': method,
'enabled': debugPaintSizeEnabled
}))
);
}
/// Manipulate the scheduler's [timeDilation] field.
Future<developer.ServiceExtensionResponse> _timeDilation(String method, Map<String, String> parameters) {
if (parameters.containsKey('timeDilation')) {
timeDilation = double.parse(parameters['timeDilation']);
}
return new Future<developer.ServiceExtensionResponse>.value(
new developer.ServiceExtensionResponse.result(JSON.encode({
'type': '_extensionType',
'method': method,
'timeDilation': '$timeDilation'
}))
);
}
......@@ -4,9 +4,9 @@
import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphStyle, TextBox;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'basic_types.dart';
import 'box.dart';
import 'object.dart';
import 'viewport.dart';
......
......@@ -797,7 +797,19 @@ class PipelineOwner {
Timeline.finishSync();
}
}
/// Cause the entire subtree rooted at the given [RenderObject] to
/// be entirely reprocessed. This is used by development tools when
/// the application code has changed, to cause the rendering tree to
/// pick up any changed implementations.
///
/// This is expensive and should not be called except during
/// development.
void reassemble(RenderObject root) {
assert(root.parent is! RenderObject);
assert(root.owner == this);
root._reassemble();
}
}
// See _performLayout.
......@@ -833,6 +845,17 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
_performLayout = performLayout;
}
void _reassemble() {
_performLayout = performLayout;
markNeedsLayout();
markNeedsCompositingBitsUpdate();
markNeedsPaint();
markNeedsSemanticsUpdate();
visitChildren((RenderObject child) {
child._reassemble();
});
}
// LAYOUT
/// Data for use by the parent render object.
......
......@@ -5,11 +5,11 @@
import 'dart:math' as math;
import 'dart:ui' show Rect;
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:sky_services/semantics/semantics.mojom.dart' as mojom;
import 'package:vector_math/vector_math_64.dart';
import 'basic_types.dart';
import 'node.dart';
/// The type of function returned by [RenderObject.getSemanticAnnotators()].
......
......@@ -82,6 +82,18 @@ abstract class Scheduler extends BindingBase {
static Scheduler get instance => _instance;
static Scheduler _instance;
@override
void initServiceExtensions() {
super.initServiceExtensions();
registerNumericServiceExtension(
name: 'timeDilation',
getter: () => timeDilation,
setter: (double value) {
timeDilation = value;
}
);
}
/// The strategy to use when deciding whether to run a task or not.
///
......
......@@ -60,8 +60,8 @@ class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Service
/// bindings from other frameworks based on the Flutter "rendering" library),
/// then WidgetFlutterBinding.instance will not be valid (and will throw in
/// checked mode).
static WidgetFlutterBinding _instance;
static WidgetFlutterBinding get instance => _instance;
static WidgetFlutterBinding _instance;
final List<BindingObserver> _observers = new List<BindingObserver>();
......@@ -116,6 +116,12 @@ class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Service
).attachToRenderTree(buildOwner, _renderViewElement);
beginFrame();
}
@override
void reassembleApplication() {
buildOwner.reassemble(_renderViewElement);
super.reassembleApplication();
}
}
/// Inflate the given widget and attach it to the screen.
......
......@@ -794,7 +794,19 @@ class BuildOwner {
assert(GlobalKey._debugCheckForDuplicates);
scheduleMicrotask(GlobalKey._notifyListeners);
}
/// Cause the entire subtree rooted at the given [Element] to
/// be entirely rebuilt. This is used by development tools when
/// the application code has changed, to cause the widget tree to
/// pick up any changed implementations.
///
/// This is expensive and should not be called except during
/// development.
void reassemble(Element root) {
assert(root._parent == null);
assert(root.owner == this);
root._reassemble();
}
}
/// Elements are the instantiations of Widget configurations.
......@@ -832,6 +844,13 @@ abstract class Element implements BuildContext {
bool _active = false;
void _reassemble() {
assert(_active);
visitChildren((Element child) {
child._reassemble();
});
}
RenderObject get renderObject {
RenderObject result;
void visit(Element element) {
......@@ -1363,6 +1382,12 @@ abstract class BuildableElement extends Element {
markNeedsBuild();
}
@override
void _reassemble() {
markNeedsBuild();
super._reassemble();
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
......@@ -1374,7 +1399,7 @@ abstract class BuildableElement extends Element {
typedef Widget WidgetBuilder(BuildContext context);
typedef Widget IndexedBuilder(BuildContext context, int index);
// See _builder.
// See ComponentElement._builder.
Widget _buildNothing(BuildContext context) => null;
/// Base class for the instantiation of [StatelessWidget], [StatefulWidget],
......@@ -1476,6 +1501,12 @@ class StatelessElement extends ComponentElement {
_dirty = true;
rebuild();
}
@override
void _reassemble() {
_builder = widget.build;
super._reassemble();
}
}
/// Instantiation of [StatefulWidget]s.
......@@ -1495,6 +1526,12 @@ class StatefulElement extends ComponentElement {
State<StatefulWidget> get state => _state;
State<StatefulWidget> _state;
@override
void _reassemble() {
_builder = state.build;
super._reassemble();
}
@override
void _firstBuild() {
assert(_state._debugLifecycleState == _StateLifecycle.created);
......@@ -1590,6 +1627,12 @@ abstract class _ProxyElement extends ComponentElement {
Widget _build(BuildContext context) => widget.child;
@override
void _reassemble() {
_builder = _build;
super._reassemble();
}
@override
void update(_ProxyWidget newWidget) {
_ProxyWidget oldWidget = widget;
......@@ -1656,7 +1699,6 @@ class ParentDataElement<T extends RenderObjectWidget> extends _ProxyElement {
}
class InheritedElement extends _ProxyElement {
InheritedElement(InheritedWidget widget) : super(widget);
......
......@@ -36,7 +36,7 @@ typedef dynamic EvaluatorFunction();
class FlutterDriver {
FlutterDriver.connectedTo(this._serviceClient, this._peer, this._appIsolate);
static const String _kFlutterExtensionMethod = 'ext.flutter_driver';
static const String _kFlutterExtensionMethod = 'ext.flutter.driver';
static const String _kSetVMTimelineFlagsMethod = '_setVMTimelineFlags';
static const String _kGetVMTimelineMethod = '_getVMTimeline';
static const Duration _kDefaultTimeout = const Duration(seconds: 5);
......
......@@ -3,12 +3,11 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter_test/src/instrumentation.dart';
import 'package:flutter_test/src/test_pointer.dart';
......@@ -18,10 +17,21 @@ import 'gesture.dart';
import 'health.dart';
import 'message.dart';
const String _extensionMethod = 'ext.flutter_driver';
const String _extensionMethodName = 'driver';
const String _extensionMethod = 'ext.flutter.$_extensionMethodName';
const Duration _kDefaultTimeout = const Duration(seconds: 5);
bool _flutterDriverExtensionEnabled = false;
class _DriverBinding extends WidgetFlutterBinding {
@override
void initServiceExtensions() {
super.initServiceExtensions();
FlutterDriverExtension extension = new FlutterDriverExtension();
registerServiceExtension(
name: _extensionMethodName,
callback: extension.call
);
}
}
/// Enables Flutter Driver VM service extension.
///
......@@ -31,13 +41,9 @@ bool _flutterDriverExtensionEnabled = false;
/// Call this function prior to running your application, e.g. before you call
/// `runApp`.
void enableFlutterDriverExtension() {
if (_flutterDriverExtensionEnabled)
return;
FlutterDriverExtension extension = new FlutterDriverExtension();
registerExtension(_extensionMethod, (String methodName, Map<String, String> params) {
return extension.call(params);
});
_flutterDriverExtensionEnabled = true;
assert(WidgetFlutterBinding.instance == null);
new _DriverBinding();
assert(WidgetFlutterBinding.instance is _DriverBinding);
}
/// Handles a command and returns a result.
......@@ -81,32 +87,19 @@ class FlutterDriverExtension {
final Map<String, CommandDeserializerCallback> _commandDeserializers = <String, CommandDeserializerCallback>{};
final Map<String, FinderCallback> _finders = <String, FinderCallback>{};
Future<ServiceExtensionResponse> call(Map<String, String> params) async {
Future<Map<String, dynamic>> call(Map<String, String> params) async {
try {
String commandKind = params['command'];
CommandHandlerCallback commandHandler = _commandHandlers[commandKind];
CommandDeserializerCallback commandDeserializer =
_commandDeserializers[commandKind];
if (commandHandler == null || commandDeserializer == null) {
return new ServiceExtensionResponse.error(
ServiceExtensionResponse.invalidParams,
'Extension $_extensionMethod does not support command $commandKind'
);
}
if (commandHandler == null || commandDeserializer == null)
throw 'Extension $_extensionMethod does not support command $commandKind';
Command command = commandDeserializer(params);
return commandHandler(command).then((Result result) {
return new ServiceExtensionResponse.result(JSON.encode(result.toJson()));
}, onError: (Object e, Object s) {
_log.warning('$e:\n$s');
return new ServiceExtensionResponse.error(ServiceExtensionResponse.extensionError, '$e');
});
} catch(error, stackTrace) {
String message = 'Uncaught extension error: $error\n$stackTrace';
_log.error(message);
return new ServiceExtensionResponse.error(
ServiceExtensionResponse.extensionError, message);
return (await commandHandler(command)).toJson();
} catch (error, stackTrace) {
_log.error('Uncaught extension error: $error\n$stackTrace');
rethrow;
}
}
......
......@@ -51,7 +51,7 @@ void main() {
test('connects to isolate paused at start', () async {
when(mockIsolate.pauseEvent).thenReturn(new MockVMPauseStartEvent());
when(mockIsolate.resume()).thenReturn(new Future<Null>.value());
when(mockIsolate.onExtensionAdded).thenReturn(new Stream<String>.fromIterable(<String>['ext.flutter_driver']));
when(mockIsolate.onExtensionAdded).thenReturn(new Stream<String>.fromIterable(<String>['ext.flutter.driver']));
FlutterDriver driver = await FlutterDriver.connect();
expect(driver, isNotNull);
......
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