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