Skip to content

Commit 1482cdf

Browse files
authored
Tags merge optimization (#4959)
Improves the performance of merging two Tags instances by taking advantage of the fact that their internal representation of tags is always sorted and deduplicated. Therefore, they can be merged more efficiently than a collection of tags that may not be sorted and deduplicated. Added benchmark to measure Tags.and(Tags) operation. See gh-5140
1 parent a61a7a2 commit 1482cdf

File tree

3 files changed

+226
-33
lines changed

3 files changed

+226
-33
lines changed

benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/TagsBenchmark.java

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package io.micrometer.benchmark.core;
1717

18+
import io.micrometer.core.instrument.Tag;
1819
import io.micrometer.core.instrument.Tags;
1920
import org.openjdk.jmh.annotations.*;
2021
import org.openjdk.jmh.runner.Runner;
@@ -31,13 +32,59 @@
3132
@OutputTimeUnit(TimeUnit.NANOSECONDS)
3233
public class TagsBenchmark {
3334

34-
@Threads(16)
35+
static final Tag[] orderedTagsSet10 = new Tag[] { Tag.of("key0", "value"), Tag.of("key1", "value"),
36+
Tag.of("key2", "value"), Tag.of("key3", "value"), Tag.of("key4", "value"), Tag.of("key5", "value"),
37+
Tag.of("key6", "value"), Tag.of("key7", "value"), Tag.of("key8", "value"), Tag.of("key9", "value") };
38+
39+
static final Tag[] orderedTagsSet4 = new Tag[] { Tag.of("key0", "value"), Tag.of("key1", "value"),
40+
Tag.of("key2", "value"), Tag.of("key3", "value"), };
41+
42+
static final Tag[] orderedTagsSet2 = new Tag[] { Tag.of("key0", "value"), Tag.of("key1", "value"), };
43+
44+
static final Tag[] unorderedTagsSet10 = new Tag[] { Tag.of("key1", "value"), Tag.of("key2", "value"),
45+
Tag.of("key3", "value"), Tag.of("key4", "value"), Tag.of("key5", "value"), Tag.of("key6", "value"),
46+
Tag.of("key7", "value"), Tag.of("key8", "value"), Tag.of("key9", "value"), Tag.of("key0", "value") };
47+
48+
static final Tag[] unorderedTagsSet4 = new Tag[] { Tag.of("key1", "value"), Tag.of("key2", "value"),
49+
Tag.of("key3", "value"), Tag.of("key0", "value"), };
50+
51+
static final Tag[] unorderedTagsSet2 = new Tag[] { Tag.of("key1", "value"), Tag.of("key0", "value") };
52+
53+
@Benchmark
54+
public Tags tagsOfOrderedTagsSet10() {
55+
return Tags.of(orderedTagsSet10);
56+
}
57+
58+
@Benchmark
59+
public Tags tagsOfOrderedTagsSet4() {
60+
return Tags.of(orderedTagsSet4);
61+
}
62+
63+
@Benchmark
64+
public Tags tagsOfOrderedTagsSet2() {
65+
return Tags.of(orderedTagsSet2);
66+
}
67+
68+
@Benchmark
69+
public Tags tagsOfUnorderedTagsSet10() {
70+
return Tags.of(unorderedTagsSet10);
71+
}
72+
73+
@Benchmark
74+
public Tags tagsOfUnorderedTagsSet4() {
75+
return Tags.of(unorderedTagsSet4);
76+
}
77+
78+
@Benchmark
79+
public Tags tagsOfUnorderedTagsSet2() {
80+
return Tags.of(unorderedTagsSet2);
81+
}
82+
3583
@Benchmark
3684
public void of() {
3785
Tags.of("key", "value", "key2", "value2", "key3", "value3", "key4", "value4", "key5", "value5");
3886
}
3987

40-
@Threads(16)
4188
@Benchmark
4289
public void dotAnd() {
4390
Tags.of("key", "value").and("key2", "value2", "key3", "value3", "key4", "value4", "key5", "value5");
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2024 VMware, 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+
* https://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 io.micrometer.benchmark.core;
17+
18+
import io.micrometer.core.instrument.Tags;
19+
import org.openjdk.jmh.annotations.*;
20+
import org.openjdk.jmh.runner.Runner;
21+
import org.openjdk.jmh.runner.RunnerException;
22+
import org.openjdk.jmh.runner.options.Options;
23+
import org.openjdk.jmh.runner.options.OptionsBuilder;
24+
25+
import java.util.concurrent.TimeUnit;
26+
27+
@Fork(1)
28+
@Measurement(iterations = 2)
29+
@Warmup(iterations = 2)
30+
@BenchmarkMode(Mode.AverageTime)
31+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
32+
@State(Scope.Thread)
33+
public class TagsMergeBenchmark {
34+
35+
static final Tags left = Tags.of("key", "value", "key2", "value2", "key6", "value6", "key7", "value7", "key8",
36+
"value8", "keyA", "valueA", "keyC", "valueC", "keyE", "valueE", "keyF", "valueF", "keyG", "valueG", "keyG",
37+
"valueG", "keyG", "valueG", "keyH", "valueH");
38+
39+
static final Tags right = Tags.of("key", "value", "key1", "value1", "key2", "value2", "key3", "value3", "key4",
40+
"value4", "key5", "value5", "keyA", "valueA", "keyB", "valueB", "keyD", "valueD");
41+
42+
@Benchmark
43+
public Tags mergeTags() {
44+
return left.and(right);
45+
}
46+
47+
public static void main(String[] args) throws RunnerException {
48+
Options opt = new OptionsBuilder().include(TagsMergeBenchmark.class.getSimpleName()).build();
49+
new Runner(opt).run();
50+
}
51+
52+
}

micrometer-core/src/main/java/io/micrometer/core/instrument/Tags.java

Lines changed: 125 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -34,24 +34,76 @@
3434
*/
3535
public final class Tags implements Iterable<Tag> {
3636

37-
private static final Tags EMPTY = new Tags(new Tag[] {});
37+
private static final Tags EMPTY = new Tags(new Tag[] {}, 0);
3838

39-
private final Tag[] tags;
39+
/**
40+
* A private array of {@code Tag} objects containing the sorted and deduplicated tags.
41+
*/
42+
private final Tag[] sortedSet;
43+
44+
/**
45+
* The number of valid tags present in the {@link #sortedSet} array.
46+
*/
47+
private final int length;
48+
49+
/**
50+
* A private constructor that initializes a {@code Tags} object with a sorted set of
51+
* tags and its length.
52+
* @param sortedSet an ordered set of unique tags by key
53+
* @param length the number of valid tags in the {@code sortedSet}
54+
*/
55+
private Tags(Tag[] sortedSet, int length) {
56+
this.sortedSet = sortedSet;
57+
this.length = length;
58+
}
4059

41-
private int last;
60+
/**
61+
* Checks if the first {@code length} elements of the {@code tags} array form an
62+
* ordered set of tags.
63+
* @param tags an array of tags.
64+
* @param length the number of items to check.
65+
* @return {@code true} if the first {@code length} items of {@code tags} form an
66+
* ordered set; otherwise {@code false}.
67+
*/
68+
private static boolean isSortedSet(Tag[] tags, int length) {
69+
if (length > tags.length) {
70+
return false;
71+
}
72+
for (int i = 0; i < length - 1; i++) {
73+
int cmp = tags[i].compareTo(tags[i + 1]);
74+
if (cmp >= 0) {
75+
return false;
76+
}
77+
}
78+
return true;
79+
}
4280

43-
private Tags(Tag[] tags) {
44-
this.tags = tags;
45-
Arrays.sort(this.tags);
46-
dedup();
81+
/**
82+
* Constructs a {@code Tags} collection from the provided array of tags.
83+
* @param tags an array of {@code Tag} objects, possibly unordered and/or containing
84+
* duplicates.
85+
* @return a {@code Tags} instance with a deduplicated and ordered set of tags.
86+
*/
87+
private static Tags make(Tag[] tags) {
88+
int len = tags.length;
89+
if (!isSortedSet(tags, len)) {
90+
Arrays.sort(tags);
91+
len = dedup(tags);
92+
}
93+
return new Tags(tags, len);
4794
}
4895

49-
private void dedup() {
96+
/**
97+
* Removes duplicate tags from an ordered array of tags.
98+
* @param tags an ordered array of {@code Tag} objects.
99+
* @return the number of unique tags in the {@code tags} array after removing
100+
* duplicates.
101+
*/
102+
private static int dedup(Tag[] tags) {
50103
int n = tags.length;
51104

52105
if (n == 0 || n == 1) {
53-
last = n;
54-
return;
106+
return n;
55107
}
56108

57109
// index of next unique element
@@ -62,7 +114,53 @@ private void dedup() {
62114
tags[j++] = tags[i];
63115

64116
tags[j++] = tags[n - 1];
65-
last = j;
117+
return j;
118+
}
119+
120+
/**
121+
* Constructs a {@code Tags} instance by merging two sets of tags in time proportional
122+
* to the sum of their sizes.
123+
* @param other the set of tags to merge with this one.
124+
* @return a {@code Tags} instance with the merged sets of tags.
125+
*/
126+
private Tags merged(Tags other) {
127+
if (other.length == 0) {
128+
return this;
129+
}
130+
if (Objects.equals(this, other)) {
131+
return this;
132+
}
133+
Tag[] sortedSet = new Tag[this.length + other.length];
134+
int sortedIdx = 0, thisIdx = 0, otherIdx = 0;
135+
while (thisIdx < this.length && otherIdx < other.length) {
136+
int cmp = this.sortedSet[thisIdx].compareTo(other.sortedSet[otherIdx]);
137+
if (cmp > 0) {
138+
sortedSet[sortedIdx] = other.sortedSet[otherIdx];
139+
otherIdx++;
140+
}
141+
else if (cmp < 0) {
142+
sortedSet[sortedIdx] = this.sortedSet[thisIdx];
143+
thisIdx++;
144+
}
145+
else {
146+
// In case of key conflict prefer tag from other set
147+
sortedSet[sortedIdx] = other.sortedSet[otherIdx];
148+
thisIdx++;
149+
otherIdx++;
150+
}
151+
sortedIdx++;
152+
}
153+
int thisRemaining = this.length - thisIdx;
154+
if (thisRemaining > 0) {
155+
System.arraycopy(this.sortedSet, thisIdx, sortedSet, sortedIdx, thisRemaining);
156+
sortedIdx += thisRemaining;
157+
}
158+
int otherRemaining = other.length - otherIdx;
159+
if (otherIdx < other.sortedSet.length) {
160+
System.arraycopy(other.sortedSet, otherIdx, sortedSet, sortedIdx, otherRemaining);
161+
sortedIdx += otherRemaining;
162+
}
163+
return new Tags(sortedSet, sortedIdx);
66164
}
67165

68166
/**
@@ -99,10 +197,7 @@ public Tags and(@Nullable Tag... tags) {
99197
if (blankVarargs(tags)) {
100198
return this;
101199
}
102-
Tag[] newTags = new Tag[last + tags.length];
103-
System.arraycopy(this.tags, 0, newTags, 0, last);
104-
System.arraycopy(tags, 0, newTags, last, tags.length);
105-
return new Tags(newTags);
200+
return and(make(tags));
106201
}
107202

108203
/**
@@ -116,11 +211,10 @@ public Tags and(@Nullable Iterable<? extends Tag> tags) {
116211
return this;
117212
}
118213

119-
if (this.tags.length == 0) {
214+
if (this.length == 0) {
120215
return Tags.of(tags);
121216
}
122-
123-
return and(Tags.of(tags).tags);
217+
return merged(Tags.of(tags));
124218
}
125219

126220
@Override
@@ -134,12 +228,12 @@ private class ArrayIterator implements Iterator<Tag> {
134228

135229
@Override
136230
public boolean hasNext() {
137-
return currentIndex < last;
231+
return currentIndex < length;
138232
}
139233

140234
@Override
141235
public Tag next() {
142-
return tags[currentIndex++];
236+
return sortedSet[currentIndex++];
143237
}
144238

145239
@Override
@@ -151,7 +245,7 @@ public void remove() {
151245

152246
@Override
153247
public Spliterator<Tag> spliterator() {
154-
return Spliterators.spliterator(tags, 0, last, Spliterator.IMMUTABLE | Spliterator.ORDERED
248+
return Spliterators.spliterator(sortedSet, 0, length, Spliterator.IMMUTABLE | Spliterator.ORDERED
155249
| Spliterator.DISTINCT | Spliterator.NONNULL | Spliterator.SORTED);
156250
}
157251

@@ -166,8 +260,8 @@ public Stream<Tag> stream() {
166260
@Override
167261
public int hashCode() {
168262
int result = 1;
169-
for (int i = 0; i < last; i++) {
170-
result = 31 * result + tags[i].hashCode();
263+
for (int i = 0; i < length; i++) {
264+
result = 31 * result + sortedSet[i].hashCode();
171265
}
172266
return result;
173267
}
@@ -178,14 +272,14 @@ public boolean equals(@Nullable Object obj) {
178272
}
179273

180274
private boolean tagsEqual(Tags obj) {
181-
if (tags == obj.tags)
275+
if (sortedSet == obj.sortedSet)
182276
return true;
183277

184-
if (last != obj.last)
278+
if (length != obj.length)
185279
return false;
186280

187-
for (int i = 0; i < last; i++) {
188-
if (!tags[i].equals(obj.tags[i]))
281+
for (int i = 0; i < length; i++) {
282+
if (!sortedSet[i].equals(obj.sortedSet[i]))
189283
return false;
190284
}
191285

@@ -229,10 +323,10 @@ else if (tags instanceof Tags) {
229323
}
230324
else if (tags instanceof Collection) {
231325
Collection<? extends Tag> tagsCollection = (Collection<? extends Tag>) tags;
232-
return new Tags(tagsCollection.toArray(new Tag[0]));
326+
return make(tagsCollection.toArray(new Tag[0]));
233327
}
234328
else {
235-
return new Tags(StreamSupport.stream(tags.spliterator(), false).toArray(Tag[]::new));
329+
return make(StreamSupport.stream(tags.spliterator(), false).toArray(Tag[]::new));
236330
}
237331
}
238332

@@ -244,7 +338,7 @@ else if (tags instanceof Collection) {
244338
* @return a new {@code Tags} instance
245339
*/
246340
public static Tags of(String key, String value) {
247-
return new Tags(new Tag[] { Tag.of(key, value) });
341+
return new Tags(new Tag[] { Tag.of(key, value) }, 1);
248342
}
249343

250344
/**
@@ -264,7 +358,7 @@ public static Tags of(@Nullable String... keyValues) {
264358
for (int i = 0; i < keyValues.length; i += 2) {
265359
tags[i / 2] = Tag.of(keyValues[i], keyValues[i + 1]);
266360
}
267-
return new Tags(tags);
361+
return make(tags);
268362
}
269363

270364
private static boolean blankVarargs(@Nullable Object[] args) {

0 commit comments

Comments
 (0)