Unverified Commit 831163f0 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

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

parent 2d3d2209
...@@ -475,6 +475,8 @@ class AndroidDevice extends Device { ...@@ -475,6 +475,8 @@ class AndroidDevice extends Device {
return true; return true;
} }
AndroidApk _package;
@override @override
Future<LaunchResult> startApp( Future<LaunchResult> startApp(
AndroidApk package, { AndroidApk package, {
...@@ -609,6 +611,7 @@ class AndroidDevice extends Device { ...@@ -609,6 +611,7 @@ class AndroidDevice extends Device {
return LaunchResult.failed(); return LaunchResult.failed();
} }
_package = package;
if (!debuggingOptions.debuggingEnabled) { if (!debuggingOptions.debuggingEnabled) {
return LaunchResult.succeeded(); return LaunchResult.succeeded();
} }
...@@ -647,6 +650,21 @@ class AndroidDevice extends Device { ...@@ -647,6 +650,21 @@ class AndroidDevice extends Device {
(int exitCode) => exitCode == 0 || allowHeapCorruptionOnWindows(exitCode)); (int exitCode) => exitCode == 0 || allowHeapCorruptionOnWindows(exitCode));
} }
@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 @override
void clearLogs() { void clearLogs() {
processUtils.runSync(adbCommandForDevice(<String>['logcat', '-c'])); processUtils.runSync(adbCommandForDevice(<String>['logcat', '-c']));
...@@ -712,6 +730,104 @@ Map<String, String> parseAdbDeviceProperties(String str) { ...@@ -712,6 +730,104 @@ Map<String, String> parseAdbDeviceProperties(String str) {
return properties; 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. /// Return the list of connected ADB devices.
List<AndroidDevice> getAdbDevices() { List<AndroidDevice> getAdbDevices() {
final String adbPath = getAdbPath(androidSdk); final String adbPath = getAdbPath(androidSdk);
...@@ -736,6 +852,42 @@ List<AndroidDevice> getAdbDevices() { ...@@ -736,6 +852,42 @@ List<AndroidDevice> getAdbDevices() {
return devices; 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. /// Get diagnostics about issues with any connected devices.
Future<List<String>> getAdbDeviceDiagnostics() async { Future<List<String>> getAdbDeviceDiagnostics() async {
final String adbPath = getAdbPath(androidSdk); final String adbPath = getAdbPath(androidSdk);
......
...@@ -423,6 +423,14 @@ abstract class Device { ...@@ -423,6 +423,14 @@ abstract class Device {
/// Stop an app package on the current device. /// Stop an app package on the current device.
Future<bool> stopApp(covariant ApplicationPackage app); 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'); Future<void> takeScreenshot(File outputFile) => Future<void>.error('unimplemented');
@override @override
...@@ -487,6 +495,25 @@ abstract class Device { ...@@ -487,6 +495,25 @@ abstract class Device {
void dispose() {} 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 { class DebuggingOptions {
DebuggingOptions.enabled( DebuggingOptions.enabled(
this.buildInfo, { this.buildInfo, {
......
...@@ -177,6 +177,7 @@ class FlutterDevice { ...@@ -177,6 +177,7 @@ class FlutterDevice {
reloadSources: reloadSources, reloadSources: reloadSources,
restart: restart, restart: restart,
compileExpression: compileExpression, compileExpression: compileExpression,
device: device,
); );
} on Exception catch (exception) { } on Exception catch (exception) {
printTrace('Fail to connect to service protocol: $observatoryUri: $exception'); printTrace('Fail to connect to service protocol: $observatoryUri: $exception');
......
...@@ -19,6 +19,7 @@ import 'base/file_system.dart'; ...@@ -19,6 +19,7 @@ import 'base/file_system.dart';
import 'base/io.dart' as io; import 'base/io.dart' as io;
import 'base/utils.dart'; import 'base/utils.dart';
import 'convert.dart' show base64, utf8; import 'convert.dart' show base64, utf8;
import 'device.dart';
import 'globals.dart'; import 'globals.dart';
import 'version.dart'; import 'version.dart';
import 'vmservice_record_replay.dart'; import 'vmservice_record_replay.dart';
...@@ -101,7 +102,13 @@ Future<StreamChannel<String>> _defaultOpenChannel(Uri uri, {io.CompressionOption ...@@ -101,7 +102,13 @@ Future<StreamChannel<String>> _defaultOpenChannel(Uri uri, {io.CompressionOption
/// Override `VMServiceConnector` in [context] to return a different VMService /// Override `VMServiceConnector` in [context] to return a different VMService
/// from [VMService.connect] (used by tests). /// 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. /// A connection to the Dart VM Service.
// TODO(mklim): Test this, https://github.com/flutter/flutter/issues/23031 // TODO(mklim): Test this, https://github.com/flutter/flutter/issues/23031
...@@ -113,6 +120,7 @@ class VMService { ...@@ -113,6 +120,7 @@ class VMService {
ReloadSources reloadSources, ReloadSources reloadSources,
Restart restart, Restart restart,
CompileExpression compileExpression, CompileExpression compileExpression,
Device device,
) { ) {
_vm = VM._empty(this); _vm = VM._empty(this);
_peer.listen().catchError(_connectionError.completeError); _peer.listen().catchError(_connectionError.completeError);
...@@ -264,6 +272,16 @@ class VMService { ...@@ -264,6 +272,16 @@ class VMService {
'alias': 'Flutter Tools', '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 /// Enables recording of VMService JSON-rpc activity to the specified base
...@@ -310,9 +328,16 @@ class VMService { ...@@ -310,9 +328,16 @@ class VMService {
Restart restart, Restart restart,
CompileExpression compileExpression, CompileExpression compileExpression,
io.CompressionOptions compression = io.CompressionOptions.compressionDefault, io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
Device device,
}) async { }) async {
final VMServiceConnector connector = context.get<VMServiceConnector>() ?? VMService._connect; 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( static Future<VMService> _connect(
...@@ -321,11 +346,12 @@ class VMService { ...@@ -321,11 +346,12 @@ class VMService {
Restart restart, Restart restart,
CompileExpression compileExpression, CompileExpression compileExpression,
io.CompressionOptions compression = io.CompressionOptions.compressionDefault, io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
Device device,
}) async { }) async {
final Uri wsUri = httpUri.replace(scheme: 'ws', path: fs.path.join(httpUri.path, 'ws')); final Uri wsUri = httpUri.replace(scheme: 'ws', path: fs.path.join(httpUri.path, 'ws'));
final StreamChannel<String> channel = await _openChannel(wsUri, compression: compression); final StreamChannel<String> channel = await _openChannel(wsUri, compression: compression);
final rpc.Peer peer = rpc.Peer.withoutJson(jsonDocument.bind(channel), onUnhandledError: _unhandledError); 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 // This call is to ensure we are able to establish a connection instead of
// keeping on trucking and failing farther down the process. // keeping on trucking and failing farther down the process.
await service._sendRequest('getVersion', const <String, dynamic>{}); await service._sendRequest('getVersion', const <String, dynamic>{});
......
...@@ -748,6 +748,7 @@ VMServiceConnector getFakeVmServiceFactory({ ...@@ -748,6 +748,7 @@ VMServiceConnector getFakeVmServiceFactory({
Restart restart, Restart restart,
CompileExpression compileExpression, CompileExpression compileExpression,
CompressionOptions compression, CompressionOptions compression,
Device device,
}) async { }) async {
final VMService vmService = VMServiceMock(); final VMService vmService = VMServiceMock();
final VM vm = VMMock(); final VM vm = VMMock();
......
...@@ -679,6 +679,89 @@ flutter: ...@@ -679,6 +679,89 @@ flutter:
ProcessManager: () => mockProcessManager, 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 {} class MockProcessManager extends Mock implements ProcessManager {}
......
...@@ -104,9 +104,7 @@ void main() { ...@@ -104,9 +104,7 @@ void main() {
.thenAnswer((Invocation invocation) async { .thenAnswer((Invocation invocation) async {
return testUri; return testUri;
}); });
when(mockFlutterDevice.vmServices).thenReturn(<VMService>[ when(mockFlutterDevice.vmServices).thenReturn(<VMService>[mockVMService]);
mockVMService,
]);
when(mockFlutterDevice.refreshViews()).thenAnswer((Invocation invocation) async { }); when(mockFlutterDevice.refreshViews()).thenAnswer((Invocation invocation) async { });
when(mockFlutterDevice.reloadSources(any, pause: anyNamed('pause'))).thenReturn(<Future<Map<String, dynamic>>>[ when(mockFlutterDevice.reloadSources(any, pause: anyNamed('pause'))).thenReturn(<Future<Map<String, dynamic>>>[
Future<Map<String, dynamic>>.value(<String, dynamic>{ Future<Map<String, dynamic>>.value(<String, dynamic>{
...@@ -635,7 +633,13 @@ void main() { ...@@ -635,7 +633,13 @@ void main() {
await flutterDevice.connect(); await flutterDevice.connect();
verify(mockLogReader.connectedVMServices = <VMService>[ mockVMService ]); verify(mockLogReader.connectedVMServices = <VMService>[ mockVMService ]);
}, overrides: <Type, Generator>{ }, 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,
})); }));
} }
......
...@@ -7,8 +7,10 @@ import 'dart:io'; ...@@ -7,8 +7,10 @@ import 'dart:io';
import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.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:flutter_tools/src/vmservice.dart';
import 'package:json_rpc_2/json_rpc_2.dart' as rpc; import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:mockito/mockito.dart';
import 'package:quiver/testing/async.dart'; import 'package:quiver/testing/async.dart';
import '../src/common.dart'; import '../src/common.dart';
...@@ -196,7 +198,7 @@ void main() { ...@@ -196,7 +198,7 @@ void main() {
bool done = false; bool done = false;
final MockPeer mockPeer = MockPeer(); final MockPeer mockPeer = MockPeer();
expect(mockPeer.returnedFromSendRequest, 0); 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')); expect(mockPeer.sentNotifications, contains('registerService'));
final List<String> registeredServices = final List<String> registeredServices =
mockPeer.sentNotifications['registerService'] mockPeer.sentNotifications['registerService']
...@@ -269,7 +271,7 @@ void main() { ...@@ -269,7 +271,7 @@ void main() {
FakeAsync().run((FakeAsync time) { FakeAsync().run((FakeAsync time) {
final MockPeer mockPeer = MockPeer(); final MockPeer mockPeer = MockPeer();
Future<void> reloadSources(String isolateId, { bool pause, bool force}) async {} 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')); expect(mockPeer.registeredMethods, contains('reloadMethod'));
}); });
...@@ -277,5 +279,21 @@ void main() { ...@@ -277,5 +279,21 @@ void main() {
Logger: () => StdoutLogger(), Logger: () => StdoutLogger(),
Stdio: () => mockStdio, 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 {}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment