Have AttributesMap extend HashMap instead of contain LinkedHashMap (#2906)
* Reduce HashMp allocations for AttributeMap. * Finish * Add comment on why wrapper is actually needed.
This commit is contained in:
		
							parent
							
								
									0b973d9a3c
								
							
						
					
					
						commit
						b393a587dd
					
				|  | @ -79,7 +79,7 @@ public interface Baggage extends ImplicitContextKeyed { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** Iterates over all the entries in this {@link Baggage}. */ |   /** Iterates over all the entries in this {@link Baggage}. */ | ||||||
|   void forEach(BiConsumer<String, BaggageEntry> consumer); |   void forEach(BiConsumer<? super String, ? super BaggageEntry> consumer); | ||||||
| 
 | 
 | ||||||
|   /** Returns a read-only view of this {@link Baggage} as a {@link Map}. */ |   /** Returns a read-only view of this {@link Baggage} as a {@link Map}. */ | ||||||
|   Map<String, BaggageEntry> asMap(); |   Map<String, BaggageEntry> asMap(); | ||||||
|  |  | ||||||
|  | @ -36,7 +36,7 @@ public interface Attributes { | ||||||
|   <T> T get(AttributeKey<T> key); |   <T> T get(AttributeKey<T> key); | ||||||
| 
 | 
 | ||||||
|   /** Iterates over all the key-value pairs of attributes contained by this instance. */ |   /** Iterates over all the key-value pairs of attributes contained by this instance. */ | ||||||
|   void forEach(BiConsumer<AttributeKey<?>, Object> consumer); |   void forEach(BiConsumer<? super AttributeKey<?>, ? super Object> consumer); | ||||||
| 
 | 
 | ||||||
|   /** The number of attributes contained in this. */ |   /** The number of attributes contained in this. */ | ||||||
|   int size(); |   int size(); | ||||||
|  |  | ||||||
|  | @ -81,7 +81,7 @@ public abstract class ImmutableKeyValuePairs<K, V> { | ||||||
| 
 | 
 | ||||||
|   /** Iterates over all the key-value pairs of labels contained by this instance. */ |   /** Iterates over all the key-value pairs of labels contained by this instance. */ | ||||||
|   @SuppressWarnings("unchecked") |   @SuppressWarnings("unchecked") | ||||||
|   public final void forEach(BiConsumer<K, V> consumer) { |   public final void forEach(BiConsumer<? super K, ? super V> consumer) { | ||||||
|     if (consumer == null) { |     if (consumer == null) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -102,7 +102,7 @@ public interface Labels { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** Iterates over all the key-value pairs of labels contained by this instance. */ |   /** Iterates over all the key-value pairs of labels contained by this instance. */ | ||||||
|   void forEach(BiConsumer<String, String> consumer); |   void forEach(BiConsumer<? super String, ? super String> consumer); | ||||||
| 
 | 
 | ||||||
|   /** The number of key-value pairs of labels in this instance. */ |   /** The number of key-value pairs of labels in this instance. */ | ||||||
|   int size(); |   int size(); | ||||||
|  |  | ||||||
|  | @ -127,20 +127,22 @@ subprojects { | ||||||
|             withType(JavaCompile::class) { |             withType(JavaCompile::class) { | ||||||
|                 options.release.set(8) |                 options.release.set(8) | ||||||
| 
 | 
 | ||||||
|                 options.compilerArgs.addAll(listOf( |                 if (name != "jmhCompileGeneratedClasses") { | ||||||
|                         "-Xlint:all", |                     options.compilerArgs.addAll(listOf( | ||||||
|                         // We suppress the "try" warning because it disallows managing an auto-closeable with |                             "-Xlint:all", | ||||||
|                         // try-with-resources without referencing the auto-closeable within the try block. |                             // We suppress the "try" warning because it disallows managing an auto-closeable with | ||||||
|                         "-Xlint:-try", |                             // try-with-resources without referencing the auto-closeable within the try block. | ||||||
|                         // We suppress the "processing" warning as suggested in |                             "-Xlint:-try", | ||||||
|                         // https://groups.google.com/forum/#!topic/bazel-discuss/_R3A9TJSoPM |                             // We suppress the "processing" warning as suggested in | ||||||
|                         "-Xlint:-processing", |                             // https://groups.google.com/forum/#!topic/bazel-discuss/_R3A9TJSoPM | ||||||
|                         // We suppress the "options" warning because it prevents compilation on modern JDKs |                             "-Xlint:-processing", | ||||||
|                         "-Xlint:-options", |                             // We suppress the "options" warning because it prevents compilation on modern JDKs | ||||||
|  |                             "-Xlint:-options", | ||||||
| 
 | 
 | ||||||
|                         // Fail build on any warning |                             // Fail build on any warning | ||||||
|                         "-Werror" |                             "-Werror" | ||||||
|                 )) |                     )) | ||||||
|  |                 } | ||||||
| 
 | 
 | ||||||
|                 options.encoding = "UTF-8" |                 options.encoding = "UTF-8" | ||||||
| 
 | 
 | ||||||
|  | @ -383,6 +385,7 @@ subprojects { | ||||||
|             // Always include the jmhreport plugin and run it after jmh task. |             // Always include the jmhreport plugin and run it after jmh task. | ||||||
|             plugins.apply("io.morethan.jmhreport") |             plugins.apply("io.morethan.jmhreport") | ||||||
|             dependencies { |             dependencies { | ||||||
|  |                 add("jmh", platform(project(":dependencyManagement"))) | ||||||
|                 add("jmh", "org.openjdk.jmh:jmh-core") |                 add("jmh", "org.openjdk.jmh:jmh-core") | ||||||
|                 add("jmh", "org.openjdk.jmh:jmh-generator-bytecode") |                 add("jmh", "org.openjdk.jmh:jmh-generator-bytecode") | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  | @ -55,7 +55,7 @@ val DEPENDENCY_SETS = listOf( | ||||||
|         ), |         ), | ||||||
|         DependencySet( |         DependencySet( | ||||||
|                 "org.openjdk.jmh", |                 "org.openjdk.jmh", | ||||||
|                 "1.26", |                 "1.27", | ||||||
|                 listOf("jmh-core", "jmh-generator-bytecode") |                 listOf("jmh-core", "jmh-generator-bytecode") | ||||||
|         ), |         ), | ||||||
|         DependencySet( |         DependencySet( | ||||||
|  |  | ||||||
|  | @ -0,0 +1,42 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright The OpenTelemetry Authors | ||||||
|  |  * SPDX-License-Identifier: Apache-2.0 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package io.opentelemetry.sdk.trace; | ||||||
|  | 
 | ||||||
|  | import io.opentelemetry.api.common.AttributeKey; | ||||||
|  | import io.opentelemetry.api.trace.SpanBuilder; | ||||||
|  | import java.util.concurrent.TimeUnit; | ||||||
|  | import org.openjdk.jmh.annotations.Benchmark; | ||||||
|  | import org.openjdk.jmh.annotations.Fork; | ||||||
|  | import org.openjdk.jmh.annotations.Measurement; | ||||||
|  | import org.openjdk.jmh.annotations.OutputTimeUnit; | ||||||
|  | import org.openjdk.jmh.annotations.Threads; | ||||||
|  | import org.openjdk.jmh.annotations.Warmup; | ||||||
|  | 
 | ||||||
|  | @Threads(value = 1) | ||||||
|  | @Fork(3) | ||||||
|  | @Warmup(iterations = 10, time = 1) | ||||||
|  | @Measurement(iterations = 20, time = 1) | ||||||
|  | @OutputTimeUnit(TimeUnit.MILLISECONDS) | ||||||
|  | public class FillSpanBenchmark { | ||||||
|  | 
 | ||||||
|  |   private static final SpanBuilder spanBuilder = | ||||||
|  |       SdkTracerProvider.builder().build().get("benchmark").spanBuilder("benchmark"); | ||||||
|  | 
 | ||||||
|  |   private static final AttributeKey<String> KEY1 = AttributeKey.stringKey("key1"); | ||||||
|  |   private static final AttributeKey<String> KEY2 = AttributeKey.stringKey("key2"); | ||||||
|  |   private static final AttributeKey<String> KEY3 = AttributeKey.stringKey("key3"); | ||||||
|  |   private static final AttributeKey<String> KEY4 = AttributeKey.stringKey("key4"); | ||||||
|  | 
 | ||||||
|  |   @Benchmark | ||||||
|  |   public void setFourAttributes() { | ||||||
|  |     spanBuilder | ||||||
|  |         .startSpan() | ||||||
|  |         .setAttribute(KEY1, "value1") | ||||||
|  |         .setAttribute(KEY2, "value2") | ||||||
|  |         .setAttribute(KEY3, "value3") | ||||||
|  |         .setAttribute(KEY4, "value4"); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -9,40 +9,32 @@ import io.opentelemetry.api.common.AttributeKey; | ||||||
| import io.opentelemetry.api.common.Attributes; | import io.opentelemetry.api.common.Attributes; | ||||||
| import io.opentelemetry.api.common.AttributesBuilder; | import io.opentelemetry.api.common.AttributesBuilder; | ||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
| import java.util.LinkedHashMap; | import java.util.HashMap; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
| import java.util.function.BiConsumer; | import java.util.function.BiConsumer; | ||||||
| 
 | 
 | ||||||
| /** | /** A map with a fixed capacity that drops attributes when the map gets full. */ | ||||||
|  * A map with a fixed capacity that drops attributes when the map gets full. |  | ||||||
|  * |  | ||||||
|  * <p>Note: this doesn't implement the Map interface, but behaves very similarly to one. |  | ||||||
|  */ |  | ||||||
| @SuppressWarnings({"rawtypes", "unchecked"}) | @SuppressWarnings({"rawtypes", "unchecked"}) | ||||||
| final class AttributesMap implements Attributes { | final class AttributesMap extends HashMap<AttributeKey<?>, Object> implements Attributes { | ||||||
|   private final Map<AttributeKey<?>, Object> data; | 
 | ||||||
|  |   private static final long serialVersionUID = -5072696312123632376L; | ||||||
| 
 | 
 | ||||||
|   private final long capacity; |   private final long capacity; | ||||||
|   private int totalAddedValues = 0; |   private int totalAddedValues = 0; | ||||||
| 
 | 
 | ||||||
|   private AttributesMap(long capacity, Map<AttributeKey<?>, Object> data) { |  | ||||||
|     this.capacity = capacity; |  | ||||||
|     this.data = data; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   AttributesMap(long capacity) { |   AttributesMap(long capacity) { | ||||||
|     this(capacity, new LinkedHashMap<>()); |     this.capacity = capacity; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public <T> void put(AttributeKey<T> key, T value) { |   <T> void put(AttributeKey<T> key, T value) { | ||||||
|     if (key == null || key.getKey() == null || value == null) { |     if (key == null || key.getKey() == null || value == null) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     totalAddedValues++; |     totalAddedValues++; | ||||||
|     if (data.size() >= capacity && !data.containsKey(key)) { |     if (size() >= capacity && !containsKey(key)) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     data.put(key, value); |     super.put(key, value); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   int getTotalAddedValues() { |   int getTotalAddedValues() { | ||||||
|  | @ -52,32 +44,27 @@ final class AttributesMap implements Attributes { | ||||||
|   @SuppressWarnings("unchecked") |   @SuppressWarnings("unchecked") | ||||||
|   @Override |   @Override | ||||||
|   public <T> T get(AttributeKey<T> key) { |   public <T> T get(AttributeKey<T> key) { | ||||||
|     return (T) data.get(key); |     return (T) super.get(key); | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   @Override |  | ||||||
|   public int size() { |  | ||||||
|     return data.size(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   @Override |  | ||||||
|   public boolean isEmpty() { |  | ||||||
|     return data.isEmpty(); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @SuppressWarnings({"rawtypes", "unchecked"}) |   @SuppressWarnings({"rawtypes", "unchecked"}) | ||||||
|   @Override |   @Override | ||||||
|   public void forEach(BiConsumer<AttributeKey<?>, Object> consumer) { |   public void forEach(BiConsumer<? super AttributeKey<?>, ? super Object> consumer) { | ||||||
|     for (Map.Entry<AttributeKey<?>, Object> entry : data.entrySet()) { |     for (Map.Entry<AttributeKey<?>, Object> entry : entrySet()) { | ||||||
|       AttributeKey key = entry.getKey(); |       AttributeKey key = entry.getKey(); | ||||||
|       Object value = entry.getValue(); |       Object value = entry.getValue(); | ||||||
|       consumer.accept(key, value); |       consumer.accept((AttributeKey<?>) key, value); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @Override |   @Override | ||||||
|   public Map<AttributeKey<?>, Object> asMap() { |   public Map<AttributeKey<?>, Object> asMap() { | ||||||
|     return Collections.unmodifiableMap(data); |     // Because Attributes is marked Immutable, IDEs may recognize this as redundant usage. However, | ||||||
|  |     // this class is private and is actually mutable, so we need to wrap with unmodifiableMap | ||||||
|  |     // anyways. We implement the immutable Attributes for this class to support the | ||||||
|  |     // Attributes.builder().putAll usage - it is tricky but an implementation detail of this private | ||||||
|  |     // class. | ||||||
|  |     return Collections.unmodifiableMap(this); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @Override |   @Override | ||||||
|  | @ -89,7 +76,7 @@ final class AttributesMap implements Attributes { | ||||||
|   public String toString() { |   public String toString() { | ||||||
|     return "AttributesMap{" |     return "AttributesMap{" | ||||||
|         + "data=" |         + "data=" | ||||||
|         + data |         + super.toString() | ||||||
|         + ", capacity=" |         + ", capacity=" | ||||||
|         + capacity |         + capacity | ||||||
|         + ", totalAddedValues=" |         + ", totalAddedValues=" | ||||||
|  | @ -98,7 +85,6 @@ final class AttributesMap implements Attributes { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   Attributes immutableCopy() { |   Attributes immutableCopy() { | ||||||
|     Map<AttributeKey<?>, Object> dataCopy = new LinkedHashMap<>(data); |     return Attributes.builder().putAll(this).build(); | ||||||
|     return new AttributesMap(capacity, Collections.unmodifiableMap(dataCopy)); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -9,29 +9,9 @@ import static io.opentelemetry.api.common.AttributeKey.longKey; | ||||||
| import static org.assertj.core.api.Assertions.assertThat; | import static org.assertj.core.api.Assertions.assertThat; | ||||||
| import static org.assertj.core.api.Assertions.entry; | import static org.assertj.core.api.Assertions.entry; | ||||||
| 
 | 
 | ||||||
| import io.opentelemetry.api.common.AttributeKey; |  | ||||||
| import io.opentelemetry.api.common.Attributes; |  | ||||||
| import java.util.Arrays; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.function.BiConsumer; |  | ||||||
| import org.junit.jupiter.api.Test; | import org.junit.jupiter.api.Test; | ||||||
| 
 | 
 | ||||||
| class AttributesMapTest { | class AttributesMapTest { | ||||||
|   @Test |  | ||||||
|   void attributesAreInOrder() { |  | ||||||
|     AttributesMap attributesMap = new AttributesMap(5); |  | ||||||
|     attributesMap.put(longKey("one"), 1L); |  | ||||||
|     attributesMap.put(longKey("three"), 3L); |  | ||||||
|     attributesMap.put(longKey("two"), 2L); |  | ||||||
|     attributesMap.put(longKey("four"), 4L); |  | ||||||
|     attributesMap.put(longKey("five"), 5L); |  | ||||||
| 
 |  | ||||||
|     List<String> expectedKeyOrder = Arrays.asList("one", "three", "two", "four", "five"); |  | ||||||
|     List<Long> expectedValueOrder = Arrays.asList(1L, 3L, 2L, 4L, 5L); |  | ||||||
| 
 |  | ||||||
|     assertOrdering(attributesMap, expectedKeyOrder, expectedValueOrder); |  | ||||||
|     assertOrdering(attributesMap.immutableCopy(), expectedKeyOrder, expectedValueOrder); |  | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   @Test |   @Test | ||||||
|   void asMap() { |   void asMap() { | ||||||
|  | @ -40,22 +20,6 @@ class AttributesMapTest { | ||||||
|     attributesMap.put(longKey("two"), 2L); |     attributesMap.put(longKey("two"), 2L); | ||||||
| 
 | 
 | ||||||
|     assertThat(attributesMap.asMap()) |     assertThat(attributesMap.asMap()) | ||||||
|         .containsExactly(entry(longKey("one"), 1L), entry(longKey("two"), 2L)); |         .containsOnly(entry(longKey("one"), 1L), entry(longKey("two"), 2L)); | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   private void assertOrdering( |  | ||||||
|       Attributes attributes, List<String> expectedKeyOrder, List<Long> expectedValueOrder) { |  | ||||||
|     attributes.forEach( |  | ||||||
|         new BiConsumer<AttributeKey<?>, Object>() { |  | ||||||
|           private int counter = 0; |  | ||||||
| 
 |  | ||||||
|           @Override |  | ||||||
|           public void accept(AttributeKey<?> key, Object value) { |  | ||||||
|             String k = key.getKey(); |  | ||||||
|             Long val = (Long) value; |  | ||||||
|             assertThat(val).isEqualTo(expectedValueOrder.get(counter)); |  | ||||||
|             assertThat(k).isEqualTo(expectedKeyOrder.get(counter++)); |  | ||||||
|           } |  | ||||||
|         }); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -875,8 +875,7 @@ class SdkSpanBuilderTest { | ||||||
|                 + "kind=INTERNAL, " |                 + "kind=INTERNAL, " | ||||||
|                 + "startEpochNanos=[0-9]+, " |                 + "startEpochNanos=[0-9]+, " | ||||||
|                 + "endEpochNanos=[0-9]+, " |                 + "endEpochNanos=[0-9]+, " | ||||||
|                 + "attributes=AttributesMap\\{data=\\{http.status_code=500, " |                 + "attributes=AttributesMap\\{data=\\{[^}]*}, capacity=128, totalAddedValues=2}, " | ||||||
|                 + "http.url=https://opentelemetry.io}, capacity=128, totalAddedValues=2}, " |  | ||||||
|                 + "totalAttributeCount=2, " |                 + "totalAttributeCount=2, " | ||||||
|                 + "events=\\[], " |                 + "events=\\[], " | ||||||
|                 + "totalRecordedEvents=0, " |                 + "totalRecordedEvents=0, " | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ pluginManagement { | ||||||
|         id("de.undercouch.download") version "4.1.1" |         id("de.undercouch.download") version "4.1.1" | ||||||
|         id("io.codearte.nexus-staging") version "0.22.0" |         id("io.codearte.nexus-staging") version "0.22.0" | ||||||
|         id("io.morethan.jmhreport") version "0.9.0" |         id("io.morethan.jmhreport") version "0.9.0" | ||||||
|         id("me.champeau.gradle.jmh") version "0.5.2" |         id("me.champeau.gradle.jmh") version "0.5.3" | ||||||
|         id("nebula.release") version "15.3.0" |         id("nebula.release") version "15.3.0" | ||||||
|         id("net.ltgt.errorprone") version "1.3.0" |         id("net.ltgt.errorprone") version "1.3.0" | ||||||
|         id("org.jetbrains.kotlin.jvm") version "1.4.21" |         id("org.jetbrains.kotlin.jvm") version "1.4.21" | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue