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}