1 /* 2 * Copyright (C) 2017 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 package com.android.media.tests; 17 18 import com.android.media.tests.AudioLoopbackImageAnalyzer.Result; 19 import com.android.tradefed.device.DeviceNotAvailableException; 20 import com.android.tradefed.device.ITestDevice; 21 import com.android.tradefed.log.LogUtil.CLog; 22 import com.android.tradefed.util.Pair; 23 24 import com.google.common.io.Files; 25 26 import java.io.BufferedReader; 27 import java.io.File; 28 import java.io.FileNotFoundException; 29 import java.io.IOException; 30 import java.io.PrintWriter; 31 import java.io.UnsupportedEncodingException; 32 import java.nio.charset.StandardCharsets; 33 import java.time.Instant; 34 import java.util.ArrayList; 35 import java.util.Arrays; 36 import java.util.Collections; 37 import java.util.Comparator; 38 import java.util.HashMap; 39 import java.util.List; 40 import java.util.Map; 41 42 /** Helper class for AudioLoopbackTest. It keeps runtime data, analytics, */ 43 public class AudioLoopbackTestHelper { 44 45 private StatisticsData mLatencyStats = null; 46 private StatisticsData mConfidenceStats = null; 47 private ArrayList<ResultData> mAllResults; 48 private ArrayList<ResultData> mGoodResults = new ArrayList<ResultData>(); 49 private ArrayList<ResultData> mBadResults = new ArrayList<ResultData>(); 50 private ArrayList<Map<String, String>> mResultDictionaries = 51 new ArrayList<Map<String, String>>(); 52 53 // Controls acceptable tolerance in ms around median latency 54 private static final double TOLERANCE = 2.0; 55 56 //=================================================================== 57 // ENUMS 58 //=================================================================== 59 public enum LogFileType { 60 RESULT, 61 WAVE, 62 GRAPH, 63 PLAYER_BUFFER, 64 PLAYER_BUFFER_HISTOGRAM, 65 PLAYER_BUFFER_PERIOD_TIMES, 66 RECORDER_BUFFER, 67 RECORDER_BUFFER_HISTOGRAM, 68 RECORDER_BUFFER_PERIOD_TIMES, 69 GLITCHES_MILLIS, 70 HEAT_MAP, 71 LOGCAT 72 } 73 74 //=================================================================== 75 // INNER CLASSES 76 //=================================================================== 77 private class StatisticsData { 78 double mMin = 0; 79 double mMax = 0; 80 double mMean = 0; 81 double mMedian = 0; 82 83 @Override 84 public String toString() { 85 return String.format( 86 "min = %1$f, max = %2$f, median=%3$f, mean = %4$f", mMin, mMax, mMedian, mMean); 87 } 88 } 89 90 /** ResultData is an inner class that holds results and logfile info from each test run */ 91 public static class ResultData { 92 private Float mLatencyMs; 93 private Float mLatencyConfidence; 94 private Integer mAudioLevel; 95 private Integer mIteration; 96 private Long mDeviceTestStartTime; 97 private boolean mIsTimedOut = false; 98 private HashMap<LogFileType, String> mLogs = new HashMap<LogFileType, String>(); 99 private Result mImageAnalyzerResult = Result.UNKNOWN; 100 private String mFailureReason = null; 101 102 // Optional 103 private Float mPeriodConfidence = Float.valueOf(0.0f); 104 private Float mRms = Float.valueOf(0.0f); 105 private Float mRmsAverage = Float.valueOf(0.0f); 106 private Integer mBblockSize = Integer.valueOf(0); 107 108 public float getLatency() { 109 return mLatencyMs.floatValue(); 110 } 111 112 public void setLatency(float latencyMs) { 113 this.mLatencyMs = Float.valueOf(latencyMs); 114 } 115 116 public float getConfidence() { 117 return mLatencyConfidence.floatValue(); 118 } 119 120 public void setConfidence(float latencyConfidence) { 121 this.mLatencyConfidence = Float.valueOf(latencyConfidence); 122 } 123 124 public float getPeriodConfidence() { 125 return mPeriodConfidence.floatValue(); 126 } 127 128 public void setPeriodConfidence(float periodConfidence) { 129 this.mPeriodConfidence = Float.valueOf(periodConfidence); 130 } 131 132 public float getRMS() { 133 return mRms.floatValue(); 134 } 135 136 public void setRMS(float rms) { 137 this.mRms = Float.valueOf(rms); 138 } 139 140 public float getRMSAverage() { 141 return mRmsAverage.floatValue(); 142 } 143 144 public void setRMSAverage(float rmsAverage) { 145 this.mRmsAverage = Float.valueOf(rmsAverage); 146 } 147 148 public int getAudioLevel() { 149 return mAudioLevel.intValue(); 150 } 151 152 public void setAudioLevel(int audioLevel) { 153 this.mAudioLevel = Integer.valueOf(audioLevel); 154 } 155 156 public int getBlockSize() { 157 return mBblockSize.intValue(); 158 } 159 160 public void setBlockSize(int blockSize) { 161 this.mBblockSize = Integer.valueOf(blockSize); 162 } 163 164 public int getIteration() { 165 return mIteration.intValue(); 166 } 167 168 public void setIteration(int iteration) { 169 this.mIteration = Integer.valueOf(iteration); 170 } 171 172 public long getDeviceTestStartTime() { 173 return mDeviceTestStartTime.longValue(); 174 } 175 176 public void setDeviceTestStartTime(long deviceTestStartTime) { 177 this.mDeviceTestStartTime = Long.valueOf(deviceTestStartTime); 178 } 179 180 public Result getImageAnalyzerResult() { 181 return mImageAnalyzerResult; 182 } 183 184 public void setImageAnalyzerResult(Result imageAnalyzerResult) { 185 this.mImageAnalyzerResult = imageAnalyzerResult; 186 } 187 188 public String getFailureReason() { 189 return mFailureReason; 190 } 191 192 public void setFailureReason(String failureReason) { 193 this.mFailureReason = failureReason; 194 } 195 196 public boolean isTimedOut() { 197 return mIsTimedOut; 198 } 199 200 public void setIsTimedOut(boolean isTimedOut) { 201 this.mIsTimedOut = isTimedOut; 202 } 203 204 public String getLogFile(LogFileType log) { 205 return mLogs.get(log); 206 } 207 208 public void setLogFile(LogFileType log, String filename) { 209 CLog.i("setLogFile: type=" + log.name() + ", filename=" + filename); 210 if (!mLogs.containsKey(log) && filename != null && !filename.isEmpty()) { 211 mLogs.put(log, filename); 212 } 213 } 214 215 public boolean hasBadResults() { 216 return hasTimedOut() 217 || hasNoTestResults() 218 || hasNoLatencyResult() 219 || hasNoLatencyConfidence() 220 || mImageAnalyzerResult == Result.FAIL; 221 } 222 223 public boolean hasTimedOut() { 224 return mIsTimedOut; 225 } 226 227 public boolean hasLogFile(LogFileType log) { 228 return mLogs.containsKey(log); 229 } 230 231 public boolean hasNoLatencyResult() { 232 return mLatencyMs == null; 233 } 234 235 public boolean hasNoLatencyConfidence() { 236 return mLatencyConfidence == null; 237 } 238 239 public boolean hasNoTestResults() { 240 return hasNoLatencyConfidence() && hasNoLatencyResult(); 241 } 242 243 public static Comparator<ResultData> latencyComparator = 244 new Comparator<ResultData>() { 245 @Override 246 public int compare(ResultData o1, ResultData o2) { 247 return o1.mLatencyMs.compareTo(o2.mLatencyMs); 248 } 249 }; 250 251 public static Comparator<ResultData> confidenceComparator = 252 new Comparator<ResultData>() { 253 @Override 254 public int compare(ResultData o1, ResultData o2) { 255 return o1.mLatencyConfidence.compareTo(o2.mLatencyConfidence); 256 } 257 }; 258 259 public static Comparator<ResultData> iteratorComparator = 260 new Comparator<ResultData>() { 261 @Override 262 public int compare(ResultData o1, ResultData o2) { 263 return Integer.compare(o1.mIteration, o2.mIteration); 264 } 265 }; 266 267 @Override 268 public String toString() { 269 final String NL = "\n"; 270 final StringBuilder sb = new StringBuilder(512); 271 sb.append("{").append(NL); 272 sb.append("{\nlatencyMs=").append(mLatencyMs).append(NL); 273 sb.append("latencyConfidence=").append(mLatencyConfidence).append(NL); 274 sb.append("isTimedOut=").append(mIsTimedOut).append(NL); 275 sb.append("iteration=").append(mIteration).append(NL); 276 sb.append("logs=").append(Arrays.toString(mLogs.values().toArray())).append(NL); 277 sb.append("audioLevel=").append(mAudioLevel).append(NL); 278 sb.append("deviceTestStartTime=").append(mDeviceTestStartTime).append(NL); 279 sb.append("rms=").append(mRms).append(NL); 280 sb.append("rmsAverage=").append(mRmsAverage).append(NL); 281 sb.append("}").append(NL); 282 return sb.toString(); 283 } 284 } 285 286 public AudioLoopbackTestHelper(int iterations) { 287 mAllResults = new ArrayList<ResultData>(iterations); 288 } 289 290 public void addTestData(ResultData data, Map<String, String> resultDictionary) { 291 mResultDictionaries.add(data.getIteration(), resultDictionary); 292 mAllResults.add(data); 293 294 // Analyze captured screenshot to see if wave form is within reason 295 final String screenshot = data.getLogFile(LogFileType.GRAPH); 296 final Pair<Result, String> result = AudioLoopbackImageAnalyzer.analyzeImage(screenshot); 297 data.setImageAnalyzerResult(result.first); 298 data.setFailureReason(result.second); 299 } 300 301 public final List<ResultData> getAllTestData() { 302 return mAllResults; 303 } 304 305 public Map<String, String> getResultDictionaryForIteration(int i) { 306 return mResultDictionaries.get(i); 307 } 308 309 /** 310 * Returns a list of the worst test result objects, up to maxNrOfWorstResults 311 * 312 * <p> 313 * 314 * <ol> 315 * <li> Tests in the bad results list are added first 316 * <li> If still space, add test results based on low confidence and then tests that are 317 * outside tolerance boundaries 318 * </ol> 319 * 320 * @param maxNrOfWorstResults 321 * @return list of worst test result objects 322 */ 323 public List<ResultData> getWorstResults(int maxNrOfWorstResults) { 324 int counter = 0; 325 final ArrayList<ResultData> worstResults = new ArrayList<ResultData>(maxNrOfWorstResults); 326 327 for (final ResultData data : mBadResults) { 328 if (counter < maxNrOfWorstResults) { 329 worstResults.add(data); 330 counter++; 331 } 332 } 333 334 for (final ResultData data : mGoodResults) { 335 if (counter < maxNrOfWorstResults) { 336 boolean failed = false; 337 if (data.getConfidence() < 1.0f) { 338 data.setFailureReason("Low confidence"); 339 failed = true; 340 } else if (data.getLatency() < (mLatencyStats.mMedian - TOLERANCE) 341 || data.getLatency() > (mLatencyStats.mMedian + TOLERANCE)) { 342 data.setFailureReason("Latency not within tolerance from median"); 343 failed = true; 344 } 345 346 if (failed) { 347 worstResults.add(data); 348 counter++; 349 } 350 } 351 } 352 353 return worstResults; 354 } 355 356 public static Map<String, String> parseKeyValuePairFromFile( 357 File result, 358 final Map<String, String> dictionary, 359 final String resultKeyPrefix, 360 final String splitOn, 361 final String keyValueFormat) 362 throws IOException { 363 364 final Map<String, String> resultMap = new HashMap<String, String>(); 365 final BufferedReader br = Files.newReader(result, StandardCharsets.UTF_8); 366 367 try { 368 String line = br.readLine(); 369 while (line != null) { 370 line = line.trim().replaceAll(" +", " "); 371 final String[] tokens = line.split(splitOn); 372 if (tokens.length >= 2) { 373 final String key = tokens[0].trim(); 374 final String value = tokens[1].trim(); 375 if (dictionary.containsKey(key)) { 376 CLog.i(String.format(keyValueFormat, key, value)); 377 resultMap.put(resultKeyPrefix + dictionary.get(key), value); 378 } 379 } 380 line = br.readLine(); 381 } 382 } finally { 383 br.close(); 384 } 385 return resultMap; 386 } 387 388 public int processTestData() { 389 390 // Collect statistics about the test run 391 int nrOfValidResults = 0; 392 double sumLatency = 0; 393 double sumConfidence = 0; 394 395 final int totalNrOfTests = mAllResults.size(); 396 mLatencyStats = new StatisticsData(); 397 mConfidenceStats = new StatisticsData(); 398 mBadResults = new ArrayList<ResultData>(); 399 mGoodResults = new ArrayList<ResultData>(totalNrOfTests); 400 401 // Copy all results into Good results list 402 mGoodResults.addAll(mAllResults); 403 404 for (final ResultData data : mAllResults) { 405 if (data.hasBadResults()) { 406 mBadResults.add(data); 407 continue; 408 } 409 // Get mean values 410 sumLatency += data.getLatency(); 411 sumConfidence += data.getConfidence(); 412 } 413 414 if (!mBadResults.isEmpty()) { 415 analyzeBadResults(mBadResults, mAllResults.size()); 416 } 417 418 // Remove bad runs from result array 419 mGoodResults.removeAll(mBadResults); 420 421 // Fail test immediately if we don't have ANY good results 422 if (mGoodResults.isEmpty()) { 423 return 0; 424 } 425 426 nrOfValidResults = mGoodResults.size(); 427 428 // ---- LATENCY: Get Median, Min and Max values ---- 429 Collections.sort(mGoodResults, ResultData.latencyComparator); 430 431 mLatencyStats.mMin = mGoodResults.get(0).mLatencyMs; 432 mLatencyStats.mMax = mGoodResults.get(nrOfValidResults - 1).mLatencyMs; 433 mLatencyStats.mMean = sumLatency / nrOfValidResults; 434 // Is array even or odd numbered 435 if (nrOfValidResults % 2 == 0) { 436 final int middle = nrOfValidResults / 2; 437 final float middleLeft = mGoodResults.get(middle - 1).mLatencyMs; 438 final float middleRight = mGoodResults.get(middle).mLatencyMs; 439 mLatencyStats.mMedian = (middleLeft + middleRight) / 2.0f; 440 } else { 441 // It's and odd numbered array, just grab the middle value 442 mLatencyStats.mMedian = mGoodResults.get(nrOfValidResults / 2).mLatencyMs; 443 } 444 445 // ---- CONFIDENCE: Get Median, Min and Max values ---- 446 Collections.sort(mGoodResults, ResultData.confidenceComparator); 447 448 mConfidenceStats.mMin = mGoodResults.get(0).mLatencyConfidence; 449 mConfidenceStats.mMax = mGoodResults.get(nrOfValidResults - 1).mLatencyConfidence; 450 mConfidenceStats.mMean = sumConfidence / nrOfValidResults; 451 // Is array even or odd numbered 452 if (nrOfValidResults % 2 == 0) { 453 final int middle = nrOfValidResults / 2; 454 final float middleLeft = mGoodResults.get(middle - 1).mLatencyConfidence; 455 final float middleRight = mGoodResults.get(middle).mLatencyConfidence; 456 mConfidenceStats.mMedian = (middleLeft + middleRight) / 2.0f; 457 } else { 458 // It's and odd numbered array, just grab the middle value 459 mConfidenceStats.mMedian = mGoodResults.get(nrOfValidResults / 2).mLatencyConfidence; 460 } 461 462 for (final ResultData data : mGoodResults) { 463 // Check if within Latency Tolerance 464 if (data.getConfidence() < 1.0f) { 465 data.setFailureReason("Low confidence"); 466 } else if (data.getLatency() < (mLatencyStats.mMedian - TOLERANCE) 467 || data.getLatency() > (mLatencyStats.mMedian + TOLERANCE)) { 468 data.setFailureReason("Latency not within tolerance from median"); 469 } 470 } 471 472 // Create histogram 473 // Strategy: Create buckets based on whole ints, like 16 ms, 17 ms, 18 ms etc. Count how 474 // many tests fall into each bucket. Just cast the float to an int, no rounding up/down 475 // required. 476 final int[] histogram = new int[(int) mLatencyStats.mMax + 1]; 477 for (final ResultData rd : mGoodResults) { 478 // Increase value in bucket 479 histogram[(int) (rd.mLatencyMs.floatValue())]++; 480 } 481 482 CLog.i("========== VALID RESULTS ============================================"); 483 CLog.i(String.format("Valid tests: %1$d of %2$d", nrOfValidResults, totalNrOfTests)); 484 CLog.i("Latency: " + mLatencyStats.toString()); 485 CLog.i("Confidence: " + mConfidenceStats.toString()); 486 CLog.i("========== HISTOGRAM ================================================"); 487 for (int i = 0; i < histogram.length; i++) { 488 if (histogram[i] > 0) { 489 CLog.i(String.format("%1$01d ms => %2$d", i, histogram[i])); 490 } 491 } 492 493 // VERIFY the good results by running image analysis on the 494 // screenshot of the incoming audio waveform 495 496 return nrOfValidResults; 497 } 498 499 public void writeAllResultsToCSVFile(File csvFile, ITestDevice device) 500 throws DeviceNotAvailableException, FileNotFoundException, 501 UnsupportedEncodingException { 502 503 final String deviceType = device.getProperty("ro.build.product"); 504 final String buildId = device.getBuildAlias(); 505 final String serialNumber = device.getSerialNumber(); 506 507 // Sort data on iteration 508 Collections.sort(mAllResults, ResultData.iteratorComparator); 509 510 final StringBuilder sb = new StringBuilder(256); 511 final PrintWriter writer = new PrintWriter(csvFile, StandardCharsets.UTF_8.name()); 512 final String SEPARATOR = ","; 513 514 // Write column labels 515 writer.println( 516 "Device Time,Device Type,Build Id,Serial Number,Iteration,Latency," 517 + "Confidence,Period Confidence,Block Size,Audio Level,RMS,RMS Average," 518 + "Image Analysis,Failure Reason"); 519 for (final ResultData data : mAllResults) { 520 final Instant instant = Instant.ofEpochSecond(data.mDeviceTestStartTime); 521 522 sb.append(instant).append(SEPARATOR); 523 sb.append(deviceType).append(SEPARATOR); 524 sb.append(buildId).append(SEPARATOR); 525 sb.append(serialNumber).append(SEPARATOR); 526 sb.append(data.getIteration()).append(SEPARATOR); 527 sb.append(data.getLatency()).append(SEPARATOR); 528 sb.append(data.getConfidence()).append(SEPARATOR); 529 sb.append(data.getPeriodConfidence()).append(SEPARATOR); 530 sb.append(data.getBlockSize()).append(SEPARATOR); 531 sb.append(data.getAudioLevel()).append(SEPARATOR); 532 sb.append(data.getRMS()).append(SEPARATOR); 533 sb.append(data.getRMSAverage()).append(SEPARATOR); 534 sb.append(data.getImageAnalyzerResult().name()).append(SEPARATOR); 535 sb.append(data.getFailureReason()); 536 537 writer.println(sb.toString()); 538 539 sb.setLength(0); 540 } 541 writer.close(); 542 } 543 544 private void analyzeBadResults(ArrayList<ResultData> badResults, int totalNrOfTests) { 545 int testNoData = 0; 546 int testTimeoutCounts = 0; 547 int testResultsNotFoundCounts = 0; 548 int testWithoutLatencyResultCount = 0; 549 int testWithoutConfidenceResultCount = 0; 550 551 for (final ResultData data : badResults) { 552 if (data.hasTimedOut()) { 553 testTimeoutCounts++; 554 testNoData++; 555 continue; 556 } 557 558 if (data.hasNoTestResults()) { 559 testResultsNotFoundCounts++; 560 testNoData++; 561 continue; 562 } 563 564 if (data.hasNoLatencyResult()) { 565 testWithoutLatencyResultCount++; 566 testNoData++; 567 continue; 568 } 569 570 if (data.hasNoLatencyConfidence()) { 571 testWithoutConfidenceResultCount++; 572 testNoData++; 573 continue; 574 } 575 } 576 577 CLog.i("========== BAD RESULTS ============================================"); 578 CLog.i(String.format("No Data: %1$d of %2$d", testNoData, totalNrOfTests)); 579 CLog.i(String.format("Timed out: %1$d of %2$d", testTimeoutCounts, totalNrOfTests)); 580 CLog.i( 581 String.format( 582 "No results: %1$d of %2$d", testResultsNotFoundCounts, totalNrOfTests)); 583 CLog.i( 584 String.format( 585 "No Latency results: %1$d of %2$d", 586 testWithoutLatencyResultCount, totalNrOfTests)); 587 CLog.i( 588 String.format( 589 "No Confidence results: %1$d of %2$d", 590 testWithoutConfidenceResultCount, totalNrOfTests)); 591 } 592 593 /** Generates metrics dictionary for stress test */ 594 public void populateStressTestMetrics( 595 Map<String, String> metrics, final String resultKeyPrefix) { 596 metrics.put(resultKeyPrefix + "total_nr_of_tests", Integer.toString(mAllResults.size())); 597 metrics.put(resultKeyPrefix + "nr_of_good_tests", Integer.toString(mGoodResults.size())); 598 metrics.put(resultKeyPrefix + "latency_max", Double.toString(mLatencyStats.mMax)); 599 metrics.put(resultKeyPrefix + "latency_min", Double.toString(mLatencyStats.mMin)); 600 metrics.put(resultKeyPrefix + "latency_mean", Double.toString(mLatencyStats.mMean)); 601 metrics.put(resultKeyPrefix + "latency_median", Double.toString(mLatencyStats.mMedian)); 602 metrics.put(resultKeyPrefix + "confidence_max", Double.toString(mConfidenceStats.mMax)); 603 metrics.put(resultKeyPrefix + "confidence_min", Double.toString(mConfidenceStats.mMin)); 604 metrics.put(resultKeyPrefix + "confidence_mean", Double.toString(mConfidenceStats.mMean)); 605 metrics.put( 606 resultKeyPrefix + "confidence_median", Double.toString(mConfidenceStats.mMedian)); 607 } 608 } 609