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