/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.wifi.util; import android.annotation.NonNull; import android.util.SparseIntArray; import com.android.server.wifi.proto.nano.WifiMetricsProto.HistogramBucketInt32; import java.lang.reflect.Array; import java.util.Arrays; import java.util.Iterator; /** * A histogram that stores the counts of values that fall within buckets, where buckets contain * the count for a range of values. This implementation is backed by a SparseIntArray, meaning * values are stored as int and counts are stored as int. */ public class IntHistogram implements Iterable { /* * Definitions: * - value: what you would like to count * - count: the number of occurrences of values that fall within a bucket * - key: mBuckets maps key => count, 0 <= key <= mBucketBoundaries.length, keys can be * uninitialized for buckets that were never incremented * - bucketIndex: the index of the initialized buckets, 0 <= bucketIndex < mBucket.size(), * all indices in this range are initialized. */ private SparseIntArray mBuckets; private final int[] mBucketBoundaries; /** * A bucket in the histogram, for the range [start, end). * An exception to this is when {@link Bucket#end} == Integer.MAX_VALUE, when the range will be * [start, end]. */ public static class Bucket { public int start; public int end; public int count; public Bucket(int start, int end, int count) { this.start = start; this.end = end; this.count = count; } } /** * Constructs a histogram given the boundary values of buckets, as an int[]. * @param bucketBoundaries The boundary values that separate each bucket. The number of * buckets is bucketBoundaries.length + 1, where the buckets are: * - < bucketBoundaries[0] * - [bucketBoundaries[0], bucketBoundaries[1]) * ... * - >= bucketBoundaries[bucketBoundaries.length() - 1] * This array must be non-null, non-empty, and strictly monotonically * increasing i.e. a[i] < a[i+1]. */ public IntHistogram(@NonNull int[] bucketBoundaries) { if (bucketBoundaries == null || bucketBoundaries.length == 0) { throw new IllegalArgumentException("bucketBoundaries must be non-null and non-empty!"); } for (int i = 0; i < bucketBoundaries.length - 1; i++) { int cur = bucketBoundaries[i]; int next = bucketBoundaries[i + 1]; if (cur >= next) { throw new IllegalArgumentException(String.format( "bucketBoundaries values must be strictly monotonically increasing, but " + "value %d at index %d is greater or equal to " + "value %d at index %d!", cur, i, next, i + 1)); } } mBucketBoundaries = bucketBoundaries.clone(); mBuckets = new SparseIntArray(); } /** * Resets this histogram to the initial state. */ public void clear() { mBuckets.clear(); } /** * Returns the number of non-empty buckets, where an empty bucket is a bucket that was never * added to. */ public int numNonEmptyBuckets() { return mBuckets.size(); } /** * Returns the maximum number of possible buckets (dictated by the number of bucket boundaries). */ public int numTotalBuckets() { return mBucketBoundaries.length + 1; } /** * Gets the nth non-empty bucket, where 0 <= n < {@link #numNonEmptyBuckets()} */ public Bucket getBucketByIndex(int bucketIndex) { int bucketKey = mBuckets.keyAt(bucketIndex); int start = bucketKey == 0 ? Integer.MIN_VALUE : mBucketBoundaries[bucketKey - 1]; int end = bucketKey == mBucketBoundaries.length ? Integer.MAX_VALUE : mBucketBoundaries[bucketKey]; int count = mBuckets.valueAt(bucketIndex); return new Bucket(start, end, count); } /** * Increments the count of the bucket that this value falls into by 1. */ public void increment(int value) { add(value, 1); } /** * Increments the count of the bucket that this value falls into by count. */ public void add(int value, int count) { int bucketKey = getBucketKey(value); int curBucketValue = mBuckets.get(bucketKey); mBuckets.put(bucketKey, curBucketValue + count); } /** * Computes the inverse of the cumulative probability distribution for the histogram. * * This is the value v such that the probability of a randomly selected datum being * less than or equal to v is the provided probability. The answer is constrained to * lie in the interval [minimum..maximum]. */ public double quantileFunction(double probability, int minimum, int maximum) { if (minimum > maximum) { throw new IllegalArgumentException("bad bounds"); } if (probability < 0.0 || probability > 1.0) { throw new IllegalArgumentException("bad roll, try again"); } long sum = 0; for (Bucket bucket : this) { sum += bucket.count; } final double target = sum * probability; double partialSum = 0.0; Bucket hitBucket = null; for (Bucket bucket : this) { if (partialSum + bucket.count >= target) { hitBucket = bucket; break; } partialSum += bucket.count; } if (hitBucket == null) { // No data at all; assume uniform between given limits return minimum + probability * (maximum - minimum); } double highValue = Math.min(hitBucket.end, maximum); double value = Math.max(hitBucket.start, minimum); if (value >= highValue - 1.0 || hitBucket.count == 0) return Math.min(value, highValue); // interpolate to estimate the value value += (highValue - value) * (target - partialSum) / hitBucket.count; return Math.min(Math.max(value, minimum), maximum); } /** * Given a value, returns the key of the bucket where it should fall into. */ private int getBucketKey(int value) { // this passes unit tests, so don't worry about it too much int insertionIndex = Arrays.binarySearch(mBucketBoundaries, value); return Math.abs(insertionIndex + 1); } /** * Returns a human-readable string representation of the contents of this histogram, suitable * for dump(). */ @Override public String toString() { if (mBuckets.size() <= 0) { return "{}"; } StringBuilder sb = new StringBuilder(); sb.append('{'); for (int bucketIndex = 0; bucketIndex < mBuckets.size(); ++bucketIndex) { if (bucketIndex > 0) { sb.append(", "); } int bucketKey = mBuckets.keyAt(bucketIndex); sb.append('['); if (bucketKey == 0) { sb.append("Integer.MIN_VALUE"); } else { sb.append(mBucketBoundaries[bucketKey - 1]); } sb.append(','); if (bucketKey == mBucketBoundaries.length) { sb.append("Integer.MAX_VALUE]"); } else { sb.append(mBucketBoundaries[bucketKey]).append(')'); } sb.append('=').append(mBuckets.valueAt(bucketIndex)); } sb.append('}'); return sb.toString(); } /** * Iterates over initialized buckets. */ @Override public Iterator iterator() { return new Iterator() { private int mBucketIndex = 0; @Override public boolean hasNext() { return mBucketIndex < mBuckets.size(); } @Override public Bucket next() { Bucket bucket = getBucketByIndex(mBucketIndex); mBucketIndex++; return bucket; } }; } /** * For backwards compatibility, can specify a conversion function that converts a bucket in this * histogram to a specified Protobuf type, used by {@link #toProto(Class, ProtobufConverter)}. * Note that for all new histograms, the standard Protobuf representation type is * {@link HistogramBucketInt32[]}, which can be generated using {@link #toProto()}. * @param The type to convert to. */ public interface ProtobufConverter { /** * Conversion function. * @param start start of the range of a bucket. * @param end end of the range of a bucket. * @param count count of values in this bucket. * @return A Protobuf representation of this bucket. */ T convert(int start, int end, int count); } /** * Converts this histogram to a Protobuf representation. See {@link ProtobufConverter} * @param protoClass the class object for the Protobuf type. * @param converter a conversion function. * @param the type of the Protobuf output. * @return an array of Protobuf representation of buckets generated by the converter function. */ public T[] toProto(Class protoClass, ProtobufConverter converter) { @SuppressWarnings("unchecked") T[] output = (T[]) Array.newInstance(protoClass, mBuckets.size()); int i = 0; for (Bucket bucket : this) { output[i] = converter.convert(bucket.start, bucket.end, bucket.count); i++; } return output; } /** * Converts this histogram to a standard Protobuf representation. */ public HistogramBucketInt32[] toProto() { return toProto(HistogramBucketInt32.class, (start, end, count) -> { HistogramBucketInt32 hb = new HistogramBucketInt32(); hb.start = start; hb.end = end; hb.count = count; return hb; }); } }