From a120435b225190e9b64cb75dbb7e153d4c63383e Mon Sep 17 00:00:00 2001 From: Carter Kozak Date: Tue, 1 Mar 2022 18:30:50 -0500 Subject: [PATCH] [WIP] Compact MetricName tag map implementation (#1368) Compact MetricName tag map implementation --- changelog/@unreleased/pr-1368.v2.yml | 5 + tritium-jmh/build.gradle | 2 +- .../NestedMetricsBenchmark.java | 47 +- .../metrics/registry/ExtraEntrySortedMap.java | 214 ------- .../metrics/registry/PrehashedSortedMap.java | 45 -- .../metrics/registry/RealMetricName.java | 40 +- .../tritium/metrics/registry/TagMap.java | 569 ++++++++++++++++++ .../registry/ExtraEntrySortedMapTest.java | 30 +- .../tritium/metrics/registry/TagMapTest.java | 75 +++ 9 files changed, 720 insertions(+), 307 deletions(-) create mode 100644 changelog/@unreleased/pr-1368.v2.yml delete mode 100644 tritium-registry/src/main/java/com/palantir/tritium/metrics/registry/ExtraEntrySortedMap.java delete mode 100644 tritium-registry/src/main/java/com/palantir/tritium/metrics/registry/PrehashedSortedMap.java create mode 100644 tritium-registry/src/main/java/com/palantir/tritium/metrics/registry/TagMap.java create mode 100644 tritium-registry/src/test/java/com/palantir/tritium/metrics/registry/TagMapTest.java diff --git a/changelog/@unreleased/pr-1368.v2.yml b/changelog/@unreleased/pr-1368.v2.yml new file mode 100644 index 000000000..bec457c1a --- /dev/null +++ b/changelog/@unreleased/pr-1368.v2.yml @@ -0,0 +1,5 @@ +type: improvement +improvement: + description: Compact MetricName tag map implementation + links: + - https://github.com/palantir/tritium/pull/1368 diff --git a/tritium-jmh/build.gradle b/tritium-jmh/build.gradle index 4760fe6c3..35fd8b591 100644 --- a/tritium-jmh/build.gradle +++ b/tritium-jmh/build.gradle @@ -13,7 +13,7 @@ tasks.jmhCompileGeneratedClasses { dependencies { - annotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess' + jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess' api 'io.dropwizard.metrics:metrics-core' jmh project(':tritium-api') diff --git a/tritium-jmh/src/jmh/java/com/palantir/tritium/microbenchmarks/NestedMetricsBenchmark.java b/tritium-jmh/src/jmh/java/com/palantir/tritium/microbenchmarks/NestedMetricsBenchmark.java index 9a122aed9..30dc6fd0e 100644 --- a/tritium-jmh/src/jmh/java/com/palantir/tritium/microbenchmarks/NestedMetricsBenchmark.java +++ b/tritium-jmh/src/jmh/java/com/palantir/tritium/microbenchmarks/NestedMetricsBenchmark.java @@ -16,11 +16,13 @@ package com.palantir.tritium.microbenchmarks; -import com.codahale.metrics.Meter; +import com.codahale.metrics.Metric; import com.palantir.tritium.metrics.registry.DefaultTaggedMetricRegistry; import com.palantir.tritium.metrics.registry.MetricName; import com.palantir.tritium.metrics.registry.TaggedMetricRegistry; +import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; @@ -38,7 +40,7 @@ import org.openjdk.jmh.runner.options.OptionsBuilder; @BenchmarkMode(Mode.AverageTime) -@OutputTimeUnit(TimeUnit.NANOSECONDS) +@OutputTimeUnit(TimeUnit.MICROSECONDS) @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) @Fork(1) @@ -46,26 +48,31 @@ @SuppressWarnings({"designforextension", "NullAway"}) public class NestedMetricsBenchmark { private TaggedMetricRegistry metrics; + private BiConsumer consumer; @Setup - public void before() { + public void before(Blackhole blackhole) { metrics = constructBaseRegistry(); + consumer = (name, metric) -> { + blackhole.consume(name.safeName()); + // name.safeTags().forEach(blackholeConsumer) would be ideal, + // however most readers will use the entrySet + for (Map.Entry entry : name.safeTags().entrySet()) { + blackhole.consume(entry.getKey()); + blackhole.consume(entry.getValue()); + } + blackhole.consume(metric); + }; } @Benchmark - public void benchmarkGetMetrics(Blackhole blackhole) { - metrics.getMetrics().forEach((name, metric) -> { - blackhole.consume(name); - blackhole.consume(metric); - }); + public void benchmarkGetMetrics() { + metrics.getMetrics().forEach(consumer); } @Benchmark - public void benchmarkForEach(Blackhole blackhole) { - metrics.forEachMetric((name, metric) -> { - blackhole.consume(name); - blackhole.consume(metric); - }); + public void benchmarkForEach() { + metrics.forEachMetric(consumer); } private static TaggedMetricRegistry constructBaseRegistry() { @@ -79,13 +86,13 @@ private static TaggedMetricRegistry constructBaseRegistry() { private static TaggedMetricRegistry constructSubRegistry() { TaggedMetricRegistry registry = new DefaultTaggedMetricRegistry(); for (int i = 0; i < 100; i++) { - Meter meter = registry.meter(MetricName.builder() - .safeName("some metric " + i) - .putSafeTags("some tag", "some tag value") - .build()); - for (int j = 0; j < 1000; j++) { - meter.mark(); - } + registry.meter(MetricName.builder() + .safeName("some metric " + i) + .putSafeTags("some tag", "some tag value") + .putSafeTags("libraryName", "tritium") + .putSafeTags("libraryVersion", "1.2.3") + .build()) + .mark(); } return registry; } diff --git a/tritium-registry/src/main/java/com/palantir/tritium/metrics/registry/ExtraEntrySortedMap.java b/tritium-registry/src/main/java/com/palantir/tritium/metrics/registry/ExtraEntrySortedMap.java deleted file mode 100644 index 2356a106e..000000000 --- a/tritium-registry/src/main/java/com/palantir/tritium/metrics/registry/ExtraEntrySortedMap.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * (c) Copyright 2019 Palantir Technologies Inc. All rights reserved. - * - * 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 - * - * http://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 com.palantir.tritium.metrics.registry; - -import static com.palantir.logsafe.Preconditions.checkArgument; -import static com.palantir.logsafe.Preconditions.checkNotNull; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; -import com.google.common.collect.Iterators; -import com.google.common.collect.Maps; -import com.google.common.collect.Ordering; -import java.util.AbstractCollection; -import java.util.AbstractMap; -import java.util.AbstractSet; -import java.util.Collection; -import java.util.Comparator; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; -import java.util.SortedMap; -import javax.annotation.Nullable; - -@SuppressWarnings("JdkObsolete") -final class ExtraEntrySortedMap extends AbstractMap implements SortedMap { - private final Ordering ordering; - private final SortedMap base; - private final K extraKey; - private final V extraValue; - private final int extraEntryHashCode; - - ExtraEntrySortedMap(SortedMap base, K extraKey, V extraValue) { - this.base = checkNotNull(base, "base"); - this.extraKey = checkNotNull(extraKey, "extraKey"); - this.extraValue = checkNotNull(extraValue, "extraValue"); - this.ordering = Ordering.from(base.comparator()); - this.extraEntryHashCode = extraKey.hashCode() ^ extraValue.hashCode(); - // This line of code is roughly a 50% perf regression for iterating through metrics. Remove if causing issues. - checkArgument(!base.containsKey(extraKey), "Base must not contain the extra key that is to be added"); - } - - @Override - public Comparator comparator() { - return base.comparator(); - } - - @Override - public SortedMap subMap(K fromKey, K toKey) { - SortedMap newBase = base.subMap(fromKey, toKey); - if (ordering.compare(fromKey, extraKey) <= 0 && ordering.compare(toKey, extraKey) > 0) { - return new ExtraEntrySortedMap<>(newBase, extraKey, extraValue); - } - return newBase; - } - - @Override - public SortedMap headMap(K toKey) { - SortedMap newBase = base.headMap(toKey); - if (ordering.compare(toKey, extraKey) > 0) { - return new ExtraEntrySortedMap<>(newBase, extraKey, extraValue); - } - return newBase; - } - - @Override - public SortedMap tailMap(K fromKey) { - SortedMap newBase = base.tailMap(fromKey); - if (ordering.compare(fromKey, extraKey) <= 0) { - return new ExtraEntrySortedMap<>(newBase, extraKey, extraValue); - } - return newBase; - } - - @Override - public K firstKey() { - if (base.isEmpty()) { - return extraKey; - } - return ordering.min(base.firstKey(), extraKey); - } - - @Override - public K lastKey() { - if (base.isEmpty()) { - return extraKey; - } - return ordering.max(base.lastKey(), extraKey); - } - - @Override - public int size() { - return base.size() + 1; - } - - @Override - public boolean isEmpty() { - return false; - } - - @Override - public boolean containsKey(Object key) { - return extraKey.equals(key) || base.containsKey(key); - } - - @Override - public boolean containsValue(Object value) { - return extraValue.equals(value) || base.containsValue(value); - } - - @Nullable - @Override - public V get(Object key) { - if (extraKey.equals(key)) { - return extraValue; - } - return base.get(key); - } - - @Override - public V put(K _key, V _value) { - throw new UnsupportedOperationException(); - } - - @Override - public V remove(Object _key) { - throw new UnsupportedOperationException(); - } - - @Override - public void putAll(Map _other) { - throw new UnsupportedOperationException(); - } - - @Override - public void clear() { - throw new UnsupportedOperationException(); - } - - @Override - public Set keySet() { - return new AbstractSet() { - @Override - public Iterator iterator() { - return Iterables.mergeSorted(ImmutableList.of(base.keySet(), ImmutableList.of(extraKey)), ordering) - .iterator(); - } - - @Override - public int size() { - return base.size() + 1; - } - }; - } - - @Override - public Collection values() { - return new AbstractCollection() { - @Override - public Iterator iterator() { - return Iterators.transform(keySet().iterator(), key -> get(key)); - } - - @Override - public int size() { - return base.values().size() + 1; - } - }; - } - - @Override - public Set> entrySet() { - return new AbstractSet>() { - @Override - public Iterator> iterator() { - return Iterators.transform(keySet().iterator(), key -> Maps.immutableEntry(key, get(key))); - } - - @Override - public int size() { - return base.size() + 1; - } - }; - } - - @Override - public boolean equals(@Nullable Object other) { - if (other instanceof ExtraEntrySortedMap) { - ExtraEntrySortedMap otherMap = (ExtraEntrySortedMap) other; - if (extraKey.equals(otherMap.extraKey) && extraValue.equals(otherMap.extraValue)) { - return base.equals(otherMap.base); - } - } - return super.equals(other); - } - - @Override - public int hashCode() { - return base.hashCode() + extraEntryHashCode; - } -} diff --git a/tritium-registry/src/main/java/com/palantir/tritium/metrics/registry/PrehashedSortedMap.java b/tritium-registry/src/main/java/com/palantir/tritium/metrics/registry/PrehashedSortedMap.java deleted file mode 100644 index d5cf6dcf2..000000000 --- a/tritium-registry/src/main/java/com/palantir/tritium/metrics/registry/PrehashedSortedMap.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * (c) Copyright 2019 Palantir Technologies Inc. All rights reserved. - * - * 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 - * - * http://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 com.palantir.tritium.metrics.registry; - -import com.google.common.collect.ForwardingSortedMap; -import com.google.common.collect.ImmutableSortedMap; - -/** - * A sorted map implementation which prehashes for faster usage in hashmaps. This is only safe for immutable underlying - * maps (both the map implementation and the entries must be immutable). - */ -final class PrehashedSortedMap extends ForwardingSortedMap { - private final ImmutableSortedMap delegate; - private final int hashCode; - - PrehashedSortedMap(ImmutableSortedMap delegate) { - this.delegate = delegate; - this.hashCode = this.delegate.hashCode(); - } - - @Override - protected ImmutableSortedMap delegate() { - return delegate; - } - - @SuppressWarnings("checkstyle:EqualsHashCode") - @Override - public int hashCode() { - return hashCode; - } -} diff --git a/tritium-registry/src/main/java/com/palantir/tritium/metrics/registry/RealMetricName.java b/tritium-registry/src/main/java/com/palantir/tritium/metrics/registry/RealMetricName.java index af20fb1b7..9a2f1003b 100644 --- a/tritium-registry/src/main/java/com/palantir/tritium/metrics/registry/RealMetricName.java +++ b/tritium-registry/src/main/java/com/palantir/tritium/metrics/registry/RealMetricName.java @@ -23,15 +23,14 @@ import javax.annotation.Nullable; final class RealMetricName implements MetricName { - private static final SortedMap EMPTY = prehash(ImmutableSortedMap.of()); + private final String safeName; - private final SortedMap safeTags; - private final int hashCode; + private final TagMap safeTags; + private int hashCode; - private RealMetricName(String safeName, SortedMap safeTags) { + private RealMetricName(String safeName, TagMap safeTags) { this.safeName = safeName; this.safeTags = safeTags; - this.hashCode = computeHashCode(); } @SuppressWarnings("JdkObsolete") // SortedMap is part of Metrics API @@ -48,7 +47,7 @@ public String safeName() { } @Override - public SortedMap safeTags() { + public TagMap safeTags() { return safeTags; } @@ -59,7 +58,12 @@ public String toString() { @Override public int hashCode() { - return hashCode; + int memoized = hashCode; + if (memoized == 0) { + memoized = computeHashCode(); + hashCode = memoized; + } + return memoized; } @Override @@ -76,22 +80,28 @@ public boolean equals(@Nullable Object other) { } static MetricName create(String safeName) { - return new RealMetricName(checkNotNull(safeName, "safeName"), EMPTY); + return new RealMetricName(checkNotNull(safeName, "safeName"), TagMap.EMPTY); } static MetricName create(MetricName other) { - return new RealMetricName(other.safeName(), prehash(other.safeTags())); + return new RealMetricName(other.safeName(), TagMap.of(other.safeTags())); } static MetricName create(MetricName other, String extraTagName, String extraTagValue) { - return new RealMetricName( - other.safeName(), new ExtraEntrySortedMap<>(prehash(other.safeTags()), extraTagName, extraTagValue)); + return new RealMetricName(other.safeName(), withEntry(other.safeTags(), extraTagName, extraTagValue)); } - private static SortedMap prehash(SortedMap map) { - if (map instanceof PrehashedSortedMap) { - return map; + private static TagMap withEntry(SortedMap tags, String extraTagName, String extraTagValue) { + if (tags instanceof TagMap) { + return ((TagMap) tags).withEntry(extraTagName, extraTagValue); } - return new PrehashedSortedMap<>(ImmutableSortedMap.copyOfSorted(map)); + return withEntryFallback(tags, extraTagName, extraTagValue); + } + + private static TagMap withEntryFallback(SortedMap tags, String extraTagName, String extraTagValue) { + return TagMap.of(ImmutableSortedMap.naturalOrder() + .putAll(tags) + .put(extraTagName, extraTagValue) + .build()); } } diff --git a/tritium-registry/src/main/java/com/palantir/tritium/metrics/registry/TagMap.java b/tritium-registry/src/main/java/com/palantir/tritium/metrics/registry/TagMap.java new file mode 100644 index 000000000..412f4b201 --- /dev/null +++ b/tritium-registry/src/main/java/com/palantir/tritium/metrics/registry/TagMap.java @@ -0,0 +1,569 @@ +/* + * (c) Copyright 2022 Palantir Technologies Inc. All rights reserved. + * + * 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 + * + * http://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 com.palantir.tritium.metrics.registry; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Ordering; +import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Set; +import java.util.SortedMap; +import java.util.function.BiConsumer; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * {@link TagMap} is a {@link SortedMap} implementation optimized for creation performance and memory overhead. + * Primarily optimized to retain as little memory as possible, and create small short-lived intermediate objects. + * + * Note that we expect fairly small tag maps which are iterated over, not used for lookups. Most {@link Map} + * methods are implemented, but use a naive linear search rather than a binary search. + */ +@SuppressWarnings("JdkObsolete") +final class TagMap implements SortedMap { + + static final TagMap EMPTY = new TagMap(new String[0]); + + /** + * Map entries arranged with keys on even indexes and values in odd indexes. + * Keys are sorted alphabetically. + * {@code ["a", "one", "b", "two"]} represents map {@code {"a"="one", "b"="two"}}. + */ + private final String[] values; + + static TagMap of(Map data) { + if (data instanceof TagMap) { + return (TagMap) data; + } + if (data.isEmpty()) { + return EMPTY; + } + if (data instanceof SortedMap) { + SortedMap sortedMap = (SortedMap) data; + if (isNaturalOrder(sortedMap.comparator())) { + return new TagMap(toArrayFromNaturalSortedMap(sortedMap)); + } + } + return new TagMap(toArray(data)); + } + + private TagMap(String[] values) { + this.values = values; + } + + @VisibleForTesting + static boolean isNaturalOrder(@Nullable Comparator comparator) { + // null comparator means natural order per SortedMap javadoc + return comparator == null + // Comparator.naturalOrder() and Ordering.natural() return singletons + || Objects.equals(comparator, Comparator.naturalOrder()) + || Objects.equals(comparator, Ordering.natural()); + } + + private static String[] toArrayFromNaturalSortedMap(SortedMap data) { + int size = data.size(); + String[] values = new String[size * 2]; + int index = 0; + for (Map.Entry entry : data.entrySet()) { + values[index++] = entry.getKey(); + values[index++] = entry.getValue(); + } + return values; + } + + private static String[] toArray(Map data) { + int size = data.size(); + String[] values = new String[size * 2]; + String[] keys = new String[size]; + int keysIndex = 0; + for (Map.Entry entry : data.entrySet()) { + keys[keysIndex++] = entry.getKey(); + } + Arrays.sort(keys); + for (int i = 0; i < keys.length; i++) { + int valuesIndex = 2 * i; + String key = keys[i]; + values[valuesIndex] = key; + values[valuesIndex + 1] = data.get(key); + } + return values; + } + + /** Returns a new {@link TagMap} with on additional entry. */ + TagMap withEntry(String key, String value) { + String[] local = this.values; + int newPosition = 0; + for (; newPosition < local.length; newPosition += 2) { + String current = local[newPosition]; + int comparisonResult = current.compareTo(key); + if (comparisonResult == 0) { + throw new SafeIllegalArgumentException("Base must not contain the extra key that is to be added"); + } + if (comparisonResult > 0) { + break; + } + } + // current is greater than the new key + String[] newArray = new String[local.length + 2]; + System.arraycopy(local, 0, newArray, 0, newPosition); + newArray[newPosition] = key; + newArray[newPosition + 1] = value; + System.arraycopy(local, newPosition, newArray, newPosition + 2, local.length - newPosition); + return new TagMap(newArray); + } + + @Nullable + @Override + public String get(Object key) { + int idx = indexOfKey(key); + return idx >= 0 ? values[idx + 1] : null; + } + + private int indexOfKey(Object key) { + String[] local = values; + for (int i = 0; i < local.length; i += 2) { + if (Objects.equals(key, local[i])) { + return i; + } + } + return -1; + } + + @Override + public void forEach(BiConsumer action) { + String[] local = this.values; + for (int i = 0; i < local.length; i += 2) { + action.accept(local[i], local[i + 1]); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder().append("{"); + String[] local = this.values; + for (int i = 0; i < local.length; i += 2) { + String key = local[i]; + String value = local[i + 1]; + if (i != 0) { + sb.append(", "); + } + sb.append(key); + sb.append('='); + sb.append(value); + } + return sb.append('}').toString(); + } + + @Override + public boolean equals(@Nullable Object other) { + if (other == this) { + return true; + } + if (other instanceof TagMap) { + return Arrays.equals(values, ((TagMap) other).values); + } + if (!(other instanceof Map)) { + return false; + } + Map otherMap = (Map) other; + if (otherMap.size() != size()) { + return false; + } + for (int i = 0; i < values.length; i += 2) { + String key = values[i]; + String value = values[i + 1]; + if (!Objects.equals(value, otherMap.get(key))) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + int hashCode = 0; + for (int i = 0; i < values.length; i += 2) { + hashCode += Objects.hashCode(values[i]) ^ Objects.hashCode(values[i + 1]); + } + return hashCode; + } + + /* Misc methods to support the SortedMap interface. */ + + @Override + @Nullable + public Comparator comparator() { + return Comparator.naturalOrder(); + } + + @Override + public SortedMap subMap(String fromKey, String toKey) { + int beginIndex = 0; + for (int i = 0; i < values.length; i += 2) { + String key = values[i]; + if (key.compareTo(fromKey) >= 0) { + beginIndex = i; + break; + } + } + for (int i = values.length - 2; i >= beginIndex; i -= 2) { + String key = values[i]; + if (key.compareTo(toKey) < 0) { + return new TagMap(Arrays.copyOfRange(values, beginIndex, i + 2)); + } + } + return EMPTY; + } + + @Override + public SortedMap headMap(String toKey) { + for (int i = values.length - 2; i >= 0; i -= 2) { + String key = values[i]; + if (key.compareTo(toKey) < 0) { + return new TagMap(Arrays.copyOfRange(values, 0, i + 2)); + } + } + return EMPTY; + } + + @Override + public SortedMap tailMap(String fromKey) { + for (int i = 0; i < values.length; i += 2) { + String key = values[i]; + if (key.compareTo(fromKey) >= 0) { + return new TagMap(Arrays.copyOfRange(values, i, values.length)); + } + } + return EMPTY; + } + + @Nullable + @Override + public String firstKey() { + String[] local = this.values; + if (local.length == 0) { + throw new NoSuchElementException(); + } + return local[0]; + } + + @Nullable + @Override + public String lastKey() { + String[] local = this.values; + if (local.length == 0) { + throw new NoSuchElementException(); + } + return local[local.length - 2]; + } + + @Override + public int size() { + return values.length / 2; + } + + @Override + public boolean isEmpty() { + return values.length == 0; + } + + @Override + public boolean containsKey(Object key) { + return indexOfKey(key) >= 0; + } + + @Override + public boolean containsValue(Object value) { + String[] local = this.values; + for (int i = 1; i < local.length; i += 2) { + if (Objects.equals(value, local[i])) { + return true; + } + } + return false; + } + + @Override + public String put(String _key, String _value) { + throw new UnsupportedOperationException("immutable"); + } + + @Override + public void putAll(@Nonnull Map _map) { + throw new UnsupportedOperationException("immutable"); + } + + @Override + public String remove(Object _key) { + throw new UnsupportedOperationException("immutable"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("immutable"); + } + + @Nonnull + @Override + public Set keySet() { + String[] local = this.values; + Set set = new LinkedHashSet<>(local.length / 2); + for (int i = 0; i < local.length; i += 2) { + set.add(local[i]); + } + return Collections.unmodifiableSet(set); + } + + @Nonnull + @Override + public Collection values() { + String[] local = this.values; + List list = new ArrayList<>(local.length / 2); + for (int i = 1; i < local.length; i += 2) { + list.add(local[i]); + } + return Collections.unmodifiableCollection(list); + } + + @Nonnull + @Override + public Set> entrySet() { + return new TagMapEntrySet(values); + } + + private static final class TagMapEntrySet implements Set> { + + private final String[] values; + + TagMapEntrySet(String[] values) { + this.values = values; + } + + @Override + public int size() { + return values.length / 2; + } + + @Override + public boolean isEmpty() { + return values.length == 0; + } + + @Override + public boolean contains(Object object) { + if (object instanceof Entry) { + Entry entry = (Entry) object; + for (int i = 0; i < values.length; i += 2) { + if (Objects.equals(entry.getKey(), values[i]) && Objects.equals(entry.getValue(), values[i + 1])) { + return true; + } + } + } + return false; + } + + @Nonnull + @Override + public Iterator> iterator() { + return new TagMapEntrySetIterator(values); + } + + @Nonnull + @Override + public Object[] toArray() { + String[] local = values; + Object[] result = new Object[local.length / 2]; + for (int i = 0; i < local.length; i += 2) { + result[i / 2] = new TagEntry(local[i], local[i + 1]); + } + return result; + } + + @Override + @SuppressWarnings("unchecked") + public T[] toArray(T[] array) { + String[] local = values; + int resultLength = local.length / 2; + T[] result = resultLength > array.length + ? (T[]) Array.newInstance(array.getClass().getComponentType(), resultLength) + : array; + for (int i = 0; i < local.length; i += 2) { + result[i / 2] = (T) new TagEntry(local[i], local[i + 1]); + } + Arrays.fill(result, resultLength, array.length, null); + return result; + } + + @Override + public boolean add(Entry _stringStringEntry) { + throw new UnsupportedOperationException("immutable"); + } + + @Override + public boolean remove(Object _object) { + throw new UnsupportedOperationException("immutable"); + } + + @Override + public boolean containsAll(Collection collection) { + for (Object object : collection) { + if (!contains(object)) { + return false; + } + } + return true; + } + + @Override + public boolean addAll(@Nonnull Collection> _collection) { + throw new UnsupportedOperationException("immutable"); + } + + @Override + public boolean retainAll(@Nonnull Collection _collection) { + throw new UnsupportedOperationException("immutable"); + } + + @Override + public boolean removeAll(@Nonnull Collection _collection) { + throw new UnsupportedOperationException("immutable"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("immutable"); + } + + @Override + @SuppressWarnings("SuspiciousMethodCalls") + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Set)) { + return false; + } + if (other instanceof TagMapEntrySet) { + return Arrays.equals(values, ((TagMapEntrySet) other).values); + } + Set otherSet = (Set) other; + return size() == otherSet.size() && containsAll(otherSet) && otherSet.containsAll(this); + } + + @Override + public int hashCode() { + int hashCode = 0; + for (int i = 0; i < values.length; i += 2) { + hashCode += Objects.hashCode(values[i]) ^ Objects.hashCode(values[i + 1]); + } + return hashCode; + } + + @Override + public String toString() { + return "TagMapEntrySet{" + Arrays.toString(values) + '}'; + } + } + + private static final class TagMapEntrySetIterator implements Iterator> { + private final String[] values; + private int current = -2; + + TagMapEntrySetIterator(String[] values) { + this.values = values; + } + + @Override + public boolean hasNext() { + return current + 2 < values.length; + } + + @Override + public Entry next() { + if (hasNext()) { + current += 2; + return new TagEntry(values[current], values[current + 1]); + } + throw new NoSuchElementException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("immutable"); + } + } + + private static final class TagEntry implements Map.Entry { + + private final String key; + private final String value; + + TagEntry(String key, String value) { + this.key = key; + this.value = value; + } + + @Override + public String getKey() { + return key; + } + + @Override + public String getValue() { + return value; + } + + @Override + public String setValue(String _value) { + throw new UnsupportedOperationException("immutable"); + } + + @Override + public String toString() { + return '{' + key + '=' + value + '}'; + } + + @Override + public boolean equals(@Nullable Object object) { + if (this == object) { + return true; + } + if (object instanceof Map.Entry) { + Map.Entry entry = (Map.Entry) object; + return Objects.equals(entry.getKey(), key) && Objects.equals(entry.getValue(), value); + } + return false; + } + + /** Matches AbstractMap#SimpleEntry in the standard library. */ + @Override + public int hashCode() { + return Objects.hashCode(key) ^ Objects.hashCode(value); + } + } +} diff --git a/tritium-registry/src/test/java/com/palantir/tritium/metrics/registry/ExtraEntrySortedMapTest.java b/tritium-registry/src/test/java/com/palantir/tritium/metrics/registry/ExtraEntrySortedMapTest.java index aa7a31e9f..3d32e0c20 100644 --- a/tritium-registry/src/test/java/com/palantir/tritium/metrics/registry/ExtraEntrySortedMapTest.java +++ b/tritium-registry/src/test/java/com/palantir/tritium/metrics/registry/ExtraEntrySortedMapTest.java @@ -28,17 +28,23 @@ import net.jqwik.api.Assume; import net.jqwik.api.ForAll; import net.jqwik.api.Property; +import net.jqwik.api.constraints.AlphaChars; import net.jqwik.api.constraints.IntRange; import net.jqwik.api.constraints.Size; +import net.jqwik.api.constraints.StringLength; @SuppressWarnings("JdkObsolete") class ExtraEntrySortedMapTest { @Property(tries = 10_000, seed = "3619154246571270871") - void check_ExtraEntrySortedMap_has_the_same_behaviour_as_an_ImmutableSortedMap_with_an_extra_entry( - @ForAll @Size(max = 10) Map initialValues, - @ForAll Short extraKey, - @ForAll Byte extraValue, + void check_TagMap_has_the_same_behaviour_as_an_ImmutableSortedMap_with_an_extra_entry( + @ForAll @Size(max = 10) + Map< + @AlphaChars @StringLength(min = 1, max = 10) String, + @AlphaChars @StringLength(min = 1, max = 10) String> + initialValues, + @ForAll @AlphaChars @StringLength(min = 1, max = 10) String extraKey, + @ForAll @AlphaChars @StringLength(min = 1, max = 10) String extraValue, @ForAll @IntRange(max = 5) int paramKeyIndex1, @ForAll @IntRange(min = 5, max = 10) int paramKeyIndex2) { @@ -47,24 +53,24 @@ void check_ExtraEntrySortedMap_has_the_same_behaviour_as_an_ImmutableSortedMap_w Assume.that(paramKeyIndex1 < initialValues.size()); Assume.that(paramKeyIndex2 < initialValues.size()); - SortedMap base = ImmutableSortedMap.copyOf(initialValues); + SortedMap base = ImmutableSortedMap.copyOf(initialValues); - SortedMap guavaWithExtra = ImmutableSortedMap.naturalOrder() + SortedMap guavaWithExtra = ImmutableSortedMap.naturalOrder() .putAll(base) .put(extraKey, extraValue) .build(); - SortedMap extraMap = new ExtraEntrySortedMap<>(base, extraKey, extraValue); + SortedMap extraMap = TagMap.of(base).withEntry(extraKey, extraValue); assertThat(extraMap).containsExactlyInAnyOrderEntriesOf(guavaWithExtra); assertThat(extraMap).hasSameHashCodeAs(guavaWithExtra); - Short paramKey1 = Iterables.get(guavaWithExtra.keySet(), paramKeyIndex1); - Byte paramValue1 = guavaWithExtra.get(paramKey1); - Short paramKey2 = Iterables.get(guavaWithExtra.keySet(), paramKeyIndex2); + String paramKey1 = Iterables.get(guavaWithExtra.keySet(), paramKeyIndex1); + String paramValue1 = guavaWithExtra.get(paramKey1); + String paramKey2 = Iterables.get(guavaWithExtra.keySet(), paramKeyIndex2); - Map, Object>> methodCalls = - ImmutableMap., Object>>builder() + Map, Object>> methodCalls = + ImmutableMap., Object>>builder() .put("subMap", sortedMap -> sortedMap.subMap(paramKey1, paramKey2)) .put("headMap", sortedMap -> sortedMap.headMap(paramKey1)) .put("tailMap", sortedMap -> sortedMap.tailMap(paramKey1)) diff --git a/tritium-registry/src/test/java/com/palantir/tritium/metrics/registry/TagMapTest.java b/tritium-registry/src/test/java/com/palantir/tritium/metrics/registry/TagMapTest.java new file mode 100644 index 000000000..8ccf21a72 --- /dev/null +++ b/tritium-registry/src/test/java/com/palantir/tritium/metrics/registry/TagMapTest.java @@ -0,0 +1,75 @@ +/* + * (c) Copyright 2022 Palantir Technologies Inc. All rights reserved. + * + * 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 + * + * http://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 com.palantir.tritium.metrics.registry; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.google.common.collect.ImmutableSortedMap; +import com.google.common.collect.Ordering; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.Collections; +import java.util.Comparator; +import org.junit.jupiter.api.Test; + +class TagMapTest { + + @Test + void testEmpty() { + TagMap map = TagMap.of(ImmutableSortedMap.of()); + assertThat(map).isEmpty(); + assertThat(map).hasSize(0); + assertThat(map.entrySet()).isEmpty(); + assertThat(map.entrySet()).hasSize(0); + assertThat(map.entrySet().iterator()).isExhausted(); + } + + @Test + void testSingleton() { + TagMap map = TagMap.of(Collections.singletonMap("foo", "bar")); + assertThat(map).isNotEmpty(); + assertThat(map).hasSize(1); + assertThat(map).containsEntry("foo", "bar"); + assertThat(map.get("foo")).isEqualTo("bar"); + assertThat(map.entrySet()).isNotEmpty(); + assertThat(map.entrySet()).hasSize(1); + assertThat(map.entrySet().iterator()).hasNext(); + assertThat(map.entrySet().iterator().next()).isEqualTo(new SimpleImmutableEntry<>("foo", "bar")); + } + + @Test + void testExtraEntryKeyAlreadyExists() { + TagMap map = TagMap.of(Collections.singletonMap("foo", "bar")); + assertThatThrownBy(() -> map.withEntry("foo", "baz")).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void testNaturalOrder() { + assertThat(TagMap.isNaturalOrder(Ordering.natural())).isTrue(); + assertThat(TagMap.isNaturalOrder(Comparator.naturalOrder())).isTrue(); + + assertThat(TagMap.isNaturalOrder(Comparator.reverseOrder())).isFalse(); + Comparator immutableSortedMapComparator = ImmutableSortedMap.naturalOrder() + .put("a", "b") + .put("c", "d") + .build() + .comparator(); + assertThat(TagMap.isNaturalOrder(immutableSortedMapComparator)) + .as("Expected ImmutableSortedMap comparator %s to be natural", immutableSortedMapComparator) + .isTrue(); + } +}