// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.base.metrics;

import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.MainDex;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * Java API for recording UMA histograms.
 *
 * Internally, histograms objects are cached on the Java side by their pointer
 * values (converted to long). This is safe to do because C++ Histogram objects
 * are never freed. Caching them on the Java side prevents needing to do costly
 * Java String to C++ string conversions on the C++ side during lookup.
 *
 * Note: the JNI calls are relatively costly - avoid calling these methods in performance-critical
 * code.
 */
@JNINamespace("base::android")
@MainDex
public class RecordHistogram {
    private static Throwable sDisabledBy;
    private static Map<String, Long> sCache =
            Collections.synchronizedMap(new HashMap<String, Long>());

    /**
     * Tests may not have native initialized, so they may need to disable metrics. The value should
     * be reset after the test done, to avoid carrying over state to unrelated tests.
     *
     * In JUnit tests this can be done automatically using
     * {@link org.chromium.chrome.browser.DisableHistogramsRule}
     */
    @VisibleForTesting
    public static void setDisabledForTests(boolean disabled) {
        if (disabled && sDisabledBy != null) {
            throw new IllegalStateException("Histograms are already disabled.", sDisabledBy);
        }
        sDisabledBy = disabled ? new Throwable() : null;
    }

    private static long getCachedHistogramKey(String name) {
        Long key = sCache.get(name);
        // Note: If key is null, we don't have it cached. In that case, pass 0
        // to the native code, which gets converted to a null histogram pointer
        // which will cause the native code to look up the object on the native
        // side.
        return (key == null ? 0 : key);
    }

    /**
     * Records a sample in a boolean UMA histogram of the given name. Boolean histogram has two
     * buckets, corresponding to success (true) and failure (false). This is the Java equivalent of
     * the UMA_HISTOGRAM_BOOLEAN C++ macro.
     * @param name name of the histogram
     * @param sample sample to be recorded, either true or false
     */
    public static void recordBooleanHistogram(String name, boolean sample) {
        if (sDisabledBy != null) return;
        long key = getCachedHistogramKey(name);
        long result = nativeRecordBooleanHistogram(name, key, sample);
        if (result != key) sCache.put(name, result);
    }

    /**
     * Records a sample in an enumerated histogram of the given name and boundary. Note that
     * |boundary| identifies the histogram - it should be the same at every invocation. This is the
     * Java equivalent of the UMA_HISTOGRAM_ENUMERATION C++ macro.
     * @param name name of the histogram
     * @param sample sample to be recorded, at least 0 and at most |boundary| - 1
     * @param boundary upper bound for legal sample values - all sample values have to be strictly
     *        lower than |boundary|
     */
    public static void recordEnumeratedHistogram(String name, int sample, int boundary) {
        if (sDisabledBy != null) return;
        long key = getCachedHistogramKey(name);
        long result = nativeRecordEnumeratedHistogram(name, key, sample, boundary);
        if (result != key) sCache.put(name, result);
    }

    /**
     * Records a sample in a count histogram. This is the Java equivalent of the
     * UMA_HISTOGRAM_COUNTS C++ macro.
     * @param name name of the histogram
     * @param sample sample to be recorded, at least 1 and at most 999999
     */
    public static void recordCountHistogram(String name, int sample) {
        recordCustomCountHistogram(name, sample, 1, 1000000, 50);
    }

    /**
     * Records a sample in a count histogram. This is the Java equivalent of the
     * UMA_HISTOGRAM_COUNTS_100 C++ macro.
     * @param name name of the histogram
     * @param sample sample to be recorded, at least 1 and at most 99
     */
    public static void recordCount100Histogram(String name, int sample) {
        recordCustomCountHistogram(name, sample, 1, 100, 50);
    }

    /**
     * Records a sample in a count histogram. This is the Java equivalent of the
     * UMA_HISTOGRAM_COUNTS_1000 C++ macro.
     * @param name name of the histogram
     * @param sample sample to be recorded, at least 1 and at most 999
     */
    public static void recordCount1000Histogram(String name, int sample) {
        recordCustomCountHistogram(name, sample, 1, 1000, 50);
    }

    /**
     * Records a sample in a count histogram. This is the Java equivalent of the
     * UMA_HISTOGRAM_CUSTOM_COUNTS C++ macro.
     * @param name name of the histogram
     * @param sample sample to be recorded, at least |min| and at most |max| - 1
     * @param min lower bound for expected sample values. It must be >= 1
     * @param max upper bounds for expected sample values
     * @param numBuckets the number of buckets
     */
    public static void recordCustomCountHistogram(
            String name, int sample, int min, int max, int numBuckets) {
        if (sDisabledBy != null) return;
        long key = getCachedHistogramKey(name);
        long result = nativeRecordCustomCountHistogram(name, key, sample, min, max, numBuckets);
        if (result != key) sCache.put(name, result);
    }

