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