Unverified Commit 8e2ca93f authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Remove all service extensions from release mode (#23038)

Service extensions can only be activated in debug or profile mode, their code should never be included in release mode. This PR adds guards around all service extension registration calls that enable Dart's tree shaker to remove the extension's code in release mode, which reduces our binary size:

Android Snapshot (uncompressed): minus 127,384 Bytes (-124.40KB)
APK (compressed): minus 38,136 Bytes (-37.24KB)
iOS Snapshot (App.framework, uncompressed): 264,304 Bytes(-258.10KB)

For details: https://docs.google.com/document/d/13JlgvliCn5sWwT2K2SfDwD1NhEfxpJH9DCf22gZZru8/edit

**Benchmark Regressions:** This PR may cause benchmarks to regress because it may change the timing of GC. If you notice a benchmark regression **please note down the exact set of benchmarks that regressed on this PR** and then feel free to revert. I will follow-up with a PR that forces a GC before the effected benchmarks run to get a clean baseline before re-applying this PR.
parent 04a26778
...@@ -93,9 +93,7 @@ abstract class BindingBase { ...@@ -93,9 +93,7 @@ abstract class BindingBase {
/// Implementations of this method must call their superclass /// Implementations of this method must call their superclass
/// implementation. /// implementation.
/// ///
/// Service extensions are only exposed when the observatory is /// {@macro flutter.foundation.bindingBase.registerServiceExtension}
/// included in the build, which should only happen in checked mode
/// and in profile mode.
/// ///
/// See also: /// See also:
/// ///
...@@ -104,18 +102,23 @@ abstract class BindingBase { ...@@ -104,18 +102,23 @@ abstract class BindingBase {
@mustCallSuper @mustCallSuper
void initServiceExtensions() { void initServiceExtensions() {
assert(!_debugServiceExtensionsRegistered); assert(!_debugServiceExtensionsRegistered);
registerSignalServiceExtension(
name: 'reassemble', assert(() {
callback: reassembleApplication, registerSignalServiceExtension(
); name: 'reassemble',
registerSignalServiceExtension( callback: reassembleApplication,
name: 'exit', );
callback: _exitApplication, return true;
); }());
registerSignalServiceExtension(
name: 'frameworkPresent', const bool isReleaseMode = bool.fromEnvironment('dart.vm.product');
callback: () => Future<Null>.value(), if (!isReleaseMode) {
); registerSignalServiceExtension(
name: 'exit',
callback: _exitApplication,
);
}
assert(() { assert(() {
registerServiceExtension( registerServiceExtension(
name: 'platformOverride', name: 'platformOverride',
...@@ -239,6 +242,8 @@ abstract class BindingBase { ...@@ -239,6 +242,8 @@ abstract class BindingBase {
/// no value. /// no value.
/// ///
/// Calls the `callback` callback when the service extension is called. /// Calls the `callback` callback when the service extension is called.
///
/// {@macro flutter.foundation.bindingBase.registerServiceExtension}
@protected @protected
void registerSignalServiceExtension({ void registerSignalServiceExtension({
@required String name, @required String name,
...@@ -267,6 +272,8 @@ abstract class BindingBase { ...@@ -267,6 +272,8 @@ abstract class BindingBase {
/// ///
/// Calls the `setter` callback with the new value when the /// Calls the `setter` callback with the new value when the
/// service extension method is called with a new value. /// service extension method is called with a new value.
///
/// {@macro flutter.foundation.bindingBase.registerServiceExtension}
@protected @protected
void registerBoolServiceExtension({ void registerBoolServiceExtension({
@required String name, @required String name,
...@@ -297,6 +304,8 @@ abstract class BindingBase { ...@@ -297,6 +304,8 @@ abstract class BindingBase {
/// ///
/// Calls the `setter` callback with the new value when the /// Calls the `setter` callback with the new value when the
/// service extension method is called with a new value. /// service extension method is called with a new value.
///
/// {@macro flutter.foundation.bindingBase.registerServiceExtension}
@protected @protected
void registerNumericServiceExtension({ void registerNumericServiceExtension({
@required String name, @required String name,
...@@ -326,6 +335,8 @@ abstract class BindingBase { ...@@ -326,6 +335,8 @@ abstract class BindingBase {
/// ///
/// Calls the `setter` callback with the new value when the /// Calls the `setter` callback with the new value when the
/// service extension method is called with a new value. /// service extension method is called with a new value.
///
/// {@macro flutter.foundation.bindingBase.registerServiceExtension}
@protected @protected
void registerStringServiceExtension({ void registerStringServiceExtension({
@required String name, @required String name,
...@@ -345,16 +356,51 @@ abstract class BindingBase { ...@@ -345,16 +356,51 @@ abstract class BindingBase {
); );
} }
/// Registers a service extension method with the given name (full /// Registers a service extension method with the given name (full name
/// name "ext.flutter.name"). The given callback is called when the /// "ext.flutter.name").
/// extension method is called. The callback must return a [Future] ///
/// that either eventually completes to a return value in the form /// The given callback is called when the extension method is called. The
/// of a name/value map where the values can all be converted to /// callback must return a [Future] that either eventually completes to a
/// JSON using `json.encode()` (see [JsonEncoder]), or fails. In case of failure, the /// return value in the form of a name/value map where the values can all be
/// failure is reported to the remote caller and is dumped to the /// converted to JSON using `json.encode()` (see [JsonEncoder]), or fails. In
/// logs. /// case of failure, the failure is reported to the remote caller and is
/// dumped to the logs.
/// ///
/// The returned map will be mutated. /// The returned map will be mutated.
///
/// {@template flutter.foundation.bindingBase.registerServiceExtension}
/// A registered service extension can only be activated if the vm-service
/// is included in the build, which only happens in debug and profile mode.
/// Although a service extension cannot be used in release mode its code may
/// still be included in the Dart snapshot and blow up binary size if it is
/// not wrapped in a guard that allows the tree shaker to remove it (see
/// sample code below).
///
/// ## Sample Code
///
/// The following code registers a service extension that is only included in
/// debug builds:
///
/// ```dart
/// assert(() {
/// // Register your service extension here.
/// return true;
/// }());
///
/// ```
///
/// A service extension registered with the following code snippet is
/// available in debug and profile mode:
///
/// ```dart
/// if (!const bool.fromEnvironment('dart.vm.product')) {
// // Register your service extension here.
// }
/// ```
///
/// Both guards ensure that Dart's tree shaker can remove the code for the
/// service extension in release builds.
/// {@endTemplate}
@protected @protected
void registerServiceExtension({ void registerServiceExtension({
@required String name, @required String name,
......
...@@ -55,7 +55,7 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul ...@@ -55,7 +55,7 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul
super.initServiceExtensions(); super.initServiceExtensions();
assert(() { assert(() {
// these service extensions only work in checked mode // these service extensions only work in debug mode
registerBoolServiceExtension( registerBoolServiceExtension(
name: 'debugPaint', name: 'debugPaint',
getter: () async => debugPaintSizeEnabled, getter: () async => debugPaintSizeEnabled,
...@@ -64,51 +64,66 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul ...@@ -64,51 +64,66 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul
return Future<Null>.value(); return Future<Null>.value();
debugPaintSizeEnabled = value; debugPaintSizeEnabled = value;
return _forceRepaint(); return _forceRepaint();
} },
); );
registerBoolServiceExtension( registerBoolServiceExtension(
name: 'debugPaintBaselinesEnabled', name: 'debugPaintBaselinesEnabled',
getter: () async => debugPaintBaselinesEnabled, getter: () async => debugPaintBaselinesEnabled,
setter: (bool value) { setter: (bool value) {
if (debugPaintBaselinesEnabled == value) if (debugPaintBaselinesEnabled == value)
return Future<Null>.value(); return Future<Null>.value();
debugPaintBaselinesEnabled = value; debugPaintBaselinesEnabled = value;
return _forceRepaint(); return _forceRepaint();
} },
); );
registerBoolServiceExtension( registerBoolServiceExtension(
name: 'repaintRainbow', name: 'repaintRainbow',
getter: () async => debugRepaintRainbowEnabled, getter: () async => debugRepaintRainbowEnabled,
setter: (bool value) { setter: (bool value) {
final bool repaint = debugRepaintRainbowEnabled && !value; final bool repaint = debugRepaintRainbowEnabled && !value;
debugRepaintRainbowEnabled = value; debugRepaintRainbowEnabled = value;
if (repaint) if (repaint)
return _forceRepaint(); return _forceRepaint();
return Future<Null>.value(); return Future<Null>.value();
} },
);
registerSignalServiceExtension(
name: 'debugDumpLayerTree',
callback: () {
debugDumpLayerTree();
return debugPrintDone;
},
); );
return true; return true;
}()); }());
registerSignalServiceExtension( const bool isReleaseMode = bool.fromEnvironment('dart.vm.product');
name: 'debugDumpRenderTree', if (!isReleaseMode) {
callback: () { debugDumpRenderTree(); return debugPrintDone; } // these service extensions work in debug or profile mode
); registerSignalServiceExtension(
name: 'debugDumpRenderTree',
registerSignalServiceExtension( callback: () {
name: 'debugDumpLayerTree', debugDumpRenderTree();
callback: () { debugDumpLayerTree(); return debugPrintDone; } return debugPrintDone;
); },
);
registerSignalServiceExtension( registerSignalServiceExtension(
name: 'debugDumpSemanticsTreeInTraversalOrder', name: 'debugDumpSemanticsTreeInTraversalOrder',
callback: () { debugDumpSemanticsTree(DebugSemanticsDumpOrder.traversalOrder); return debugPrintDone; } callback: () {
); debugDumpSemanticsTree(DebugSemanticsDumpOrder.traversalOrder);
return debugPrintDone;
},
);
registerSignalServiceExtension( registerSignalServiceExtension(
name: 'debugDumpSemanticsTreeInInverseHitTestOrder', name: 'debugDumpSemanticsTreeInInverseHitTestOrder',
callback: () { debugDumpSemanticsTree(DebugSemanticsDumpOrder.inverseHitTest); return debugPrintDone; } callback: () {
); debugDumpSemanticsTree(DebugSemanticsDumpOrder.inverseHitTest);
return debugPrintDone;
},
);
}
} }
/// Creates a [RenderView] object to be the root of the /// Creates a [RenderView] object to be the root of the
......
...@@ -207,13 +207,17 @@ abstract class SchedulerBinding extends BindingBase with ServicesBinding { ...@@ -207,13 +207,17 @@ abstract class SchedulerBinding extends BindingBase with ServicesBinding {
@override @override
void initServiceExtensions() { void initServiceExtensions() {
super.initServiceExtensions(); super.initServiceExtensions();
registerNumericServiceExtension(
name: 'timeDilation', const bool isReleaseMode = bool.fromEnvironment('dart.vm.product');
getter: () async => timeDilation, if (!isReleaseMode) {
setter: (double value) async { registerNumericServiceExtension(
timeDilation = value; name: 'timeDilation',
} getter: () async => timeDilation,
); setter: (double value) async {
timeDilation = value;
},
);
}
} }
/// Whether the application is visible, and if so, whether it is currently /// Whether the application is visible, and if so, whether it is currently
......
...@@ -90,17 +90,21 @@ abstract class ServicesBinding extends BindingBase { ...@@ -90,17 +90,21 @@ abstract class ServicesBinding extends BindingBase {
@override @override
void initServiceExtensions() { void initServiceExtensions() {
super.initServiceExtensions(); super.initServiceExtensions();
registerStringServiceExtension(
// ext.flutter.evict value=foo.png will cause foo.png to be evicted from assert(() {
// the rootBundle cache and cause the entire image cache to be cleared. registerStringServiceExtension(
// This is used by hot reload mode to clear out the cache of resources // ext.flutter.evict value=foo.png will cause foo.png to be evicted from
// that have changed. // the rootBundle cache and cause the entire image cache to be cleared.
name: 'evict', // This is used by hot reload mode to clear out the cache of resources
getter: () async => '', // that have changed.
setter: (String value) async { name: 'evict',
evict(value); getter: () async => '',
} setter: (String value) async {
); evict(value);
},
);
return true;
}());
} }
/// Called in response to the `ext.flutter.evict` service extension. /// Called in response to the `ext.flutter.evict` service extension.
......
...@@ -267,37 +267,41 @@ abstract class WidgetsBinding extends BindingBase with SchedulerBinding, Gesture ...@@ -267,37 +267,41 @@ abstract class WidgetsBinding extends BindingBase with SchedulerBinding, Gesture
void initServiceExtensions() { void initServiceExtensions() {
super.initServiceExtensions(); super.initServiceExtensions();
registerSignalServiceExtension( const bool isReleaseMode = bool.fromEnvironment('dart.vm.product');
name: 'debugDumpApp', if (!isReleaseMode) {
callback: () { registerSignalServiceExtension(
debugDumpApp(); name: 'debugDumpApp',
return debugPrintDone; callback: () {
} debugDumpApp();
); return debugPrintDone;
},
registerBoolServiceExtension( );
name: 'showPerformanceOverlay',
getter: () => Future<bool>.value(WidgetsApp.showPerformanceOverlayOverride), registerBoolServiceExtension(
setter: (bool value) { name: 'showPerformanceOverlay',
if (WidgetsApp.showPerformanceOverlayOverride == value) getter: () =>
return Future<Null>.value(); Future<bool>.value(WidgetsApp.showPerformanceOverlayOverride),
WidgetsApp.showPerformanceOverlayOverride = value; setter: (bool value) {
return _forceRebuild(); if (WidgetsApp.showPerformanceOverlayOverride == value)
} return Future<Null>.value();
); WidgetsApp.showPerformanceOverlayOverride = value;
return _forceRebuild();
registerBoolServiceExtension( },
name: 'debugAllowBanner', );
getter: () => Future<bool>.value(WidgetsApp.debugAllowBannerOverride), }
setter: (bool value) {
if (WidgetsApp.debugAllowBannerOverride == value)
return Future<Null>.value();
WidgetsApp.debugAllowBannerOverride = value;
return _forceRebuild();
}
);
assert(() { assert(() {
registerBoolServiceExtension(
name: 'debugAllowBanner',
getter: () => Future<bool>.value(WidgetsApp.debugAllowBannerOverride),
setter: (bool value) {
if (WidgetsApp.debugAllowBannerOverride == value)
return Future<Null>.value();
WidgetsApp.debugAllowBannerOverride = value;
return _forceRebuild();
},
);
// Expose the ability to send Widget rebuilds as [Timeline] events. // Expose the ability to send Widget rebuilds as [Timeline] events.
registerBoolServiceExtension( registerBoolServiceExtension(
name: 'profileWidgetBuilds', name: 'profileWidgetBuilds',
...@@ -305,25 +309,13 @@ abstract class WidgetsBinding extends BindingBase with SchedulerBinding, Gesture ...@@ -305,25 +309,13 @@ abstract class WidgetsBinding extends BindingBase with SchedulerBinding, Gesture
setter: (bool value) async { setter: (bool value) async {
if (debugProfileBuildsEnabled != value) if (debugProfileBuildsEnabled != value)
debugProfileBuildsEnabled = value; debugProfileBuildsEnabled = value;
} },
); );
return true;
}());
// This service extension is deprecated and will be removed by 7/1/2018. WidgetInspectorService.instance.initServiceExtensions(registerServiceExtension);
// Use ext.flutter.inspector.show instead.
registerBoolServiceExtension(
name: 'debugWidgetInspector',
getter: () async => WidgetsApp.debugShowWidgetInspectorOverride,
setter: (bool value) {
if (WidgetsApp.debugShowWidgetInspectorOverride == value)
return Future<Null>.value();
WidgetsApp.debugShowWidgetInspectorOverride = value;
return _forceRebuild();
}
);
WidgetInspectorService.instance.initServiceExtensions(registerServiceExtension); return true;
}());
} }
Future<Null> _forceRebuild() { Future<Null> _forceRebuild() {
......
...@@ -921,13 +921,11 @@ class WidgetInspectorService { ...@@ -921,13 +921,11 @@ class WidgetInspectorService {
/// Called to register service extensions. /// Called to register service extensions.
/// ///
/// 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: /// See also:
/// ///
/// * <https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#rpcs-requests-and-responses> /// * <https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#rpcs-requests-and-responses>
/// * [BindingBase.initServiceExtensions], which explains when service
/// extensions can be used.
void initServiceExtensions( void initServiceExtensions(
_RegisterServiceExtensionCallback registerServiceExtensionCallback) { _RegisterServiceExtensionCallback registerServiceExtensionCallback) {
_registerServiceExtensionCallback = registerServiceExtensionCallback; _registerServiceExtensionCallback = registerServiceExtensionCallback;
...@@ -1027,36 +1025,33 @@ class WidgetInspectorService { ...@@ -1027,36 +1025,33 @@ class WidgetInspectorService {
name: 'isWidgetCreationTracked', name: 'isWidgetCreationTracked',
callback: isWidgetCreationTracked, callback: isWidgetCreationTracked,
); );
assert(() { registerServiceExtension(
registerServiceExtension( name: 'screenshot',
name: 'screenshot', callback: (Map<String, String> parameters) async {
callback: (Map<String, String> parameters) async { assert(parameters.containsKey('id'));
assert(parameters.containsKey('id')); assert(parameters.containsKey('width'));
assert(parameters.containsKey('width')); assert(parameters.containsKey('height'));
assert(parameters.containsKey('height'));
final ui.Image image = await screenshot(
final ui.Image image = await screenshot( toObject(parameters['id']),
toObject(parameters['id']), width: double.parse(parameters['width']),
width: double.parse(parameters['width']), height: double.parse(parameters['height']),
height: double.parse(parameters['height']), margin: parameters.containsKey('margin') ?
margin: parameters.containsKey('margin') ? double.parse(parameters['margin']) : 0.0,
double.parse(parameters['margin']) : 0.0, maxPixelRatio: parameters.containsKey('maxPixelRatio') ?
maxPixelRatio: parameters.containsKey('maxPixelRatio') ? double.parse(parameters['maxPixelRatio']) : 1.0,
double.parse(parameters['maxPixelRatio']) : 1.0, debugPaint: parameters['debugPaint'] == 'true',
debugPaint: parameters['debugPaint'] == 'true', );
); if (image == null) {
if (image == null) { return <String, Object>{'result': null};
return <String, Object>{'result': null}; }
} final ByteData byteData = await image.toByteData(format:ui.ImageByteFormat.png);
final ByteData byteData = await image.toByteData(format:ui.ImageByteFormat.png);
return <String, Object>{ return <String, Object>{
'result': base64.encoder.convert(Uint8List.view(byteData.buffer)), 'result': base64.encoder.convert(Uint8List.view(byteData.buffer)),
}; };
}, },
); );
return true;
}());
} }
/// Clear all InspectorService object references. /// Clear all InspectorService object references.
......
...@@ -359,13 +359,6 @@ void main() { ...@@ -359,13 +359,6 @@ void main() {
expect(binding.extensions.containsKey('exit'), isTrue); expect(binding.extensions.containsKey('exit'), isTrue);
}); });
test('Service extensions - frameworkPresent', () async {
Map<String, dynamic> result;
result = await binding.testExtension('frameworkPresent', <String, String>{});
expect(result, <String, String>{});
});
test('Service extensions - platformOverride', () async { test('Service extensions - platformOverride', () async {
Map<String, dynamic> result; Map<String, dynamic> result;
...@@ -489,29 +482,6 @@ void main() { ...@@ -489,29 +482,6 @@ void main() {
expect(binding.frameScheduled, isFalse); expect(binding.frameScheduled, isFalse);
}); });
test('Service extensions - debugWidgetInspector', () async {
Map<String, dynamic> result;
expect(binding.frameScheduled, isFalse);
expect(WidgetsApp.debugShowWidgetInspectorOverride, false);
result = await binding.testExtension('debugWidgetInspector', <String, String>{});
expect(result, <String, String>{ 'enabled': 'false' });
expect(WidgetsApp.debugShowWidgetInspectorOverride, false);
result = await binding.testExtension('debugWidgetInspector', <String, String>{ 'enabled': 'true' });
expect(result, <String, String>{ 'enabled': 'true' });
expect(WidgetsApp.debugShowWidgetInspectorOverride, true);
result = await binding.testExtension('debugWidgetInspector', <String, String>{});
expect(result, <String, String>{ 'enabled': 'true' });
expect(WidgetsApp.debugShowWidgetInspectorOverride, true);
result = await binding.testExtension('debugWidgetInspector', <String, String>{ 'enabled': 'false' });
expect(result, <String, String>{ 'enabled': 'false' });
expect(WidgetsApp.debugShowWidgetInspectorOverride, false);
result = await binding.testExtension('debugWidgetInspector', <String, String>{});
expect(result, <String, String>{ 'enabled': 'false' });
expect(WidgetsApp.debugShowWidgetInspectorOverride, false);
expect(binding.frameScheduled, isFalse);
});
test('Service extensions - timeDilation', () async { test('Service extensions - timeDilation', () async {
Map<String, dynamic> result; Map<String, dynamic> result;
...@@ -541,7 +511,7 @@ void main() { ...@@ -541,7 +511,7 @@ void main() {
// If you add a service extension... TEST IT! :-) // If you add a service extension... TEST IT! :-)
// ...then increment this number. // ...then increment this number.
expect(binding.extensions.length, 39); expect(binding.extensions.length, 37);
expect(console, isEmpty); expect(console, isEmpty);
debugPrint = debugPrintThrottled; debugPrint = debugPrintThrottled;
......
...@@ -1295,10 +1295,6 @@ class Isolate extends ServiceObjectOwner { ...@@ -1295,10 +1295,6 @@ class Isolate extends ServiceObjectOwner {
); );
} }
Future<bool> flutterFrameworkPresent() async {
return await invokeFlutterExtensionRpcRaw('ext.flutter.frameworkPresent') != null;
}
Future<Map<String, dynamic>> uiWindowScheduleFrame() async { Future<Map<String, dynamic>> uiWindowScheduleFrame() async {
return await invokeFlutterExtensionRpcRaw('ext.ui.window.scheduleFrame'); return await invokeFlutterExtensionRpcRaw('ext.ui.window.scheduleFrame');
} }
......
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