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>{});
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart
index 429dc11..eeb31dc 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart
@@ -748,6 +748,7 @@
     Restart restart,
     CompileExpression compileExpression,
     CompressionOptions compression,
+    Device device,
   }) async {
     final VMService vmService = VMServiceMock();
     final VM vm = VMMock();
@@ -816,4 +817,4 @@
 class MockProcess extends Mock implements Process {}
 class MockHttpClientRequest extends Mock implements HttpClientRequest {}
 class MockHttpClientResponse extends Mock implements HttpClientResponse {}
-class MockHttpHeaders extends Mock implements HttpHeaders {}
\ No newline at end of file
+class MockHttpHeaders extends Mock implements HttpHeaders {}
diff --git a/packages/flutter_tools/test/general.shard/android/android_device_test.dart b/packages/flutter_tools/test/general.shard/android/android_device_test.dart
index a765a5b..a260457 100644
--- a/packages/flutter_tools/test/general.shard/android/android_device_test.dart
+++ b/packages/flutter_tools/test/general.shard/android/android_device_test.dart
@@ -679,6 +679,89 @@
       ProcessManager: () => mockProcessManager,
     });
   });
+
+  test('Can parse adb shell dumpsys info', () {
+    const String exampleOutput = r'''
+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
+''';
+
+    final AndroidMemoryInfo result = parseMeminfoDump(exampleOutput);
+
+    // Parses correctly
+    expect(result.javaHeap, 4296);
+    expect(result.nativeHeap, 8620);
+    expect(result.code, 11288);
+    expect(result.stack, 496);
+    expect(result.graphics, 2584);
+    expect(result.privateOther, 65228);
+    expect(result.system, 2573);
+
+    // toJson works correctly
+    final Map<String, Object> json = result.toJson();
+
+    expect(json, containsPair('Java Heap', 4296));
+    expect(json, containsPair('Native Heap', 8620));
+    expect(json, containsPair('Code', 11288));
+    expect(json, containsPair('Stack', 496));
+    expect(json, containsPair('Graphics', 2584));
+    expect(json, containsPair('Private Other', 65228));
+    expect(json, containsPair('System', 2573));
+
+    // computed from summation of other fields.
+    expect(json, containsPair('Total', 95085));
+
+    // contains identifier for platform in memory info.
+    expect(json, containsPair('platform', 'Android'));
+  });
 }
 
 class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
index 85a14da..4af49d6 100644
--- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart
+++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
@@ -104,9 +104,7 @@
       .thenAnswer((Invocation invocation) async {
         return testUri;
       });
-    when(mockFlutterDevice.vmServices).thenReturn(<VMService>[
-      mockVMService,
-    ]);
+    when(mockFlutterDevice.vmServices).thenReturn(<VMService>[mockVMService]);
     when(mockFlutterDevice.refreshViews()).thenAnswer((Invocation invocation) async { });
     when(mockFlutterDevice.reloadSources(any, pause: anyNamed('pause'))).thenReturn(<Future<Map<String, dynamic>>>[
       Future<Map<String, dynamic>>.value(<String, dynamic>{
@@ -635,7 +633,13 @@
     await flutterDevice.connect();
     verify(mockLogReader.connectedVMServices = <VMService>[ mockVMService ]);
   }, overrides: <Type, Generator>{
-    VMServiceConnector: () => (Uri httpUri, { ReloadSources reloadSources, Restart restart, CompileExpression compileExpression, io.CompressionOptions compression }) async => mockVMService,
+    VMServiceConnector: () => (Uri httpUri, {
+      ReloadSources reloadSources,
+      Restart restart,
+      CompileExpression compileExpression,
+      io.CompressionOptions compression,
+      Device device,
+    }) async => mockVMService,
   }));
 }
 
diff --git a/packages/flutter_tools/test/general.shard/vmservice_test.dart b/packages/flutter_tools/test/general.shard/vmservice_test.dart
index 842802e..8b085a4 100644
--- a/packages/flutter_tools/test/general.shard/vmservice_test.dart
+++ b/packages/flutter_tools/test/general.shard/vmservice_test.dart
@@ -7,8 +7,10 @@
 
 import 'package:flutter_tools/src/base/io.dart';
 import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/device.dart';
 import 'package:flutter_tools/src/vmservice.dart';
 import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
+import 'package:mockito/mockito.dart';
 import 'package:quiver/testing/async.dart';
 
 import '../src/common.dart';
@@ -196,7 +198,7 @@
         bool done = false;
         final MockPeer mockPeer = MockPeer();
         expect(mockPeer.returnedFromSendRequest, 0);
-        final VMService vmService = VMService(mockPeer, null, null, null, null, null);
+        final VMService vmService = VMService(mockPeer, null, null, null, null, null, null);
         expect(mockPeer.sentNotifications, contains('registerService'));
         final List<String> registeredServices =
           mockPeer.sentNotifications['registerService']
@@ -265,11 +267,11 @@
       Stdio: () => mockStdio,
     });
 
-     testUsingContext('registers hot UI method', () {
+    testUsingContext('registers hot UI method', () {
       FakeAsync().run((FakeAsync time) {
         final MockPeer mockPeer = MockPeer();
         Future<void> reloadSources(String isolateId, { bool pause, bool force}) async {}
-        VMService(mockPeer, null, null, reloadSources, null, null);
+        VMService(mockPeer, null, null, reloadSources, null, null, null);
 
         expect(mockPeer.registeredMethods, contains('reloadMethod'));
       });
@@ -277,5 +279,21 @@
       Logger: () => StdoutLogger(),
       Stdio: () => mockStdio,
     });
+
+    testUsingContext('registers flutterMemoryInfo service', () {
+      FakeAsync().run((FakeAsync time) {
+        final MockDevice mockDevice = MockDevice();
+        final MockPeer mockPeer = MockPeer();
+        Future<void> reloadSources(String isolateId, { bool pause, bool force}) async {}
+        VMService(mockPeer, null, null, reloadSources, null, null, mockDevice);
+
+        expect(mockPeer.registeredMethods, contains('flutterMemoryInfo'));
+      });
+    }, overrides: <Type, Generator>{
+      Logger: () => StdoutLogger(),
+      Stdio: () => mockStdio,
+    });
   });
 }
+
+class MockDevice extends Mock implements Device {}