1 /* 2 * Copyright (C) 2023 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.modules.expresslog; 18 19 import android.annotation.FloatRange; 20 import android.annotation.IntRange; 21 import android.annotation.NonNull; 22 23 import java.util.Arrays; 24 25 /** Histogram encapsulates StatsD write API calls */ 26 public final class Histogram { 27 28 private final String mMetricId; 29 private final BinOptions mBinOptions; 30 31 /** 32 * Creates Histogram metric logging wrapper 33 * 34 * @param metricId to log, logging will be no-op if metricId is not defined in the TeX catalog 35 * @param binOptions to calculate bin index for samples 36 */ Histogram(@onNull String metricId, @NonNull BinOptions binOptions)37 public Histogram(@NonNull String metricId, @NonNull BinOptions binOptions) { 38 mMetricId = metricId; 39 mBinOptions = binOptions; 40 } 41 42 /** 43 * Logs increment sample count for automatically calculated bin 44 * 45 * @param sample value 46 */ logSample(float sample)47 public void logSample(float sample) { 48 final long hash = MetricIds.getMetricIdHash(mMetricId, MetricIds.METRIC_TYPE_HISTOGRAM); 49 final int binIndex = mBinOptions.getBinForSample(sample); 50 StatsExpressLog.write( 51 StatsExpressLog.EXPRESS_HISTOGRAM_SAMPLE_REPORTED, hash, /*count*/ 1, binIndex); 52 } 53 54 /** 55 * Logs increment sample count for automatically calculated bin 56 * 57 * @param uid used as a dimension for the count metric 58 * @param sample value 59 */ logSampleWithUid(int uid, float sample)60 public void logSampleWithUid(int uid, float sample) { 61 final long hash = 62 MetricIds.getMetricIdHash(mMetricId, MetricIds.METRIC_TYPE_HISTOGRAM_WITH_UID); 63 final int binIndex = mBinOptions.getBinForSample(sample); 64 StatsExpressLog.write( 65 StatsExpressLog.EXPRESS_UID_HISTOGRAM_SAMPLE_REPORTED, 66 hash, /*count*/ 67 1, 68 binIndex, 69 uid); 70 } 71 72 /** Used by Histogram to map data sample to corresponding bin */ 73 public interface BinOptions { 74 /** 75 * Returns bins count to be used by a histogram 76 * 77 * @return bins count used to initialize Options, including overflow & underflow bins 78 */ getBinsCount()79 int getBinsCount(); 80 81 /** 82 * Returns bin index for the input sample value 83 * index == 0 stands for underflow 84 * index == getBinsCount() - 1 stands for overflow 85 * 86 * @return zero based index 87 */ getBinForSample(float sample)88 int getBinForSample(float sample); 89 } 90 91 /** Used by Histogram to map data sample to corresponding bin for uniform bins */ 92 public static final class UniformOptions implements BinOptions { 93 94 private final int mBinCount; 95 private final float mMinValue; 96 private final float mExclusiveMaxValue; 97 private final float mBinSize; 98 99 /** 100 * Creates options for uniform (linear) sized bins 101 * 102 * @param binCount amount of histogram bins. 2 bin indexes will be calculated 103 * automatically to represent underflow & overflow bins 104 * @param minValue is included in the first bin, values less than minValue 105 * go to underflow bin 106 * @param exclusiveMaxValue is included in the overflow bucket. For accurate 107 * measure up to kMax, then exclusiveMaxValue 108 * should be set to kMax + 1 109 */ UniformOptions(@ntRangefrom = 1) int binCount, float minValue, float exclusiveMaxValue)110 public UniformOptions(@IntRange(from = 1) int binCount, float minValue, 111 float exclusiveMaxValue) { 112 if (binCount < 1) { 113 throw new IllegalArgumentException("Bin count should be positive number"); 114 } 115 116 if (exclusiveMaxValue <= minValue) { 117 throw new IllegalArgumentException("Bins range invalid (maxValue < minValue)"); 118 } 119 120 mMinValue = minValue; 121 mExclusiveMaxValue = exclusiveMaxValue; 122 mBinSize = (mExclusiveMaxValue - minValue) / binCount; 123 124 // Implicitly add 2 for the extra underflow & overflow bins 125 mBinCount = binCount + 2; 126 } 127 128 @Override getBinsCount()129 public int getBinsCount() { 130 return mBinCount; 131 } 132 133 @Override getBinForSample(float sample)134 public int getBinForSample(float sample) { 135 if (sample < mMinValue) { 136 // goes to underflow 137 return 0; 138 } else if (sample >= mExclusiveMaxValue) { 139 // goes to overflow 140 return mBinCount - 1; 141 } 142 return (int) ((sample - mMinValue) / mBinSize + 1); 143 } 144 } 145 146 /** Used by Histogram to map data sample to corresponding bin for scaled bins */ 147 public static final class ScaledRangeOptions implements BinOptions { 148 // store minimum value per bin 149 final long[] mBins; 150 151 /** 152 * Creates options for scaled range bins 153 * 154 * @param binCount amount of histogram bins. 2 bin indexes will be calculated 155 * automatically to represent underflow & overflow bins 156 * @param minValue is included in the first bin, values less than minValue 157 * go to underflow bin 158 * @param firstBinWidth used to represent first bin width and as a reference to calculate 159 * width for consecutive bins 160 * @param scaleFactor used to calculate width for consecutive bins 161 */ ScaledRangeOptions(@ntRangefrom = 1) int binCount, int minValue, @FloatRange(from = 1.f) float firstBinWidth, @FloatRange(from = 1.f) float scaleFactor)162 public ScaledRangeOptions(@IntRange(from = 1) int binCount, int minValue, 163 @FloatRange(from = 1.f) float firstBinWidth, 164 @FloatRange(from = 1.f) float scaleFactor) { 165 if (binCount < 1) { 166 throw new IllegalArgumentException("Bin count should be positive number"); 167 } 168 169 if (firstBinWidth < 1.f) { 170 throw new IllegalArgumentException( 171 "First bin width invalid (should be 1.f at minimum)"); 172 } 173 174 if (scaleFactor < 1.f) { 175 throw new IllegalArgumentException( 176 "Scaled factor invalid (should be 1.f at minimum)"); 177 } 178 179 // precalculating bins ranges (no need to create a bin for underflow reference value) 180 mBins = initBins(binCount + 1, minValue, firstBinWidth, scaleFactor); 181 } 182 183 @Override getBinsCount()184 public int getBinsCount() { 185 return mBins.length + 1; 186 } 187 188 @Override getBinForSample(float sample)189 public int getBinForSample(float sample) { 190 if (sample < mBins[0]) { 191 // goes to underflow 192 return 0; 193 } else if (sample >= mBins[mBins.length - 1]) { 194 // goes to overflow 195 return mBins.length; 196 } 197 198 return lower_bound(mBins, (long) sample) + 1; 199 } 200 201 // To find lower bound using binary search implementation of Arrays utility class lower_bound(long[] array, long sample)202 private static int lower_bound(long[] array, long sample) { 203 int index = Arrays.binarySearch(array, sample); 204 // If key is not present in the array 205 if (index < 0) { 206 // Index specify the position of the key when inserted in the sorted array 207 // so the element currently present at this position will be the lower bound 208 return Math.abs(index) - 2; 209 } 210 return index; 211 } 212 initBins(int count, int minValue, float firstBinWidth, float scaleFactor)213 private static long[] initBins(int count, int minValue, float firstBinWidth, 214 float scaleFactor) { 215 long[] bins = new long[count]; 216 bins[0] = minValue; 217 double lastWidth = firstBinWidth; 218 for (int i = 1; i < count; i++) { 219 // current bin minValue = previous bin width * scaleFactor 220 double currentBinMinValue = bins[i - 1] + lastWidth; 221 if (currentBinMinValue > Integer.MAX_VALUE) { 222 throw new IllegalArgumentException( 223 "Attempted to create a bucket larger than maxint"); 224 } 225 226 bins[i] = (long) currentBinMinValue; 227 lastWidth *= scaleFactor; 228 } 229 return bins; 230 } 231 } 232 } 233