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