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 representation of native histogram buckets.
013 *
014 * <p>The bucket index defines the boundaries of the bucket, depending on the histogram's {@link
015 * HistogramSnapshot.HistogramDataPointSnapshot#getNativeSchema() schema}.
016 *
017 * <pre>
018 *     base = 2^(2^-schema)
019 *     lower bound = base^(index - 1)
020 *     upper bound = base^index
021 * </pre>
022 */
023@StableApi
024public class NativeHistogramBuckets implements Iterable<NativeHistogramBucket> {
025
026  public static final NativeHistogramBuckets EMPTY =
027      new NativeHistogramBuckets(new int[] {}, new long[] {});
028  private final int[] bucketIndexes; // sorted
029  private final long[] counts;
030
031  private NativeHistogramBuckets(int[] bucketIndexes, long[] counts) {
032    this.bucketIndexes = bucketIndexes;
033    this.counts = counts;
034  }
035
036  /**
037   * To create a new {@link NativeHistogramBuckets} instance, you can either use one of the static
038   * {@code of(...)} methods, or use {@link NativeHistogramBuckets#builder()}.
039   *
040   * @param bucketIndexes see class javadoc of {@link NativeHistogramBuckets}. May be empty.
041   * @param counts must have the same length as bucketIndexes
042   */
043  public static NativeHistogramBuckets of(int[] bucketIndexes, long[] counts) {
044    int[] bucketIndexesCopy = Arrays.copyOf(bucketIndexes, bucketIndexes.length);
045    long[] countsCopy = Arrays.copyOf(counts, counts.length);
046    sortAndValidate(bucketIndexesCopy, countsCopy);
047    return new NativeHistogramBuckets(bucketIndexesCopy, countsCopy);
048  }
049
050  /**
051   * To create a new {@link NativeHistogramBuckets} instance, you can either use one of the static
052   * {@code of(...)} methods, or use {@link NativeHistogramBuckets#builder()}.
053   *
054   * @param bucketIndexes see class javadoc of {@link NativeHistogramBuckets}. May be empty.
055   * @param counts must have the same size as bucketIndexes
056   */
057  public static NativeHistogramBuckets of(List<Integer> bucketIndexes, List<Long> counts) {
058    int[] bucketIndexesCopy = new int[bucketIndexes.size()];
059    for (int i = 0; i < bucketIndexes.size(); i++) {
060      bucketIndexesCopy[i] = bucketIndexes.get(i);
061    }
062    long[] countsCopy = new long[counts.size()];
063    for (int i = 0; i < counts.size(); i++) {
064      countsCopy[i] = counts.get(i);
065    }
066    sortAndValidate(bucketIndexesCopy, countsCopy);
067    return new NativeHistogramBuckets(bucketIndexesCopy, countsCopy);
068  }
069
070  public int size() {
071    return bucketIndexes.length;
072  }
073
074  private List<NativeHistogramBucket> asList() {
075    List<NativeHistogramBucket> result = new ArrayList<>(size());
076    for (int i = 0; i < bucketIndexes.length; i++) {
077      result.add(new NativeHistogramBucket(bucketIndexes[i], counts[i]));
078    }
079    return Collections.unmodifiableList(result);
080  }
081
082  @Override
083  public Iterator<NativeHistogramBucket> iterator() {
084    return asList().iterator();
085  }
086
087  public Stream<NativeHistogramBucket> stream() {
088    return asList().stream();
089  }
090
091  public int getBucketIndex(int i) {
092    return bucketIndexes[i];
093  }
094
095  public long getCount(int i) {
096    return counts[i];
097  }
098
099  private static void sortAndValidate(int[] bucketIndexes, long[] counts) {
100    if (bucketIndexes.length != counts.length) {
101      throw new IllegalArgumentException(
102          "bucketIndexes.length == "
103              + bucketIndexes.length
104              + " but counts.length == "
105              + counts.length
106              + ". Expected the same length.");
107    }
108    sort(bucketIndexes, counts);
109    validate(bucketIndexes, counts);
110  }
111
112  /**
113   * Sorts bucketIndexes and counts in place using introspective quicksort.
114   *
115   * <p>Algorithm: 3-way quicksort with insertion sort for tiny partitions and heapsort fallback at
116   * the recursion depth limit. Parallel arrays are swapped in lockstep.
117   *
118   * <p>Complexity: O(n log n) average and worst case.
119   */
120  private static void sort(int[] bucketIndexes, long[] counts) {
121    IntArraySorter.sort(bucketIndexes, counts);
122  }
123
124  private static void validate(int[] bucketIndexes, long[] counts) {
125    // Preconditions:
126    // * bucketIndexes sorted
127    // * bucketIndexes and counts have the same length
128    for (int i = 0; i < bucketIndexes.length; i++) {
129      if (counts[i] < 0) {
130        throw new IllegalArgumentException("Bucket counts cannot be negative.");
131      }
132      if (i > 0) {
133        if (bucketIndexes[i - 1] == bucketIndexes[i]) {
134          throw new IllegalArgumentException("Duplicate bucket index " + bucketIndexes[i]);
135        }
136      }
137    }
138  }
139
140  public static Builder builder() {
141    return new Builder();
142  }
143
144  public static class Builder {
145
146    private final List<Integer> bucketIndexes = new ArrayList<>();
147    private final List<Long> counts = new ArrayList<>();
148
149    private Builder() {}
150
151    /** Add a native histogram bucket. Call multiple times to add multiple buckets. */
152    public Builder bucket(int bucketIndex, long count) {
153      bucketIndexes.add(bucketIndex);
154      counts.add(count);
155      return this;
156    }
157
158    public NativeHistogramBuckets build() {
159      return NativeHistogramBuckets.of(bucketIndexes, counts);
160    }
161  }
162
163  /**
164   * In-place introsort for {@code bucketIndexes} and parallel {@code counts}.
165   *
166   * <p>Uses 3-way quicksort partitioning for large ranges, insertion sort for tiny ranges, and a
167   * heapsort fallback at the recursion-depth limit to guarantee O(n log n) worst-case complexity.
168   */
169  private static final class IntArraySorter {
170
171    private static final int INSERTION_SORT_THRESHOLD = 24;
172
173    private static void sort(int[] bucketIndexes, long[] counts) {
174      int right = bucketIndexes.length - 1;
175      if (right <= 0) {
176        return;
177      }
178      introSort(bucketIndexes, counts, 0, right, depthLimit(bucketIndexes.length));
179    }
180
181    private static void introSort(
182        int[] bucketIndexes, long[] counts, int left, int right, int depthLimit) {
183      while (left < right) {
184        if (right - left + 1 <= INSERTION_SORT_THRESHOLD) {
185          insertionSort(bucketIndexes, counts, left, right);
186          return;
187        }
188        if (depthLimit == 0) {
189          heapSort(bucketIndexes, counts, left, right);
190          return;
191        }
192        depthLimit--;
193
194        int mid = left + ((right - left) >>> 1);
195        int pivotIndex = medianOf3(bucketIndexes, left, mid, right);
196        int pivot = bucketIndexes[pivotIndex];
197
198        int lt = left;
199        int i = left;
200        int gt = right;
201        while (i <= gt) {
202          int cmp = compare(bucketIndexes[i], pivot);
203          if (cmp < 0) {
204            swap(i, lt, bucketIndexes, counts);
205            i++;
206            lt++;
207          } else if (cmp > 0) {
208            swap(i, gt, bucketIndexes, counts);
209            gt--;
210          } else {
211            i++;
212          }
213        }
214
215        if (lt - left < right - gt) {
216          introSort(bucketIndexes, counts, left, lt - 1, depthLimit);
217          left = gt + 1;
218        } else {
219          introSort(bucketIndexes, counts, gt + 1, right, depthLimit);
220          right = lt - 1;
221        }
222      }
223    }
224
225    private static void insertionSort(int[] bucketIndexes, long[] counts, int left, int right) {
226      for (int i = left + 1; i <= right; i++) {
227        int bucketIndex = bucketIndexes[i];
228        long count = counts[i];
229        int j = i - 1;
230        while (j >= left && compare(bucketIndexes[j], bucketIndex) > 0) {
231          bucketIndexes[j + 1] = bucketIndexes[j];
232          counts[j + 1] = counts[j];
233          j--;
234        }
235        bucketIndexes[j + 1] = bucketIndex;
236        counts[j + 1] = count;
237      }
238    }
239
240    private static void heapSort(int[] bucketIndexes, long[] counts, int left, int right) {
241      int size = right - left + 1;
242      for (int i = (size >>> 1) - 1; i >= 0; i--) {
243        siftDown(bucketIndexes, counts, left, i, size);
244      }
245      for (int end = size - 1; end > 0; end--) {
246        swap(left, left + end, bucketIndexes, counts);
247        siftDown(bucketIndexes, counts, left, 0, end);
248      }
249    }
250
251    private static void siftDown(int[] bucketIndexes, long[] counts, int base, int root, int size) {
252      while (true) {
253        int child = (root << 1) + 1;
254        if (child >= size) {
255          return;
256        }
257        int rightChild = child + 1;
258        if (rightChild < size
259            && compare(bucketIndexes[base + child], bucketIndexes[base + rightChild]) < 0) {
260          child = rightChild;
261        }
262        if (compare(bucketIndexes[base + root], bucketIndexes[base + child]) >= 0) {
263          return;
264        }
265        swap(base + root, base + child, bucketIndexes, counts);
266        root = child;
267      }
268    }
269
270    private static int depthLimit(int length) {
271      int result = 0;
272      while (length > 1) {
273        result++;
274        length >>>= 1;
275      }
276      return result << 1;
277    }
278
279    private static int medianOf3(int[] bucketIndexes, int i, int j, int k) {
280      if (compare(bucketIndexes[i], bucketIndexes[j]) > 0) {
281        int tmp = i;
282        i = j;
283        j = tmp;
284      }
285      if (compare(bucketIndexes[j], bucketIndexes[k]) > 0) {
286        int tmp = j;
287        j = k;
288        k = tmp;
289      }
290      if (compare(bucketIndexes[i], bucketIndexes[j]) > 0) {
291        int tmp = i;
292        i = j;
293        j = tmp;
294      }
295      return j;
296    }
297
298    private static int compare(int a, int b) {
299      return Integer.compare(a, b);
300    }
301
302    private static void swap(int i, int j, int[] bucketIndexes, long[] counts) {
303      if (i == j) {
304        return;
305      }
306      int bucketIndex = bucketIndexes[i];
307      bucketIndexes[i] = bucketIndexes[j];
308      bucketIndexes[j] = bucketIndex;
309      long count = counts[i];
310      counts[i] = counts[j];
311      counts[j] = count;
312    }
313  }
314}