From 18708d3f8820004847b4503caf964b7b9e9fabee Mon Sep 17 00:00:00 2001 From: Yury Yarashevich Date: Wed, 10 Apr 2024 22:36:09 +0200 Subject: [PATCH] Optimised Tags merge. --- .../benchmark/core/TagsBenchmark.java | 55 ++++++ .../benchmark/core/TagsMergeBenchmark.java | 14 +- .../io/micrometer/core/instrument/Tags.java | 156 ++++++++++++++---- 3 files changed, 187 insertions(+), 38 deletions(-) diff --git a/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/TagsBenchmark.java b/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/TagsBenchmark.java index b930006011..6af0d726fb 100644 --- a/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/TagsBenchmark.java +++ b/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/TagsBenchmark.java @@ -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; @@ -31,6 +32,60 @@ @OutputTimeUnit(TimeUnit.NANOSECONDS) public class TagsBenchmark { + 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") }; + + @Threads(1) + @Benchmark + public Tags tagsOfOrderedTagsSet10() { + return Tags.of(orderedTagsSet10); + } + + @Threads(1) + @Benchmark + public Tags tagsOfOrderedTagsSet4() { + return Tags.of(orderedTagsSet4); + } + + @Threads(1) + @Benchmark + public Tags tagsOfOrderedTagsSet2() { + return Tags.of(orderedTagsSet2); + } + + @Threads(1) + @Benchmark + public Tags tagsOfUnorderedTagsSet10() { + return Tags.of(unorderedTagsSet10); + } + + @Threads(1) + @Benchmark + public Tags tagsOfUnorderedTagsSet4() { + return Tags.of(unorderedTagsSet4); + } + + @Threads(1) + @Benchmark + public Tags tagsOfUnorderedTagsSet2() { + return Tags.of(unorderedTagsSet2); + } + @Threads(16) @Benchmark public void of() { diff --git a/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/TagsMergeBenchmark.java b/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/TagsMergeBenchmark.java index d592f2bb3f..04a204feca 100644 --- a/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/TagsMergeBenchmark.java +++ b/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/TagsMergeBenchmark.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 VMware, Inc. + * 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. @@ -32,14 +32,14 @@ @State(Scope.Thread) public class TagsMergeBenchmark { - 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 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"); - Tags right = Tags.of("key", "value", "key1", "value1", "key2", "value2", "key3", "value3", "key4", "value4", "key5", - "value5", "keyA", "valueA", "keyB", "valueB", "keyD", "valueD"); + static final Tags right = Tags.of("key", "value", "key1", "value1", "key2", "value2", "key3", "value3", "key4", + "value4", "key5", "value5", "keyA", "valueA", "keyB", "valueB", "keyD", "valueD"); - @Threads(16) + @Threads(1) @Benchmark public Tags mergeTags() { return left.and(right); diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/Tags.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/Tags.java index 204be4b138..523f274c1d 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/Tags.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/Tags.java @@ -34,24 +34,76 @@ */ public final class Tags implements Iterable { - 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 @@ -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); } /** @@ -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)); } /** @@ -116,11 +211,10 @@ public Tags and(@Nullable Iterable 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 @@ -134,12 +228,12 @@ private class ArrayIterator implements Iterator { @Override public boolean hasNext() { - return currentIndex < last; + return currentIndex < length; } @Override public Tag next() { - return tags[currentIndex++]; + return sortedSet[currentIndex++]; } @Override @@ -151,7 +245,7 @@ public void remove() { @Override public Spliterator 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); } @@ -166,8 +260,8 @@ public Stream 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; } @@ -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; } @@ -229,10 +323,10 @@ else if (tags instanceof Tags) { } else if (tags instanceof Collection) { Collection tagsCollection = (Collection) 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)); } } @@ -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); } /** @@ -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) {