1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.catbox.result;
18 
19 import com.android.annotations.VisibleForTesting;
20 
21 import com.android.catbox.util.TestMetricsUtil;
22 
23 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
24 import com.android.compatibility.common.tradefed.util.CollectorUtil;
25 import com.android.compatibility.common.util.MetricsReportLog;
26 import com.android.compatibility.common.util.ResultType;
27 import com.android.compatibility.common.util.ResultUnit;
28 
29 import com.android.ddmlib.Log.LogLevel;
30 
31 import com.android.tradefed.build.IBuildInfo;
32 import com.android.tradefed.config.Option;
33 import com.android.tradefed.config.OptionClass;
34 import com.android.tradefed.invoker.IInvocationContext;
35 
36 import com.android.tradefed.log.LogUtil.CLog;
37 
38 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
39 
40 import com.android.tradefed.result.ITestInvocationListener;
41 import com.android.tradefed.result.TestDescription;
42 
43 import com.android.tradefed.testtype.suite.ModuleDefinition;
44 
45 import com.android.tradefed.util.FileUtil;
46 import com.android.tradefed.util.proto.TfMetricProtoUtil;
47 
48 import java.io.File;
49 import java.io.IOException;
50 
51 import java.util.HashMap;
52 import java.util.List;
53 import java.util.Map;
54 
55 /** JsonResultReporter aggregates and writes performance test metrics to a Json file. */
56 @OptionClass(alias = "json-result-reporter")
57 public class JsonResultReporter implements ITestInvocationListener {
58     private CompatibilityBuildHelper mBuildHelper;
59     private IInvocationContext mContext;
60     private IInvocationContext mModuleContext;
61     private IBuildInfo mBuildInfo;
62     private TestMetricsUtil mTestMetricsUtil;
63 
64     @Option(
65             name = "dest-dir",
66             description =
67                     "The directory under the result to store the files. "
68                             + "Default to 'report-log-files'.")
69     private String mDestDir = "report-log-files";
70 
71     private String mTempReportFolder = "temp-report-logs";
72 
73     @Option(name = "report-log-name", description = "Name of the JSON report file.")
74     private String mReportLogName = null;
75 
76     @Option(
77             name = "report-test-name-mapping",
78             description = "Mapping for test name to use in report.")
79     private Map<String, String> mReportTestNameMap = new HashMap<String, String>();
80 
81     @Option(
82             name = "report-all-metrics",
83             description = "Report all the generated metrics. Default to 'true'.")
84     private boolean mReportAllMetrics = true;
85 
86     @Option(
87             name = "report-metric-key-mapping",
88             description =
89                     "Mapping for Metric Keys to be reported. "
90                             + "Only report the keys provided in the mapping.")
91     private Map<String, String> mReportMetricKeyMap = new HashMap<String, String>();
92 
93     @Option(name = "test-iteration-separator", description = "Separator used in between the test"
94             + " class name and the iteration number. Default separator is '$'")
95     private String mTestIterationSeparator = "$";
96 
97     @Option(name = "aggregate-similar-tests", description = "To aggregate the metrics from test"
98             + " cases which differ only by iteration number or having the same test name."
99             + " Used only in context with the microbenchmark test runner. Set this flag to false"
100             + " to disable aggregating the metrics.")
101     private boolean mAggregateSimilarTests = false;
102 
JsonResultReporter()103     public JsonResultReporter() {
104         // Default Constructor
105         // Nothing to do
106     }
107 
108     /**
109      * Return the primary build info that was reported via {@link
110      * #invocationStarted(IInvocationContext)}. Primary build is the build returned by the first
111      * build provider of the running configuration. Returns null if there is no context (no build to
112      * test case).
113      */
getPrimaryBuildInfo()114     private IBuildInfo getPrimaryBuildInfo() {
115         if (mContext == null) {
116             return null;
117         } else {
118             return mContext.getBuildInfos().get(0);
119         }
120     }
121 
122     /** Create Build Helper */
123     @VisibleForTesting
createBuildHelper()124     CompatibilityBuildHelper createBuildHelper() {
125         return new CompatibilityBuildHelper(getPrimaryBuildInfo());
126     }
127 
128     /** Get Device ABI Information */
129     @VisibleForTesting
getAbiInfo()130     String getAbiInfo() {
131         CLog.logAndDisplay(LogLevel.INFO, "Getting ABI Information.");
132         if (mModuleContext == null) {
133             // Return Empty String
134             return "";
135         }
136         List<String> abis = mModuleContext.getAttributes().get(ModuleDefinition.MODULE_ABI);
137         if (abis == null || abis.isEmpty()) {
138             // Return Empty String
139             return "";
140         }
141         if (abis.size() > 1) {
142             CLog.logAndDisplay(
143                     LogLevel.WARN,
144                     String.format(
145                             "More than one ABI name specified (using first one): %s",
146                             abis.toString()));
147         }
148         return abis.get(0);
149     }
150 
151     /** Initialize Test Metrics Util */
152     @VisibleForTesting
initializeTestMetricsUtil()153     TestMetricsUtil initializeTestMetricsUtil() {
154         return new TestMetricsUtil();
155     }
156 
157     /** Initialize configurations for Result Reporter */
initializeReporterConfig()158     private void initializeReporterConfig() {
159         CLog.logAndDisplay(LogLevel.INFO, "Initializing Test Metrics Result Reporter Config.");
160         // Initialize Build Info
161         mBuildInfo = getPrimaryBuildInfo();
162 
163         // Initialize Build Helper
164         if (mBuildHelper == null) {
165             mBuildHelper = createBuildHelper();
166         }
167 
168         // Initialize Report Log Name
169         // Use test tag as the report name if not provided
170         if (mReportLogName == null) {
171             mReportLogName = mContext.getTestTag();
172         }
173 
174         // Initialize Test Metrics Util
175         if (mTestMetricsUtil == null) {
176             mTestMetricsUtil = initializeTestMetricsUtil();
177         }
178         mTestMetricsUtil.setIterationSeparator(mTestIterationSeparator);
179     }
180 
181     /** Write Test Metrics to JSON */
writeTestMetrics( String classMethodName, Map<String, String> metrics)182     private void writeTestMetrics(
183             String classMethodName, Map<String, String> metrics) {
184 
185         // Use class method name as stream name if mapping is not provided
186         String streamName = classMethodName;
187         if (mReportTestNameMap != null && mReportTestNameMap.containsKey(classMethodName)) {
188             streamName = mReportTestNameMap.get(classMethodName);
189         }
190 
191         // Get ABI Info
192         String abiName = getAbiInfo();
193 
194         // Initialize Metrics Report Log
195         // TODO: b/194103027 [Remove MetricsReportLog dependency as it is being deprecated].
196         MetricsReportLog reportLog =
197                 new MetricsReportLog(
198                         mBuildInfo, abiName, classMethodName, mReportLogName, streamName);
199 
200         // Write Test Metrics in the Log
201         if (mReportAllMetrics) {
202             // Write all the metrics to the report
203             writeAllMetrics(reportLog, metrics);
204         } else {
205             // Write metrics for given keys to the report
206             writeMetricsForGivenKeys(reportLog, metrics);
207         }
208 
209         // Submit Report Log
210         reportLog.submit();
211     }
212 
213     /** Write all the metrics to JSON Report */
writeAllMetrics(MetricsReportLog reportLog, Map<String, String> metrics)214     private void writeAllMetrics(MetricsReportLog reportLog, Map<String, String> metrics) {
215         CLog.logAndDisplay(LogLevel.INFO, "Writing all the metrics to JSON report.");
216         for (String key : metrics.keySet()) {
217             try {
218                 double value = Double.parseDouble(metrics.get(key));
219                 reportLog.addValue(key, value, ResultType.NEUTRAL, ResultUnit.NONE);
220             } catch (NumberFormatException exception) {
221                 CLog.logAndDisplay(
222                         LogLevel.ERROR,
223                         String.format(
224                                 "Unable to parse value '%s' for '%s' metric key.",
225                                 metrics.get(key), key));
226             }
227         }
228         CLog.logAndDisplay(
229                 LogLevel.INFO, "Successfully completed writing the metrics to JSON report.");
230     }
231 
232     /** Write given set of metrics to JSON Report */
writeMetricsForGivenKeys( MetricsReportLog reportLog, Map<String, String> metrics)233     private void writeMetricsForGivenKeys(
234             MetricsReportLog reportLog, Map<String, String> metrics) {
235         CLog.logAndDisplay(LogLevel.INFO, "Writing given set of metrics to JSON report.");
236         if (mReportMetricKeyMap == null || mReportMetricKeyMap.isEmpty()) {
237             CLog.logAndDisplay(
238                     LogLevel.WARN, "Skip reporting metrics. Metric keys are not provided.");
239             return;
240         }
241         for (String key : mReportMetricKeyMap.keySet()) {
242             if (!metrics.containsKey(key) || metrics.get(key) == null) {
243                 CLog.logAndDisplay(LogLevel.WARN, String.format("%s metric key is missing.", key));
244                 continue;
245             }
246             try {
247                 double value = Double.parseDouble(metrics.get(key));
248                 reportLog.addValue(
249                         mReportMetricKeyMap.get(key), value, ResultType.NEUTRAL, ResultUnit.NONE);
250             } catch (NumberFormatException exception) {
251                 CLog.logAndDisplay(
252                         LogLevel.ERROR,
253                         String.format(
254                                 "Unable to parse value '%s' for '%s' metric key.",
255                                 metrics.get(key), key));
256             }
257         }
258         CLog.logAndDisplay(
259                 LogLevel.INFO, "Successfully completed writing the metrics to JSON report.");
260     }
261 
262     /** Copy the report generated at temporary path to the given destination path in Results */
copyGeneratedReportToResultsDirectory()263     private void copyGeneratedReportToResultsDirectory() {
264         CLog.logAndDisplay(LogLevel.INFO, "Copying the report log to results directory.");
265         // Copy report log files to results dir.
266         try {
267             // Get Result Directory
268             File resultDir = mBuildHelper.getResultDir();
269             // Create a directory ( if it does not exist ) in results for report logs
270             if (mDestDir != null) {
271                 resultDir = new File(resultDir, mDestDir);
272             }
273             if (!resultDir.exists()) {
274                 resultDir.mkdirs();
275             }
276             if (!resultDir.isDirectory()) {
277                 CLog.logAndDisplay(
278                         LogLevel.ERROR,
279                         String.format("%s is not a directory", resultDir.getAbsolutePath()));
280                 return;
281             }
282             // Temp directory for report logs
283             final File hostReportDir = FileUtil.createNamedTempDir(mTempReportFolder);
284             if (!hostReportDir.isDirectory()) {
285                 CLog.logAndDisplay(
286                         LogLevel.ERROR,
287                         String.format("%s is not a directory", hostReportDir.getAbsolutePath()));
288                 return;
289             }
290             // Copy the report logs from temp directory and to the results directory
291             CollectorUtil.pullFromHost(hostReportDir, resultDir);
292             CollectorUtil.reformatRepeatedStreams(resultDir);
293             CLog.logAndDisplay(LogLevel.INFO, "Copying the report log completed successfully.");
294         } catch (IOException exception) {
295             CLog.logAndDisplay(LogLevel.ERROR, exception.getMessage());
296         }
297     }
298 
299     /** {@inheritDoc} */
300     @Override
invocationStarted(IInvocationContext context)301     public void invocationStarted(IInvocationContext context) {
302         mContext = context;
303         initializeReporterConfig();
304     }
305 
306     /** {@inheritDoc} */
307     @Override
invocationEnded(long elapsedTime)308     public void invocationEnded(long elapsedTime) {
309         // Copy the generated report to Results Directory
310         copyGeneratedReportToResultsDirectory();
311     }
312 
313     /** Overrides parent to explicitly to store test metrics */
314     @Override
testEnded(TestDescription testDescription, HashMap<String, Metric> metrics)315     public void testEnded(TestDescription testDescription, HashMap<String, Metric> metrics) {
316         // If metrics are available and aggregate-similar-metrics is set to true, store the metrics
317         if (metrics != null && !metrics.isEmpty() && mAggregateSimilarTests) {
318             // Store the metrics
319             mTestMetricsUtil.storeTestMetrics(testDescription, metrics);
320         }
321     }
322 
323     /** Overrides parent to explicitly to process and write metrics  */
324     @Override
testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics)325     public final void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
326         // If aggregate-similar-metrics is set to true, aggregate the metrics
327         if (mAggregateSimilarTests) {
328             // Aggregate Metrics for Similar Tests and write to the file
329             Map<String, Map<String, String>> aggregatedMetrics =
330                     mTestMetricsUtil.getAggregatedStoredTestMetrics();
331             for (String testName: aggregatedMetrics.keySet()) {
332                 writeTestMetrics(testName, aggregatedMetrics.get(testName));
333             }
334         }
335     }
336 
337     /** {@inheritDoc} */
338     @Override
testModuleStarted(IInvocationContext moduleContext)339     public void testModuleStarted(IInvocationContext moduleContext) {
340         mModuleContext = moduleContext;
341     }
342 
343     /** {@inheritDoc} */
344     @Override
testModuleEnded()345     public void testModuleEnded() {
346         mModuleContext = null;
347     }
348 }
349