Skip to content

Commit c0bb648

Browse files
authored
add TagListBuilder helper (#1205)
Helper to support more efficient building of tag lists with fewer allocations than relying on varargs `withTags` method. If the builder is reused, then there would only be a single allocation to copy the buffer array for most uses. The user would be responsible for managing the reuse of the builder instances. This could be a thread local, concurrent queue, or other options that make sense for their use-case.
1 parent affe680 commit c0bb648

File tree

5 files changed

+592
-18
lines changed

5 files changed

+592
-18
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ subprojects {
100100
profilers = ['stack', 'gc']
101101
includeTests = false
102102
duplicateClassesStrategy = DuplicatesStrategy.WARN
103-
includes = ['.*IdHash.*']
103+
includes = ['.*Ids.*']
104104
}
105105

106106
checkstyle {

spectator-api/src/jmh/java/com/netflix/spectator/perf/Ids.java

Lines changed: 87 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import com.netflix.spectator.api.DefaultRegistry;
1919
import com.netflix.spectator.api.Id;
2020
import com.netflix.spectator.api.Registry;
21+
import com.netflix.spectator.api.TagList;
22+
import com.netflix.spectator.api.TagListBuilder;
2123
import org.openjdk.jmh.annotations.Benchmark;
2224
import org.openjdk.jmh.annotations.Scope;
2325
import org.openjdk.jmh.annotations.State;
@@ -29,21 +31,47 @@
2931

3032
/**
3133
* <pre>
32-
* Benchmark Mode Cnt Score Error Units
33-
* Ids.append1 thrpt 10 22,932,447.185 ± 558909.207 ops/s
34-
* Ids.append2 thrpt 10 14,126,066.627 ± 2205349.084 ops/s
35-
* Ids.append4 thrpt 10 5,165,821.740 ± 144524.852 ops/s
36-
* Ids.append4sorted thrpt 10 5,923,827.749 ± 258122.285 ops/s
37-
* Ids.baseline thrpt 10 14,868,887.021 ± 4627616.416 ops/s
38-
* Ids.emptyAppend1 thrpt 10 63,193,729.846 ± 1158843.888 ops/s
39-
* Ids.emptyAppend2 thrpt 10 28,797,024.419 ± 2348775.496 ops/s
40-
* Ids.emptyAppend4 thrpt 10 9,818,389.953 ± 227597.860 ops/s
41-
* Ids.emptyAppend4sorted thrpt 10 11,342,478.015 ± 315543.929 ops/s
42-
* Ids.justName thrpt 10 166,275,032.184 ± 5252541.293 ops/s
43-
* Ids.withTag thrpt 10 1,586,379.085 ± 40204.926 ops/s
44-
* Ids.withTagsMap thrpt 10 1,841,867.329 ± 32378.659 ops/s
45-
* Ids.withTagsVararg thrpt 10 1,946,970.522 ± 37919.937 ops/s
46-
* Ids.withTagsVarargSorted thrpt 10 3,426,008.758 ± 115232.165 ops/s
34+
* Benchmark Throughput (ops/s) Error
35+
* -----------------------------------------------------------
36+
* append1 56,794,079.309 ±1,529,009.842
37+
* append2 29,813,536.825 ±242,156.195
38+
* append4 6,473,803.925 ±915,892.515
39+
* append4sorted 7,021,400.576 ±251,550.333
40+
* baseline 49,883,390.694 ±431,626.254
41+
* emptyAppend1 200,362,500.375 ±1,438,114.615
42+
* emptyAppend2 78,510,857.178 ±125,060.880
43+
* emptyAppend4 14,228,870.597 ±350,789.283
44+
* emptyAppend4sorted 16,585,176.379 ±387,667.119
45+
* justName 437,797,027.428 ±24,884,978.092
46+
* unsafeCreate 12,531,161.873 ±4,295,429.889
47+
* withTag 3,294,585.839 ±124,723.980
48+
* withTagsBuilder 3,213,126.689 ±37,382.051
49+
* withTagsBuilderSorted 10,095,083.206 ±569,033.538
50+
* withTagsMap 4,088,836.194 ±104,959.027
51+
* withTagsVararg 2,860,050.248 ±39,131.668
52+
* withTagsVarargSorted 8,168,124.655 ±154,663.209
53+
* </pre>
54+
*
55+
* <pre>
56+
* Benchmark Alloc Rate Norm (B/op) Error
57+
* -----------------------------------------------------------
58+
* append1 136.008 ±0.001
59+
* append2 208.015 ±0.001
60+
* append4 256.071 ±0.011
61+
* append4sorted 256.066 ±0.002
62+
* baseline 312.009 ±0.001
63+
* emptyAppend1 72.002 ±0.001
64+
* emptyAppend2 112.006 ±0.001
65+
* emptyAppend4 144.032 ±0.001
66+
* emptyAppend4sorted 144.027 ±0.001
67+
* justName 24.001 ±0.001
68+
* unsafeCreate 48.036 ±0.013
69+
* withTag 1,416.133 ±0.005
70+
* withTagsBuilder 184.140 ±0.003
71+
* withTagsBuilderSorted 184.044 ±0.003
72+
* withTagsMap 224.115 ±0.002
73+
* withTagsVararg 296.167 ±0.002
74+
* withTagsVarargSorted 296.058 ±0.001
4775
* </pre>
4876
*/
4977
@State(Scope.Thread)
@@ -97,6 +125,8 @@ private Map<String, String> getTags() {
97125
.withTag( "nf.zone", "us-east-1e")
98126
.withTag( "nf.node", "i-1234567890");
99127

128+
private final TagListBuilder builder = TagListBuilder.create();
129+
100130
@Threads(1)
101131
@Benchmark
102132
public void justName(Blackhole bh) {
@@ -186,6 +216,48 @@ public void withTagsVarargSorted(Blackhole bh) {
186216
bh.consume(id);
187217
}
188218

219+
@Threads(1)
220+
@Benchmark
221+
public void withTagsBuilder(Blackhole bh) {
222+
TagList ts = builder
223+
.add("nf.app", "test_app")
224+
.add("nf.cluster", "test_app-main")
225+
.add("nf.asg", "test_app-main-v042")
226+
.add("nf.stack", "main")
227+
.add("nf.ami", "ami-0987654321")
228+
.add("nf.region", "us-east-1")
229+
.add("nf.zone", "us-east-1e")
230+
.add("nf.node", "i-1234567890")
231+
.add("country", "US")
232+
.add("device", "xbox")
233+
.add("status", "200")
234+
.add("client", "ab")
235+
.buildAndReset();
236+
Id id = registry.createId("http.req.complete").withTags(ts);
237+
bh.consume(id);
238+
}
239+
240+
@Threads(1)
241+
@Benchmark
242+
public void withTagsBuilderSorted(Blackhole bh) {
243+
TagList ts = builder
244+
.add("client", "ab")
245+
.add("country", "US")
246+
.add("device", "xbox")
247+
.add("nf.ami", "ami-0987654321")
248+
.add("nf.app", "test_app")
249+
.add("nf.asg", "test_app-main-v042")
250+
.add("nf.cluster", "test_app-main")
251+
.add("nf.node", "i-1234567890")
252+
.add("nf.region", "us-east-1")
253+
.add("nf.stack", "main")
254+
.add("nf.zone", "us-east-1e")
255+
.add("status", "200")
256+
.buildAndReset();
257+
Id id = registry.createId("http.req.complete").withTags(ts);
258+
bh.consume(id);
259+
}
260+
189261
@Threads(1)
190262
@Benchmark
191263
public void withTagsMap(Blackhole bh) {

spectator-api/src/main/java/com/netflix/spectator/api/ArrayTagSet.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2023 Netflix, Inc.
2+
* Copyright 2014-2025 Netflix, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -216,7 +216,7 @@ private ArrayTagSet addAll(String[] ts, int tsLength) {
216216
}
217217

218218
/** Add a collection of tags to the set. */
219-
private ArrayTagSet addAll(String[] ts, int tsLength, boolean sorted, boolean distinct) {
219+
ArrayTagSet addAll(String[] ts, int tsLength, boolean sorted, boolean distinct) {
220220
if (tsLength == 0) {
221221
return this;
222222
} else if (length == 0) {
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/*
2+
* Copyright 2014-2025 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.netflix.spectator.api;
17+
18+
import com.netflix.spectator.impl.Preconditions;
19+
20+
import java.util.Arrays;
21+
22+
/**
23+
* Builder for creating TagList instances efficiently. This builder can be reused multiple times
24+
* and provides optimizations for sorted tag insertion to avoid unnecessary sorting operations
25+
* later. A builder instance is not thread safe and should only be used from a single thread at
26+
* a time. The user is responsible for managing how to reuse builder instances to avoid initial
27+
* allocation of the builder each time.
28+
*
29+
* <p>The builder tracks whether tags are added in sorted order and passes this information
30+
* to the underlying TagList implementation to optimize performance. For best performance,
31+
* add tags in lexicographically sorted order by key.</p>
32+
*
33+
* <p>Example usage:</p>
34+
* <pre>{@code
35+
* TagListBuilder builder = TagListBuilder.create();
36+
* TagList tags = builder
37+
* .add("app", "myapp")
38+
* .add("env", "prod")
39+
* .add("region", "us-east-1")
40+
* .buildAndReset();
41+
* }</pre>
42+
*
43+
* <p>The builder can be reused after calling {@link #buildAndReset()}:</p>
44+
* <pre>{@code
45+
* TagList tags1 = builder.add("key1", "value1").buildAndReset();
46+
* TagList tags2 = builder.add("key2", "value2").buildAndReset();
47+
* }</pre>
48+
*/
49+
public final class TagListBuilder {
50+
51+
/**
52+
* Creates a new TagListBuilder instance.
53+
*
54+
* @return a new builder instance
55+
*/
56+
public static TagListBuilder create() {
57+
return new TagListBuilder(ArrayTagSet.EMPTY);
58+
}
59+
60+
/**
61+
* Creates a new TagListBuilder instance with a set of base tags that will be included
62+
* in all TagLists built by this builder. The base tags are automatically added when
63+
* the builder is created and after each {@link #buildAndReset()} call.
64+
*
65+
* @param baseTags the base tags to include in all built TagLists, must not be null
66+
* @return a new builder instance with the specified base tags
67+
*/
68+
public static TagListBuilder create(TagList baseTags) {
69+
return new TagListBuilder(Preconditions.checkNotNull(baseTags, "baseTags"));
70+
}
71+
72+
private final int basePos;
73+
private final String baseKey;
74+
private final boolean baseSorted;
75+
76+
private String[] tags;
77+
private int pos;
78+
79+
private String lastKey;
80+
private boolean sorted;
81+
82+
/**
83+
* Creates a new builder with initial capacity for 10 key-value pairs.
84+
*/
85+
TagListBuilder(TagList baseTags) {
86+
int n = baseTags.size() * 2 + 20;
87+
tags = new String[n];
88+
pos = 0;
89+
lastKey = null;
90+
sorted = true;
91+
92+
// Add base tags and keep track of state for the reset
93+
add(baseTags);
94+
basePos = pos;
95+
baseKey = lastKey;
96+
baseSorted = sorted;
97+
}
98+
99+
/**
100+
* Returns whether the tags added so far are in sorted order by key.
101+
* This is used internally to optimize TagList creation.
102+
*
103+
* @return true if tags are sorted, false otherwise
104+
*/
105+
boolean isSorted() {
106+
return sorted;
107+
}
108+
109+
/**
110+
* Doubles the capacity of the internal array if needed.
111+
*/
112+
private void resizeIfNeeded() {
113+
if (pos >= tags.length) {
114+
tags = Arrays.copyOf(tags, tags.length * 2);
115+
}
116+
}
117+
118+
/**
119+
* Checks if the given key is lexicographically greater than the last added key.
120+
*
121+
* @param key the key to check
122+
* @return true if key is greater than the last key, or if this is the first key
123+
*/
124+
private boolean isGreaterThanLastKey(String key) {
125+
return lastKey == null || lastKey.compareTo(key) < 0;
126+
}
127+
128+
/**
129+
* Adds all tags from the provided TagList to this builder.
130+
*
131+
* @param tags the TagList containing tags to add, must not be null
132+
* @return this builder for method chaining
133+
*/
134+
public TagListBuilder add(TagList tags) {
135+
final int n = tags.size();
136+
for (int i = 0; i < n; ++i) {
137+
add(tags.getKey(i), tags.getValue(i));
138+
}
139+
return this;
140+
}
141+
142+
/**
143+
* Adds a key-value pair to the builder. Null keys or values are ignored.
144+
*
145+
* <p>For optimal performance, add tags in lexicographically sorted order by key.
146+
* The builder tracks sort order and passes this information to the underlying
147+
* TagList implementation to avoid unnecessary sorting operations.</p>
148+
*
149+
* @param key the tag key, must not be null
150+
* @param value the tag value, must not be null
151+
* @return this builder for method chaining
152+
*/
153+
public TagListBuilder add(String key, String value) {
154+
if (key != null && value != null) {
155+
resizeIfNeeded();
156+
sorted = sorted && isGreaterThanLastKey(key);
157+
lastKey = key;
158+
tags[pos++] = key;
159+
tags[pos++] = value;
160+
}
161+
return this;
162+
}
163+
164+
/**
165+
* Adds a Tag object to the builder. Null keys or values are ignored.
166+
*
167+
* @param tag the tag to add, must not be null and must have non-null key and value
168+
* @return this builder for method chaining
169+
*/
170+
public TagListBuilder add(Tag tag) {
171+
return add(tag.key(), tag.value());
172+
}
173+
174+
/**
175+
* Resets the builder to its initial empty state, allowing it to be reused.
176+
*/
177+
public void reset() {
178+
pos = basePos;
179+
lastKey = baseKey;
180+
sorted = baseSorted;
181+
}
182+
183+
/**
184+
* Builds a TagList from the accumulated tags and resets the builder for reuse.
185+
*
186+
* @return a new TagList containing all the added tags
187+
*/
188+
public TagList buildAndReset() {
189+
String[] copy = Arrays.copyOf(tags, pos);
190+
TagList ts = ArrayTagSet.EMPTY.addAll(copy, pos, sorted, sorted);
191+
reset();
192+
return ts;
193+
}
194+
}

0 commit comments

Comments
 (0)