blob: 168b3a2d6a9042e0646f31b96ac76a759b8e03b3 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.helpers;
import android.support.test.uiautomator.UiDevice;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* This is a collector helper to use adb "dumpsys meminfo -a" command to get important memory
* metrics like PSS, Shared Dirty, Private Dirty, e.t.c. for the specified packages
*/
public class DumpsysMeminfoHelper implements ICollectorHelper<Long> {
private static final String TAG = DumpsysMeminfoHelper.class.getSimpleName();
private static final String SEPARATOR = "\\s+";
private static final String LINE_SEPARATOR = "\\n";
private static final String DUMPSYS_MEMINFO_CMD = "dumpsys meminfo -a %s";
private static final String PIDOF_CMD = "pidof %s";
private static final String METRIC_SOURCE = "dumpsys";
private static final String METRIC_UNIT = "kb";
// Prefixes of the lines in the output of "dumpsys meminfo -a" command that will be parsed
private static final String NATIVE_HEAP_PREFIX = "Native Heap";
private static final String DALVIK_HEAP_PREFIX = "Dalvik Heap";
private static final String TOTAL_PREFIX = "TOTAL";
// The metric names corresponding to the columns in the output of "dumpsys meminfo -a" command
private static final String PSS_TOTAL = "pss_total";
private static final String SHARED_DIRTY = "shared_dirty";
private static final String PRIVATE_DIRTY = "private_dirty";
private static final String HEAP_SIZE = "heap_size";
private static final String HEAP_ALLOC = "heap_alloc";
private static final String PSS = "pss";
// Mapping from prefixes of lines to metric category names
private static final Map<String, String> CATEGORIES =
Stream.of(
new String[][] {
{NATIVE_HEAP_PREFIX, "native"},
{DALVIK_HEAP_PREFIX, "dalvik"},
{TOTAL_PREFIX, "total"},
}).collect(Collectors.toMap(data -> data[0], data -> data[1]));
// Mapping from metric keys to its column index (exclude prefix) in dumpsys meminfo output.
// The index might change across different Android releases
private static final Map<String, Integer> METRIC_POSITIONS =
Stream.of(
new Object[][] {
{PSS_TOTAL, 0},
{SHARED_DIRTY, 2},
{PRIVATE_DIRTY, 3},
{HEAP_SIZE, 7},
{HEAP_ALLOC, 8},
})
.collect(Collectors.toMap(data -> (String) data[0], data -> (Integer) data[1]));
private String[] mProcessNames = {};
private UiDevice mUiDevice;
public void setUp(String... processNames) {
if (processNames == null) {
return;
}
mProcessNames = processNames;
}
@Override
public boolean startCollecting() {
mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
return true;
}
@Override
public Map<String, Long> getMetrics() {
Map<String, Long> metrics = new HashMap<>();
for (String processName : mProcessNames) {
String rawOutput = getRawDumpsysMeminfo(processName);
metrics.putAll(parseMetrics(processName, rawOutput));
}
return metrics;
}
@Override
public boolean stopCollecting() {
return true;
}
private String getRawDumpsysMeminfo(String processName) {
if (isEmpty(processName)) {
return "";
}
try {
String pidStr = mUiDevice.executeShellCommand(String.format(PIDOF_CMD, processName));
if (isEmpty(pidStr)) {
return "";
}
return mUiDevice.executeShellCommand(String.format(DUMPSYS_MEMINFO_CMD, pidStr));
} catch (IOException e) {
Log.e(TAG, String.format("Failed to execute command. %s", e));
return "";
}
}
private Map<String, Long> parseMetrics(String processName, String rawOutput) {
String[] lines = rawOutput.split(LINE_SEPARATOR);
Map<String, Long> metrics = new HashMap<>();
for (String line : lines) {
String[] tokens = line.trim().split(SEPARATOR);
if (tokens.length < 2) {
continue;
}
String firstToken = tokens[0];
String firstTwoTokens = String.join(" ", tokens[0], tokens[1]);
if (firstTwoTokens.equals(NATIVE_HEAP_PREFIX)
|| firstTwoTokens.equals(DALVIK_HEAP_PREFIX)) {
if (tokens.length < 11) {
continue;
}
int offset = 2;
for (Map.Entry<String, Integer> metric : METRIC_POSITIONS.entrySet()) {
metrics.put(
MetricUtility.constructKey(
METRIC_SOURCE,
CATEGORIES.get(firstTwoTokens),
metric.getKey(),
METRIC_UNIT,
processName),
Long.parseLong(tokens[offset + metric.getValue()]));
}
} else if (firstToken.equals(TOTAL_PREFIX)) {
int offset = 1;
metrics.put(
MetricUtility.constructKey(
METRIC_SOURCE,
CATEGORIES.get(firstToken),
PSS_TOTAL,
METRIC_UNIT,
processName),
Long.parseLong(tokens[offset + METRIC_POSITIONS.get(PSS_TOTAL)]));
}
}
return metrics;
}
private boolean isEmpty(String input) {
return input == null || input.isEmpty();
}
}