diff --git a/benchmark/pom.xml b/benchmark/pom.xml index 5a8b768af..feaeb93c1 100644 --- a/benchmark/pom.xml +++ b/benchmark/pom.xml @@ -28,12 +28,12 @@ org.openjdk.jmh jmh-core - 1.3.2 + 1.21 org.openjdk.jmh jmh-generator-annprocess - 1.3.2 + 1.21 diff --git a/benchmark/src/main/java/io/prometheus/benchmark/LabelsToChildLookupBenchmark.java b/benchmark/src/main/java/io/prometheus/benchmark/LabelsToChildLookupBenchmark.java new file mode 100644 index 000000000..14415133e --- /dev/null +++ b/benchmark/src/main/java/io/prometheus/benchmark/LabelsToChildLookupBenchmark.java @@ -0,0 +1,69 @@ +package io.prometheus.benchmark; + +import io.prometheus.client.Counter; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.concurrent.TimeUnit; + +@State(Scope.Benchmark) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@BenchmarkMode(Mode.AverageTime) +public class LabelsToChildLookupBenchmark { + + private static final String LABEL1 = "label1", LABEL2 = "label2", LABEL3 = "label3"; + private static final String LABEL4 = "label4", LABEL5 = "label5"; + + private Counter noLabelsCollector, oneLabelCollector, twoLabelsCollector, threeLabelsCollector; + private Counter fourLabelsCollector, fiveLabelsCollector; + + @Setup + public void setup() { + Counter.Builder builder = new Counter.Builder().name("testCollector").help("testHelp"); + noLabelsCollector = builder.create(); + oneLabelCollector = builder.labelNames("name1").create(); + twoLabelsCollector = builder.labelNames("name1", "name2").create(); + threeLabelsCollector = builder.labelNames("name1", "name2", "name3").create(); + fourLabelsCollector = builder.labelNames("name1", "name2", "name3", "name4").create(); + fiveLabelsCollector = builder.labelNames("name1", "name2", "name3", "name4", "name5").create(); + } + + @Benchmark + public void baseline(LabelsToChildLookupBenchmark state) { + noLabelsCollector.inc(); + } + + @Benchmark + public void oneLabel(LabelsToChildLookupBenchmark state) { + oneLabelCollector.labels(LABEL1).inc(); + } + + @Benchmark + public void twoLabels(LabelsToChildLookupBenchmark state) { + twoLabelsCollector.labels(LABEL1, LABEL2).inc(); + } + + @Benchmark + public void threeLabels(LabelsToChildLookupBenchmark state) { + threeLabelsCollector.labels(LABEL1, LABEL2, LABEL3).inc(); + } + + @Benchmark + public void fourLabels(LabelsToChildLookupBenchmark state) { + fourLabelsCollector.labels(LABEL1, LABEL2, LABEL3, LABEL4).inc(); + } + + @Benchmark + public void fiveLabels(LabelsToChildLookupBenchmark state) { + fiveLabelsCollector.labels(LABEL1, LABEL2, LABEL3, LABEL4, LABEL5).inc(); + } + + public static void main(String[] args) throws RunnerException { + new Runner(new OptionsBuilder() + .include(LabelsToChildLookupBenchmark.class.getSimpleName()) + .build()).run(); + } +} diff --git a/simpleclient/src/main/java/io/prometheus/client/SimpleCollector.java b/simpleclient/src/main/java/io/prometheus/client/SimpleCollector.java index ed8c5c8a0..5b857b7c5 100644 --- a/simpleclient/src/main/java/io/prometheus/client/SimpleCollector.java +++ b/simpleclient/src/main/java/io/prometheus/client/SimpleCollector.java @@ -1,10 +1,12 @@ package io.prometheus.client; +import java.util.AbstractList; import java.util.ArrayList; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; /** * Common functionality for {@link Gauge}, {@link Counter}, {@link Summary} and {@link Histogram}. @@ -53,30 +55,159 @@ public abstract class SimpleCollector extends Collector { protected final ConcurrentMap, Child> children = new ConcurrentHashMap, Child>(); protected Child noLabelsChild; + private final ThreadLocal> labelNamesPool = new ThreadLocal>(); - /** - * Return the Child with the given labels, creating it if needed. - *

- * Must be passed the same number of labels are were passed to {@link #labelNames}. - */ - public Child labels(String... labelValues) { - if (labelValues.length != labelNames.size()) { - throw new IllegalArgumentException("Incorrect number of labels."); + /** + * It is just reimplementing in a more JIT-friendly way both equals/hashCode to avoid + * using Iterators like the original {@link AbstractList}. + */ + private static final class LabelNames extends ArrayList { + + public LabelNames(int capacity) { + super(capacity); + } + + public boolean equals(Object o) { + if (o == this) + return true; + if (!(o instanceof ArrayList)) { + //what if o is a singleton list or empty? + //We can just use the common fast path + if (o instanceof List) { + if (((List) o).size() > 1) { + return super.equals(o); + } + } else { + return super.equals(o); + } + } + final int size = size(); + final List other = (List) o; + if (size != other.size()) { + return false; + } + for (int i = 0; i < size; i++) { + final Object a = get(i); + final Object b = other.get(i); + final boolean eq = (a == b) || (a != null && a.equals(b)); + if (!eq) { + return false; + } + } + return true; + } + + /** + * Returns the hash code value for this list. + * + *

This implementation uses exactly the code that is used to define the + * list hash function in the documentation for the {@link List#hashCode} + * method. + * + * @return the hash code value for this list + */ + public int hashCode() { + int hashCode = 1; + for (int i = 0, size = size(); i < size; i++) { + final Object e = get(i); + final int objHash = (e == null ? 0 : e.hashCode()); + hashCode = 31 * hashCode + objHash; + } + return hashCode; + } } - for (String label: labelValues) { - if (label == null) { - throw new IllegalArgumentException("Label cannot be null."); - } + + /** + * Return the Child with the given labels, creating it if needed. + *

+ * Must be passed the same number of labels are were passed to {@link #labelNames}. + */ + public Child labels(String... labelValues) { + final List labels; + if (labelValues.length > 0) { + labels = getPooledLabels(); + for (String label : labelValues) { + labels.add(label); + } + } else { + labels = Collections.emptyList(); + } + return getOrCreateChild(labels); + } + + public Child labels(String l1) { + ArrayList pooledLabels = getPooledLabels(); + pooledLabels.add(l1); + return getOrCreateChild(pooledLabels); } - List key = Arrays.asList(labelValues); - Child c = children.get(key); - if (c != null) { - return c; + public Child labels(String l1, String l2) { + ArrayList pooledLabels = getPooledLabels(); + pooledLabels.add(l1); + pooledLabels.add(l2); + return getOrCreateChild(pooledLabels); + } + public Child labels(String l1, String l2, String l3) { + ArrayList pooledLabels = getPooledLabels(); + pooledLabels.add(l1); + pooledLabels.add(l2); + pooledLabels.add(l3); + return getOrCreateChild(pooledLabels); + } + public Child labels(String l1, String l2, String l3, String l4) { + ArrayList pooledLabels = getPooledLabels(); + pooledLabels.add(l1); + pooledLabels.add(l2); + pooledLabels.add(l3); + pooledLabels.add(l4); + return getOrCreateChild(pooledLabels); + } + + private ArrayList getPooledLabels() { + final ThreadLocal> labelNamesPool = this.labelNamesPool; + ArrayList pooledLabels = labelNamesPool.get(); + if (pooledLabels == null) { + pooledLabels = new LabelNames(10); + labelNamesPool.set(pooledLabels); + } else { + pooledLabels.clear(); + } + return pooledLabels; + } + + private Child getOrCreateChild(List labels) { + Child c = children.get(labels); + if (c != null) { + return c; + } + return tryCreateChild(labels); + } + + private Child tryCreateChild(List labels) { + validateLabels(labels); + Child c2 = newChild(); + Child tmp = children.putIfAbsent(labels, c2); + if (tmp == null) { + // given that putIfAbsent return null only when a new + // labels has been added, we need to clear up + // the pool to avoid labels to be both in the pool + // and as children key + labelNamesPool.set(null); + return c2; + } else { + return tmp; + } + } + + private void validateLabels(List labelValues) { + if (labelValues.size() != labelNames.size()) { + throw new IllegalArgumentException("Incorrect number of labels."); + } + for (String label : labelValues) { + if (label == null) { + throw new IllegalArgumentException("Label cannot be null."); + } + } } - Child c2 = newChild(); - Child tmp = children.putIfAbsent(key, c2); - return tmp == null ? c2 : tmp; - } /** * Remove the Child with the given labels. @@ -164,9 +295,11 @@ protected SimpleCollector(Builder b) { checkMetricName(fullname); if (b.help.isEmpty()) throw new IllegalStateException("Help hasn't been set."); help = b.help; - labelNames = Arrays.asList(b.labelNames); - - for (String n: labelNames) { + labelNames = new LabelNames(b.labelNames.length); + for (String label : b.labelNames) { + labelNames.add(label); + } + for (String n: b.labelNames) { checkMetricLabelName(n); } diff --git a/simpleclient/src/test/java/io/prometheus/client/SimpleCollectorTest.java b/simpleclient/src/test/java/io/prometheus/client/SimpleCollectorTest.java index d5c7e3e32..6c5a4078e 100644 --- a/simpleclient/src/test/java/io/prometheus/client/SimpleCollectorTest.java +++ b/simpleclient/src/test/java/io/prometheus/client/SimpleCollectorTest.java @@ -39,6 +39,11 @@ public void testNullLabelThrows() { metric.labels(new String[]{null}); } + @Test(expected=IllegalArgumentException.class) + public void testNullLabelsThrows() { + metric.labels(new String[]{null, null}); + } + @Test(expected=IllegalArgumentException.class) public void testTooManyLabelsThrows() { metric.labels("a", "b");