blob: e3bd233128ecc0344194759f7d738df30682b004 [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 static com.android.helpers.MetricUtility.constructKey;
import android.support.test.uiautomator.UiDevice;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.InputMismatchException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Helper to collect memory information for a list of processes from showmap.
*/
public class ShowmapSnapshotHelper implements ICollectorHelper<String> {
private static final String TAG = ShowmapSnapshotHelper.class.getSimpleName();
private static final String DROP_CACHES_CMD = "echo %d > /proc/sys/vm/drop_caches";
private static final String PIDOF_CMD = "pidof %s";
public static final String ALL_PROCESSES_CMD = "ps -A";
private static final String SHOWMAP_CMD = "showmap -v %d";
private static final String CHILD_PROCESSES_CMD = "ps -A --ppid %d";
public static final String OUTPUT_METRIC_PATTERN = "showmap_%s_bytes";
public static final String OUTPUT_FILE_PATH_KEY = "showmap_output_file";
public static final String PROCESS_COUNT = "process_count";
public static final String CHILD_PROCESS_COUNT_PREFIX = "child_processes_count";
public static final String OUTPUT_CHILD_PROCESS_COUNT_KEY = CHILD_PROCESS_COUNT_PREFIX + "_%s";
public static final String PROCESS_WITH_CHILD_PROCESS_COUNT =
"process_with_child_process_count";
private String[] mProcessNames = null;
private String mTestOutputDir = null;
private String mTestOutputFile = null;
private int mDropCacheOption;
private boolean mCollectForAllProcesses = false;
private UiDevice mUiDevice;
// Map to maintain per-process memory info
private Map<String, String> mMemoryMap = new HashMap<>();
// Maintain metric name and the index it corresponds to in the showmap output
// summary
private Map<Integer, String> mMetricNameIndexMap = new HashMap<>();
public void setUp(String testOutputDir, String... processNames) {
mProcessNames = processNames;
mTestOutputDir = testOutputDir;
mDropCacheOption = 0;
mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
}
@Override
public boolean startCollecting() {
if (mTestOutputDir == null) {
Log.e(TAG, String.format("Invalid test setup"));
return false;
}
File directory = new File(mTestOutputDir);
String filePath = String.format("%s/showmap_snapshot%d.txt", mTestOutputDir,
UUID.randomUUID().hashCode());
File file = new File(filePath);
// Make sure directory exists and file does not
if (directory.exists()) {
if (file.exists() && !file.delete()) {
Log.e(TAG, String.format("Failed to delete result output file %s", filePath));
return false;
}
} else {
if (!directory.mkdirs()) {
Log.e(TAG, String.format("Failed to create result output directory %s",
mTestOutputDir));
return false;
}
}
// Create an empty file to fail early in case there are no write permissions
try {
if (!file.createNewFile()) {
// This should not happen unless someone created the file right after we deleted it
Log.e(TAG,
String.format("Race with another user of result output file %s", filePath));
return false;
}
} catch (IOException e) {
Log.e(TAG, String.format("Failed to create result output file %s", filePath), e);
return false;
}
mTestOutputFile = filePath;
return true;
}
@Override
public Map<String, String> getMetrics() {
try {
// Drop cache if requested
if (mDropCacheOption > 0) {
dropCache(mDropCacheOption);
}
if (mCollectForAllProcesses) {
Log.i(TAG, "Collecting memory metrics for all processes.");
mProcessNames = getAllProcessNames();
} else if (mProcessNames.length > 0) {
Log.i(TAG, "Collecting memory only for given list of process");
} else if (mProcessNames.length == 0) {
// No processes specified, just return empty map
return mMemoryMap;
}
FileWriter writer = new FileWriter(new File(mTestOutputFile), true);
for (String processName : mProcessNames) {
List<Integer> pids = new ArrayList<>();
// Collect required data
try {
pids = getPids(processName);
for (Integer pid : pids) {
String showmapOutput = execShowMap(processName, pid);
parseAndUpdateMemoryInfo(processName, showmapOutput);
// Store showmap output into file. If there are more than one process
// with same name write the individual showmap associated with pid.
storeToFile(mTestOutputFile, processName, pid, showmapOutput, writer);
// Parse number of child processes for the given pid and update the
// total number of child process count for the process name that pid
// is associated with.
updateChildProcessesCount(processName, pid);
}
} catch (RuntimeException e) {
Log.e(TAG, e.getMessage(), e.getCause());
// Skip this process and continue with the next one
continue;
}
}
// To track total number of process with child processes.
if (mMemoryMap.size() != 0) {
Set<String> parentWithChildProcessSet = mMemoryMap.keySet()
.stream()
.filter(s -> s.startsWith(CHILD_PROCESS_COUNT_PREFIX))
.collect(Collectors.toSet());
mMemoryMap.put(PROCESS_WITH_CHILD_PROCESS_COUNT,
Long.toString(parentWithChildProcessSet.size()));
}
// Store the unique process count. -1 to exclude the "ps" process name.
mMemoryMap.put(PROCESS_COUNT, Integer.toString(mProcessNames.length - 1));
writer.close();
mMemoryMap.put(OUTPUT_FILE_PATH_KEY, mTestOutputFile);
} catch (RuntimeException e) {
Log.e(TAG, e.getMessage(), e.getCause());
} catch (IOException e) {
Log.e(TAG, String.format("Failed to write output file %s", mTestOutputFile), e);
}
return mMemoryMap;
}
@Override
public boolean stopCollecting() {
return true;
}
/**
* Set drop cache option.
*
* @param dropCacheOption drop pagecache (1), slab (2) or all (3) cache
* @return true on success, false if input option is invalid
*/
public boolean setDropCacheOption(int dropCacheOption) {
// Valid values are 1..3
if (dropCacheOption < 1 || dropCacheOption > 3) {
return false;
}
mDropCacheOption = dropCacheOption;
return true;
}
/**
* Drops kernel memory cache.
*
* @param cacheOption drop pagecache (1), slab (2) or all (3) caches
*/
private void dropCache(int cacheOption) throws RuntimeException {
try {
mUiDevice.executeShellCommand(String.format(DROP_CACHES_CMD, cacheOption));
} catch (IOException e) {
throw new RuntimeException("Unable to drop caches", e);
}
}
/**
* Get pid's of the process with {@code processName} name.
*
* @param processName name of the process to get pid
* @return pid's of the specified process
*/
private List<Integer> getPids(String processName) throws RuntimeException {
try {
String pidofOutput = mUiDevice
.executeShellCommand(String.format(PIDOF_CMD, processName));
// Sample output for the process with more than 1 pid.
// Sample command : "pidof init"
// Sample output : 1 559
String[] pids = pidofOutput.split("\\s+");
List<Integer> pidList = new ArrayList<>();
for (String pid : pids) {
pidList.add(Integer.parseInt(pid.trim()));
}
return pidList;
} catch (IOException e) {
throw new RuntimeException(String.format("Unable to get pid of %s ", processName), e);
}
}
/**
* Executes showmap command for the process with {@code processName} name and {@code pid} pid.
*
* @param processName name of the process to run showmap for
* @param pid pid of the process to run showmap for
* @return the output of showmap command
*/
private String execShowMap(String processName, long pid) throws IOException {
try {
return mUiDevice.executeShellCommand(String.format(SHOWMAP_CMD, pid));
} catch (IOException e) {
throw new RuntimeException(
String.format("Unable to execute showmap command for %s ", processName), e);
}
}
/**
* Extract memory metrics from showmap command output for the process with {@code processName}
* name.
*
* @param processName name of the process to extract memory info for
* @param showmapOutput showmap command output
*/
private void parseAndUpdateMemoryInfo(String processName, String showmapOutput)
throws RuntimeException {
try {
// -------- -------- -------- -------- -------- -------- -------- -------- ----- ------
// ----
// virtual shared shared private private
// size RSS PSS clean dirty clean dirty swap swapPSS flags object
// ------- -------- -------- -------- -------- -------- -------- -------- ------ -----
// ----
// 10810272 5400 1585 3800 168 264 1168 0 0 TOTAL
int pos = showmapOutput.lastIndexOf("----");
String summarySplit[] = showmapOutput.substring(pos).trim().split("\\s+");
for (Map.Entry<Integer, String> entry : mMetricNameIndexMap.entrySet()) {
String metricKey = constructKey(
String.format(OUTPUT_METRIC_PATTERN, entry.getValue()),
processName);
// If there are multiple pids associated with the process name then update the
// existing entry in the map otherwise add new entry in the map.
if (mMemoryMap.containsKey(metricKey)) {
long currValue = Long.parseLong(mMemoryMap.get(metricKey));
mMemoryMap.put(metricKey, Long.toString(currValue +
(Long.parseLong(summarySplit[entry.getKey() + 1]) * 1024)));
} else {
mMemoryMap.put(metricKey, Long.toString(Long.parseLong(
summarySplit[entry.getKey() + 1]) * 1024));
}
}
} catch (IndexOutOfBoundsException | InputMismatchException e) {
throw new RuntimeException(
String.format("Unexpected showmap format for %s ", processName), e);
}
}
/**
* Store test results for one process into file.
*
* @param fileName name of the file being written
* @param processName name of the process
* @param pid pid of the process
* @param showmapOutput showmap command output
* @param writer file writer to write the data
*/
private void storeToFile(String fileName, String processName, long pid, String showmapOutput,
FileWriter writer) throws RuntimeException {
try {
writer.write(String.format(">>> %s (%d) <<<\n", processName, pid));
writer.write(showmapOutput);
writer.write('\n');
} catch (IOException e) {
throw new RuntimeException(String.format("Unable to write file %s ", fileName), e);
}
}
/**
* Set the memory metric name and corresponding index to parse from the showmap output summary.
*
* @param metricNameIndexStr comma separated metric_name:index TODO: Pre-process the string into
* map and pass the map to this method.
*/
public void setMetricNameIndex(String metricNameIndexStr) {
Log.i(TAG, String.format("Metric Name index %s", metricNameIndexStr));
String metricDetails[] = metricNameIndexStr.split(",");
for (String metricDetail : metricDetails) {
String metricDetailsSplit[] = metricDetail.split(":");
if (metricDetailsSplit.length == 2) {
mMetricNameIndexMap.put(Integer.parseInt(
metricDetailsSplit[1]), metricDetailsSplit[0]);
}
}
Log.i(TAG, String.format("Metric Name index map size %s", mMetricNameIndexMap.size()));
}
/**
* Retrieves the number of child processes for the given process id and updates the total
* process count for the process name that pid is associated with.
*
* @param processName
* @param pid
*/
private void updateChildProcessesCount(String processName, long pid) {
try {
Log.i(TAG,
String.format("Retrieving child processes count for process name: %s with"
+ " process id %d.", processName, pid));
String childProcessesStr = mUiDevice
.executeShellCommand(String.format(CHILD_PROCESSES_CMD, pid));
Log.i(TAG, String.format("Child processes cmd output: %s", childProcessesStr));
String[] childProcessStrSplit = childProcessesStr.split("\\n");
// To discard the header line in the command output.
int childProcessCount = childProcessStrSplit.length - 1;
String childCountMetricKey = String.format(OUTPUT_CHILD_PROCESS_COUNT_KEY, processName);
if (childProcessCount > 0) {
mMemoryMap.put(childCountMetricKey,
Long.toString(
Long.parseLong(mMemoryMap.getOrDefault(childCountMetricKey, "0"))
+ childProcessCount));
}
} catch (IOException e) {
throw new RuntimeException("Unable to run child process count command.", e);
}
}
/**
* Enables memory collection for all processes.
*/
public void setAllProcesses() {
mCollectForAllProcesses = true;
}
/**
* Get all process names running in the system.
*/
private String[] getAllProcessNames() {
Set<String> allProcessNames = new LinkedHashSet<>();
try {
String psOutput = mUiDevice.executeShellCommand(ALL_PROCESSES_CMD);
// Split the lines
String allProcesses[] = psOutput.split("\\n");
for (String invidualProcessDetails : allProcesses) {
Log.i(TAG, String.format("Process detail: %s", invidualProcessDetails));
// Sample process detail line
// system 603 1 41532 5396 SyS_epoll+ 0 S servicemanager
String processSplit[] = invidualProcessDetails.split("\\s+");
// Parse process name
String processName = processSplit[processSplit.length - 1].trim();
// Include the process name which are not enclosed in [].
if (!processName.startsWith("[") && !processName.endsWith("]")) {
// Skip the first (i.e header) line from "ps -A" output.
if (processName.equalsIgnoreCase("NAME")) {
continue;
}
Log.i(TAG, String.format("Including the process %s", processName));
allProcessNames.add(processName);
}
}
} catch (IOException ioe) {
throw new RuntimeException(
String.format("Unable execute all processes command %s ", ALL_PROCESSES_CMD),
ioe);
}
return allProcessNames.toArray(new String[0]);
}
}