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 {}