    /**
     * Records a sample in a linear histogram. This is the Java equivalent for using
     * base::LinearHistogram.
     * @param name name of the histogram
     * @param sample sample to be recorded, at least |min| and at most |max| - 1.
     * @param min lower bound for expected sample values, should be at least 1.
     * @param max upper bounds for expected sample values
     * @param numBuckets the number of buckets
     */
    public static void recordLinearCountHistogram(
            String name, int sample, int min, int max, int numBuckets) {
        if (sDisabledBy != null) return;
        long key = getCachedHistogramKey(name);
        long result = nativeRecordLinearCountHistogram(name, key, sample, min, max, numBuckets);
        if (result != key) sCache.put(name, result);
    }

    /**
     * Records a sample in a percentage histogram. This is the Java equivalent of the
     * UMA_HISTOGRAM_PERCENTAGE C++ macro.
     * @param name name of the histogram
     * @param sample sample to be recorded, at least 0 and at most 100.
     */
    public static void recordPercentageHistogram(String name, int sample) {
        if (sDisabledBy != null) return;
        long key = getCachedHistogramKey(name);
        long result = nativeRecordEnumeratedHistogram(name, key, sample, 101);
        if (result != key) sCache.put(name, result);
    }

    /**
     * Records a sparse histogram. This is the Java equivalent of UmaHistogramSparse.
     * @param name name of the histogram
     * @param sample sample to be recorded. All values of |sample| are valid, including negative
     *        values.
     */
    public static void recordSparseSlowlyHistogram(String name, int sample) {
        if (sDisabledBy != null) return;
        long key = getCachedHistogramKey(name);
        long result = nativeRecordSparseHistogram(name, key, sample);
        if (result != key) sCache.put(name, result);
    }

    /**
     * Records a sample in a histogram of times. Useful for recording short durations. This is the
     * Java equivalent of the UMA_HISTOGRAM_TIMES C++ macro.
     * Note that histogram samples will always be converted to milliseconds when logged.
     * @param name name of the histogram
     * @param duration duration to be recorded
     * @param timeUnit the unit of the duration argument (must be >= MILLISECONDS)
     */
    public static void recordTimesHistogram(String name, long duration, TimeUnit timeUnit) {
        assertTimesHistogramSupportsUnit(timeUnit);
        recordCustomTimesHistogramMilliseconds(
                name, timeUnit.toMillis(duration), 1, TimeUnit.SECONDS.toMillis(10), 50);
    }

    /**
     * Records a sample in a histogram of times. Useful for recording medium durations. This is the
     * Java equivalent of the UMA_HISTOGRAM_MEDIUM_TIMES C++ macro.
     * Note that histogram samples will always be converted to milliseconds when logged.
     * @param name name of the histogram
     * @param duration duration to be recorded
     * @param timeUnit the unit of the duration argument (must be >= MILLISECONDS)
     */
    public static void recordMediumTimesHistogram(String name, long duration, TimeUnit timeUnit) {
        assertTimesHistogramSupportsUnit(timeUnit);
        recordCustomTimesHistogramMilliseconds(
                name, timeUnit.toMillis(duration), 10, TimeUnit.MINUTES.toMillis(3), 50);
    }

    /**
     * Records a sample in a histogram of times. Useful for recording long durations. This is the
     * Java equivalent of the UMA_HISTOGRAM_LONG_TIMES C++ macro.
     * Note that histogram samples will always be converted to milliseconds when logged.
     * @param name name of the histogram
     * @param duration duration to be recorded
     * @param timeUnit the unit of the duration argument (must be >= MILLISECONDS)
     */
    public static void recordLongTimesHistogram(String name, long duration, TimeUnit timeUnit) {
        assertTimesHistogramSupportsUnit(timeUnit);
        recordCustomTimesHistogramMilliseconds(
                name, timeUnit.toMillis(duration), 1, TimeUnit.HOURS.toMillis(1), 50);
    }

    /**
     * Records a sample in a histogram of times. Useful for recording long durations. This is the
     * Java equivalent of the UMA_HISTOGRAM_LONG_TIMES_100 C++ macro.
     * Note that histogram samples will always be converted to milliseconds when logged.
     * @param name name of the histogram
     * @param duration duration to be recorded
     * @param timeUnit the unit of the duration argument (must be >= MILLISECONDS)
     */
    public static void recordLongTimesHistogram100(String name, long duration, TimeUnit timeUnit) {
        assertTimesHistogramSupportsUnit(timeUnit);
        recordCustomTimesHistogramMilliseconds(
                name, timeUnit.toMillis(duration), 1, TimeUnit.HOURS.toMillis(1), 100);
    }

