001package io.prometheus.metrics.model.snapshots;
002
003import java.util.ArrayList;
004import java.util.Arrays;
005import java.util.Collections;
006import java.util.Iterator;
007import java.util.List;
008import java.util.stream.Stream;
009
010/**
011 * Immutable container for histogram buckets with fixed bucket boundaries.
012 * Note that the counts are <i>not</i> cumulative.
013 */
014public class ClassicHistogramBuckets implements Iterable<ClassicHistogramBucket> {
015
016    /**
017     * Used in native histograms to indicate that no classic histogram buckets are present.
018     */
019    public static final ClassicHistogramBuckets EMPTY = new ClassicHistogramBuckets(new double[]{}, new long[]{});
020
021    private final double[] upperBounds;
022    private final long[] counts; // not cumulative
023
024    private ClassicHistogramBuckets(double[] upperBounds, long[] counts) {
025        this.upperBounds = upperBounds;
026        this.counts = counts;
027    }
028
029    /**
030     * To create new {@link ClassicHistogramBuckets}, you can either use one of the static {@code of(...)} methods,
031     * or use {@link ClassicHistogramBuckets#builder()}.
032     * <p>
033     * This method will create a copy of upperBounds and counts.
034     *
035     * @param upperBounds must have the same length as counts. Must not contain duplicates.
036     *                    Must contain at least {@link Double#POSITIVE_INFINITY} for the {@code +Inf} bucket.
037     *                    An upper bound must not be {@link Double#NaN}.
038     *                    The upperBounds array does not need to be sorted.
039     * @param counts      must have the same length as {@code upperBounds}.
040     *                    The entry at index {@code i} is the count for the {@code upperBound} at index {@code i}.
041     *                    For each count, {@link Number#longValue()} is called to get the value.
042     *                    Counts are <i>not</i> cumulative. Counts must not be negative.
043     */
044    public static ClassicHistogramBuckets of(List<Double> upperBounds, List<? extends Number> counts) {
045        double[] upperBoundsCopy = new double[upperBounds.size()];
046        for (int i = 0; i < upperBounds.size(); i++) {
047            upperBoundsCopy[i] = upperBounds.get(i);
048        }
049        long[] countsCopy = new long[counts.size()];
050        for (int i = 0; i < counts.size(); i++) {
051            countsCopy[i] = counts.get(i).longValue();
052        }
053        sortAndValidate(upperBoundsCopy, countsCopy);
054        return new ClassicHistogramBuckets(upperBoundsCopy, countsCopy);
055    }
056
057    /**
058     * To create new {@link ClassicHistogramBuckets}, you can either use one of the static {@code of(...)} methods,
059     * or use {@link ClassicHistogramBuckets#builder()}.
060     * <p>
061     * This method will create a copy of upperBounds and counts.
062     *
063     * @param upperBounds must have the same length as counts. Must not contain duplicates.
064     *                    Must contain at least {@link Double#POSITIVE_INFINITY} for the {@code +Inf} bucket.
065     *                    An upper bound must not be {@link Double#NaN}.
066     *                    The upperBounds array does not need to be sorted.
067     * @param counts      must have the same length as {@code upperBounds}.
068     *                    The entry at index {@code i} is the count for the {@code upperBound} at index {@code i}.
069     *                    For each count, {@link Number#longValue()} is called to get the value.
070     *                    Counts are <i>not</i> cumulative. Counts must not be negative.
071     */
072    public static ClassicHistogramBuckets of(double[] upperBounds, Number[] counts) {
073        double[] upperBoundsCopy = Arrays.copyOf(upperBounds, upperBounds.length);
074        long[] countsCopy = new long[counts.length];
075        for (int i = 0; i < counts.length; i++) {
076            countsCopy[i] = counts[i].longValue();
077        }
078        sortAndValidate(upperBoundsCopy, countsCopy);
079        return new ClassicHistogramBuckets(upperBoundsCopy, countsCopy);
080    }
081
082    /**
083     * To create new {@link ClassicHistogramBuckets}, you can either use one of the static {@code of(...)} methods,
084     * or use {@link ClassicHistogramBuckets#builder()}.
085     * <p>
086     * This method will create a copy of upperBounds and counts.
087     *
088     * @param upperBounds must have the same length as counts. Must not contain duplicates.
089     *                    Must contain at least {@link Double#POSITIVE_INFINITY} for the {@code +Inf} bucket.
090     *                    An upper bound must not be {@link Double#NaN}.
091     *                    The upperBounds array does not need to be sorted.
092     * @param counts      must have the same length as {@code upperBounds}.
093     *                    The entry at index {@code i} is the count for the {@code upperBound} at index {@code i}.
094     *                    Counts are <i>not</i> cumulative. Counts must not be negative.
095     */
096    public static ClassicHistogramBuckets of(double[] upperBounds, long[] counts) {
097        double[] upperBoundsCopy = Arrays.copyOf(upperBounds, upperBounds.length);
098        long[] countsCopy = Arrays.copyOf(counts, counts.length);
099        sortAndValidate(upperBoundsCopy, countsCopy);
100        return new ClassicHistogramBuckets(upperBoundsCopy, countsCopy);
101    }
102
103    private static void sortAndValidate(double[] upperBounds, long[] counts) {
104        if (upperBounds.length != counts.length) {
105            throw new IllegalArgumentException("upperBounds.length == " + upperBounds.length + " but counts.length == " + counts.length + ". Expected the same length.");
106        }
107        sort(upperBounds, counts);
108        validate(upperBounds, counts);
109    }
110
111    private static void sort(double[] upperBounds, long[] counts) {
112        // Bubblesort. Should be efficient here as in most cases upperBounds is already sorted.
113        int n = upperBounds.length;
114        for (int i = 0; i < n - 1; i++) {
115            for (int j = 0; j < n - i - 1; j++) {
116                if (upperBounds[j] > upperBounds[j + 1]) {
117                    swap(j, j + 1, upperBounds, counts);
118                }
119            }
120        }
121    }
122
123    private static void swap(int i, int j, double[] upperBounds, long[] counts) {
124        double tmpDouble = upperBounds[j];
125        upperBounds[j] = upperBounds[i];
126        upperBounds[i] = tmpDouble;
127        long tmpLong = counts[j];
128        counts[j] = counts[i];
129        counts[i] = tmpLong;
130    }
131
132    private static void validate(double[] upperBounds, long[] counts) {
133        // Preconditions:
134        // * upperBounds sorted
135        // * upperBounds and counts have the same length
136        if (upperBounds.length == 0) {
137            throw new IllegalArgumentException(ClassicHistogramBuckets.class.getSimpleName() + " cannot be empty. They must contain at least the +Inf bucket.");
138        }
139        if (upperBounds[upperBounds.length - 1] != Double.POSITIVE_INFINITY) {
140            throw new IllegalArgumentException(ClassicHistogramBuckets.class.getSimpleName() + " must contain the +Inf bucket.");
141        }
142        for (int i = 0; i < upperBounds.length; i++) {
143            if (Double.isNaN(upperBounds[i])) {
144                throw new IllegalArgumentException("Cannot use NaN as an upper bound in " + ClassicHistogramBuckets.class.getSimpleName());
145            }
146            if (counts[i] < 0) {
147                throw new IllegalArgumentException("Counts in " + ClassicHistogramBuckets.class.getSimpleName() + " cannot be negative.");
148            }
149            if (i > 0) {
150                if (upperBounds[i - 1] == upperBounds[i]) {
151                    throw new IllegalArgumentException("Duplicate upper bound " + upperBounds[i]);
152                }
153            }
154        }
155    }
156
157    public int size() {
158        return upperBounds.length;
159    }
160
161    public double getUpperBound(int i) {
162        return upperBounds[i];
163    }
164
165    /**
166     * The count is <i>not</i> cumulative.
167     */
168    public long getCount(int i) {
169        return counts[i];
170    }
171
172    public boolean isEmpty() {
173        return this.upperBounds.length == 0;
174    }
175
176    private List<ClassicHistogramBucket> asList() {
177        List<ClassicHistogramBucket> result = new ArrayList<>(size());
178        for (int i = 0; i < upperBounds.length; i++) {
179            result.add(new ClassicHistogramBucket(upperBounds[i], counts[i]));
180        }
181        return Collections.unmodifiableList(result);
182    }
183
184    @Override
185    public Iterator<ClassicHistogramBucket> iterator() {
186        return asList().iterator();
187    }
188
189    public Stream<ClassicHistogramBucket> stream() {
190        return asList().stream();
191    }
192
193    /**
194     * To create new {@link ClassicHistogramBuckets}, you can either use one of the static {@code of(...)} methods,
195     * or use {@code builder()}.
196     */
197    public static Builder builder() {
198        return new Builder();
199    }
200
201    public static class Builder {
202        private final List<Double> upperBounds = new ArrayList<>();
203        private final List<Long> counts = new ArrayList<>();
204
205        private Builder() {}
206
207        /**
208         * Must be called at least once for the {@link Double#POSITIVE_INFINITY} bucket.
209         */
210        public Builder bucket(double upperBound, long count) {
211            upperBounds.add(upperBound);
212            counts.add(count);
213            return this;
214        }
215
216        /**
217         * Will throw an {@link IllegalArgumentException} if the {@link Double#POSITIVE_INFINITY} bucket is missing.
218         */
219        public ClassicHistogramBuckets build() {
220            return ClassicHistogramBuckets.of(upperBounds, counts);
221        }
222    }
223}