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}