Reland "Remove all service extensions from release mode (#23038)" (#23291)

diff --git a/packages/flutter/lib/src/foundation/binding.dart b/packages/flutter/lib/src/foundation/binding.dart
index abdf6b0..c6f1f08 100644
--- a/packages/flutter/lib/src/foundation/binding.dart
+++ b/packages/flutter/lib/src/foundation/binding.dart
@@ -93,9 +93,7 @@
   /// 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.
+  /// {@macro flutter.foundation.bindingBase.registerServiceExtension}
   ///
   /// See also:
   ///
@@ -104,18 +102,23 @@
   @mustCallSuper
   void initServiceExtensions() {
     assert(!_debugServiceExtensionsRegistered);
-    registerSignalServiceExtension(
-      name: 'reassemble',
-      callback: reassembleApplication,
-    );
-    registerSignalServiceExtension(
-      name: 'exit',
-      callback: _exitApplication,
-    );
-    registerSignalServiceExtension(
-      name: 'frameworkPresent',
-      callback: () => Future<void>.value(),
-    );
+
+    assert(() {
+      registerSignalServiceExtension(
+        name: 'reassemble',
+        callback: reassembleApplication,
+      );
+      return true;
+    }());
+
+    const bool isReleaseMode = bool.fromEnvironment('dart.vm.product');
+    if (!isReleaseMode) {
+      registerSignalServiceExtension(
+        name: 'exit',
+        callback: _exitApplication,
+      );
+    }
+
     assert(() {
       registerServiceExtension(
         name: 'platformOverride',
@@ -239,6 +242,8 @@
   /// no value.
   ///
   /// Calls the `callback` callback when the service extension is called.
+  ///
+  /// {@macro flutter.foundation.bindingBase.registerServiceExtension}
   @protected
   void registerSignalServiceExtension({
     @required String name,
@@ -267,6 +272,8 @@
   ///
   /// Calls the `setter` callback with the new value when the
   /// service extension method is called with a new value.
+  ///
+  /// {@macro flutter.foundation.bindingBase.registerServiceExtension}
   @protected
   void registerBoolServiceExtension({
     @required String name,
@@ -297,6 +304,8 @@
   ///
   /// Calls the `setter` callback with the new value when the
   /// service extension method is called with a new value.
+  ///
+  /// {@macro flutter.foundation.bindingBase.registerServiceExtension}
   @protected
   void registerNumericServiceExtension({
     @required String name,
@@ -326,6 +335,8 @@
   ///
   /// Calls the `setter` callback with the new value when the
   /// service extension method is called with a new value.
+  ///
+  /// {@macro flutter.foundation.bindingBase.registerServiceExtension}
   @protected
   void registerStringServiceExtension({
     @required String name,
@@ -345,16 +356,51 @@
     );
   }
 
-  /// Registers a service extension method with the given name (full
-  /// name "ext.flutter.name"). The given callback is called 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()` (see [JsonEncoder]), or fails. In case of failure, the
-  /// failure is reported to the remote caller and is dumped to the
-  /// logs.
+  /// Registers a service extension method with the given name (full name
+  /// "ext.flutter.name").
+  ///
+  /// The given callback is called 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()` (see [JsonEncoder]), 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.
+  ///
+  /// {@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
   void registerServiceExtension({
     @required String name,
diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart
index f3ab270..41e823d 100644
--- a/packages/flutter/lib/src/rendering/binding.dart
+++ b/packages/flutter/lib/src/rendering/binding.dart
@@ -51,7 +51,7 @@
     super.initServiceExtensions();
 
     assert(() {
-      // these service extensions only work in checked mode
+      // these service extensions only work in debug mode
       registerBoolServiceExtension(
         name: 'debugPaint',
         getter: () async => debugPaintSizeEnabled,
@@ -60,17 +60,17 @@
             return Future<void>.value();
           debugPaintSizeEnabled = value;
           return _forceRepaint();
-        }
+        },
       );
       registerBoolServiceExtension(
-          name: 'debugPaintBaselinesEnabled',
-          getter: () async => debugPaintBaselinesEnabled,
-          setter: (bool value) {
+        name: 'debugPaintBaselinesEnabled',
+        getter: () async => debugPaintBaselinesEnabled,
+        setter: (bool value) {
           if (debugPaintBaselinesEnabled == value)
             return Future<void>.value();
           debugPaintBaselinesEnabled = value;
           return _forceRepaint();
-        }
+        },
       );
       registerBoolServiceExtension(
         name: 'repaintRainbow',
@@ -83,28 +83,43 @@
           return Future<void>.value();
         },
       );
+      registerSignalServiceExtension(
+        name: 'debugDumpLayerTree',
+        callback: () {
+          debugDumpLayerTree();
+          return debugPrintDone;
+        },
+      );
       return true;
     }());
 
-    registerSignalServiceExtension(
-      name: 'debugDumpRenderTree',
-      callback: () { debugDumpRenderTree(); return debugPrintDone; }
-    );
+    const bool isReleaseMode = bool.fromEnvironment('dart.vm.product');
+    if (!isReleaseMode) {
+      // these service extensions work in debug or profile mode
+      registerSignalServiceExtension(
+        name: 'debugDumpRenderTree',
+        callback: () {
+          debugDumpRenderTree();
+          return debugPrintDone;
+        },
+      );
 
-    registerSignalServiceExtension(
-      name: 'debugDumpLayerTree',
-      callback: () { debugDumpLayerTree(); return debugPrintDone; }
-    );
+      registerSignalServiceExtension(
+        name: 'debugDumpSemanticsTreeInTraversalOrder',
+        callback: () {
+          debugDumpSemanticsTree(DebugSemanticsDumpOrder.traversalOrder);
+          return debugPrintDone;
+        },
+      );
 
-    registerSignalServiceExtension(
-      name: 'debugDumpSemanticsTreeInTraversalOrder',
-      callback: () { debugDumpSemanticsTree(DebugSemanticsDumpOrder.traversalOrder); return debugPrintDone; }
-    );
-
-    registerSignalServiceExtension(
-      name: 'debugDumpSemanticsTreeInInverseHitTestOrder',
-      callback: () { debugDumpSemanticsTree(DebugSemanticsDumpOrder.inverseHitTest); return debugPrintDone; }
-    );
+      registerSignalServiceExtension(
+        name: 'debugDumpSemanticsTreeInInverseHitTestOrder',
+        callback: () {
+          debugDumpSemanticsTree(DebugSemanticsDumpOrder.inverseHitTest);
+          return debugPrintDone;
+        },
+      );
+    }
   }
 
   /// Creates a [RenderView] object to be the root of the
diff --git a/packages/flutter/lib/src/scheduler/binding.dart b/packages/flutter/lib/src/scheduler/binding.dart
index ab2c384..df8ffc3 100644
--- a/packages/flutter/lib/src/scheduler/binding.dart
+++ b/packages/flutter/lib/src/scheduler/binding.dart
@@ -203,13 +203,17 @@
   @override
   void initServiceExtensions() {
     super.initServiceExtensions();
-    registerNumericServiceExtension(
-      name: 'timeDilation',
-      getter: () async => timeDilation,
-      setter: (double value) async {
-        timeDilation = value;
-      }
-    );
+
+    const bool isReleaseMode = bool.fromEnvironment('dart.vm.product');
+    if (!isReleaseMode) {
+      registerNumericServiceExtension(
+        name: 'timeDilation',
+        getter: () async => timeDilation,
+        setter: (double value) async {
+          timeDilation = value;
+        },
+      );
+    }
   }
 
   /// Whether the application is visible, and if so, whether it is currently
diff --git a/packages/flutter/lib/src/services/binding.dart b/packages/flutter/lib/src/services/binding.dart
index 1112969..ccaa815 100644
--- a/packages/flutter/lib/src/services/binding.dart
+++ b/packages/flutter/lib/src/services/binding.dart
@@ -86,17 +86,21 @@
   @override
   void initServiceExtensions() {
     super.initServiceExtensions();
-    registerStringServiceExtension(
-      // ext.flutter.evict value=foo.png will cause foo.png to be evicted from
-      // the rootBundle cache and cause the entire image cache to be cleared.
-      // This is used by hot reload mode to clear out the cache of resources
-      // that have changed.
-      name: 'evict',
-      getter: () async => '',
-      setter: (String value) async {
-        evict(value);
-      }
-    );
+
+    assert(() {
+      registerStringServiceExtension(
+        // ext.flutter.evict value=foo.png will cause foo.png to be evicted from
+        // the rootBundle cache and cause the entire image cache to be cleared.
+        // This is used by hot reload mode to clear out the cache of resources
+        // that have changed.
+        name: 'evict',
+        getter: () async => '',
+        setter: (String value) async {
+          evict(value);
+        },
+      );
+      return true;
+    }());
   }
 
   /// Called in response to the `ext.flutter.evict` service extension.
diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart
index cd1f752..e73934f 100644
--- a/packages/flutter/lib/src/widgets/binding.dart
+++ b/packages/flutter/lib/src/widgets/binding.dart
@@ -265,37 +265,41 @@
   void initServiceExtensions() {
     super.initServiceExtensions();
 
-    registerSignalServiceExtension(
-      name: 'debugDumpApp',
-      callback: () {
-        debugDumpApp();
-        return debugPrintDone;
-      }
-    );
+    const bool isReleaseMode = bool.fromEnvironment('dart.vm.product');
+    if (!isReleaseMode) {
+      registerSignalServiceExtension(
+        name: 'debugDumpApp',
+        callback: () {
+          debugDumpApp();
+          return debugPrintDone;
+        },
+      );
 
-    registerBoolServiceExtension(
-      name: 'showPerformanceOverlay',
-      getter: () => Future<bool>.value(WidgetsApp.showPerformanceOverlayOverride),
-      setter: (bool value) {
-        if (WidgetsApp.showPerformanceOverlayOverride == value)
-          return Future<void>.value();
-        WidgetsApp.showPerformanceOverlayOverride = value;
-        return _forceRebuild();
-      }
-    );
-
-    registerBoolServiceExtension(
-      name: 'debugAllowBanner',
-      getter: () => Future<bool>.value(WidgetsApp.debugAllowBannerOverride),
-      setter: (bool value) {
-        if (WidgetsApp.debugAllowBannerOverride == value)
-          return Future<void>.value();
-        WidgetsApp.debugAllowBannerOverride = value;
-        return _forceRebuild();
-      }
-    );
+      registerBoolServiceExtension(
+        name: 'showPerformanceOverlay',
+        getter: () =>
+        Future<bool>.value(WidgetsApp.showPerformanceOverlayOverride),
+        setter: (bool value) {
+          if (WidgetsApp.showPerformanceOverlayOverride == value)
+            return Future<void>.value();
+          WidgetsApp.showPerformanceOverlayOverride = value;
+          return _forceRebuild();
+        },
+      );
+    }
 
     assert(() {
+      registerBoolServiceExtension(
+        name: 'debugAllowBanner',
+        getter: () => Future<bool>.value(WidgetsApp.debugAllowBannerOverride),
+        setter: (bool value) {
+          if (WidgetsApp.debugAllowBannerOverride == value)
+            return Future<void>.value();
+          WidgetsApp.debugAllowBannerOverride = value;
+          return _forceRebuild();
+        },
+      );
+
       // Expose the ability to send Widget rebuilds as [Timeline] events.
       registerBoolServiceExtension(
         name: 'profileWidgetBuilds',
@@ -303,25 +307,26 @@
         setter: (bool value) async {
           if (debugProfileBuildsEnabled != value)
             debugProfileBuildsEnabled = value;
-        }
+        },
       );
+
+      // This service extension is deprecated and will be removed by 12/1/2018.
+      // Use ext.flutter.inspector.show instead.
+      registerBoolServiceExtension(
+          name: 'debugWidgetInspector',
+          getter: () async => WidgetsApp.debugShowWidgetInspectorOverride,
+          setter: (bool value) {
+            if (WidgetsApp.debugShowWidgetInspectorOverride == value)
+              return Future<void>.value();
+            WidgetsApp.debugShowWidgetInspectorOverride = value;
+            return _forceRebuild();
+          }
+      );
+
+      WidgetInspectorService.instance.initServiceExtensions(registerServiceExtension);
+
       return true;
     }());
-
-    // This service extension is deprecated and will be removed by 7/1/2018.
-    // Use ext.flutter.inspector.show instead.
-    registerBoolServiceExtension(
-        name: 'debugWidgetInspector',
-        getter: () async => WidgetsApp.debugShowWidgetInspectorOverride,
-        setter: (bool value) {
-          if (WidgetsApp.debugShowWidgetInspectorOverride == value)
-            return Future<void>.value();
-          WidgetsApp.debugShowWidgetInspectorOverride = value;
-          return _forceRebuild();
-        }
-    );
-
-    WidgetInspectorService.instance.initServiceExtensions(registerServiceExtension);
   }
 
   Future<void> _forceRebuild() {
diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart
index e65a08e..19dec90 100644
--- a/packages/flutter/lib/src/widgets/widget_inspector.dart
+++ b/packages/flutter/lib/src/widgets/widget_inspector.dart
@@ -917,13 +917,11 @@
 
   /// 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:
   ///
   ///  * <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(
       _RegisterServiceExtensionCallback registerServiceExtensionCallback) {
     _registerServiceExtensionCallback = registerServiceExtensionCallback;
@@ -1023,36 +1021,33 @@
       name: 'isWidgetCreationTracked',
       callback: isWidgetCreationTracked,
     );
-    assert(() {
-      registerServiceExtension(
-        name: 'screenshot',
-        callback: (Map<String, String> parameters) async {
-          assert(parameters.containsKey('id'));
-          assert(parameters.containsKey('width'));
-          assert(parameters.containsKey('height'));
+    registerServiceExtension(
+      name: 'screenshot',
+      callback: (Map<String, String> parameters) async {
+        assert(parameters.containsKey('id'));
+        assert(parameters.containsKey('width'));
+        assert(parameters.containsKey('height'));
 
-          final ui.Image image = await screenshot(
-            toObject(parameters['id']),
-            width: double.parse(parameters['width']),
-            height: double.parse(parameters['height']),
-            margin: parameters.containsKey('margin') ?
-                double.parse(parameters['margin']) : 0.0,
-            maxPixelRatio: parameters.containsKey('maxPixelRatio') ?
-                double.parse(parameters['maxPixelRatio']) : 1.0,
-            debugPaint: parameters['debugPaint'] == 'true',
-          );
-          if (image == null) {
-            return <String, Object>{'result': null};
-          }
-          final ByteData byteData = await image.toByteData(format:ui.ImageByteFormat.png);
+        final ui.Image image = await screenshot(
+          toObject(parameters['id']),
+          width: double.parse(parameters['width']),
+          height: double.parse(parameters['height']),
+          margin: parameters.containsKey('margin') ?
+              double.parse(parameters['margin']) : 0.0,
+          maxPixelRatio: parameters.containsKey('maxPixelRatio') ?
+              double.parse(parameters['maxPixelRatio']) : 1.0,
+          debugPaint: parameters['debugPaint'] == 'true',
+        );
+        if (image == null) {
+          return <String, Object>{'result': null};
+        }
+        final ByteData byteData = await image.toByteData(format:ui.ImageByteFormat.png);
 
-          return <String, Object>{
-            'result': base64.encoder.convert(Uint8List.view(byteData.buffer)),
-          };
-        },
-      );
-      return true;
-    }());
+        return <String, Object>{
+          'result': base64.encoder.convert(Uint8List.view(byteData.buffer)),
+        };
+      },
+    );
   }
 
   /// Clear all InspectorService object references.
diff --git a/packages/flutter/test/foundation/service_extensions_test.dart b/packages/flutter/test/foundation/service_extensions_test.dart
index a5be612..775163e 100644
--- a/packages/flutter/test/foundation/service_extensions_test.dart
+++ b/packages/flutter/test/foundation/service_extensions_test.dart
@@ -359,13 +359,6 @@
     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 {
     Map<String, dynamic> result;
 
@@ -491,7 +484,6 @@
 
   test('Service extensions - debugWidgetInspector', () async {
     Map<String, dynamic> result;
-
     expect(binding.frameScheduled, isFalse);
     expect(WidgetsApp.debugShowWidgetInspectorOverride, false);
     result = await binding.testExtension('debugWidgetInspector', <String, String>{});
@@ -541,7 +533,7 @@
 
     // If you add a service extension... TEST IT! :-)
     // ...then increment this number.
-    expect(binding.extensions.length, 39);
+    expect(binding.extensions.length, 38);
 
     expect(console, isEmpty);
     debugPrint = debugPrintThrottled;
diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart
index a66e4f8..bc1586f 100644
--- a/packages/flutter_tools/lib/src/vmservice.dart
+++ b/packages/flutter_tools/lib/src/vmservice.dart
@@ -1296,10 +1296,6 @@
     );
   }
 
-  Future<bool> flutterFrameworkPresent() async {
-    return await invokeFlutterExtensionRpcRaw('ext.flutter.frameworkPresent') != null;
-  }
-
   Future<Map<String, dynamic>> uiWindowScheduleFrame() async {
     return await invokeFlutterExtensionRpcRaw('ext.ui.window.scheduleFrame');
   }