1 /* 2 * Copyright (C) 2019 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.server.wifi.util; 18 19 import android.annotation.NonNull; 20 import android.util.SparseIntArray; 21 22 import com.android.server.wifi.proto.nano.WifiMetricsProto.HistogramBucketInt32; 23 24 import java.lang.reflect.Array; 25 import java.util.Arrays; 26 import java.util.Iterator; 27 28 29 /** 30 * A histogram that stores the counts of values that fall within buckets, where buckets contain 31 * the count for a range of values. This implementation is backed by a SparseIntArray, meaning 32 * values are stored as int and counts are stored as int. 33 */ 34 public class IntHistogram implements Iterable<IntHistogram.Bucket> { 35 /* 36 * Definitions: 37 * - value: what you would like to count 38 * - count: the number of occurrences of values that fall within a bucket 39 * - key: mBuckets maps key => count, 0 <= key <= mBucketBoundaries.length, keys can be 40 * uninitialized for buckets that were never incremented 41 * - bucketIndex: the index of the initialized buckets, 0 <= bucketIndex < mBucket.size(), 42 * all indices in this range are initialized. 43 */ 44 private SparseIntArray mBuckets; 45 private final int[] mBucketBoundaries; 46 47 /** 48 * A bucket in the histogram, for the range [start, end). 49 * An exception to this is when {@link Bucket#end} == Integer.MAX_VALUE, when the range will be 50 * [start, end]. 51 */ 52 public static class Bucket { 53 public int start; 54 public int end; 55 public int count; 56 Bucket(int start, int end, int count)57 public Bucket(int start, int end, int count) { 58 this.start = start; 59 this.end = end; 60 this.count = count; 61 } 62 } 63 64 /** 65 * Constructs a histogram given the boundary values of buckets, as an int[]. 66 * @param bucketBoundaries The boundary values that separate each bucket. The number of 67 * buckets is bucketBoundaries.length + 1, where the buckets are: 68 * - < bucketBoundaries[0] 69 * - [bucketBoundaries[0], bucketBoundaries[1]) 70 * ... 71 * - >= bucketBoundaries[bucketBoundaries.length() - 1] 72 * This array must be non-null, non-empty, and strictly monotonically 73 * increasing i.e. a[i] < a[i+1]. 74 */ IntHistogram(@onNull int[] bucketBoundaries)75 public IntHistogram(@NonNull int[] bucketBoundaries) { 76 if (bucketBoundaries == null || bucketBoundaries.length == 0) { 77 throw new IllegalArgumentException("bucketBoundaries must be non-null and non-empty!"); 78 } 79 for (int i = 0; i < bucketBoundaries.length - 1; i++) { 80 int cur = bucketBoundaries[i]; 81 int next = bucketBoundaries[i + 1]; 82 if (cur >= next) { 83 throw new IllegalArgumentException(String.format( 84 "bucketBoundaries values must be strictly monotonically increasing, but " 85 + "value %d at index %d is greater or equal to " 86 + "value %d at index %d!", 87 cur, i, next, i + 1)); 88 } 89 } 90 mBucketBoundaries = bucketBoundaries.clone(); 91 mBuckets = new SparseIntArray(); 92 } 93 94 /** 95 * Resets this histogram to the initial state. 96 */ clear()97 public void clear() { 98 mBuckets.clear(); 99 } 100 101 /** 102 * Returns the number of non-empty buckets, where an empty bucket is a bucket that was never 103 * added to. 104 */ numNonEmptyBuckets()105 public int numNonEmptyBuckets() { 106 return mBuckets.size(); 107 } 108 109 /** 110 * Returns the maximum number of possible buckets (dictated by the number of bucket boundaries). 111 */ numTotalBuckets()112 public int numTotalBuckets() { 113 return mBucketBoundaries.length + 1; 114 } 115 116 /** 117 * Gets the nth non-empty bucket, where 0 <= n < {@link #numNonEmptyBuckets()} 118 */ getBucketByIndex(int bucketIndex)119 public Bucket getBucketByIndex(int bucketIndex) { 120 int bucketKey = mBuckets.keyAt(bucketIndex); 121 int start = bucketKey == 0 122 ? Integer.MIN_VALUE : mBucketBoundaries[bucketKey - 1]; 123 int end = bucketKey == mBucketBoundaries.length 124 ? Integer.MAX_VALUE : mBucketBoundaries[bucketKey]; 125 int count = mBuckets.valueAt(bucketIndex); 126 return new Bucket(start, end, count); 127 } 128 129 /** 130 * Increments the count of the bucket that this value falls into by 1. 131 */ increment(int value)132 public void increment(int value) { 133 add(value, 1); 134 } 135 136 /** 137 * Increments the count of the bucket that this value falls into by <code>count</code>. 138 */ add(int value, int count)139 public void add(int value, int count) { 140 int bucketKey = getBucketKey(value); 141 int curBucketValue = mBuckets.get(bucketKey); 142 mBuckets.put(bucketKey, curBucketValue + count); 143 } 144 145 146 /** 147 * Computes the inverse of the cumulative probability distribution for the histogram. 148 * 149 * This is the value v such that the probability of a randomly selected datum being 150 * less than or equal to v is the provided probability. The answer is constrained to 151 * lie in the interval [minimum..maximum]. 152 */ quantileFunction(double probability, int minimum, int maximum)153 public double quantileFunction(double probability, int minimum, int maximum) { 154 if (minimum > maximum) { 155 throw new IllegalArgumentException("bad bounds"); 156 } 157 if (probability < 0.0 || probability > 1.0) { 158 throw new IllegalArgumentException("bad roll, try again"); 159 } 160 long sum = 0; 161 for (Bucket bucket : this) { 162 sum += bucket.count; 163 } 164 final double target = sum * probability; 165 double partialSum = 0.0; 166 Bucket hitBucket = null; 167 for (Bucket bucket : this) { 168 if (partialSum + bucket.count >= target) { 169 hitBucket = bucket; 170 break; 171 } 172 partialSum += bucket.count; 173 } 174 if (hitBucket == null) { 175 // No data at all; assume uniform between given limits 176 return minimum + probability * (maximum - minimum); 177 } 178 double highValue = Math.min(hitBucket.end, maximum); 179 double value = Math.max(hitBucket.start, minimum); 180 if (value >= highValue - 1.0 || hitBucket.count == 0) return Math.min(value, highValue); 181 // interpolate to estimate the value 182 value += (highValue - value) * (target - partialSum) / hitBucket.count; 183 return Math.min(Math.max(value, minimum), maximum); 184 } 185 186 /** 187 * Given a value, returns the key of the bucket where it should fall into. 188 */ getBucketKey(int value)189 private int getBucketKey(int value) { 190 // this passes unit tests, so don't worry about it too much 191 int insertionIndex = Arrays.binarySearch(mBucketBoundaries, value); 192 return Math.abs(insertionIndex + 1); 193 } 194 195 /** 196 * Returns a human-readable string representation of the contents of this histogram, suitable 197 * for dump(). 198 */ 199 @Override toString()200 public String toString() { 201 if (mBuckets.size() <= 0) { 202 return "{}"; 203 } 204 205 StringBuilder sb = new StringBuilder(); 206 sb.append('{'); 207 for (int bucketIndex = 0; bucketIndex < mBuckets.size(); ++bucketIndex) { 208 if (bucketIndex > 0) { 209 sb.append(", "); 210 } 211 int bucketKey = mBuckets.keyAt(bucketIndex); 212 sb.append('['); 213 if (bucketKey == 0) { 214 sb.append("Integer.MIN_VALUE"); 215 } else { 216 sb.append(mBucketBoundaries[bucketKey - 1]); 217 } 218 sb.append(','); 219 if (bucketKey == mBucketBoundaries.length) { 220 sb.append("Integer.MAX_VALUE]"); 221 } else { 222 sb.append(mBucketBoundaries[bucketKey]).append(')'); 223 } 224 sb.append('=').append(mBuckets.valueAt(bucketIndex)); 225 } 226 sb.append('}'); 227 return sb.toString(); 228 } 229 230 /** 231 * Iterates over initialized buckets. 232 */ 233 @Override iterator()234 public Iterator<Bucket> iterator() { 235 return new Iterator<Bucket>() { 236 private int mBucketIndex = 0; 237 238 @Override 239 public boolean hasNext() { 240 return mBucketIndex < mBuckets.size(); 241 } 242 243 @Override 244 public Bucket next() { 245 Bucket bucket = getBucketByIndex(mBucketIndex); 246 mBucketIndex++; 247 return bucket; 248 } 249 }; 250 } 251 252 /** 253 * For backwards compatibility, can specify a conversion function that converts a bucket in this 254 * histogram to a specified Protobuf type, used by {@link #toProto(Class, ProtobufConverter)}. 255 * Note that for all new histograms, the standard Protobuf representation type is 256 * {@link HistogramBucketInt32[]}, which can be generated using {@link #toProto()}. 257 * @param <T> The type to convert to. 258 */ 259 public interface ProtobufConverter<T> { 260 /** 261 * Conversion function. 262 * @param start start of the range of a bucket. 263 * @param end end of the range of a bucket. 264 * @param count count of values in this bucket. 265 * @return A Protobuf representation of this bucket. 266 */ convert(int start, int end, int count)267 T convert(int start, int end, int count); 268 } 269 270 /** 271 * Converts this histogram to a Protobuf representation. See {@link ProtobufConverter} 272 * @param protoClass the class object for the Protobuf type. 273 * @param converter a conversion function. 274 * @param <T> the type of the Protobuf output. 275 * @return an array of Protobuf representation of buckets generated by the converter function. 276 */ toProto(Class<T> protoClass, ProtobufConverter<T> converter)277 public <T> T[] toProto(Class<T> protoClass, ProtobufConverter<T> converter) { 278 @SuppressWarnings("unchecked") 279 T[] output = (T[]) Array.newInstance(protoClass, mBuckets.size()); 280 int i = 0; 281 for (Bucket bucket : this) { 282 output[i] = converter.convert(bucket.start, bucket.end, bucket.count); 283 i++; 284 } 285 return output; 286 } 287 288 /** 289 * Converts this histogram to a standard Protobuf representation. 290 */ toProto()291 public HistogramBucketInt32[] toProto() { 292 return toProto(HistogramBucketInt32.class, (start, end, count) -> { 293 HistogramBucketInt32 hb = new HistogramBucketInt32(); 294 hb.start = start; 295 hb.end = end; 296 hb.count = count; 297 return hb; 298 }); 299 } 300 } 301