1 /* 2 * Copyright (C) 2015 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 org.drrickorang.loopback; 18 19 import android.util.Log; 20 21 22 /** 23 * This class is used to automatically the audio performance according to recorder/player buffer 24 * period. 25 */ 26 27 public class PerformanceMeasurement { 28 public static final String TAG = "PerformanceMeasurement"; 29 30 // this is used to enlarge the benchmark, so that it can be displayed with better accuracy on 31 // the dashboard 32 private static final int mMultiplicationFactor = 10000; 33 34 private int mExpectedBufferPeriodMs; 35 private int[] mBufferData; 36 private int mTotalOccurrence; 37 38 // used to determine buffer sizes mismatch 39 private static final double mPercentOccurrenceThreshold = 0.95; 40 // used to count the number of outliers 41 private static final int mOutliersThreshold = 3; 42 43 44 /** 45 * Note: if mBufferSize * Constant.MILLIS_PER_SECOND / mSamplingRate == Integer is satisfied, 46 * the measurement will be more accurate, but this is not necessary. 47 */ 48 public PerformanceMeasurement(int expectedBufferPeriod, int[] bufferData) { 49 mBufferData = bufferData; 50 51 mTotalOccurrence = 0; 52 for (int i = 0; i < mBufferData.length; i++) { 53 mTotalOccurrence += mBufferData[i]; 54 } 55 56 mExpectedBufferPeriodMs = expectedBufferPeriod; 57 } 58 59 60 /** 61 * Measure the performance according to the collected buffer period. 62 * First, determine if there is a buffer sizes mismatch. If there is, then the performance 63 * measurement should be disregarded since it won't be accurate. If there isn't a mismatch, 64 * then a benchmark and a count on outliers can be produced as a measurement of performance. 65 * The benchmark should be as small as possible, so is the number of outliers. 66 * Note: This is a wrapper method that calls different methods and prints their results. It is 67 * also possible to call individual method to obtain specific result. 68 * Note: Should try to compare the number of outliers with the number of glitches and see if 69 * they match. 70 */ 71 public void measurePerformance() { 72 // calculate standard deviation and mean of mBufferData 73 double mean = computeMean(mBufferData); 74 double standardDeviation = computeStandardDeviation(mBufferData, mean); 75 log("mean before discarding 99% data: " + mean); 76 log("standard deviation before discarding 99% data: " + standardDeviation); 77 log("stdev/mean before discarding 99% data: " + (standardDeviation / mean)); 78 79 // calculate standard deviation and mean of dataAfterDiscard 80 int[] dataAfterDiscard = computeDataAfterDiscard(mBufferData); 81 double meanAfterDiscard = computeMean(dataAfterDiscard); 82 double standardDeviationAfterDiscard = computeStandardDeviation(dataAfterDiscard, 83 meanAfterDiscard); 84 log("mean after discarding 99% data: " + meanAfterDiscard); 85 log("standard deviation after discarding 99% data: " + standardDeviationAfterDiscard); 86 log("stdev/mean after discarding 99% data: " + (standardDeviationAfterDiscard / 87 meanAfterDiscard)); 88 log("percent difference between two means: " + (Math.abs(meanAfterDiscard - mean) / mean)); 89 90 // determine if there's a buffer sizes mismatch 91 boolean isBufferSizesMismatch = 92 percentBufferPeriodsAtExpected() > mPercentOccurrenceThreshold; 93 94 // compute benchmark and count the number of outliers 95 double benchmark = computeWeightedBenchmark(); 96 int outliers = countOutliers(); 97 98 log("total occurrence: " + mTotalOccurrence); 99 log("buffer size mismatch: " + isBufferSizesMismatch); 100 log("benchmark: " + benchmark); 101 log("number of outliers: " + outliers); 102 log("expected buffer period: " + mExpectedBufferPeriodMs + " ms"); 103 int maxPeriod = (mBufferData.length - 1); 104 log("max buffer period: " + maxPeriod + " ms"); 105 } 106 107 108 /** 109 * Determine percent of Buffer Period Callbacks that occurred at the expected time 110 * Returns a value between 0 and 1 111 */ 112 public float percentBufferPeriodsAtExpected() { 113 int occurrenceNearExpectedBufferPeriod = 0; 114 // indicate how many buckets around mExpectedBufferPeriod do we want to add to the count 115 int acceptableOffset = 2; 116 int start = Math.max(0, mExpectedBufferPeriodMs - acceptableOffset); 117 int end = Math.min(mBufferData.length - 1, mExpectedBufferPeriodMs + acceptableOffset); 118 // include the next bucket too because the period is rounded up 119 for (int i = start; i <= end; i++) { 120 occurrenceNearExpectedBufferPeriod += mBufferData[i]; 121 } 122 return ((float) occurrenceNearExpectedBufferPeriod) / mTotalOccurrence; 123 } 124 125 126 /** 127 * Compute a benchmark using the following formula: 128 * (1/totalOccurrence) sum_i(|i - expectedBufferPeriod|^2 * occurrence_i / expectedBufferPeriod) 129 * , for i < expectedBufferPeriod * mOutliersThreshold 130 * Also, the benchmark is additionally multiplied by mMultiplicationFactor. This is not in the 131 * original formula, and it is used only because the original benchmark will be too small to 132 * be displayed accurately on the dashboard. 133 */ 134 public double computeWeightedBenchmark() { 135 double weightedCount = 0; 136 double weight; 137 double benchmark; 138 139 // don't count mExpectedBufferPeriodMs + 1 towards benchmark, cause this beam may be large 140 // due to rounding issue (all results are rounded up when collecting buffer period.) 141 int threshold = Math.min(mBufferData.length, mExpectedBufferPeriodMs * mOutliersThreshold); 142 for (int i = 0; i < threshold; i++) { 143 if (mBufferData[i] != 0 && (i != mExpectedBufferPeriodMs + 1)) { 144 weight = Math.abs(i - mExpectedBufferPeriodMs); 145 weight *= weight; // squared 146 weightedCount += weight * mBufferData[i]; 147 } 148 } 149 weightedCount /= mExpectedBufferPeriodMs; 150 151 benchmark = (weightedCount / mTotalOccurrence) * mMultiplicationFactor; 152 return benchmark; 153 } 154 155 156 /** 157 * All occurrence that happens after (mExpectedBufferPeriodMs * mOutliersThreshold) ms, will 158 * be considered as outliers. 159 */ 160 public int countOutliers() { 161 int outliersThresholdInMs = mExpectedBufferPeriodMs * mOutliersThreshold; 162 int outliersCount = 0; 163 for (int i = outliersThresholdInMs; i < mBufferData.length; i++) { 164 outliersCount += mBufferData[i]; 165 } 166 return outliersCount; 167 } 168 169 170 /** 171 * Output an array that has discarded 99 % of the data in the middle. In this array, 172 * data[i] = x means there are x occurrences of value i. 173 */ 174 private int[] computeDataAfterDiscard(int[] data) { 175 // calculate the total amount of data 176 int totalCount = 0; 177 int length = data.length; 178 for (int i = 0; i < length; i++) { 179 totalCount += data[i]; 180 } 181 182 // we only want to keep a certain percent of data at the bottom and top 183 final double percent = 0.005; 184 int bar = (int) (totalCount * percent); 185 if (bar == 0) { // at least keep the lowest and highest data 186 bar = 1; 187 } 188 int count = 0; 189 int[] dataAfterDiscard = new int[length]; 190 191 // for bottom data 192 for (int i = 0; i < length; i++) { 193 if (count > bar) { 194 break; 195 } else if (count + data[i] > bar) { 196 dataAfterDiscard[i] += bar - count; 197 break; 198 } else { 199 dataAfterDiscard[i] += data[i]; 200 count += data[i]; 201 } 202 } 203 204 // for top data 205 count = 0; 206 for (int i = length - 1; i >= 0; i--) { 207 if (count > bar) { 208 break; 209 } else if (count + data[i] > bar) { 210 dataAfterDiscard[i] += bar - count; 211 break; 212 } else { 213 dataAfterDiscard[i] += data[i]; 214 count += data[i]; 215 } 216 } 217 218 return dataAfterDiscard; 219 } 220 221 222 /** 223 * Calculate the mean of int array "data". In this array, data[i] = x means there are 224 * x occurrences of value i. 225 */ 226 private double computeMean(int[] data) { 227 int count = 0; 228 int sum = 0; 229 for (int i = 0; i < data.length; i++) { 230 count += data[i]; 231 sum += data[i] * i; 232 } 233 234 double mean; 235 if (count != 0) { 236 mean = (double) sum / count; 237 } else { 238 mean = 0; 239 log("zero count!"); 240 } 241 242 return mean; 243 } 244 245 246 /** 247 * Calculate the standard deviation of int array "data". In this array, data[i] = x means 248 * there are x occurrences of value i. 249 */ 250 private double computeStandardDeviation(int[] data, double mean) { 251 double sumDeviation = 0; 252 int count = 0; 253 double standardDeviation; 254 255 for (int i = 0; i < data.length; i++) { 256 if (data[i] != 0) { 257 count += data[i]; 258 sumDeviation += (i - mean) * (i - mean) * data[i]; 259 } 260 } 261 262 standardDeviation = Math.sqrt(sumDeviation / (count - 1)); 263 return standardDeviation; 264 } 265 266 267 private static void log(String msg) { 268 Log.v(TAG, msg); 269 } 270 271 } 272