Home | History | Annotate | Download | only in tests
      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