| /* |
| * Copyright (C) 2023 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.modules.expresslog; |
| |
| import android.annotation.FloatRange; |
| import android.annotation.IntRange; |
| import android.annotation.NonNull; |
| |
| import java.util.Arrays; |
| |
| /** Histogram encapsulates StatsD write API calls */ |
| public final class Histogram { |
| |
| private final String mMetricId; |
| private final BinOptions mBinOptions; |
| |
| /** |
| * Creates Histogram metric logging wrapper |
| * |
| * @param metricId to log, logging will be no-op if metricId is not defined in the TeX catalog |
| * @param binOptions to calculate bin index for samples |
| */ |
| public Histogram(@NonNull String metricId, @NonNull BinOptions binOptions) { |
| mMetricId = metricId; |
| mBinOptions = binOptions; |
| } |
| |
| /** |
| * Logs increment sample count for automatically calculated bin |
| * |
| * @param sample value |
| */ |
| public void logSample(float sample) { |
| final long hash = MetricIds.getMetricIdHash(mMetricId, MetricIds.METRIC_TYPE_HISTOGRAM); |
| final int binIndex = mBinOptions.getBinForSample(sample); |
| StatsExpressLog.write( |
| StatsExpressLog.EXPRESS_HISTOGRAM_SAMPLE_REPORTED, hash, /*count*/ 1, binIndex); |
| } |
| |
| /** |
| * Logs increment sample count for automatically calculated bin |
| * |
| * @param uid used as a dimension for the count metric |
| * @param sample value |
| */ |
| public void logSampleWithUid(int uid, float sample) { |
| final long hash = |
| MetricIds.getMetricIdHash(mMetricId, MetricIds.METRIC_TYPE_HISTOGRAM_WITH_UID); |
| final int binIndex = mBinOptions.getBinForSample(sample); |
| StatsExpressLog.write( |
| StatsExpressLog.EXPRESS_UID_HISTOGRAM_SAMPLE_REPORTED, |
| hash, /*count*/ |
| 1, |
| binIndex, |
| uid); |
| } |
| |
| /** Used by Histogram to map data sample to corresponding bin */ |
| public interface BinOptions { |
| /** |
| * Returns bins count to be used by a histogram |
| * |
| * @return bins count used to initialize Options, including overflow & underflow bins |
| */ |
| int getBinsCount(); |
| |
| /** |
| * Returns bin index for the input sample value |
| * index == 0 stands for underflow |
| * index == getBinsCount() - 1 stands for overflow |
| * |
| * @return zero based index |
| */ |
| int getBinForSample(float sample); |
| } |
| |
| /** Used by Histogram to map data sample to corresponding bin for uniform bins */ |
| public static final class UniformOptions implements BinOptions { |
| |
| private final int mBinCount; |
| private final float mMinValue; |
| private final float mExclusiveMaxValue; |
| private final float mBinSize; |
| |
| /** |
| * Creates options for uniform (linear) sized bins |
| * |
| * @param binCount amount of histogram bins. 2 bin indexes will be calculated |
| * automatically to represent underflow & overflow bins |
| * @param minValue is included in the first bin, values less than minValue |
| * go to underflow bin |
| * @param exclusiveMaxValue is included in the overflow bucket. For accurate |
| * measure up to kMax, then exclusiveMaxValue |
| * should be set to kMax + 1 |
| */ |
| public UniformOptions(@IntRange(from = 1) int binCount, float minValue, |
| float exclusiveMaxValue) { |
| if (binCount < 1) { |
| throw new IllegalArgumentException("Bin count should be positive number"); |
| } |
| |
| if (exclusiveMaxValue <= minValue) { |
| throw new IllegalArgumentException("Bins range invalid (maxValue < minValue)"); |
| } |
| |
| mMinValue = minValue; |
| mExclusiveMaxValue = exclusiveMaxValue; |
| mBinSize = (mExclusiveMaxValue - minValue) / binCount; |
| |
| // Implicitly add 2 for the extra underflow & overflow bins |
| mBinCount = binCount + 2; |
| } |
| |
| @Override |
| public int getBinsCount() { |
| return mBinCount; |
| } |
| |
| @Override |
| public int getBinForSample(float sample) { |
| if (sample < mMinValue) { |
| // goes to underflow |
| return 0; |
| } else if (sample >= mExclusiveMaxValue) { |
| // goes to overflow |
| return mBinCount - 1; |
| } |
| return (int) ((sample - mMinValue) / mBinSize + 1); |
| } |
| } |
| |
| /** Used by Histogram to map data sample to corresponding bin for scaled bins */ |
| public static final class ScaledRangeOptions implements BinOptions { |
| // store minimum value per bin |
| final long[] mBins; |
| |
| /** |
| * Creates options for scaled range bins |
| * |
| * @param binCount amount of histogram bins. 2 bin indexes will be calculated |
| * automatically to represent underflow & overflow bins |
| * @param minValue is included in the first bin, values less than minValue |
| * go to underflow bin |
| * @param firstBinWidth used to represent first bin width and as a reference to calculate |
| * width for consecutive bins |
| * @param scaleFactor used to calculate width for consecutive bins |
| */ |
| public ScaledRangeOptions(@IntRange(from = 1) int binCount, int minValue, |
| @FloatRange(from = 1.f) float firstBinWidth, |
| @FloatRange(from = 1.f) float scaleFactor) { |
| if (binCount < 1) { |
| throw new IllegalArgumentException("Bin count should be positive number"); |
| } |
| |
| if (firstBinWidth < 1.f) { |
| throw new IllegalArgumentException( |
| "First bin width invalid (should be 1.f at minimum)"); |
| } |
| |
| if (scaleFactor < 1.f) { |
| throw new IllegalArgumentException( |
| "Scaled factor invalid (should be 1.f at minimum)"); |
| } |
| |
| // precalculating bins ranges (no need to create a bin for underflow reference value) |
| mBins = initBins(binCount + 1, minValue, firstBinWidth, scaleFactor); |
| } |
| |
| @Override |
| public int getBinsCount() { |
| return mBins.length + 1; |
| } |
| |
| @Override |
| public int getBinForSample(float sample) { |
| if (sample < mBins[0]) { |
| // goes to underflow |
| return 0; |
| } else if (sample >= mBins[mBins.length - 1]) { |
| // goes to overflow |
| return mBins.length; |
| } |
| |
| return lower_bound(mBins, (long) sample) + 1; |
| } |
| |
| // To find lower bound using binary search implementation of Arrays utility class |
| private static int lower_bound(long[] array, long sample) { |
| int index = Arrays.binarySearch(array, sample); |
| // If key is not present in the array |
| if (index < 0) { |
| // Index specify the position of the key when inserted in the sorted array |
| // so the element currently present at this position will be the lower bound |
| return Math.abs(index) - 2; |
| } |
| return index; |
| } |
| |
| private static long[] initBins(int count, int minValue, float firstBinWidth, |
| float scaleFactor) { |
| long[] bins = new long[count]; |
| bins[0] = minValue; |
| double lastWidth = firstBinWidth; |
| for (int i = 1; i < count; i++) { |
| // current bin minValue = previous bin width * scaleFactor |
| double currentBinMinValue = bins[i - 1] + lastWidth; |
| if (currentBinMinValue > Integer.MAX_VALUE) { |
| throw new IllegalArgumentException( |
| "Attempted to create a bucket larger than maxint"); |
| } |
| |
| bins[i] = (long) currentBinMinValue; |
| lastWidth *= scaleFactor; |
| } |
| return bins; |
| } |
| } |
| } |