Support --start-paused and retain profile history after disconnect/crash. (#2080)

* Support --start-paused, support review profile history after disconnect/crash.
diff --git a/packages/devtools_app/lib/src/connected_app.dart b/packages/devtools_app/lib/src/connected_app.dart
index 8183406..7a8f52e 100644
--- a/packages/devtools_app/lib/src/connected_app.dart
+++ b/packages/devtools_app/lib/src/connected_app.dart
@@ -4,8 +4,8 @@
 
 import 'dart:async';
 
+import 'eval_on_dart_library.dart';
 import 'globals.dart';
-import 'service_extensions.dart' as extensions;
 
 const flutterLibraryUri = 'package:flutter/src/widgets/binding.dart';
 const dartHtmlLibraryUri = 'dart:html';
@@ -27,8 +27,10 @@
 
   bool _isFlutterApp;
 
-  Future<bool> get isProfileBuild async =>
-      _isProfileBuild ??= await _connectedToProfileBuild();
+  Future<bool> get isProfileBuild async {
+    _isProfileBuild ??= await _connectedToProfileBuild();
+    return _isProfileBuild;
+  }
 
   bool get isProfileBuildNow {
     assert(_isProfileBuild != null);
@@ -60,6 +62,16 @@
   bool get isDartCliAppNow => isRunningOnDartVM && !isFlutterAppNow;
 
   Future<bool> _connectedToProfileBuild() async {
+    // If eval works we're not a profile build.
+    final io = EvalOnDartLibrary(['dart:io'], serviceManager.service);
+    final value = await io.eval('Platform.isAndroid', isAlive: null);
+    return !(value?.kind == 'Bool');
+
+    // TODO(terry): Disabled below code, it will hang if flutter run --start-paused
+    //              see issue https://github.com/flutter/devtools/issues/2082.
+    //              Currently, if eval (see above) doesn't work then we're
+    //              running in Profile mode.
+    /*
     assert(serviceManager.isServiceAvailable);
     // Only flutter apps have profile and non-profile builds. If this changes in
     // the future (flutter web), we can modify this check.
@@ -71,6 +83,7 @@
     final hasDebugExtension = serviceManager.serviceExtensionManager
         .isServiceExtensionAvailable(extensions.debugAllowBanner.extension);
     return !hasDebugExtension;
+    */
   }
 
   Future<void> initializeValues() async {
diff --git a/packages/devtools_app/lib/src/debugger/debugger_controller.dart b/packages/devtools_app/lib/src/debugger/debugger_controller.dart
index 8f46bb5..5094898 100644
--- a/packages/devtools_app/lib/src/debugger/debugger_controller.dart
+++ b/packages/devtools_app/lib/src/debugger/debugger_controller.dart
@@ -292,18 +292,34 @@
     _exceptionPauseMode.value = mode;
   }
 
+  /// Flutter starting with '--start-paused'. All subsequent isolates, after
+  /// the first isolate, are in a pauseStart state too.  If _resuming, then
+  /// resume any future isolate created with pause start.
+  Future<Success> _resumeIsolatePauseStart(Event event) {
+    assert(event.kind == EventKind.kPauseStart);
+    assert(_resuming.value);
+
+    final id = event.isolate.id;
+    _log.log('resume() $id');
+    return _service.resume(id);
+  }
+
   void _handleDebugEvent(Event event) {
     _log.log('event: ${event.kind}');
 
+    // We're resuming and another isolate has started in a paused state,
+    // resume any pauseState isolates.
+    if (_resuming.value &&
+        event.isolate.id != isolateRef?.id &&
+        event.kind == EventKind.kPauseStart) {
+      _resumeIsolatePauseStart(event);
+    }
+
     if (event.isolate.id != isolateRef?.id) return;
 
     _hasFrames.value = event.topFrame != null;
     _lastEvent = event;
 
-    // Any event we receive here indicates that any resume/step request has been
-    // processed.
-    _resuming.value = false;
-
     switch (event.kind) {
       case EventKind.kResume:
         _pause(false);
@@ -314,6 +330,9 @@
       case EventKind.kPauseInterrupted:
       case EventKind.kPauseException:
       case EventKind.kPausePostRequest:
+        // Any event we receive here indicates that any resume/step request has been
+        // processed.
+        _resuming.value = false;
         _pause(true, pauseEvent: event);
         break;
       // TODO(djshuckerow): switch the _breakpoints notifier to a 'ListNotifier'
diff --git a/packages/devtools_app/lib/src/initializer.dart b/packages/devtools_app/lib/src/initializer.dart
index 2202596..6063282 100644
--- a/packages/devtools_app/lib/src/initializer.dart
+++ b/packages/devtools_app/lib/src/initializer.dart
@@ -144,6 +144,12 @@
                   style: theme.textTheme.bodyText2,
                 ),
               const Spacer(),
+              RaisedButton(
+                onPressed: () {
+                  overlay.remove();
+                },
+                child: const Text('Review History'),
+              ),
             ],
           ),
         ),
