001package io.prometheus.metrics.model.snapshots;
002
003import io.prometheus.metrics.annotations.StableApi;
004import java.util.ArrayList;
005import java.util.Arrays;
006import java.util.Collections;
007import java.util.Iterator;
008import java.util.List;
009import java.util.stream.Stream;
010
011/**
012 * Immutable container for histogram buckets with fixed bucket boundaries. Note that the counts are
013 * <i>not</i> cumulative.
014 */
015@StableApi
016public class ClassicHistogramBuckets implements Iterable<ClassicHistogramBucket> {
017
018  /** Used in native histograms to indicate that no classic histogram buckets are present. */
019  public static final ClassicHistogramBuckets EMPTY =
020      new ClassicHistogramBuckets(new double[] {}, new long[] {});
021
022  private final double[] upperBounds;
023  private final long[] counts; // not cumulative
024
025  private ClassicHistogramBuckets(double[] upperBounds, long[] counts) {
026    this.upperBounds = upperBounds;
027    this.counts = counts;
028  }
029
030  /**
031   * To create new {@link ClassicHistogramBuckets}, you can either use one of the static {@code
032   * of(...)} methods, or use {@link ClassicHistogramBuckets#builder()}.
033   *
034   * <p>This method will create a copy of upperBounds and counts.
035   *
036   * @param upperBounds must have the same length as counts. Must not contain duplicates. Must
037   *     contain at least {@link Double#POSITIVE_INFINITY} for the {@code +Inf} bucket. An upper
038   *     bound must not be {@link Double#NaN}. The upperBounds array does not need to be sorted.
039   * @param counts must have the same length as {@code upperBounds}. The entry at index {@code i} is
040   *     the count for the {@code upperBounds} at index {@code i}. For each count, {@link
041   *     Number#longValue()} is called to get the value. Counts are <i>not</i> cumulative. Counts
042   *     must not be negative.
043   */
044  public static ClassicHistogramBuckets of(
045      List<Double> upperBounds, List<? extends Number> counts) {
046    double[] upperBoundsCopy = new double[upperBounds.size()];
047    for (int i = 0; i < upperBounds.size(); i++) {
048      upperBoundsCopy[i] = upperBounds.get(i);
049    }
050    long[] countsCopy = new long[counts.size()];
051    for (int i = 0; i < counts.size(); i++) {
052      countsCopy[i] = counts.get(i).longValue();
053    }
054    sortAndValidate(upperBoundsCopy, countsCopy);
055    return new ClassicHistogramBuckets(upperBoundsCopy, countsCopy);
056  }
057
058  /**
059   * To create new {@link ClassicHistogramBuckets}, you can either use one of the static {@code
060   * of(...)} methods, or use {@link ClassicHistogramBuckets#builder()}.
061   *
062   * <p>This method will create a copy of upperBounds and counts.
063   *
064   * @param upperBounds must have the same length as counts. Must not contain duplicates. Must
065   *     contain at least {@link Double#POSITIVE_INFINITY} for the {@code +Inf} bucket. An upper
066   *     bound must not be {@link Double#NaN}. The upperBounds array does not need to be sorted.
067   * @param counts must have the same length as {@code upperBounds}. The entry at index {@code i} is
068   *     the count for the {@code upperBounds} at index {@code i}. For each count, {@link
069   *     Number#longValue()} is called to get the value. Counts are <i>not</i> cumulative. Counts
070   *     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
084   * of(...)} methods, or use {@link ClassicHistogramBuckets#builder()}.
085   *
086   * <p>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. Must
089   *     contain at least {@link Double#POSITIVE_INFINITY} for the {@code +Inf} bucket. An upper
090   *     bound must not be {@link Double#NaN}. The upperBounds array does not need to be sorted.
091   * @param counts must have the same length as {@code upperBounds}. The entry at index {@code i} is
092   *     the count for the {@code upperBounds} at index {@code i}. Counts are <i>not</i> cumulative.
093   *     Counts must not be negative.
094   */
095  public static ClassicHistogramBuckets of(double[] upperBounds, long[] counts) {
096    double[] upperBoundsCopy = Arrays.copyOf(upperBounds, upperBounds.length);
097    long[] countsCopy = Arrays.copyOf(counts, counts.length);
098    sortAndValidate(upperBoundsCopy, countsCopy);
099    return new ClassicHistogramBuckets(upperBoundsCopy, countsCopy);
100  }
101
102  private static void sortAndValidate(double[] upperBounds, long[] counts) {
103    if (upperBounds.length != counts.length) {
104      throw new IllegalArgumentException(
105          "upperBounds.length == "
106              + upperBounds.length
107              + " but counts.length == "
108              + counts.length
109              + ". Expected the same length.");
110    }
111    sort(upperBounds, counts);
112    validate(upperBounds, counts);
113  }
114
115  /**
116   * Sorts upperBounds and counts in place using introspective quicksort.
117   *
118   * <p>Algorithm: 3-way quicksort with insertion sort for tiny partitions and heapsort fallback at
119   * the recursion depth limit. Parallel arrays are swapped in lockstep.
120   *
121   * <p>Complexity: O(n log n) average and worst case.
122   */
123  private static void sort(double[] upperBounds, long[] counts) {
124    DoubleArraySorter.sort(upperBounds, counts);
125  }
126
127  private static void validate(double[] upperBounds, long[] counts) {
128    // Preconditions:
129    // * upperBounds sorted
130    // * upperBounds and counts have the same length
131    if (upperBounds.length == 0) {
132      throw new IllegalArgumentException(
133          ClassicHistogramBuckets.class.getSimpleName()
134              + " cannot be empty. They must contain at least the +Inf bucket.");
135    }
136    if (upperBounds[upperBounds.length - 1] != Double.POSITIVE_INFINITY) {
137      throw new IllegalArgumentException(
138          ClassicHistogramBuckets.class.getSimpleName() + " must contain the +Inf bucket.");
139    }
140    for (int i = 0; i < upperBounds.length; i++) {
141      if (Double.isNaN(upperBounds[i])) {
142        throw new IllegalArgumentException(
143            "Cannot use NaN as an upper bound in " + ClassicHistogramBuckets.class.getSimpleName());
144      }
145      if (counts[i] < 0) {
146        throw new IllegalArgumentException(
147            "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  /** The count is <i>not</i> cumulative. */
166  public long getCount(int i) {
167    return counts[i];
168  }
169
170  public boolean isEmpty() {
171    return this.upperBounds.length == 0;
172  }
173
174  private List<ClassicHistogramBucket> asList() {
175    List<ClassicHistogramBucket> result = new ArrayList<>(size());
176    for (int i = 0; i < upperBounds.length; i++) {
177      result.add(new ClassicHistogramBucket(upperBounds[i], counts[i]));
178    }
179    return Collections.unmodifiableList(result);
180  }
181
182  @Override
183  public Iterator<ClassicHistogramBucket> iterator() {
184    return asList().iterator();
185  }
186
187  public Stream<ClassicHistogramBucket> stream() {
188    return asList().stream();
189  }
190
191  /**
192   * To create new {@link ClassicHistogramBuckets}, you can either use one of the static {@code
193   * of(...)} methods, or use {@code builder()}.
194   */
195  public static Builder builder() {
196    return new Builder();
197  }
198
199  public static class Builder {
200    private final List<Double> upperBounds = new ArrayList<>();
201    private final List<Long> counts = new ArrayList<>();
202
203    private Builder() {}
204
205    /** Must be called at least once for the {@link Double#POSITIVE_INFINITY} bucket. */
206    public Builder bucket(double upperBound, long count) {
207      upperBounds.add(upperBound);
208      counts.add(count);
209      return this;
210    }
211
212    /**
213     * Will throw an {@link IllegalArgumentException} if the {@link Double#POSITIVE_INFINITY} bucket
214     * is missing.
215     */
216    public ClassicHistogramBuckets build() {
217      return ClassicHistogramBuckets.of(upperBounds, counts);
218    }
219  }
220
221  /**
222   * In-place introsort for {@code upperBounds} and parallel {@code counts}.
223   *
224   * <p>Uses 3-way quicksort partitioning for large ranges, insertion sort for tiny ranges, and a
225   * heapsort fallback at the recursion-depth limit to guarantee O(n log n) worst-case complexity.
226   */
227  private static final class DoubleArraySorter {
228
229    private static final int INSERTION_SORT_THRESHOLD = 24;
230
231    private static void sort(double[] upperBounds, long[] counts) {
232      int right = upperBounds.length - 1;
233      if (right <= 0) {
234        return;
235      }
236      introSort(upperBounds, counts, 0, right, depthLimit(upperBounds.length));
237    }
238
239    private static void introSort(
240        double[] upperBounds, long[] counts, int left, int right, int depthLimit) {
241      while (left < right) {
242        if (right - left + 1 <= INSERTION_SORT_THRESHOLD) {
243          insertionSort(upperBounds, counts, left, right);
244          return;
245        }
246        if (depthLimit == 0) {
247          heapSort(upperBounds, counts, left, right);
248          return;
249        }
250        depthLimit--;
251
252        int mid = left + ((right - left) >>> 1);
253        int pivotIndex = medianOf3(upperBounds, left, mid, right);
254        double pivot = upperBounds[pivotIndex];
255
256        int lt = left;
257        int i = left;
258        int gt = right;
259        while (i <= gt) {
260          int cmp = compare(upperBounds[i], pivot);
261          if (cmp < 0) {
262            swap(i, lt, upperBounds, counts);
263            i++;
264            lt++;
265          } else if (cmp > 0) {
266            swap(i, gt, upperBounds, counts);
267            gt--;
268          } else {
269            i++;
270          }
271        }
272
273        if (lt - left < right - gt) {
274          introSort(upperBounds, counts, left, lt - 1, depthLimit);
275          left = gt + 1;
276        } else {
277          introSort(upperBounds, counts, gt + 1, right, depthLimit);
278          right = lt - 1;
279        }
280      }
281    }
282
283    private static void insertionSort(double[] upperBounds, long[] counts, int left, int right) {
284      for (int i = left + 1; i <= right; i++) {
285        double upperBound = upperBounds[i];
286        long count = counts[i];
287        int j = i - 1;
288        while (j >= left && compare(upperBounds[j], upperBound) > 0) {
289          upperBounds[j + 1] = upperBounds[j];
290          counts[j + 1] = counts[j];
291          j--;
292        }
293        upperBounds[j + 1] = upperBound;
294        counts[j + 1] = count;
295      }
296    }
297
298    private static void heapSort(double[] upperBounds, long[] counts, int left, int right) {
299      int size = right - left + 1;
300      for (int i = (size >>> 1) - 1; i >= 0; i--) {
301        siftDown(upperBounds, counts, left, i, size);
302      }
303      for (int end = size - 1; end > 0; end--) {
304        swap(left, left + end, upperBounds, counts);
305        siftDown(upperBounds, counts, left, 0, end);
306      }
307    }
308
309    private static void siftDown(
310        double[] upperBounds, long[] counts, int base, int root, int size) {
311      while (true) {
312        int child = (root << 1) + 1;
313        if (child >= size) {
314          return;
315        }
316        int rightChild = child + 1;
317        if (rightChild < size
318            && compare(upperBounds[base + child], upperBounds[base + rightChild]) < 0) {
319          child = rightChild;
320        }
321        if (compare(upperBounds[base + root], upperBounds[base + child]) >= 0) {
322          return;
323        }
324        swap(base + root, base + child, upperBounds, counts);
325        root = child;
326      }
327    }
328
329    private static int depthLimit(int length) {
330      int result = 0;
331      while (length > 1) {
332        result++;
333        length >>>= 1;
334      }
335      return result << 1;
336    }
337
338    private static int medianOf3(double[] upperBounds, int i, int j, int k) {
339      if (compare(upperBounds[i], upperBounds[j]) > 0) {
340        int tmp = i;
341        i = j;
342        j = tmp;
343      }
344      if (compare(upperBounds[j], upperBounds[k]) > 0) {
345        int tmp = j;
346        j = k;
347        k = tmp;
348      }
349      if (compare(upperBounds[i], upperBounds[j]) > 0) {
350        int tmp = i;
351        i = j;
352        j = tmp;
353      }
354      return j;
355    }
356
357    private static int compare(double a, double b) {
358      return Double.compare(a, b);
359    }
360
361    private static void swap(int i, int j, double[] upperBounds, long[] counts) {
362      if (i == j) {
363        return;
364      }
365      double upperBound = upperBounds[i];
366      upperBounds[i] = upperBounds[j];
367      upperBounds[j] = upperBound;
368      long count = counts[i];
369      counts[i] = counts[j];
370      counts[j] = count;
371    }
372  }
373}