Skip to content

Commit

Permalink
Tags merge optimization (#4959)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mstyura authored Oct 3, 2024
1 parent a61a7a2 commit 1482cdf
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.micrometer.benchmark.core;

import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
Expand All @@ -31,13 +32,59 @@
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class TagsBenchmark {

@Threads(16)
static final Tag[] orderedTagsSet10 = new Tag[] { Tag.of("key0", "value"), Tag.of("key1", "value"),
Tag.of("key2", "value"), Tag.of("key3", "value"), Tag.of("key4", "value"), Tag.of("key5", "value"),
Tag.of("key6", "value"), Tag.of("key7", "value"), Tag.of("key8", "value"), Tag.of("key9", "value") };

static final Tag[] orderedTagsSet4 = new Tag[] { Tag.of("key0", "value"), Tag.of("key1", "value"),
Tag.of("key2", "value"), Tag.of("key3", "value"), };

static final Tag[] orderedTagsSet2 = new Tag[] { Tag.of("key0", "value"), Tag.of("key1", "value"), };

static final Tag[] unorderedTagsSet10 = new Tag[] { Tag.of("key1", "value"), Tag.of("key2", "value"),
Tag.of("key3", "value"), Tag.of("key4", "value"), Tag.of("key5", "value"), Tag.of("key6", "value"),
Tag.of("key7", "value"), Tag.of("key8", "value"), Tag.of("key9", "value"), Tag.of("key0", "value") };

static final Tag[] unorderedTagsSet4 = new Tag[] { Tag.of("key1", "value"), Tag.of("key2", "value"),
Tag.of("key3", "value"), Tag.of("key0", "value"), };

static final Tag[] unorderedTagsSet2 = new Tag[] { Tag.of("key1", "value"), Tag.of("key0", "value") };

@Benchmark
public Tags tagsOfOrderedTagsSet10() {
return Tags.of(orderedTagsSet10);
}

@Benchmark
public Tags tagsOfOrderedTagsSet4() {
return Tags.of(orderedTagsSet4);
}

@Benchmark
public Tags tagsOfOrderedTagsSet2() {
return Tags.of(orderedTagsSet2);
}

@Benchmark
public Tags tagsOfUnorderedTagsSet10() {
return Tags.of(unorderedTagsSet10);
}

@Benchmark
public Tags tagsOfUnorderedTagsSet4() {
return Tags.of(unorderedTagsSet4);
}

@Benchmark
public Tags tagsOfUnorderedTagsSet2() {
return Tags.of(unorderedTagsSet2);
}

@Benchmark
public void of() {
Tags.of("key", "value", "key2", "value2", "key3", "value3", "key4", "value4", "key5", "value5");
}

@Threads(16)
@Benchmark
public void dotAnd() {
Tags.of("key", "value").and("key2", "value2", "key3", "value3", "key4", "value4", "key5", "value5");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2024 VMware, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micrometer.benchmark.core;

import io.micrometer.core.instrument.Tags;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

@Fork(1)
@Measurement(iterations = 2)
@Warmup(iterations = 2)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class TagsMergeBenchmark {

static final Tags left = Tags.of("key", "value", "key2", "value2", "key6", "value6", "key7", "value7", "key8",
"value8", "keyA", "valueA", "keyC", "valueC", "keyE", "valueE", "keyF", "valueF", "keyG", "valueG", "keyG",
"valueG", "keyG", "valueG", "keyH", "valueH");

static final Tags right = Tags.of("key", "value", "key1", "value1", "key2", "value2", "key3", "value3", "key4",
"value4", "key5", "value5", "keyA", "valueA", "keyB", "valueB", "keyD", "valueD");

@Benchmark
public Tags mergeTags() {
return left.and(right);
}

public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder().include(TagsMergeBenchmark.class.getSimpleName()).build();
new Runner(opt).run();
}

}
156 changes: 125 additions & 31 deletions micrometer-core/src/main/java/io/micrometer/core/instrument/Tags.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,76 @@
*/
public final class Tags implements Iterable<Tag> {

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

private final Tag[] tags;
/**
* A private array of {@code Tag} objects containing the sorted and deduplicated tags.
*/
private final Tag[] sortedSet;

/**
* The number of valid tags present in the {@link #sortedSet} array.
*/
private final int length;

/**
* A private constructor that initializes a {@code Tags} object with a sorted set of
* tags and its length.
* @param sortedSet an ordered set of unique tags by key
* @param length the number of valid tags in the {@code sortedSet}
*/
private Tags(Tag[] sortedSet, int length) {
this.sortedSet = sortedSet;
this.length = length;
}

private int last;
/**
* Checks if the first {@code length} elements of the {@code tags} array form an
* ordered set of tags.
* @param tags an array of tags.
* @param length the number of items to check.
* @return {@code true} if the first {@code length} items of {@code tags} form an
* ordered set; otherwise {@code false}.
*/
private static boolean isSortedSet(Tag[] tags, int length) {
if (length > tags.length) {
return false;
}
for (int i = 0; i < length - 1; i++) {
int cmp = tags[i].compareTo(tags[i + 1]);
if (cmp >= 0) {
return false;
}
}
return true;
}

private Tags(Tag[] tags) {
this.tags = tags;
Arrays.sort(this.tags);
dedup();
/**
* Constructs a {@code Tags} collection from the provided array of tags.
* @param tags an array of {@code Tag} objects, possibly unordered and/or containing
* duplicates.
* @return a {@code Tags} instance with a deduplicated and ordered set of tags.
*/
private static Tags make(Tag[] tags) {
int len = tags.length;
if (!isSortedSet(tags, len)) {
Arrays.sort(tags);
len = dedup(tags);
}
return new Tags(tags, len);
}

private void dedup() {
/**
* Removes duplicate tags from an ordered array of tags.
* @param tags an ordered array of {@code Tag} objects.
* @return the number of unique tags in the {@code tags} array after removing
* duplicates.
*/
private static int dedup(Tag[] tags) {
int n = tags.length;

if (n == 0 || n == 1) {
last = n;
return;
return n;
}

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

tags[j++] = tags[n - 1];
last = j;
return j;
}

/**
* Constructs a {@code Tags} instance by merging two sets of tags in time proportional
* to the sum of their sizes.
* @param other the set of tags to merge with this one.
* @return a {@code Tags} instance with the merged sets of tags.
*/
private Tags merged(Tags other) {
if (other.length == 0) {
return this;
}
if (Objects.equals(this, other)) {
return this;
}
Tag[] sortedSet = new Tag[this.length + other.length];
int sortedIdx = 0, thisIdx = 0, otherIdx = 0;
while (thisIdx < this.length && otherIdx < other.length) {
int cmp = this.sortedSet[thisIdx].compareTo(other.sortedSet[otherIdx]);
if (cmp > 0) {
sortedSet[sortedIdx] = other.sortedSet[otherIdx];
otherIdx++;
}
else if (cmp < 0) {
sortedSet[sortedIdx] = this.sortedSet[thisIdx];
thisIdx++;
}
else {
// In case of key conflict prefer tag from other set
sortedSet[sortedIdx] = other.sortedSet[otherIdx];
thisIdx++;
otherIdx++;
}
sortedIdx++;
}
int thisRemaining = this.length - thisIdx;
if (thisRemaining > 0) {
System.arraycopy(this.sortedSet, thisIdx, sortedSet, sortedIdx, thisRemaining);
sortedIdx += thisRemaining;
}
int otherRemaining = other.length - otherIdx;
if (otherIdx < other.sortedSet.length) {
System.arraycopy(other.sortedSet, otherIdx, sortedSet, sortedIdx, otherRemaining);
sortedIdx += otherRemaining;
}
return new Tags(sortedSet, sortedIdx);
}

/**
Expand Down Expand Up @@ -99,10 +197,7 @@ public Tags and(@Nullable Tag... tags) {
if (blankVarargs(tags)) {
return this;
}
Tag[] newTags = new Tag[last + tags.length];
System.arraycopy(this.tags, 0, newTags, 0, last);
System.arraycopy(tags, 0, newTags, last, tags.length);
return new Tags(newTags);
return and(make(tags));
}

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

if (this.tags.length == 0) {
if (this.length == 0) {
return Tags.of(tags);
}

return and(Tags.of(tags).tags);
return merged(Tags.of(tags));
}

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

@Override
public boolean hasNext() {
return currentIndex < last;
return currentIndex < length;
}

@Override
public Tag next() {
return tags[currentIndex++];
return sortedSet[currentIndex++];
}

@Override
Expand All @@ -151,7 +245,7 @@ public void remove() {

@Override
public Spliterator<Tag> spliterator() {
return Spliterators.spliterator(tags, 0, last, Spliterator.IMMUTABLE | Spliterator.ORDERED
return Spliterators.spliterator(sortedSet, 0, length, Spliterator.IMMUTABLE | Spliterator.ORDERED
| Spliterator.DISTINCT | Spliterator.NONNULL | Spliterator.SORTED);
}

Expand All @@ -166,8 +260,8 @@ public Stream<Tag> stream() {
@Override
public int hashCode() {
int result = 1;
for (int i = 0; i < last; i++) {
result = 31 * result + tags[i].hashCode();
for (int i = 0; i < length; i++) {
result = 31 * result + sortedSet[i].hashCode();
}
return result;
}
Expand All @@ -178,14 +272,14 @@ public boolean equals(@Nullable Object obj) {
}

private boolean tagsEqual(Tags obj) {
if (tags == obj.tags)
if (sortedSet == obj.sortedSet)
return true;

if (last != obj.last)
if (length != obj.length)
return false;

for (int i = 0; i < last; i++) {
if (!tags[i].equals(obj.tags[i]))
for (int i = 0; i < length; i++) {
if (!sortedSet[i].equals(obj.sortedSet[i]))
return false;
}

Expand Down Expand Up @@ -229,10 +323,10 @@ else if (tags instanceof Tags) {
}
else if (tags instanceof Collection) {
Collection<? extends Tag> tagsCollection = (Collection<? extends Tag>) tags;
return new Tags(tagsCollection.toArray(new Tag[0]));
return make(tagsCollection.toArray(new Tag[0]));
}
else {
return new Tags(StreamSupport.stream(tags.spliterator(), false).toArray(Tag[]::new));
return make(StreamSupport.stream(tags.spliterator(), false).toArray(Tag[]::new));
}
}

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

/**
Expand All @@ -264,7 +358,7 @@ public static Tags of(@Nullable String... keyValues) {
for (int i = 0; i < keyValues.length; i += 2) {
tags[i / 2] = Tag.of(keyValues[i], keyValues[i + 1]);
}
return new Tags(tags);
return make(tags);
}

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

0 comments on commit 1482cdf

Please sign in to comment.