blob: 05ef83ab3ba0470e444fcaa9ff789a3019634557 [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.catbox.result;
import com.android.annotations.VisibleForTesting;
import com.android.catbox.util.TestMetricsUtil;
import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
import com.android.compatibility.common.tradefed.util.CollectorUtil;
import com.android.compatibility.common.util.MetricsReportLog;
import com.android.compatibility.common.util.ResultType;
import com.android.compatibility.common.util.ResultUnit;
import com.android.ddmlib.Log.LogLevel;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.invoker.IInvocationContext;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.TestDescription;
import com.android.tradefed.testtype.suite.ModuleDefinition;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.proto.TfMetricProtoUtil;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** JsonResultReporter aggregates and writes performance test metrics to a Json file. */
@OptionClass(alias = "json-result-reporter")
public class JsonResultReporter implements ITestInvocationListener {
private CompatibilityBuildHelper mBuildHelper;
private IInvocationContext mContext;
private IInvocationContext mModuleContext;
private IBuildInfo mBuildInfo;
private TestMetricsUtil mTestMetricsUtil;
@Option(
name = "dest-dir",
description =
"The directory under the result to store the files. "
+ "Default to 'report-log-files'.")
private String mDestDir = "report-log-files";
private String mTempReportFolder = "temp-report-logs";
@Option(name = "report-log-name", description = "Name of the JSON report file.")
private String mReportLogName = null;
@Option(
name = "report-test-name-mapping",
description = "Mapping for test name to use in report.")
private Map<String, String> mReportTestNameMap = new HashMap<String, String>();
@Option(
name = "report-all-metrics",
description = "Report all the generated metrics. Default to 'true'.")
private boolean mReportAllMetrics = true;
@Option(
name = "report-metric-key-mapping",
description =
"Mapping for Metric Keys to be reported. "
+ "Only report the keys provided in the mapping.")
private Map<String, String> mReportMetricKeyMap = new HashMap<String, String>();
@Option(name = "test-iteration-separator", description = "Separator used in between the test"
+ " class name and the iteration number. Default separator is '$'")
private String mTestIterationSeparator = "$";
@Option(name = "aggregate-similar-tests", description = "To aggregate the metrics from test"
+ " cases which differ only by iteration number or having the same test name."
+ " Used only in context with the microbenchmark test runner. Set this flag to false"
+ " to disable aggregating the metrics.")
private boolean mAggregateSimilarTests = false;
public JsonResultReporter() {
// Default Constructor
// Nothing to do
}
/**
* Return the primary build info that was reported via {@link
* #invocationStarted(IInvocationContext)}. Primary build is the build returned by the first
* build provider of the running configuration. Returns null if there is no context (no build to
* test case).
*/
private IBuildInfo getPrimaryBuildInfo() {
if (mContext == null) {
return null;
} else {
return mContext.getBuildInfos().get(0);
}
}
/** Create Build Helper */
@VisibleForTesting
CompatibilityBuildHelper createBuildHelper() {
return new CompatibilityBuildHelper(getPrimaryBuildInfo());
}
/** Get Device ABI Information */
@VisibleForTesting
String getAbiInfo() {
CLog.logAndDisplay(LogLevel.INFO, "Getting ABI Information.");
if (mModuleContext == null) {
// Return Empty String
return "";
}
List<String> abis = mModuleContext.getAttributes().get(ModuleDefinition.MODULE_ABI);
if (abis == null || abis.isEmpty()) {
// Return Empty String
return "";
}
if (abis.size() > 1) {
CLog.logAndDisplay(
LogLevel.WARN,
String.format(
"More than one ABI name specified (using first one): %s",
abis.toString()));
}
return abis.get(0);
}
/** Initialize Test Metrics Util */
@VisibleForTesting
TestMetricsUtil initializeTestMetricsUtil() {
return new TestMetricsUtil();
}
/** Initialize configurations for Result Reporter */
private void initializeReporterConfig() {
CLog.logAndDisplay(LogLevel.INFO, "Initializing Test Metrics Result Reporter Config.");
// Initialize Build Info
mBuildInfo = getPrimaryBuildInfo();
// Initialize Build Helper
if (mBuildHelper == null) {
mBuildHelper = createBuildHelper();
}
// Initialize Report Log Name
// Use test tag as the report name if not provided
if (mReportLogName == null) {
mReportLogName = mContext.getTestTag();
}
// Initialize Test Metrics Util
if (mTestMetricsUtil == null) {
mTestMetricsUtil = initializeTestMetricsUtil();
}
mTestMetricsUtil.setIterationSeparator(mTestIterationSeparator);
}
/** Re-initialize object to erase all existing test metrics */
private void reInitializeTestMetricsUtil() {
mTestMetricsUtil = initializeTestMetricsUtil();
mTestMetricsUtil.setIterationSeparator(mTestIterationSeparator);
}
/** Write Test Metrics to JSON */
private void writeTestMetrics(
String classMethodName, Map<String, String> metrics) {
// Use class method name as stream name if mapping is not provided
String streamName = classMethodName;
if (mReportTestNameMap != null && mReportTestNameMap.containsKey(classMethodName)) {
streamName = mReportTestNameMap.get(classMethodName);
}
// Get ABI Info
String abiName = getAbiInfo();
// Initialize Metrics Report Log
// TODO: b/194103027 [Remove MetricsReportLog dependency as it is being deprecated].
MetricsReportLog reportLog =
new MetricsReportLog(
mBuildInfo, abiName, classMethodName, mReportLogName, streamName);
// Write Test Metrics in the Log
if (mReportAllMetrics) {
// Write all the metrics to the report
writeAllMetrics(reportLog, metrics);
} else {
// Write metrics for given keys to the report
writeMetricsForGivenKeys(reportLog, metrics);
}
// Submit Report Log
reportLog.submit();
}
/** Write all the metrics to JSON Report */
private void writeAllMetrics(MetricsReportLog reportLog, Map<String, String> metrics) {
CLog.logAndDisplay(LogLevel.INFO, "Writing all the metrics to JSON report.");
for (String key : metrics.keySet()) {
try {
double value = Double.parseDouble(metrics.get(key));
reportLog.addValue(key, value, ResultType.NEUTRAL, ResultUnit.NONE);
} catch (NumberFormatException exception) {
CLog.logAndDisplay(
LogLevel.ERROR,
String.format(
"Unable to parse value '%s' for '%s' metric key.",
metrics.get(key), key));
}
}
CLog.logAndDisplay(
LogLevel.INFO, "Successfully completed writing the metrics to JSON report.");
}
/** Write given set of metrics to JSON Report */
private void writeMetricsForGivenKeys(
MetricsReportLog reportLog, Map<String, String> metrics) {
CLog.logAndDisplay(LogLevel.INFO, "Writing given set of metrics to JSON report.");
if (mReportMetricKeyMap == null || mReportMetricKeyMap.isEmpty()) {
CLog.logAndDisplay(
LogLevel.WARN, "Skip reporting metrics. Metric keys are not provided.");
return;
}
for (String key : mReportMetricKeyMap.keySet()) {
if (!metrics.containsKey(key) || metrics.get(key) == null) {
CLog.logAndDisplay(LogLevel.WARN, String.format("%s metric key is missing.", key));
continue;
}
try {
double value = Double.parseDouble(metrics.get(key));
reportLog.addValue(
mReportMetricKeyMap.get(key), value, ResultType.NEUTRAL, ResultUnit.NONE);
} catch (NumberFormatException exception) {
CLog.logAndDisplay(
LogLevel.ERROR,
String.format(
"Unable to parse value '%s' for '%s' metric key.",
metrics.get(key), key));
}
}
CLog.logAndDisplay(
LogLevel.INFO, "Successfully completed writing the metrics to JSON report.");
}
/** Copy the report generated at temporary path to the given destination path in Results */
private void copyGeneratedReportToResultsDirectory() {
CLog.logAndDisplay(LogLevel.INFO, "Copying the report log to results directory.");
// Copy report log files to results dir.
try {
// Get Result Directory
File resultDir = mBuildHelper.getResultDir();
// Create a directory ( if it does not exist ) in results for report logs
if (mDestDir != null) {
resultDir = new File(resultDir, mDestDir);
}
if (!resultDir.exists()) {
resultDir.mkdirs();
}
if (!resultDir.isDirectory()) {
CLog.logAndDisplay(
LogLevel.ERROR,
String.format("%s is not a directory", resultDir.getAbsolutePath()));
return;
}
// Temp directory for report logs
final File hostReportDir = FileUtil.createNamedTempDir(mTempReportFolder);
if (!hostReportDir.isDirectory()) {
CLog.logAndDisplay(
LogLevel.ERROR,
String.format("%s is not a directory", hostReportDir.getAbsolutePath()));
return;
}
// Copy the report logs from temp directory and to the results directory
CollectorUtil.pullFromHost(hostReportDir, resultDir);
CollectorUtil.reformatRepeatedStreams(resultDir);
CLog.logAndDisplay(LogLevel.INFO, "Copying the report log completed successfully.");
} catch (IOException exception) {
CLog.logAndDisplay(LogLevel.ERROR, exception.getMessage());
}
}
/** {@inheritDoc} */
@Override
public void invocationStarted(IInvocationContext context) {
mContext = context;
initializeReporterConfig();
}
/** {@inheritDoc} */
@Override
public void invocationEnded(long elapsedTime) {
// Copy the generated report to Results Directory
copyGeneratedReportToResultsDirectory();
}
/** Overrides parent to explicitly to store test metrics */
@Override
public void testEnded(TestDescription testDescription, HashMap<String, Metric> metrics) {
// If metrics are available and aggregate-similar-metrics is set to true, store the metrics
if (metrics != null && !metrics.isEmpty() && mAggregateSimilarTests) {
// Store the metrics
mTestMetricsUtil.storeTestMetrics(testDescription, metrics);
}
}
/** Overrides parent to explicitly to process and write metrics */
@Override
public final void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
// If aggregate-similar-metrics is set to true, aggregate the metrics
if (mAggregateSimilarTests) {
// Aggregate Metrics for Similar Tests and write to the file
Map<String, Map<String, String>> aggregatedMetrics =
mTestMetricsUtil.getAggregatedStoredTestMetrics();
for (String testName: aggregatedMetrics.keySet()) {
writeTestMetrics(testName, aggregatedMetrics.get(testName));
}
}
// Avoid reporting duplicate metrics by erasing metrics from previous runs
reInitializeTestMetricsUtil();
}
/** {@inheritDoc} */
@Override
public void testModuleStarted(IInvocationContext moduleContext) {
mModuleContext = moduleContext;
}
/** {@inheritDoc} */
@Override
public void testModuleEnded() {
mModuleContext = null;
}
}