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.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 * Given a value, returns the key of the bucket where it should fall into. 147 */ getBucketKey(int value)148 private int getBucketKey(int value) { 149 // this passes unit tests, so don't worry about it too much 150 int insertionIndex = Arrays.binarySearch(mBucketBoundaries, value); 151 return Math.abs(insertionIndex + 1); 152 } 153 154 /** 155 * Returns a human-readable string representation of the contents of this histogram, suitable 156 * for dump(). 157 */ 158 @Override toString()159 public String toString() { 160 if (mBuckets.size() <= 0) { 161 return "{}"; 162 } 163 164 StringBuilder sb = new StringBuilder(); 165 sb.append('{'); 166 for (int bucketIndex = 0; bucketIndex < mBuckets.size(); ++bucketIndex) { 167 if (bucketIndex > 0) { 168 sb.append(", "); 169 } 170 int bucketKey = mBuckets.keyAt(bucketIndex); 171 sb.append('['); 172 if (bucketKey == 0) { 173 sb.append("Integer.MIN_VALUE"); 174 } else { 175 sb.append(mBucketBoundaries[bucketKey - 1]); 176 } 177 sb.append(','); 178 if (bucketKey == mBucketBoundaries.length) { 179 sb.append("Integer.MAX_VALUE]"); 180 } else { 181 sb.append(mBucketBoundaries[bucketKey]).append(')'); 182 } 183 sb.append('=').append(mBuckets.valueAt(bucketIndex)); 184 } 185 sb.append('}'); 186 return sb.toString(); 187 } 188 189 /** 190 * Iterates over initialized buckets. 191 */ 192 @Override iterator()193 public Iterator<Bucket> iterator() { 194 return new Iterator<Bucket>() { 195 private int mBucketIndex = 0; 196 197 @Override 198 public boolean hasNext() { 199 return mBucketIndex < mBuckets.size(); 200 } 201 202 @Override 203 public Bucket next() { 204 Bucket bucket = getBucketByIndex(mBucketIndex); 205 mBucketIndex++; 206 return bucket; 207 } 208 }; 209 } 210 211 /** 212 * For backwards compatibility, can specify a conversion function that converts a bucket in this 213 * histogram to a specified Protobuf type, used by {@link #toProto(Class, ProtobufConverter)}. 214 * Note that for all new histograms, the standard Protobuf representation type is 215 * {@link HistogramBucketInt32[]}, which can be generated using {@link #toProto()}. 216 * @param <T> The type to convert to. 217 */ 218 public interface ProtobufConverter<T> { 219 /** 220 * Conversion function. 221 * @param start start of the range of a bucket. 222 * @param end end of the range of a bucket. 223 * @param count count of values in this bucket. 224 * @return A Protobuf representation of this bucket. 225 */ convert(int start, int end, int count)226 T convert(int start, int end, int count); 227 } 228 229 /** 230 * Converts this histogram to a Protobuf representation. See {@link ProtobufConverter} 231 * @param protoClass the class object for the Protobuf type. 232 * @param converter a conversion function. 233 * @param <T> the type of the Protobuf output. 234 * @return an array of Protobuf representation of buckets generated by the converter function. 235 */ toProto(Class<T> protoClass, ProtobufConverter<T> converter)236 public <T> T[] toProto(Class<T> protoClass, ProtobufConverter<T> converter) { 237 @SuppressWarnings("unchecked") 238 T[] output = (T[]) Array.newInstance(protoClass, mBuckets.size()); 239 int i = 0; 240 for (Bucket bucket : this) { 241 output[i] = converter.convert(bucket.start, bucket.end, bucket.count); 242 i++; 243 } 244 return output; 245 } 246 247 /** 248 * Converts this histogram to a standard Protobuf representation. 249 */ toProto()250 public HistogramBucketInt32[] toProto() { 251 return toProto(HistogramBucketInt32.class, (start, end, count) -> { 252 HistogramBucketInt32 hb = new HistogramBucketInt32(); 253 hb.start = start; 254 hb.end = end; 255 hb.count = count; 256 return hb; 257 }); 258 } 259 } 260