Register memory info command on vmservice for Android devices (#45568)

diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart
index 22dbcd9..3278850 100644
--- a/packages/flutter_tools/lib/src/android/android_device.dart
+++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -475,6 +475,8 @@
     return true;
   }
 
+  AndroidApk _package;
+
   @override
   Future<LaunchResult> startApp(
     AndroidApk package, {
@@ -609,6 +611,7 @@
       return LaunchResult.failed();
     }
 
+    _package = package;
     if (!debuggingOptions.debuggingEnabled) {
       return LaunchResult.succeeded();
     }
@@ -648,6 +651,21 @@
   }
 
   @override
+  Future<MemoryInfo> queryMemoryInfo() async {
+    final RunResult runResult = await processUtils.run(adbCommandForDevice(<String>[
+      'shell',
+      'dumpsys',
+      'meminfo',
+      _package.launchActivity,
+      '-d',
+    ]));
+    if (runResult.exitCode != 0) {
+      return const MemoryInfo.empty();
+    }
+    return parseMeminfoDump(runResult.stdout);
+  }
+
+  @override
   void clearLogs() {
     processUtils.runSync(adbCommandForDevice(<String>['logcat', '-c']));
   }
@@ -712,6 +730,104 @@
   return properties;
 }
 
+/// Process the dumpsys info formatted in a table-like structure.
+///
+/// Currently this only pulls information from the  "App Summary" subsection.
+///
+/// Example output:
+///
+/// Applications Memory Usage (in Kilobytes):
+/// Uptime: 441088659 Realtime: 521464097
+///
+/// ** MEMINFO in pid 16141 [io.flutter.demo.gallery] **
+///                    Pss  Private  Private  SwapPss     Heap     Heap     Heap
+///                  Total    Dirty    Clean    Dirty     Size    Alloc     Free
+///                 ------   ------   ------   ------   ------   ------   ------
+///   Native Heap     8648     8620        0       16    20480    12403     8076
+///   Dalvik Heap      547      424       40       18     2628     1092     1536
+///  Dalvik Other      464      464        0        0
+///         Stack      496      496        0        0
+///        Ashmem        2        0        0        0
+///       Gfx dev      212      204        0        0
+///     Other dev       48        0       48        0
+///      .so mmap    10770      708     9372       25
+///     .apk mmap      240        0        0        0
+///     .ttf mmap       35        0       32        0
+///     .dex mmap     2205        4     1172        0
+///     .oat mmap       64        0        0        0
+///     .art mmap     4228     3848       24        2
+///    Other mmap    20713        4    20704        0
+///     GL mtrack     2380     2380        0        0
+///       Unknown    43971    43968        0        1
+///         TOTAL    95085    61120    31392       62    23108    13495     9612
+///
+///  App Summary
+///                        Pss(KB)
+///                         ------
+///            Java Heap:     4296
+///          Native Heap:     8620
+///                 Code:    11288
+///                Stack:      496
+///             Graphics:     2584
+///        Private Other:    65228
+///               System:     2573
+///
+///                TOTAL:    95085       TOTAL SWAP PSS:       62
+///
+///  Objects
+///                Views:        9         ViewRootImpl:        1
+///          AppContexts:        3           Activities:        1
+///              Assets:        4        AssetManagers:        3
+///        Local Binders:       10        Proxy Binders:       18
+///        Parcel memory:        6         Parcel count:       24
+///     Death Recipients:        0      OpenSSL Sockets:        0
+///             WebViews:        0
+///
+///  SQL
+///          MEMORY_USED:        0
+///   PAGECACHE_OVERFLOW:        0          MALLOC_SIZE:        0
+/// ...
+///
+/// For more information, see https://developer.android.com/studio/command-line/dumpsys.
+@visibleForTesting
+AndroidMemoryInfo parseMeminfoDump(String input) {
+  final AndroidMemoryInfo androidMemoryInfo = AndroidMemoryInfo();
+  input
+    .split('\n')
+    .skipWhile((String line) => !line.contains('App Summary'))
+    .takeWhile((String line) => !line.contains('TOTAL'))
+    .where((String line) => line.contains(':'))
+    .forEach((String line) {
+      final List<String> sections = line.trim().split(':');
+      final String key = sections.first.trim();
+      final int value = int.tryParse(sections.last.trim()) ?? 0;
+      switch (key) {
+        case AndroidMemoryInfo._kJavaHeapKey:
+          androidMemoryInfo.javaHeap = value;
+          break;
+        case AndroidMemoryInfo._kNativeHeapKey:
+          androidMemoryInfo.nativeHeap = value;
+          break;
+        case AndroidMemoryInfo._kCodeKey:
+          androidMemoryInfo.code = value;
+          break;
+        case AndroidMemoryInfo._kStackKey:
+          androidMemoryInfo.stack = value;
+          break;
+        case AndroidMemoryInfo._kGraphicsKey:
+          androidMemoryInfo.graphics = value;
+          break;
+        case AndroidMemoryInfo._kPrivateOtherKey:
+          androidMemoryInfo.privateOther = value;
+          break;
+        case AndroidMemoryInfo._kSystemKey:
+          androidMemoryInfo.system = value;
+          break;
+      }
+  });
+  return androidMemoryInfo;
+}
+
 /// Return the list of connected ADB devices.
 List<AndroidDevice> getAdbDevices() {
   final String adbPath = getAdbPath(androidSdk);
@@ -736,6 +852,42 @@
   return devices;
 }
 
