blob: 4f61c852e5ac170fbb20b6ac66776fc6fe4e8f55 [file] [log] [blame]
/*
* 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;
}
}
}