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}