Skip to content

Commit 03576e6

Browse files
committed
Reduced garbage produced on label lookup
Introduced pooling of label Names to reduce garbage in the hot path, updated benchmarks to measure it, improved SimpleCollector creation when are used labels with no label names or with a single element. Introduced a new ArrayList implementation with faster hashCode/equals to allow faster lookups. Signed-off-by: Francesco Nigro <[email protected]>
1 parent 340251a commit 03576e6

File tree

5 files changed

+272
-29
lines changed

5 files changed

+272
-29
lines changed

benchmark/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@
2828
<dependency>
2929
<groupId>org.openjdk.jmh</groupId>
3030
<artifactId>jmh-core</artifactId>
31-
<version>1.3.2</version>
31+
<version>1.21</version>
3232
</dependency>
3333
<dependency>
3434
<groupId>org.openjdk.jmh</groupId>
3535
<artifactId>jmh-generator-annprocess</artifactId>
36-
<version>1.3.2</version>
36+
<version>1.21</version>
3737
</dependency>
3838

3939
<dependency>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package io.prometheus.benchmark;
2+
3+
import io.prometheus.client.Histogram;
4+
import org.openjdk.jmh.annotations.Benchmark;
5+
import org.openjdk.jmh.annotations.BenchmarkMode;
6+
import org.openjdk.jmh.annotations.Mode;
7+
import org.openjdk.jmh.annotations.OutputTimeUnit;
8+
import org.openjdk.jmh.annotations.Param;
9+
import org.openjdk.jmh.annotations.Scope;
10+
import org.openjdk.jmh.annotations.Setup;
11+
import org.openjdk.jmh.annotations.State;
12+
import org.openjdk.jmh.profile.GCProfiler;
13+
import org.openjdk.jmh.runner.Runner;
14+
import org.openjdk.jmh.runner.RunnerException;
15+
import org.openjdk.jmh.runner.options.Options;
16+
import org.openjdk.jmh.runner.options.OptionsBuilder;
17+
18+
import java.util.concurrent.TimeUnit;
19+
20+
@State(Scope.Benchmark)
21+
public class LabelNamesLookupBenchmark {
22+
23+
@Param({"1","2"})
24+
public int labelNamesCount;
25+
26+
String[] labelNames;
27+
io.prometheus.client.Histogram prometheusSimpleHistogram;
28+
29+
@Setup
30+
public void setup() {
31+
final String baseLabelName = "label";
32+
labelNames = new String[labelNamesCount];
33+
for (int i = 0; i< labelNamesCount; i++){
34+
labelNames[i] = baseLabelName + '_' + i;
35+
}
36+
prometheusSimpleHistogram = io.prometheus.client.Histogram.build()
37+
.name("name")
38+
.help("some description..")
39+
.labelNames(labelNames).create();
40+
if (labelNamesCount == 1) {
41+
prometheusSimpleHistogram.labels(labelNames[0]);
42+
} else {
43+
prometheusSimpleHistogram.labels(labelNames);
44+
}
45+
}
46+
47+
@Benchmark
48+
@BenchmarkMode({Mode.AverageTime})
49+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
50+
public Histogram.Child labelNamesLookupBenchmark() {
51+
if (labelNamesCount == 1) {
52+
return prometheusSimpleHistogram.labels(labelNames[0]);
53+
} else {
54+
return prometheusSimpleHistogram.labels(labelNames);
55+
}
56+
}
57+
58+
public static void main(String[] args) throws RunnerException {
59+
60+
Options opt = new OptionsBuilder()
61+
.include(LabelNamesLookupBenchmark.class.getSimpleName())
62+
.jvmArgs("-XX:+UseBiasedLocking", "-XX:BiasedLockingStartupDelay=0")
63+
.addProfiler(GCProfiler.class)
64+
.warmupIterations(5)
65+
.measurementIterations(4)
66+
.threads(1)
67+
.forks(2)
68+
.build();
69+
70+
new Runner(opt).run();
71+
}
72+
}