diff --git a/packages/devtools_app/lib/src/inspector/inspector_service.dart b/packages/devtools_app/lib/src/inspector/inspector_service.dart
index 024cd2d..49828d4 100644
--- a/packages/devtools_app/lib/src/inspector/inspector_service.dart
+++ b/packages/devtools_app/lib/src/inspector/inspector_service.dart
@@ -380,8 +380,14 @@
 
   Future<Object> invokeServiceMethodDaemonNoGroup(
       String methodName, Map<String, Object> args) async {
+    final callMethodName = 'ext.flutter.inspector.$methodName';
+    if (!serviceManager.serviceExtensionManager
+        .isServiceExtensionAvailable(callMethodName)) {
+      return {'result': null};
+    }
+
     final r = await vmService.callServiceExtension(
-      'ext.flutter.inspector.$methodName',
+      callMethodName,
       isolateId: inspectorLibrary.isolateId,
       args: args,
     );
@@ -555,7 +561,13 @@
     String methodName,
     Map<String, Object> params,
   ) {
-    return _callServiceExtension('ext.flutter.inspector.$methodName', params);
+    final callMethodName = 'ext.flutter.inspector.$methodName';
+    if (!serviceManager.serviceExtensionManager
+        .isServiceExtensionAvailable(callMethodName)) {
+      return null;
+    }
+
+    return _callServiceExtension(callMethodName, params);
   }
 
   Future<Object> invokeServiceMethodDaemonInspectorRef(
diff --git a/packages/devtools_app/lib/src/memory/memory_controller.dart b/packages/devtools_app/lib/src/memory/memory_controller.dart
index 8862453..5687d7f 100644
--- a/packages/devtools_app/lib/src/memory/memory_controller.dart
+++ b/packages/devtools_app/lib/src/memory/memory_controller.dart
@@ -510,8 +510,8 @@
   }
 
   Future<HeapSnapshotGraph> snapshotMemory() async {
-    return await serviceManager.service
-        .getHeapSnapshotGraph(serviceManager.isolateManager.selectedIsolate);
+    return await serviceManager?.service
+        ?.getHeapSnapshotGraph(serviceManager?.isolateManager?.selectedIsolate);
   }
 
   Future<List<ClassHeapDetailStats>> resetAllocationProfile() =>
@@ -538,7 +538,7 @@
   }
 
   bool get isConnectedDeviceAndroid {
-    return serviceManager.vm.operatingSystem == 'android';
+    return serviceManager?.vm?.operatingSystem == 'android';
   }
 
   Future<List<InstanceSummary>> getInstances(
diff --git a/packages/devtools_app/lib/src/memory/memory_heap_tree_view.dart b/packages/devtools_app/lib/src/memory/memory_heap_tree_view.dart
index 299a914..70cf4f7 100644
--- a/packages/devtools_app/lib/src/memory/memory_heap_tree_view.dart
+++ b/packages/devtools_app/lib/src/memory/memory_heap_tree_view.dart
@@ -13,6 +13,7 @@
 import '../auto_dispose_mixin.dart';
 import '../common_widgets.dart';
 import '../config_specific/logger/logger.dart' as logger;
+import '../globals.dart';
 import '../table.dart';
 import '../table_data.dart';
 import '../theme.dart';
@@ -570,6 +571,9 @@
   }
 
   void _snapshot() async {
+    // VmService not available (disconnected/crashed).
+    if (serviceManager.service == null) return;
+
     setState(() {
       snapshotState = SnapshotStatus.streaming;
     });
@@ -577,6 +581,10 @@
     final snapshotTimestamp = DateTime.now();
 
     final graph = await controller.snapshotMemory();
+
+    // No snapshot collected, disconnected/crash application.
+    if (graph == null) return;
+
     final snapshotCollectionTime = DateTime.now();
 
     setState(() {
diff --git a/packages/devtools_app/lib/src/memory/memory_protocol.dart b/packages/devtools_app/lib/src/memory/memory_protocol.dart
index 4afe2b6..8d28855 100644
--- a/packages/devtools_app/lib/src/memory/memory_protocol.dart
+++ b/packages/devtools_app/lib/src/memory/memory_protocol.dart
@@ -55,6 +55,10 @@
   }
 
   void _updateLiveDataPolling([bool paused]) {
+    if (service == null) {
+      // A service of null implies we're disconnected - signal paused.
+      memoryController.pauseLiveFeed();
+    }
     paused ??= memoryController.paused.value;
 
     if (paused) {
@@ -62,7 +66,7 @@
       _gcStreamListener?.cancel();
     } else {
       _pollingTimer = Timer(Duration.zero, _pollMemory);
-      _gcStreamListener = service.onGCEvent.listen(_handleGCEvent);
+      _gcStreamListener = service?.onGCEvent?.listen(_handleGCEvent);
     }
   }
 
diff --git a/packages/devtools_app/lib/src/memory/memory_screen.dart b/packages/devtools_app/lib/src/memory/memory_screen.dart
index 4b79b0c..acf4a7a 100644
--- a/packages/devtools_app/lib/src/memory/memory_screen.dart
+++ b/packages/devtools_app/lib/src/memory/memory_screen.dart
@@ -60,7 +60,8 @@
 
   @override
   Widget build(BuildContext context) {
-    return !serviceManager.connectedApp.isDartWebAppNow
+    final connected = serviceManager?.connectedApp;
+    return connected != null && !connected.isDartWebAppNow
         ? const MemoryBody()
         : const DisabledForWebAppMessage();
   }
diff --git a/packages/devtools_app/lib/src/service_manager.dart b/packages/devtools_app/lib/src/service_manager.dart
index 952b7a4..26b1abb 100644
--- a/packages/devtools_app/lib/src/service_manager.dart
+++ b/packages/devtools_app/lib/src/service_manager.dart
@@ -643,7 +643,7 @@
               _service,
             );
             final InstanceRef value = await flutterLibrary.eval(
-              'WidgetsBinding.instance.debugDidSendFirstFrameEvent',
+              'WidgetsBinding?.instance?.debugDidSendFirstFrameEvent ?? false',
               isAlive: null,
             );