+/// Android specific implementation of memory info.
+class AndroidMemoryInfo extends MemoryInfo {
+  static const String _kJavaHeapKey = 'Java Heap';
+  static const String _kNativeHeapKey = 'Native Heap';
+  static const String _kCodeKey = 'Code';
+  static const String _kStackKey = 'Stack';
+  static const String _kGraphicsKey = 'Graphics';
+  static const String _kPrivateOtherKey = 'Private Other';
+  static const String _kSystemKey = 'System';
+  static const String _kTotalKey = 'Total';
+
+  // Each measurement has KB as a unit.
+  int javaHeap = 0;
+  int nativeHeap = 0;
+  int code = 0;
+  int stack = 0;
+  int graphics = 0;
+  int privateOther = 0;
+  int system = 0;
+
+  @override
+  Map<String, Object> toJson() {
+    return <String, Object>{
+      'platform': 'Android',
+      _kJavaHeapKey: javaHeap,
+      _kNativeHeapKey: nativeHeap,
+      _kCodeKey: code,
+      _kStackKey: stack,
+      _kGraphicsKey: graphics,
+      _kPrivateOtherKey: privateOther,
+      _kSystemKey: system,
+      _kTotalKey: javaHeap + nativeHeap + code + stack + graphics + privateOther + system,
+    };
+  }
+}
+
 /// Get diagnostics about issues with any connected devices.
 Future<List<String>> getAdbDeviceDiagnostics() async {
   final String adbPath = getAdbPath(androidSdk);
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index 46b65a5..3a539b8 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -423,6 +423,14 @@
   /// Stop an app package on the current device.
   Future<bool> stopApp(covariant ApplicationPackage app);
 
+  /// Query the current application memory usage..
+  ///
+  /// If the device does not support this callback, an empty map
+  /// is returned.
+  Future<MemoryInfo> queryMemoryInfo() {
+    return Future<MemoryInfo>.value(const MemoryInfo.empty());
+  }
+
   Future<void> takeScreenshot(File outputFile) => Future<void>.error('unimplemented');
 
   @override
@@ -487,6 +495,25 @@
   void dispose() {}
 }
 
