Skip to content

Commit c28c6a6

Browse files
committed
Improved Tags performance
Signed-off-by: etki <[email protected]>
1 parent 169f38a commit c28c6a6

File tree

2 files changed

+91
-18
lines changed
  • micrometer-core/src

2 files changed

+91
-18
lines changed

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

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,20 @@ private static boolean isSortedSet(Tag[] tags, int length) {
7171
if (length > tags.length) {
7272
return false;
7373
}
74+
75+
// This style is intentionally chosen to have only one array
76+
// access per iteration (tags[i].compareTo(tags[i + 1]) would
77+
// have two).
78+
Tag current = tags[0];
79+
Tag next;
80+
7481
for (int i = 0; i < length - 1; i++) {
75-
int cmp = tags[i].compareTo(tags[i + 1]);
82+
next = tags[i + 1];
83+
int cmp = current.compareTo(next);
7684
if (cmp >= 0) {
7785
return false;
7886
}
87+
current = next;
7988
}
8089
return true;
8190
}
@@ -96,7 +105,8 @@ private static Tags toTags(Tag[] tags) {
96105
}
97106

98107
/**
99-
* Removes duplicate tags from an ordered array of tags.
108+
* Removes duplicate tags from an ordered array of tags. In the case of several
109+
* consecutive tags with the same key, only the last one is preserved.
100110
* @param tags an ordered array of {@code Tag} objects.
101111
* @return the number of unique tags in the {@code tags} array after removing
102112
* duplicates.
@@ -111,9 +121,24 @@ private static int dedup(Tag[] tags) {
111121
// index of next unique element
112122
int j = 0;
113123

114-
for (int i = 0; i < n - 1; i++)
115-
if (!tags[i].getKey().equals(tags[i + 1].getKey()))
124+
// The following is intentionally written in this style to facilitate performance.
125+
// Normally one would just do tags[i].getKey().equals(tags[i + 1].getKey()),
126+
// but the tags[i + 1].getKey() value is exactly the same as tags[i].getKey()
127+
// for the next iteration, thus caching it in a variable halves the number
128+
// of lookups.
129+
// The compiler is very unlikely to do this for us because of the absence of
130+
// "this data is immutable" signs, even if MM doesn't enforce any HB
131+
// relations with external modifications in this case. You can run the
132+
// associated benchmarks to check the behavior for your specific JVM.
133+
String current = tags[0].getKey();
134+
String next;
135+
136+
for (int i = 0; i < n - 1; i++) {
137+
next = tags[i + 1].getKey();
138+
if (!current.equals(next))
116139
tags[j++] = tags[i];
140+
current = next;
141+
}
117142

118143
tags[j++] = tags[n - 1];
119144
return j;
@@ -126,12 +151,14 @@ private static int dedup(Tag[] tags) {
126151
* @return a {@code Tags} instance with the merged sets of tags.
127152
*/
128153
private Tags merge(Tags other) {
129-
if (other.length == 0) {
130-
return this;
154+
if (length == 0) {
155+
return other;
131156
}
132-
if (Objects.equals(this, other)) {
157+
158+
if (other.length == 0 || tagsEqual(other)) {
133159
return this;
134160
}
161+
135162
Tag[] sortedSet = new Tag[this.length + other.length];
136163
int sortedIndex = 0;
137164
int thisIndex = 0;
@@ -215,13 +242,12 @@ public Tags and(@Nullable Tag... tags) {
215242
* @return a new {@code Tags} instance
216243
*/
217244
public Tags and(@Nullable Iterable<? extends Tag> tags) {
218-
if (tags == null || tags == EMPTY || !tags.iterator().hasNext()) {
219-
return this;
220-
}
221-
222245
if (this.length == 0) {
223246
return Tags.of(tags);
224247
}
248+
249+
// Tags.of() will take care of nulls, empty iterables and so on
250+
// merge() then will check if the argument is empty and reduce to no-op
225251
return merge(Tags.of(tags));
226252
}
227253

@@ -276,7 +302,7 @@ public int hashCode() {
276302

277303
@Override
278304
public boolean equals(@Nullable Object obj) {
279-
return this == obj || obj != null && getClass() == obj.getClass() && tagsEqual((Tags) obj);
305+
return this == obj || (obj != null && getClass() == obj.getClass() && tagsEqual((Tags) obj));
280306
}
281307

282308
private boolean tagsEqual(Tags obj) {
@@ -323,16 +349,19 @@ public static Tags concat(@Nullable Iterable<? extends Tag> tags, @Nullable Stri
323349
* @return a new {@code Tags} instance
324350
*/
325351
public static Tags of(@Nullable Iterable<? extends Tag> tags) {
326-
if (tags == null || tags == EMPTY || !tags.iterator().hasNext()) {
327-
return Tags.empty();
328-
}
329-
else if (tags instanceof Tags) {
352+
if (tags instanceof Tags) {
330353
return (Tags) tags;
331354
}
332355
else if (tags instanceof Collection) {
333356
Collection<? extends Tag> tagsCollection = (Collection<? extends Tag>) tags;
357+
if (tagsCollection.isEmpty()) {
358+
return Tags.empty();
359+
}
334360
return toTags(tagsCollection.toArray(EMPTY_TAG_ARRAY));
335361
}
362+
else if (emptyIterable(tags)) {
363+
return Tags.empty();
364+
}
336365
else {
337366
return toTags(StreamSupport.stream(tags.spliterator(), false).toArray(Tag[]::new));
338367
}
@@ -346,7 +375,7 @@ else if (tags instanceof Collection) {
346375
* @return a new {@code Tags} instance
347376
*/
348377
public static Tags of(String key, String value) {
349-
return new Tags(new Tag[] { Tag.of(key, value) }, 1);
378+
return of(Tag.of(key, value));
350379
}
351380

352381
/**
@@ -373,6 +402,26 @@ private static boolean blankVarargs(@Nullable Object[] args) {
373402
return args == null || args.length == 0 || (args.length == 1 && args[0] == null);
374403
}
375404

405+
private static boolean emptyIterable(@Nullable Iterable<? extends Tag> iterable) {
406+
// Doing the checks in the ascending cost order
407+
if (iterable == null || iterable == EMPTY) {
408+
return true;
409+
}
410+
411+
if (iterable instanceof Tags) {
412+
return ((Tags) iterable).length == 0;
413+
}
414+
415+
if (iterable instanceof Collection) {
416+
return ((Collection<?>) iterable).isEmpty();
417+
}
418+
419+
// While the compiler can theoretically avoid Iterator allocation here
420+
// (via scalarization), it is not guaranteed, thus leaving this check as the last
421+
// one
422+
return !iterable.iterator().hasNext();
423+
}
424+
376425
/**
377426
* Return a new {@code Tags} instance containing tags constructed from the specified
378427
* tags.

micrometer-core/src/test/java/io/micrometer/core/instrument/TagsTest.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
import com.sun.management.ThreadMXBean;
1919
import io.micrometer.core.Issue;
2020
import org.junit.jupiter.api.Test;
21-
import org.junit.jupiter.api.condition.*;
21+
import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
22+
import org.junit.jupiter.params.ParameterizedTest;
23+
import org.junit.jupiter.params.provider.Arguments;
24+
import org.junit.jupiter.params.provider.MethodSource;
2225

2326
import java.lang.management.ManagementFactory;
2427
import java.util.*;
@@ -139,6 +142,27 @@ void concatOnTwoTagsWithSameKeyAreMergedIntoOneTag() {
139142
assertThat(tags).containsExactly(Tag.of("k", "v2"));
140143
}
141144

145+
static Stream<Arguments> concatenatedIterables() {
146+
return Stream.of(Arguments.of(Tags.empty(), Tags.empty(), Tags.empty()),
147+
Arguments.of(Tags.of("k1", "v1"), Tags.empty(), Tags.of("k1", "v1")),
148+
Arguments.of(Tags.empty(), Tags.of("k1", "v1"), Tags.of("k1", "v1")),
149+
Arguments.of(Tags.of("k1", "v1", "k2", "v2", "k4", "v4", "k3", "v3", "k5", "v5"),
150+
Tags.of("k0", "v0", "k2", "override", "k4", "override", "k6", "v6", "k7", "v7"),
151+
Tags.of("k0", "v0", "k1", "v1", "k2", "override", "k3", "v3", "k4", "override", "k5", "v5",
152+
"k6", "v6", "k7", "v7")));
153+
}
154+
155+
@ParameterizedTest
156+
@MethodSource("concatenatedIterables")
157+
void concatOnTwoIterablesWithSameKeyAreMergedIntoOneTag(Tags left, Tags right, Tags expectation) {
158+
// Converting to classes that are only iterables, not collections nor Tags
159+
Iterable<Tag> first = left::iterator;
160+
Iterable<Tag> second = right::iterator;
161+
162+
Iterable<Tag> tags = Tags.concat(first, second);
163+
assertThat(tags).containsExactlyElementsOf(expectation);
164+
}
165+
142166
@Issue("#3851")
143167
@Test
144168
void concatWhenKeyValuesAreNullShouldReturnCurrentInstance() {

0 commit comments

Comments
 (0)