001package io.prometheus.metrics.model.snapshots; 002 003import io.prometheus.metrics.annotations.StableApi; 004import io.prometheus.metrics.config.EscapingScheme; 005import java.util.ArrayList; 006import java.util.Arrays; 007import java.util.Collection; 008import java.util.Collections; 009import java.util.Iterator; 010import java.util.List; 011import java.util.stream.Stream; 012import javax.annotation.Nullable; 013 014/** Immutable snapshot of a StateSet metric. */ 015@StableApi 016public final class StateSetSnapshot extends MetricSnapshot { 017 018 /** 019 * To create a new {@link StateSetSnapshot}, you can either call the constructor directly or use 020 * the builder with {@link StateSetSnapshot#builder()}. 021 * 022 * @param metadata See {@link MetricMetadata} for more naming conventions. 023 * @param data the constructor will create a sorted copy of the collection. 024 */ 025 public StateSetSnapshot(MetricMetadata metadata, Collection<StateSetDataPointSnapshot> data) { 026 this(metadata, data, false); 027 validate(); 028 } 029 030 private StateSetSnapshot( 031 MetricMetadata metadata, Collection<StateSetDataPointSnapshot> data, boolean internal) { 032 super(metadata, data, internal); 033 } 034 035 private void validate() { 036 if (getMetadata().hasUnit()) { 037 throw new IllegalArgumentException("An state set metric cannot have a unit."); 038 } 039 for (StateSetDataPointSnapshot entry : getDataPoints()) { 040 if (entry.getLabels().contains(getMetadata().getPrometheusName())) { 041 throw new IllegalArgumentException( 042 "Label name " + getMetadata().getPrometheusName() + " is reserved."); 043 } 044 } 045 } 046 047 @SuppressWarnings("unchecked") 048 @Override 049 public List<StateSetDataPointSnapshot> getDataPoints() { 050 return (List<StateSetDataPointSnapshot>) dataPoints; 051 } 052 053 @SuppressWarnings("unchecked") 054 @Override 055 MetricSnapshot escape( 056 EscapingScheme escapingScheme, List<? extends DataPointSnapshot> dataPointSnapshots) { 057 return new StateSetSnapshot( 058 getMetadata().escape(escapingScheme), 059 (List<StateSetSnapshot.StateSetDataPointSnapshot>) dataPointSnapshots, 060 true); 061 } 062 063 public static class StateSetDataPointSnapshot extends DataPointSnapshot 064 implements Iterable<State> { 065 final String[] names; 066 final boolean[] values; 067 068 /** 069 * To create a new {@link StateSetDataPointSnapshot}, you can either call the constructor 070 * directly or use the Builder with {@link StateSetDataPointSnapshot#builder()}. 071 * 072 * @param names state names. Must have at least 1 entry. The constructor will create a copy of 073 * the array. 074 * @param values state values. Must have the same length as {@code names}. The constructor will 075 * create a copy of the array. 076 * @param labels must not be null. Use {@link Labels#EMPTY} if there are no labels. 077 */ 078 public StateSetDataPointSnapshot(String[] names, boolean[] values, Labels labels) { 079 this(names, values, labels, 0L); 080 } 081 082 /** 083 * Constructor with an additional scrape timestamp. This is only useful in rare cases as the 084 * scrape timestamp is usually set by the Prometheus server during scraping. Exceptions include 085 * mirroring metrics with given timestamps from other metric sources. 086 */ 087 public StateSetDataPointSnapshot( 088 String[] names, boolean[] values, Labels labels, long scrapeTimestampMillis) { 089 this(names, values, labels, scrapeTimestampMillis, false); 090 } 091 092 private StateSetDataPointSnapshot( 093 String[] names, 094 boolean[] values, 095 Labels labels, 096 long scrapeTimestampMillis, 097 boolean internal) { 098 super(labels, 0L, scrapeTimestampMillis, false); 099 if (internal) { 100 this.names = names; 101 this.values = values; 102 } else { 103 if (names.length == 0) { 104 throw new IllegalArgumentException("StateSet must have at least one state."); 105 } 106 if (names.length != values.length) { 107 throw new IllegalArgumentException("names[] and values[] must have the same length"); 108 } 109 110 String[] namesCopy = Arrays.copyOf(names, names.length); 111 boolean[] valuesCopy = Arrays.copyOf(values, names.length); 112 sort(namesCopy, valuesCopy); 113 this.names = namesCopy; 114 this.values = valuesCopy; 115 validate(); 116 } 117 } 118 119 public int size() { 120 return names.length; 121 } 122 123 public String getName(int i) { 124 return names[i]; 125 } 126 127 public boolean isTrue(int i) { 128 return values[i]; 129 } 130 131 private void validate() { 132 for (int i = 0; i < names.length; i++) { 133 if (names[i].isEmpty()) { 134 throw new IllegalArgumentException("Empty string as state name"); 135 } 136 if (i > 0 && names[i - 1].equals(names[i])) { 137 throw new IllegalArgumentException(names[i] + " duplicate state name"); 138 } 139 } 140 } 141 142 @Override 143 DataPointSnapshot escape(EscapingScheme escapingScheme) { 144 return new StateSetSnapshot.StateSetDataPointSnapshot( 145 names, 146 values, 147 SnapshotEscaper.escapeLabels(getLabels(), escapingScheme), 148 getScrapeTimestampMillis(), 149 true); 150 } 151 152 private List<State> asList() { 153 List<State> result = new ArrayList<>(size()); 154 for (int i = 0; i < names.length; i++) { 155 result.add(new State(names[i], values[i])); 156 } 157 return Collections.unmodifiableList(result); 158 } 159 160 @Override 161 public Iterator<State> iterator() { 162 return asList().iterator(); 163 } 164 165 public Stream<State> stream() { 166 return asList().stream(); 167 } 168 169 /** 170 * Sorts names and values in place using introspective quicksort. 171 * 172 * <p>Algorithm: 3-way quicksort with insertion sort for tiny partitions and heapsort fallback 173 * at the recursion depth limit. Parallel arrays are swapped in lockstep. 174 * 175 * <p>Complexity: O(n log n) average and worst case. 176 */ 177 private static void sort(String[] names, boolean[] values) { 178 StringBooleanArraySorter.sort(names, values); 179 } 180 181 public static Builder builder() { 182 return new Builder(); 183 } 184 185 public static class Builder extends DataPointSnapshot.Builder<Builder> { 186 187 private final List<String> names = new ArrayList<>(); 188 private final List<Boolean> values = new ArrayList<>(); 189 190 private Builder() {} 191 192 /** Add a state. Call multiple times to add multiple states. */ 193 public Builder state(String name, boolean value) { 194 names.add(name); 195 values.add(value); 196 return this; 197 } 198 199 @Override 200 protected Builder self() { 201 return this; 202 } 203 204 public StateSetDataPointSnapshot build() { 205 boolean[] valuesArray = new boolean[values.size()]; 206 for (int i = 0; i < values.size(); i++) { 207 valuesArray[i] = values.get(i); 208 } 209 return new StateSetDataPointSnapshot( 210 names.toArray(new String[] {}), valuesArray, labels, scrapeTimestampMillis); 211 } 212 } 213 } 214 215 public static class State { 216 private final String name; 217 private final boolean value; 218 219 private State(String name, boolean value) { 220 this.name = name; 221 this.value = value; 222 } 223 224 public String getName() { 225 return name; 226 } 227 228 public boolean isTrue() { 229 return value; 230 } 231 } 232 233 public static Builder builder() { 234 return new Builder(); 235 } 236 237 public static class Builder extends MetricSnapshot.Builder<Builder> { 238 239 private final List<StateSetDataPointSnapshot> dataPoints = new ArrayList<>(); 240 241 private Builder() {} 242 243 /** Add a data point. Call multiple times to add multiple data points. */ 244 public Builder dataPoint(StateSetDataPointSnapshot dataPoint) { 245 dataPoints.add(dataPoint); 246 return this; 247 } 248 249 @Override 250 public Builder unit(@Nullable Unit unit) { 251 throw new IllegalArgumentException("StateSet metric cannot have a unit."); 252 } 253 254 @Override 255 public StateSetSnapshot build() { 256 return new StateSetSnapshot(buildMetadata(), dataPoints); 257 } 258 259 @Override 260 protected Builder self() { 261 return this; 262 } 263 } 264 265 /** 266 * In-place introsort for state {@code names} and parallel boolean {@code values}. 267 * 268 * <p>Uses 3-way quicksort partitioning for large ranges, insertion sort for tiny ranges, and a 269 * heapsort fallback at the recursion-depth limit to guarantee O(n log n) worst-case complexity. 270 */ 271 private static final class StringBooleanArraySorter { 272 273 private static final int INSERTION_SORT_THRESHOLD = 24; 274 275 private static void sort(String[] names, boolean[] values) { 276 int right = names.length - 1; 277 if (right <= 0) { 278 return; 279 } 280 introSort(names, values, 0, right, depthLimit(names.length)); 281 } 282 283 private static void introSort( 284 String[] names, boolean[] values, int left, int right, int depthLimit) { 285 while (left < right) { 286 if (right - left + 1 <= INSERTION_SORT_THRESHOLD) { 287 insertionSort(names, values, left, right); 288 return; 289 } 290 if (depthLimit == 0) { 291 heapSort(names, values, left, right); 292 return; 293 } 294 depthLimit--; 295 296 int mid = left + ((right - left) >>> 1); 297 int pivotIndex = medianOf3(names, left, mid, right); 298 String pivot = names[pivotIndex]; 299 300 int lt = left; 301 int i = left; 302 int gt = right; 303 while (i <= gt) { 304 int cmp = compare(names[i], pivot); 305 if (cmp < 0) { 306 swap(i, lt, names, values); 307 i++; 308 lt++; 309 } else if (cmp > 0) { 310 swap(i, gt, names, values); 311 gt--; 312 } else { 313 i++; 314 } 315 } 316 317 if (lt - left < right - gt) { 318 introSort(names, values, left, lt - 1, depthLimit); 319 left = gt + 1; 320 } else { 321 introSort(names, values, gt + 1, right, depthLimit); 322 right = lt - 1; 323 } 324 } 325 } 326 327 private static void insertionSort(String[] names, boolean[] values, int left, int right) { 328 for (int i = left + 1; i <= right; i++) { 329 String name = names[i]; 330 boolean value = values[i]; 331 int j = i - 1; 332 while (j >= left && compare(names[j], name) > 0) { 333 names[j + 1] = names[j]; 334 values[j + 1] = values[j]; 335 j--; 336 } 337 names[j + 1] = name; 338 values[j + 1] = value; 339 } 340 } 341 342 private static void heapSort(String[] names, boolean[] values, int left, int right) { 343 int size = right - left + 1; 344 for (int i = (size >>> 1) - 1; i >= 0; i--) { 345 siftDown(names, values, left, i, size); 346 } 347 for (int end = size - 1; end > 0; end--) { 348 swap(left, left + end, names, values); 349 siftDown(names, values, left, 0, end); 350 } 351 } 352 353 private static void siftDown(String[] names, boolean[] values, int base, int root, int size) { 354 while (true) { 355 int child = (root << 1) + 1; 356 if (child >= size) { 357 return; 358 } 359 int rightChild = child + 1; 360 if (rightChild < size && compare(names[base + child], names[base + rightChild]) < 0) { 361 child = rightChild; 362 } 363 if (compare(names[base + root], names[base + child]) >= 0) { 364 return; 365 } 366 swap(base + root, base + child, names, values); 367 root = child; 368 } 369 } 370 371 private static int depthLimit(int length) { 372 int result = 0; 373 while (length > 1) { 374 result++; 375 length >>>= 1; 376 } 377 return result << 1; 378 } 379 380 private static int medianOf3(String[] names, int i, int j, int k) { 381 if (compare(names[i], names[j]) > 0) { 382 int tmp = i; 383 i = j; 384 j = tmp; 385 } 386 if (compare(names[j], names[k]) > 0) { 387 int tmp = j; 388 j = k; 389 k = tmp; 390 } 391 if (compare(names[i], names[j]) > 0) { 392 int tmp = i; 393 i = j; 394 j = tmp; 395 } 396 return j; 397 } 398 399 private static int compare(String left, String right) { 400 return left.compareTo(right); 401 } 402 403 private static void swap(int i, int j, String[] names, boolean[] values) { 404 if (i == j) { 405 return; 406 } 407 String name = names[i]; 408 names[i] = names[j]; 409 names[j] = name; 410 boolean value = values[i]; 411 values[i] = values[j]; 412 values[j] = value; 413 } 414 } 415}