+/// Information about an application's memory usage.
+abstract class MemoryInfo {
+  /// Const constructor to allow subclasses to be const.
+  const MemoryInfo();
+
+  /// Create a [MemoryInfo] object with no information.
+  const factory MemoryInfo.empty() = _NoMemoryInfo;
+
+  /// Convert the object to a JSON representation suitable for serialization.
+  Map<String, Object> toJson();
+}
+
+class _NoMemoryInfo implements MemoryInfo {
+  const _NoMemoryInfo();
+
+  @override
+  Map<String, Object> toJson() => <String, Object>{};
+}
+
 class DebuggingOptions {
   DebuggingOptions.enabled(
     this.buildInfo, {
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index f8d6faf..0457172 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -177,6 +177,7 @@
           reloadSources: reloadSources,
           restart: restart,
           compileExpression: compileExpression,
+          device: device,
         );
       } on Exception catch (exception) {
         printTrace('Fail to connect to service protocol: $observatoryUri: $exception');
diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart
index 58a3931..657b719 100644
--- a/packages/flutter_tools/lib/src/vmservice.dart
+++ b/packages/flutter_tools/lib/src/vmservice.dart
@@ -19,6 +19,7 @@
 import 'base/io.dart' as io;
 import 'base/utils.dart';
 import 'convert.dart' show base64, utf8;
+import 'device.dart';
 import 'globals.dart';
 import 'version.dart';
 import 'vmservice_record_replay.dart';
@@ -101,7 +102,13 @@
 
 /// Override `VMServiceConnector` in [context] to return a different VMService
 /// from [VMService.connect] (used by tests).
-typedef VMServiceConnector = Future<VMService> Function(Uri httpUri, { ReloadSources reloadSources, Restart restart, CompileExpression compileExpression, io.CompressionOptions compression });
+typedef VMServiceConnector = Future<VMService> Function(Uri httpUri, {
+  ReloadSources reloadSources,
+  Restart restart,
+  CompileExpression compileExpression,
+  io.CompressionOptions compression,
+  Device device,
+});
 
 /// A connection to the Dart VM Service.
 // TODO(mklim): Test this, https://github.com/flutter/flutter/issues/23031
@@ -113,6 +120,7 @@
     ReloadSources reloadSources,
     Restart restart,
     CompileExpression compileExpression,
+    Device device,
   ) {
     _vm = VM._empty(this);
     _peer.listen().catchError(_connectionError.completeError);
@@ -264,6 +272,16 @@
         'alias': 'Flutter Tools',
       });
     }
+    if (device != null) {
+      _peer.registerMethod('flutterMemoryInfo', (rpc.Parameters params) async {
+        final MemoryInfo result = await device.queryMemoryInfo();
+        return result.toJson();
+      });
+      _peer.sendNotification('registerService', <String, String>{
+        'service': 'flutterMemoryInfo',
+        'alias': 'Flutter Tools',
+      });
+    }
   }
 
   /// Enables recording of VMService JSON-rpc activity to the specified base
@@ -310,9 +328,16 @@
       Restart restart,
       CompileExpression compileExpression,
       io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
+      Device device,
     }) async {
     final VMServiceConnector connector = context.get<VMServiceConnector>() ?? VMService._connect;
-    return connector(httpUri, reloadSources: reloadSources, restart: restart, compileExpression: compileExpression, compression: compression);
+    return connector(httpUri,
+      reloadSources: reloadSources,
+      restart: restart,
+      compileExpression: compileExpression,
+      compression: compression,
+      device: device,
+    );
   }
 
   static Future<VMService> _connect(
@@ -321,11 +346,12 @@
     Restart restart,
     CompileExpression compileExpression,
     io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
+    Device device,
   }) async {
     final Uri wsUri = httpUri.replace(scheme: 'ws', path: fs.path.join(httpUri.path, 'ws'));
     final StreamChannel<String> channel = await _openChannel(wsUri, compression: compression);
     final rpc.Peer peer = rpc.Peer.withoutJson(jsonDocument.bind(channel), onUnhandledError: _unhandledError);
-    final VMService service = VMService(peer, httpUri, wsUri, reloadSources, restart, compileExpression);
+    final VMService service = VMService(peer, httpUri, wsUri, reloadSources, restart, compileExpression, device);
     // This call is to ensure we are able to establish a connection instead of
     // keeping on trucking and failing farther down the process.
     await service._sendRequest('getVersion', const <String, dynamic>{});