Skip to content

Commit dfdd0b9

Browse files
committed
Make Collector labels-to-child map implementation pluggable
#460 proposed an optimized zero-GC version of the child lookup logic which was deemed too specialized for inclusion in the library. Subjectively less complex alternatives were also proposed which provide some but not as much performance/garbage improvement, and rely for example on some per-thread overhead. This PR aims to add a minimally-invasive mechanism to allow users to plug in an implementation of their choice, so that performance sensitive consumers can opt for minimal overhead without the core library having to include the corresponding code. Signed-off-by: nickhill <[email protected]>
1 parent b61dd23 commit dfdd0b9

File tree

2 files changed

+151
-24
lines changed

2 files changed

+151
-24
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package io.prometheus.client;
2+
3+
import java.util.Arrays;
4+
import java.util.List;
5+
import java.util.concurrent.ConcurrentHashMap;
6+
import java.util.concurrent.ConcurrentMap;
7+
8+
/**
9+
* Abstraction for the internal labels-to-child concurrent map.
10+
* <p>
11+
* The default implementation is based on a {@link ConcurrentHashMap} but users who are
12+
* interested in performance can provide an optimized implementation (which could be
13+
* garbage-free for example).
14+
* <p>
15+
* Implementations must have a no-arg or default constructor.
16+
*/
17+
public interface ConcurrentChildMap<Child> extends ConcurrentMap<List<String>, Child> {
18+
19+
interface ChildFactory<Child> {
20+
Child newChild(String[] labels);
21+
}
22+
23+
Child labels(ChildFactory<Child> childFactory, String... labelValues);
24+
25+
Child labels(ChildFactory<Child> childFactory, String v1);
26+
27+
Child labels(ChildFactory<Child> childFactory, String v1, String v2);
28+
29+
Child labels(ChildFactory<Child> childFactory, String v1, String v2, String v3);
30+
31+
Child labels(ChildFactory<Child> childFactory, String v1, String v2, String v3, String v4);
32+
33+
void setChild(Child child, String... labelValues);
34+
35+
void remove(String... labelValues);
36+
37+
/**
38+
* The default {@link ConcurrentHashMap}-based implementation.
39+
*/
40+
static class ConcurrentChildHashMap<Child> extends ConcurrentHashMap<List<String>, Child>
41+
implements ConcurrentChildMap<Child> {
42+
43+
@Override
44+
public Child labels(ChildFactory<Child> childFactory, String... labelValues) {
45+
List<String> key = Arrays.asList(labelValues);
46+
Child c = get(key);
47+
if (c != null) {
48+
return c;
49+
}
50+
for (String label: labelValues) {
51+
if (label == null) {
52+
throw new IllegalArgumentException("Label cannot be null.");
53+
}
54+
}
55+
Child c2 = childFactory.newChild(labelValues);
56+
Child tmp = putIfAbsent(key, c2);
57+
return tmp == null ? c2 : tmp;
58+
}
59+
60+
@Override
61+
public Child labels(ChildFactory<Child> childFactory, String v1) {
62+
return labels(childFactory, arr(v1));
63+
}
64+
65+
@Override
66+
public Child labels(ChildFactory<Child> childFactory, String v1, String v2) {
67+
return labels(childFactory, arr(v1, v2));
68+
}
69+
70+
@Override
71+
public Child labels(ChildFactory<Child> childFactory, String v1, String v2, String v3) {
72+
return labels(childFactory, arr(v1, v2, v3));
73+
}
74+
75+
@Override
76+
public Child labels(ChildFactory<Child> childFactory, String v1, String v2, String v3, String v4) {
77+
return labels(childFactory, arr(v1, v2, v3, v4));
78+
}
79+
80+
@Override
81+
public void setChild(Child child, String... labelValues) {
82+
put(Arrays.asList(labelValues), child);
83+
}
84+
85+
@Override
86+
public void remove(String... labelValues) {
87+
remove(Arrays.asList(labelValues));
88+
}
89+
90+
private static String[] arr(String... arr) {
91+
return arr;
92+
}
93+
}
94+
}

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

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

33
import java.util.ArrayList;
4-
import java.util.concurrent.ConcurrentHashMap;
5-
import java.util.concurrent.ConcurrentMap;
64
import java.util.Arrays;
75
import java.util.List;
6+
import io.prometheus.client.ConcurrentChildMap.ConcurrentChildHashMap;
87