benchmark/src/main/java/io/prometheus/benchmark/SummaryBenchmark.java

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.openjdk.jmh.annotations.Scope;
1111
import org.openjdk.jmh.annotations.Setup;
1212
import org.openjdk.jmh.annotations.State;
13+
import org.openjdk.jmh.profile.GCProfiler;
1314
import org.openjdk.jmh.runner.Runner;
1415
import org.openjdk.jmh.runner.RunnerException;
1516
import org.openjdk.jmh.runner.options.Options;
@@ -29,6 +30,7 @@ public class SummaryBenchmark {
2930
io.prometheus.client.Histogram prometheusSimpleHistogram;
3031
io.prometheus.client.Histogram.Child prometheusSimpleHistogramChild;
3132
io.prometheus.client.Histogram prometheusSimpleHistogramNoLabels;
33+
String[] labelNames;
3234

3335
@Setup
3436
public void setup() {
@@ -42,7 +44,9 @@ public void setup() {
4244
.name("name")
4345
.help("some description..")
4446
.labelNames("some", "group").create();
45-
prometheusSimpleSummaryChild = prometheusSimpleSummary.labels("test", "group");
47+
48+
labelNames = new String[]{"tests", "group"};
49+
prometheusSimpleSummaryChild = prometheusSimpleSummary.labels(labelNames);
4650

4751
prometheusSimpleSummaryNoLabels = io.prometheus.client.Summary.build()
4852
.name("name")
@@ -53,7 +57,7 @@ public void setup() {
5357
.name("name")
5458
.help("some description..")
5559
.labelNames("some", "group").create();
56-
prometheusSimpleHistogramChild = prometheusSimpleHistogram.labels("test", "group");
60+
prometheusSimpleHistogramChild = prometheusSimpleHistogram.labels(labelNames);
5761

5862
prometheusSimpleHistogramNoLabels = io.prometheus.client.Histogram.build()
5963
.name("name")
@@ -85,6 +89,13 @@ public void prometheusSimpleSummaryBenchmark() {
8589
prometheusSimpleSummary.labels("test", "group").observe(1) ;
8690
}
8791

92+
@Benchmark
93+
@BenchmarkMode({Mode.AverageTime})
94+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
95+
public void prometheusSimpleSummaryPooledLabelNamesBenchmark() {
96+
prometheusSimpleSummary.labels(labelNames).observe(1) ;
97+
}
98+
8899
@Benchmark
89100
@BenchmarkMode({Mode.AverageTime})
90101
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@@ -105,6 +116,26 @@ public void prometheusSimpleSummaryNoLabelsBenchmark() {
105116
public void prometheusSimpleHistogramBenchmark() {
106117
prometheusSimpleHistogram.labels("test", "group").observe(1) ;
107118
}
119+
@Benchmark
120+
@BenchmarkMode({Mode.AverageTime})
121+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
122+
public void prometheusSimpleHistogramPooledLabelNamesBenchmark() {
123+
prometheusSimpleHistogram.labels(labelNames).observe(1) ;
124+
}
125+
126+
@Benchmark
127+
@BenchmarkMode({Mode.AverageTime})
128+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
129+
public double prometheusSimpleHistogramTimerBenchmark() {
130+
return prometheusSimpleHistogram.labels("test", "group").startTimer().observeDuration();
131+
}
132+
133+
@Benchmark
134+
@BenchmarkMode({Mode.AverageTime})
135+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
136+
public double prometheusSimpleHistogramTimerPooledLabelNamesBenchmark() {
137+
return prometheusSimpleHistogram.labels(labelNames).startTimer().observeDuration();
138+
}
108139

109140
@Benchmark
110141
@BenchmarkMode({Mode.AverageTime})
@@ -131,6 +162,8 @@ public static void main(String[] args) throws RunnerException {
131162

132163
Options opt = new OptionsBuilder()
133164
.include(SummaryBenchmark.class.getSimpleName())
165+
.jvmArgs("-XX:+UseBiasedLocking", "-XX:BiasedLockingStartupDelay=0")
166+
.addProfiler(GCProfiler.class)
134167
.warmupIterations(5)
135168
.measurementIterations(4)
136169
.threads(4)

simpleclient/src/main/java/io/prometheus/client/SimpleCollector.java

Lines changed: 158 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package io.prometheus.client;
22

3+
import java.util.AbstractList;
34
import java.util.ArrayList;
4-
import java.util.concurrent.ConcurrentHashMap;
5-
import java.util.concurrent.ConcurrentMap;
65
import java.util.Arrays;
6+
import java.util.Collections;
77
import java.util.List;
8+
import java.util.concurrent.ConcurrentHashMap;
9+
import java.util.concurrent.ConcurrentMap;
810

911
/**
1012
* Common functionality for {@link Gauge}, {@link Counter}, {@link Summary} and {@link Histogram}.
@@ -53,30 +55,159 @@ public abstract class SimpleCollector<Child> extends Collector {
5355

5456
protected final ConcurrentMap<List<String>, Child> children = new ConcurrentHashMap<List<String>, Child>();
5557
protected Child noLabelsChild;
58+
private final ThreadLocal<ArrayList<String>> labelNamesPool = new ThreadLocal<ArrayList<String>>();
5659

57-
/**
58-
* Return the Child with the given labels, creating it if needed.
59-
* <p>
60-
* Must be passed the same number of labels are were passed to {@link #labelNames}.
61-
*/
62-
public Child labels(String... labelValues) {
63-
if (labelValues.length != labelNames.size()) {
64-
throw new IllegalArgumentException("Incorrect number of labels.");
60+
/**
61+
* It is just reimplementing in a more JIT-friendly way both equals/hashCode to avoid
62+
* using Iterators like the original {@link AbstractList}.
63+
*/
64+
private static final class LabelNames extends ArrayList<String> {
65+
66+
public LabelNames(int capacity) {
67+
super(capacity);
68+
}
69+
70+
public boolean equals(Object o) {
71+
if (o == this)
72+
return true;
73+
if (!(o instanceof ArrayList)) {
74+
//what if o is a singleton list or empty?
75+
//We can just use the common fast path
76+
if (o instanceof List) {
77+
if (((List) o).size() > 1) {
78+
return super.equals(o);
79+
}
80+
} else {
81+
return super.equals(o);
82+
}
83+
}
84+
final int size = size();
85+
final List<?> other = (List<?>) o;
86+
if (size != other.size()) {
87+
return false;
88+
}
89+
for (int i = 0; i < size; i++) {
90+
final Object a = get(i);
91+
final Object b = other.get(i);
92+
final boolean eq = (a == b) || (a != null && a.equals(b));
93+
if (!eq) {
94+
return false;
95+
}
96+
}
97+
return true;
98+
}
99+
100+
/**
101+
* Returns the hash code value for this list.
102+
*
103+
* <p>This implementation uses exactly the code that is used to define the
104+
* list hash function in the documentation for the {@link List#hashCode}
105+
* method.
106+
*
107+
* @return the hash code value for this list
108+
*/
109+
public int hashCode() {
110+
int hashCode = 1;
111+
for (int i = 0, size = size(); i < size; i++) {
112+
final Object e = get(i);
113+
final int objHash = (e == null ? 0 : e.hashCode());
114+
hashCode = 31 * hashCode + objHash;
115+
}
116+
return hashCode;
117+
}
65118
}
66-
for (String label: labelValues) {
67-
if (label == null) {
68-
throw new IllegalArgumentException("Label cannot be null.");
69-
}
119+
120+
/**
121+
* Return the Child with the given labels, creating it if needed.
122+
* <p>
123+
* Must be passed the same number of labels are were passed to {@link #labelNames}.
124+
*/
125+
public Child labels(String... labelValues) {
126+
validateLabels(labelValues);
127+
final List<String> labels;
128+
if (labelValues.length > 0) {
129+
labels = pooledLabelNamesOf(labelValues);
130+
} else {
131+
labels = Collections.emptyList();
132+
}
133+
return getOrCreateChild(labels);
70134
}
71-
List<String> key = Arrays.asList(labelValues);
72-
Child c = children.get(key);
73-
if (c != null) {
74-
return c;
135+
136+
private List<String> pooledLabelNamesOf(String... labelValues) {
137+
final ThreadLocal<ArrayList<String>> labelNamesPool = this.labelNamesPool;
138+
ArrayList<String> pooledLabels = labelNamesPool.get();
139+
final int labelValuesCount = labelValues.length;
140+
if (pooledLabels == null) {
141+
pooledLabels = new LabelNames(labelValuesCount);
142+
labelNamesPool.set(pooledLabels);
143+
}
144+
for (String label : labelValues) {
145+
pooledLabels.add(label);
146+
}
147+
return pooledLabels;
148+
}
149+
150+
private Child getOrCreateChild(List<String> labels) {
151+
Child c = children.get(labels);
152+
if (c != null) {
153+
labels.clear();
154+
return c;
155+
}
156+
return tryCreateChild(labels);
157+
}
158+
159+
private Child tryCreateChild(List<String> labels) {
160+
Child c2 = newChild();
161+
Child tmp = children.putIfAbsent(labels, c2);
162+
if (tmp == null) {
163+
//given that putIfAbsent return null only when a new
164+
//labels has been added, we need to clear up
165+
//the pool to avoid labels to be both in the pool
166+
//and as children key
167+
labelNamesPool.set(null);
168+
return c2;
169+
} else {
170+
labels.clear();
171+
return tmp;
172+
}
173+
}
174+
175+
private void validateLabels(String... labelValues) {
176+
if (labelValues.length != labelNames.size()) {
177+
throw new IllegalArgumentException("Incorrect number of labels.");
178+
}
179+
for (String label : labelValues) {
180+
if (label == null) {
181+
throw new IllegalArgumentException("Label cannot be null.");
182+
}
183+
}
184+
}
185+
186+
private void validateLabel(String labelValue) {
187+
if (labelNames.size() != 1) {
188+
throw new IllegalArgumentException("Incorrect number of labels.");
189+
}
190+
if (labelValue == null) {
191+
throw new IllegalArgumentException("Label cannot be null.");
192+
}
193+
}
194+
195+
/**
196+
* Return the Child with the given labels, creating it if needed.
197+
* <p>
198+
* Must be passed the same number of labels are were passed to {@link #labelNames}.
199+
*/
200+
public Child labels(String labelValue) {
201+
validateLabel(labelValue);
202+
final ThreadLocal<ArrayList<String>> labelNamesPool = this.labelNamesPool;
203+
ArrayList<String> labels = labelNamesPool.get();
204+
if (labels == null) {
205+
labels = new LabelNames(1);
206+
labelNamesPool.set(labels);
207+
}
208+
labels.add(labelValue);
209+
return getOrCreateChild(labels);
75210
}
76-
Child c2 = newChild();
77-
Child tmp = children.putIfAbsent(key, c2);
78-
return tmp == null ? c2 : tmp;
79-
}
80211

81212
/**
82213
* Remove the Child with the given labels.
@@ -164,9 +295,11 @@ protected SimpleCollector(Builder b) {
164295
checkMetricName(fullname);
165296
if (b.help.isEmpty()) throw new IllegalStateException("Help hasn't been set.");
166297
help = b.help;
167-
labelNames = Arrays.asList(b.labelNames);
168-
169-
for (String n: labelNames) {
298+
labelNames = new LabelNames(b.labelNames.length);
299+
for (String label : b.labelNames) {
300+
labelNames.add(label);
301+
}
302+
for (String n: b.labelNames) {
170303
checkMetricLabelName(n);
171304
}
172305

simpleclient/src/test/java/io/prometheus/client/SimpleCollectorTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ public void testNullLabelThrows() {
3939
metric.labels(new String[]{null});
4040
}
4141

42+
@Test(expected=IllegalArgumentException.class)
43+
public void testNullLabelsThrows() {
44+
metric.labels(new String[]{null, null});
45+
}
46+
4247
@Test(expected=IllegalArgumentException.class)
4348
public void testTooManyLabelsThrows() {
4449
metric.labels("a", "b");

0 commit comments

Comments
 (0)