    /**
     * Records a sample in a histogram of times with custom buckets. This is the Java equivalent of
     * the UMA_HISTOGRAM_CUSTOM_TIMES C++ macro.
     * Note that histogram samples will always be converted to milliseconds when logged.
     * @param name name of the histogram
     * @param duration duration to be recorded
     * @param min the minimum bucket value
     * @param max the maximum bucket value
     * @param timeUnit the unit of the duration, min, and max arguments (must be >= MILLISECONDS)
     * @param numBuckets the number of buckets
     */
    public static void recordCustomTimesHistogram(
            String name, long duration, long min, long max, TimeUnit timeUnit, int numBuckets) {
        assertTimesHistogramSupportsUnit(timeUnit);
        recordCustomTimesHistogramMilliseconds(name, timeUnit.toMillis(duration),
                timeUnit.toMillis(min), timeUnit.toMillis(max), numBuckets);
    }

    /**
     * Records a sample in a histogram of sizes in KB. This is the Java equivalent of the
     * UMA_HISTOGRAM_MEMORY_KB C++ macro.
     *
     * Good for sizes up to about 500MB.
     *
     * @param name name of the histogram.
     * @param sizeInkB Sample to record in KB.
     */
    public static void recordMemoryKBHistogram(String name, int sizeInKB) {
        recordCustomCountHistogram(name, sizeInKB, 1000, 500000, 50);
    }

    /**
     * Asserts that the time unit is supported by TimesHistogram.
     * @param timeUnit the unit, must be >= MILLISECONDS
     */
    /* package */ static void assertTimesHistogramSupportsUnit(TimeUnit timeUnit) {
        // Use extra variable, or else 'git cl format' produces weird results.
        boolean supported = timeUnit != TimeUnit.NANOSECONDS && timeUnit != TimeUnit.MICROSECONDS;
        assert supported : "TimesHistogram doesn't support MICROSECOND and NANOSECONDS time units. "
                           + "Consider using CountHistogram instead.";
    }

    private static int clampToInt(long value) {
        if (value > Integer.MAX_VALUE) return Integer.MAX_VALUE;
        // Note: Clamping to MIN_VALUE rather than 0, to let base/ histograms code
        // do its own handling of negative values in the future.
        if (value < Integer.MIN_VALUE) return Integer.MIN_VALUE;
        return (int) value;
    }

    private static void recordCustomTimesHistogramMilliseconds(
            String name, long duration, long min, long max, int numBuckets) {
        if (sDisabledBy != null) return;
        long key = getCachedHistogramKey(name);
        // Note: Duration, min and max are clamped to int here because that's what's expected by
        // the native histograms API. Callers of these functions still pass longs because that's
        // the types returned by TimeUnit and System.currentTimeMillis() APIs, from which these
        // values come.
        assert max == clampToInt(max);
        long result = nativeRecordCustomTimesHistogramMilliseconds(
                name, key, clampToInt(duration), clampToInt(min), clampToInt(max), numBuckets);
        if (result != key) sCache.put(name, result);
    }

    /**
     * Returns the number of samples recorded in the given bucket of the given histogram.
     * @param name name of the histogram to look up
     * @param sample the bucket containing this sample value will be looked up
     */
    @VisibleForTesting
    public static int getHistogramValueCountForTesting(String name, int sample) {
        return nativeGetHistogramValueCountForTesting(name, sample);
    }

    /**
     * Returns the number of samples recorded for the given histogram.
     * @param name name of the histogram to look up.
     */
    @VisibleForTesting
    public static int getHistogramTotalCountForTesting(String name) {
        return nativeGetHistogramTotalCountForTesting(name);
    }

    private static native long nativeRecordCustomTimesHistogramMilliseconds(
            String name, long key, int duration, int min, int max, int numBuckets);

    private static native long nativeRecordBooleanHistogram(String name, long key, boolean sample);
    private static native long nativeRecordEnumeratedHistogram(
            String name, long key, int sample, int boundary);
    private static native long nativeRecordCustomCountHistogram(
            String name, long key, int sample, int min, int max, int numBuckets);
    private static native long nativeRecordLinearCountHistogram(
            String name, long key, int sample, int min, int max, int numBuckets);
    private static native long nativeRecordSparseHistogram(String name, long key, int sample);

    private static native int nativeGetHistogramValueCountForTesting(String name, int sample);
    private static native int nativeGetHistogramTotalCountForTesting(String name);
}