98
/**
109
* Common functionality for {@link Gauge}, {@link Counter}, {@link Summary} and {@link Histogram}.
@@ -46,12 +45,12 @@
4645
* by each. If the cardinality is in the hundreds, you may wish to consider removing the breakout
4746
* by one of the dimensions altogether.
4847
*/
49-
public abstract class SimpleCollector<Child> extends Collector {
48+
public abstract class SimpleCollector<Child> extends Collector implements ConcurrentChildMap.ChildFactory<Child> {
5049
protected final String fullname;
5150
protected final String help;
5251
protected final List<String> labelNames;
5352

54-
protected final ConcurrentMap<List<String>, Child> children = new ConcurrentHashMap<List<String>, Child>();
53+
protected final ConcurrentChildMap<Child> children;
5554
protected Child noLabelsChild;
5655

5756
/**
@@ -60,22 +59,39 @@ public abstract class SimpleCollector<Child> extends Collector {
6059
* Must be passed the same number of labels are were passed to {@link #labelNames}.
6160
*/
6261
public Child labels(String... labelValues) {
63-
if (labelValues.length != labelNames.size()) {
62+
validateCount(labelValues.length);
63+
return children.labels(this, labelValues);
64+
}
65+
66+
public Child labels() {
67+
validateCount(0);
68+
return noLabelsChild;
69+
}
70+
71+
public Child labels(String lv1) {
72+
validateCount(1);
73+
return children.labels(this, lv1);
74+
}
75+
76+
public Child labels(String lv1, String lv2) {
77+
validateCount(2);
78+
return children.labels(this, lv1, lv2);
79+
}
80+
81+
public Child labels(String lv1, String lv2, String lv3) {
82+
validateCount(3);
83+
return children.labels(this, lv1, lv2, lv3);
84+
}
85+
86+
public Child labels(String lv1, String lv2, String lv3, String lv4) {
87+
validateCount(4);
88+
return children.labels(this, lv1, lv2, lv3, lv4);
89+
}
90+
91+
private void validateCount(int count) {
92+
if (count != labelNames.size()) {
6493
throw new IllegalArgumentException("Incorrect number of labels.");
6594
}
66-
for (String label: labelValues) {
67-
if (label == null) {
68-
throw new IllegalArgumentException("Label cannot be null.");
69-
}
70-
}
71-
List<String> key = Arrays.asList(labelValues);
72-
Child c = children.get(key);
73-
if (c != null) {
74-
return c;
75-
}
76-
Child c2 = newChild();
77-
Child tmp = children.putIfAbsent(key, c2);
78-
return tmp == null ? c2 : tmp;
7995
}
8096

8197
/**
@@ -84,7 +100,7 @@ public Child labels(String... labelValues) {
84100
* Any references to the Child are invalidated.
85101
*/
86102
public void remove(String... labelValues) {
87-
children.remove(Arrays.asList(labelValues));
103+
children.remove(labelValues);
88104
initializeNoLabelsChild();
89105
}
90106

@@ -104,7 +120,7 @@ public void clear() {
104120
protected void initializeNoLabelsChild() {
105121
// Initialize metric if it has no labels.
106122
if (labelNames.size() == 0) {
107-
noLabelsChild = labels();
123+
noLabelsChild = labels(new String[0]);
108124
}
109125
}
110126

@@ -132,10 +148,8 @@ protected void initializeNoLabelsChild() {
132148
* A metric should be either all callbacks, or none.
133149
*/
134150
public <T extends Collector> T setChild(Child child, String... labelValues) {
135-
if (labelValues.length != labelNames.size()) {
136-
throw new IllegalArgumentException("Incorrect number of labels.");
137-
}
138-
children.put(Arrays.asList(labelValues), child);
151+
validateCount(labelValues.length);
152+
children.setChild(child, labelValues);
139153
return (T)this;
140154
}
141155

@@ -144,6 +158,11 @@ public <T extends Collector> T setChild(Child child, String... labelValues) {
144158
*/
145159
protected abstract Child newChild();
146160

161+
@Override
162+
public Child newChild(String[] labels) {
163+
return newChild();
164+
}
165+
147166
protected List<MetricFamilySamples> familySamplesList(Collector.Type type, List<MetricFamilySamples.Sample> samples) {
148167
MetricFamilySamples mfs = new MetricFamilySamples(fullname, type, help, samples);
149168
List<MetricFamilySamples> mfsList = new ArrayList<MetricFamilySamples>(1);
@@ -152,6 +171,11 @@ protected List<MetricFamilySamples> familySamplesList(Collector.Type type, List<
152171
}
153172

154173
protected SimpleCollector(Builder b) {
174+
try {
175+
children = (ConcurrentChildMap) b.childMapClass.newInstance();
176+
} catch (ReflectiveOperationException e) {
177+
throw new RuntimeException("Error instantiating child map class", e);
178+
}
155179
if (b.name.isEmpty()) throw new IllegalStateException("Name hasn't been set.");
156180
String name = b.name;
157181
if (!b.subsystem.isEmpty()) {
@@ -187,6 +211,15 @@ public abstract static class Builder<B extends Builder<B, C>, C extends SimpleCo
187211
String[] labelNames = new String[]{};
188212
// Some metrics require additional setup before the initialization can be done.
189213
boolean dontInitializeNoLabelsChild;
214+
Class<? extends ConcurrentChildMap> childMapClass = ConcurrentChildHashMap.class;
215+
216+
/**
217+
* Set a custom implementation for the internal labels-to-Child map.
218+
*/
219+
public B childMap(Class<? extends ConcurrentChildMap> childMapClass) {
220+
this.childMapClass = childMapClass;
221+
return (B)this;
222+
}
190223

191224
/**
192225
* Set the name of the metric. Required.

0 commit comments

Comments
 (0)