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}