• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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     /** Re-initialize object to erase all existing test metrics */
reInitializeTestMetricsUtil()182     private void reInitializeTestMetricsUtil() {
183         mTestMetricsUtil = initializeTestMetricsUtil();
184         mTestMetricsUtil.setIterationSeparator(mTestIterationSeparator);
185     }
186 
187     /** Write Test Metrics to JSON */
writeTestMetrics( String classMethodName, Map<String, String> metrics)188     private void writeTestMetrics(
189             String classMethodName, Map<String, String> metrics) {
190 
191         // Use class method name as stream name if mapping is not provided
192         String streamName = classMethodName;
193         if (mReportTestNameMap != null && mReportTestNameMap.containsKey(classMethodName)) {
194             streamName = mReportTestNameMap.get(classMethodName);
195         }
196 
197         // Get ABI Info
198         String abiName = getAbiInfo();
199 
200         // Initialize Metrics Report Log
201         // TODO: b/194103027 [Remove MetricsReportLog dependency as it is being deprecated].
202         MetricsReportLog reportLog =
203                 new MetricsReportLog(
204                         mBuildInfo, abiName, classMethodName, mReportLogName, streamName);
205 
206         // Write Test Metrics in the Log
207         if (mReportAllMetrics) {
208             // Write all the metrics to the report
209             writeAllMetrics(reportLog, metrics);
210         } else {
211             // Write metrics for given keys to the report
212             writeMetricsForGivenKeys(reportLog, metrics);
213         }
214 
215         // Submit Report Log
216         reportLog.submit();
217     }
218 
219     /** Write all the metrics to JSON Report */
writeAllMetrics(MetricsReportLog reportLog, Map<String, String> metrics)220     private void writeAllMetrics(MetricsReportLog reportLog, Map<String, String> metrics) {
221         CLog.logAndDisplay(LogLevel.INFO, "Writing all the metrics to JSON report.");
222         for (String key : metrics.keySet()) {
223             try {
224                 double value = Double.parseDouble(metrics.get(key));
225                 reportLog.addValue(key, value, ResultType.NEUTRAL, ResultUnit.NONE);
226             } catch (NumberFormatException exception) {
227                 CLog.logAndDisplay(
228                         LogLevel.ERROR,
229                         String.format(
230                                 "Unable to parse value '%s' for '%s' metric key.",
231                                 metrics.get(key), key));
232             }
233         }
234         CLog.logAndDisplay(
235                 LogLevel.INFO, "Successfully completed writing the metrics to JSON report.");
236     }
237 
238     /** Write given set of metrics to JSON Report */
writeMetricsForGivenKeys( MetricsReportLog reportLog, Map<String, String> metrics)239     private void writeMetricsForGivenKeys(
240             MetricsReportLog reportLog, Map<String, String> metrics) {
241         CLog.logAndDisplay(LogLevel.INFO, "Writing given set of metrics to JSON report.");
242         if (mReportMetricKeyMap == null || mReportMetricKeyMap.isEmpty()) {
243             CLog.logAndDisplay(
244                     LogLevel.WARN, "Skip reporting metrics. Metric keys are not provided.");
245             return;
246         }
247         for (String key : mReportMetricKeyMap.keySet()) {
248             if (!metrics.containsKey(key) || metrics.get(key) == null) {
249                 CLog.logAndDisplay(LogLevel.WARN, String.format("%s metric key is missing.", key));
250                 continue;
251             }
252             try {
253                 double value = Double.parseDouble(metrics.get(key));
254                 reportLog.addValue(
255                         mReportMetricKeyMap.get(key), value, ResultType.NEUTRAL, ResultUnit.NONE);
256             } catch (NumberFormatException exception) {
257                 CLog.logAndDisplay(
258                         LogLevel.ERROR,
259                         String.format(
260                                 "Unable to parse value '%s' for '%s' metric key.",
261                                 metrics.get(key), key));
262             }
263         }
264         CLog.logAndDisplay(
265                 LogLevel.INFO, "Successfully completed writing the metrics to JSON report.");
266     }
267 
268     /** Copy the report generated at temporary path to the given destination path in Results */
copyGeneratedReportToResultsDirectory()269     private void copyGeneratedReportToResultsDirectory() {
270         CLog.logAndDisplay(LogLevel.INFO, "Copying the report log to results directory.");
271         // Copy report log files to results dir.
272         try {
273             // Get Result Directory
274             File resultDir = mBuildHelper.getResultDir();
275             // Create a directory ( if it does not exist ) in results for report logs
276             if (mDestDir != null) {
277                 resultDir = new File(resultDir, mDestDir);
278             }
279             if (!resultDir.exists()) {
280                 resultDir.mkdirs();
281             }
282             if (!resultDir.isDirectory()) {
283                 CLog.logAndDisplay(
284                         LogLevel.ERROR,
285                         String.format("%s is not a directory", resultDir.getAbsolutePath()));
286                 return;
287             }
288             // Temp directory for report logs
289             final File hostReportDir = FileUtil.createNamedTempDir(mTempReportFolder);
290             if (!hostReportDir.isDirectory()) {
291                 CLog.logAndDisplay(
292                         LogLevel.ERROR,
293                         String.format("%s is not a directory", hostReportDir.getAbsolutePath()));
294                 return;
295             }
296             // Copy the report logs from temp directory and to the results directory
297             CollectorUtil.pullFromHost(hostReportDir, resultDir);
298             CollectorUtil.reformatRepeatedStreams(resultDir);
299             CLog.logAndDisplay(LogLevel.INFO, "Copying the report log completed successfully.");
300         } catch (IOException exception) {
301             CLog.logAndDisplay(LogLevel.ERROR, exception.getMessage());
302         }
303     }
304 
305     /** {@inheritDoc} */
306     @Override
invocationStarted(IInvocationContext context)307     public void invocationStarted(IInvocationContext context) {
308         mContext = context;
309         initializeReporterConfig();
310     }
311 
312     /** {@inheritDoc} */
313     @Override
invocationEnded(long elapsedTime)314     public void invocationEnded(long elapsedTime) {
315         // Copy the generated report to Results Directory
316         copyGeneratedReportToResultsDirectory();
317     }
318 
319     /** Overrides parent to explicitly to store test metrics */
320     @Override
testEnded(TestDescription testDescription, HashMap<String, Metric> metrics)321     public void testEnded(TestDescription testDescription, HashMap<String, Metric> metrics) {
322         // If metrics are available and aggregate-similar-metrics is set to true, store the metrics
323         if (metrics != null && !metrics.isEmpty() && mAggregateSimilarTests) {
324             // Store the metrics
325             mTestMetricsUtil.storeTestMetrics(testDescription, metrics);
326         }
327     }
328 
329     /** Overrides parent to explicitly to process and write metrics  */
330     @Override
testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics)331     public final void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
332         // If aggregate-similar-metrics is set to true, aggregate the metrics
333         if (mAggregateSimilarTests) {
334             // Aggregate Metrics for Similar Tests and write to the file
335             Map<String, Map<String, String>> aggregatedMetrics =
336                     mTestMetricsUtil.getAggregatedStoredTestMetrics();
337             for (String testName: aggregatedMetrics.keySet()) {
338                 writeTestMetrics(testName, aggregatedMetrics.get(testName));
339             }
340         }
341 
342         // Avoid reporting duplicate metrics by erasing metrics from previous runs
343         reInitializeTestMetricsUtil();
344     }
345 
346     /** {@inheritDoc} */
347     @Override
testModuleStarted(IInvocationContext moduleContext)348     public void testModuleStarted(IInvocationContext moduleContext) {
349         mModuleContext = moduleContext;
350     }
351 
352     /** {@inheritDoc} */
353     @Override
testModuleEnded()354     public void testModuleEnded() {
355         mModuleContext = null;
356     }
357 }
358