blob: 525c2610722de71226c69854b059642cdc31d676 [file] [log] [blame]
/*
* Copyright (C) 2021 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.tradefed.device.metric;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.metrics.proto.MetricMeasurement;
import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.google.common.collect.ImmutableSet;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.stream.Collectors;
/**
* Base implementation of {@link FilePullerDeviceMetricCollector} that allows pulling the showmap
* files from the device and collect the metrics from it.
*/
@OptionClass(alias = "showmap-metric-collector")
public class ShowmapPullerMetricCollector extends FilePullerDeviceMetricCollector {
private static final String PROCESS_NAME_REGEX = "(>>>\\s)(\\S+)(\\s.*<<<)";
private static final String METRIC_START_END_TEXT = "------";
private static final String METRIC_VALUE_SEPARATOR = "_";
private static final String METRIC_UNIT = "bytes";
private static final Set<String> SKIP_COLUMNS = ImmutableSet.of("flags", "object", "locked");
private Boolean processFound = false;
private String processName = null;
private Map<String, Long> mGranularInfo = new HashMap<>();
private Set<String> mProcessObjInfo = new HashSet<>();
private Map<String, Integer> mColumnNameToColumnIndex = new HashMap<>();
@Option(
name = "showmap-metric-prefix",
description = "Prefix to be used with the metrics collected from showmap.")
private String mMetricPrefix = "showmap_granular";
@Option(
name = "showmap-process-name",
description = "Process names to be parsed in showmap file.")
private Collection<String> mProcessNames = new ArrayList<>();
/**
* Process the showmap output file for the additional metrics and add it to final metrics.
*
* @param key the option key associated to the file that was pulled from the device.
* @param metricFile the {@link File} pulled from the device matching the option key.
* @param data where metrics will be stored.
*/
@Override
public void processMetricFile(String key, File metricFile, DeviceMetricData data) {
String line;
Boolean metricFound = false;
if (metricFile != null) {
List<String> headerList = new ArrayList<>();
try (BufferedReader mBufferReader = new BufferedReader(new FileReader(metricFile))) {
while ((line = mBufferReader.readLine()) != null) {
if (!processFound) {
processFound = isProcessFound(line);
continue;
}
metricFound =
metricFound
? computeGranularMetrics(line, processName)
: isMetricParsingStartEnd(line);
// We found the process name but have not found the headers
if (mColumnNameToColumnIndex.isEmpty()) {
if (!metricFound) {
// We have not reached the "----" line yet.
// Save the multi-line headers for later.
headerList.add(line);
} else if (metricFound) {
// we reach the "----" line, and the mColumnNameToColumnIndex is empty
// So we can get the headers now.
extractHeaders(line, headerList);
}
}
}
} catch (IOException e) {
CLog.e("Error parsing showmap granular metrics");
CLog.e(e);
} finally {
writeGranularMetricData(data);
uploadMetricFile(metricFile);
}
}
}
@Override
public void processMetricDirectory(String key, File metricDirectory, DeviceMetricData runData) {
// Implement if all the files under specific directory have to be post processed.
}
/**
* Extract the showmap file name used for constructing the output metric file
*
* @param showmapFileName
* @return String name of the showmap file name excluding the UUID.
*/
private String getShowmapFileName(String showmapFileName) {
// For example return showmap_<test_name>-1_ from
// showmap_<test_name>-1_13388308985625987330.txt excluding the UID.
int lastIndex = showmapFileName.lastIndexOf("_");
if (lastIndex != -1) {
return showmapFileName.substring(0, lastIndex + 1);
}
return showmapFileName;
}
/**
* Computing granular metrics by adding individual memory values for every object and create
* final metric value
*
* @param line
* @param processName
*/
private Boolean computeGranularMetrics(String line, String processName) {
String objectName;
long mGranularValue;
long metricCounter;
String completeGranularMetric;
if (isMetricParsingStartEnd(line)) {
computeObjectsPerProcess(processName);
processFound = false;
return false;
}
String[] metricLine = line.trim().split("\\s+");
try {
objectName = metricLine[mColumnNameToColumnIndex.get("object")];
} catch (ArrayIndexOutOfBoundsException e) {
CLog.e("Error parsing granular metrics for %s", processName);
computeObjectsPerProcess(processName);
processFound = false;
return false;
}
for (Map.Entry<String, Integer> entry : mColumnNameToColumnIndex.entrySet()) {
String memName = entry.getKey();
if (SKIP_COLUMNS.contains(memName)) {
continue;
}
try {
mGranularValue =
Long.parseLong(metricLine[mColumnNameToColumnIndex.get(memName)]) * 1024;
} catch (NumberFormatException e) {
CLog.e("Error parsing granular metrics for %s", processName);
computeObjectsPerProcess(processName);
processFound = false;
return false;
}
/**
* final metric will be of following format
* showmap_granular_<memory>_bytes_<process>_<object></object>
* showmap_granular_rss_bytes_system_server_/system/fonts/SourceSansPro-Italic.ttf:104
*/
completeGranularMetric =
String.join(
METRIC_VALUE_SEPARATOR,
mMetricPrefix,
memName,
METRIC_UNIT,
processName,
objectName);
metricCounter =
mGranularInfo.containsKey(completeGranularMetric)
? mGranularInfo.get(completeGranularMetric)
: 0L;
mGranularInfo.put(completeGranularMetric, metricCounter + mGranularValue);
}
mProcessObjInfo.add(objectName);
return true;
}
/**
* Append granular metrics to DeviceMetricData object
*
* @param data
*/
private void writeGranularMetricData(DeviceMetricData data) {
for (Map.Entry<String, Long> granularData : mGranularInfo.entrySet()) {
MetricMeasurement.Metric.Builder metricBuilder = MetricMeasurement.Metric.newBuilder();
metricBuilder.getMeasurementsBuilder().setSingleInt(granularData.getValue());
data.addMetric(
String.format("%s", granularData.getKey()),
metricBuilder.setType(MetricMeasurement.DataType.RAW));
}
}
/**
* Uploads showmap text file to artifacts
*
* @param uploadFile
*/
private void uploadMetricFile(File uploadFile) {
try (InputStreamSource source = new FileInputStreamSource(uploadFile, true)) {
testLog(getShowmapFileName(uploadFile.getName()), LogDataType.TEXT, source);
}
}
/**
* Extract the showmap column header used for computeGranularMetrics
*
* <p>We first get each hyphens lengths in a row first which indicates the length of the column.
* and then we split the header string by hyphens length including the empty space. At the end,
* we concat the split string among multiple rows as a machine-readable header.
*
* <p>In the showmap output file, the hyphens/dashes give the most predictable delineation of
* columns. The showmap output format was meant to be human-readable, not machine-readable, so
* there will always be some levels of hackiness involved.
*
* @param hyphens expect to extract headers when reaching ----
* @param headerList multi-line headers in a list
*/
private void extractHeaders(String hyphens, List<String> headerList) {
List<Integer> steps =
Stream.of(hyphens.trim().split("\\s"))
.map(s -> s.length())
.collect(Collectors.toList());
// For each segment of hyphens, calculate its column's start and end indices.
int columnStart = 0;
for (int i = 0; i < steps.size(); i++) {
int columnEnd = columnStart + steps.get(i) + 1;
String header = "";
// Then, for each header row, get the header part with the start and end indices.
for (String row : headerList) {
String h = row.toLowerCase();
header =
header.concat(
h.substring(
columnStart > h.length() ? h.length() : columnStart,
columnEnd > h.length() ? h.length() : columnEnd)
.trim());
}
columnStart = columnEnd;
mColumnNameToColumnIndex.put(header, i);
}
}
/**
* Returns if line contains '------' text
*
* @param line
* @return true or false
*/
private Boolean isMetricParsingStartEnd(String line) {
if (line.contains(METRIC_START_END_TEXT)) {
return true;
}
return false;
}
/**
* Returns if particular process needs to be parsed
*
* @param line
* @return true or false
*/
private Boolean isProcessFound(String line) {
if (mProcessNames.isEmpty()) return false;
boolean psResult;
Pattern psPattern = Pattern.compile(PROCESS_NAME_REGEX);
Matcher psMatcher = psPattern.matcher(line);
if (psMatcher.find()) {
processName = psMatcher.group(2);
psResult = mProcessNames.contains(processName);
return psResult;
}
return false;
}
/**
* Counts total no. of unique objects per process showmap_granular_<process>_total_object_count
*
* @param processName
*/
private void computeObjectsPerProcess(String processName) {
String objCounterMetric =
String.join(
METRIC_VALUE_SEPARATOR, mMetricPrefix, processName, "total_object_count");
if (mProcessObjInfo.size() > 0) {
mGranularInfo.put(objCounterMetric, (long) mProcessObjInfo.size());
mProcessObjInfo.clear();
}
}
}