From d7df58af76e693aa1fe897d2757e2bdb50ab9798 Mon Sep 17 00:00:00 2001 From: EvgeniiR Date: Fri, 26 Jun 2026 20:15:50 +0200 Subject: [PATCH 1/7] fix: enforce last-value-wins for same-name different-type attributes (fixes #7897) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AttributesMap previously extended HashMap, Object>, where AttributeKey.equals() includes the AttributeType. This caused attributes with the same string name but different types to coexist as separate entries, violating the OTel spec last-value-wins rule. Replace the HashMap backing with LinkedHashMap keyed by raw attribute name. Overwrites with a different type now update the existing entry in place, so size() stays correct and capacity limits are not consumed. Also eliminates the double hash-probe in put() (containsKey + get → single get). --- .../internal/AttributesMapBenchmark.java | 195 ++++++++++++++++++ .../sdk/common/internal/AttributesMap.java | 94 ++++++--- .../common/internal/AttributesMapTest.java | 36 ++++ .../sdk/trace/SdkSpanBuilderTest.java | 21 ++ 4 files changed, 321 insertions(+), 25 deletions(-) create mode 100644 sdk/common/src/jmh/java/io/opentelemetry/sdk/common/internal/AttributesMapBenchmark.java diff --git a/sdk/common/src/jmh/java/io/opentelemetry/sdk/common/internal/AttributesMapBenchmark.java b/sdk/common/src/jmh/java/io/opentelemetry/sdk/common/internal/AttributesMapBenchmark.java new file mode 100644 index 00000000000..ebf0764c1a4 --- /dev/null +++ b/sdk/common/src/jmh/java/io/opentelemetry/sdk/common/internal/AttributesMapBenchmark.java @@ -0,0 +1,195 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.common.internal; + +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import io.opentelemetry.api.common.AttributeKey; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +/** + * Microbenchmark for {@link AttributesMap}. Covers writes ({@code put}) and reads ({@code get}, + * {@code forEach}), parametrized by number of attributes. + * + *

Write scenarios: + * + *

    + *
  • {@code uniqueKeys} — normal case: each key name is distinct + *
  • {@code sameKeySameType} — repeated put with the same AttributeKey (pure overwrite) + *
  • {@code sameKeyDifferentType} — regression benchmark: same string name cycling through four + * types (pre-fix: produces N entries; post-fix: produces 1 entry) + *
  • {@code mixedUniqueAndOverwrite} — primary realistic case: unique keys followed by + * same-name overwrites with a different type, exactly {@code numAttributes} puts total + *
+ * + *

Read scenarios (run on a pre-filled map of {@code numAttributes} unique string entries): + * + *

    + *
  • {@code getHit} — lookup with the exact stored key type (hit) + *
  • {@code getTypeMiss} — lookup with a different type for the same key name (miss by type; + * behaviour changes after the fix: post-fix only one entry exists per name) + *
  • {@code forEachAll} — full iteration over all entries + *
+ */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Fork(2) +@State(Scope.Thread) +public class AttributesMapBenchmark { + + @Param({"4", "16", "128"}) + int numAttributes; + + private List> stringKeys; + private List> boolKeys; + private List> longKeys; + private List> doubleKeys; + private List values; + + // Pre-filled map used by read benchmarks — populated once in @Setup. + private AttributesMap filledMap; + + @Setup + public void setup() { + stringKeys = new ArrayList<>(numAttributes); + boolKeys = new ArrayList<>(numAttributes); + longKeys = new ArrayList<>(numAttributes); + doubleKeys = new ArrayList<>(numAttributes); + values = new ArrayList<>(numAttributes); + for (int i = 0; i < numAttributes; i++) { + stringKeys.add(stringKey("key" + i)); + boolKeys.add(booleanKey("key" + i)); + longKeys.add(longKey("key" + i)); + doubleKeys.add(doubleKey("key" + i)); + values.add("value" + i); + } + filledMap = AttributesMap.create(numAttributes, Integer.MAX_VALUE); + for (int i = 0; i < numAttributes; i++) { + filledMap.put(stringKeys.get(i), values.get(i)); + } + } + + /** Each key name is unique — the common production case. */ + @Benchmark + public AttributesMap uniqueKeys() { + AttributesMap map = AttributesMap.create(numAttributes, Integer.MAX_VALUE); + for (int i = 0; i < numAttributes; i++) { + map.put(stringKeys.get(i), values.get(i)); + } + return map; + } + + /** Same AttributeKey reused — standard same-type overwrite path. Capacity=1 since only one + * entry ever occupies the map. */ + @Benchmark + public AttributesMap sameKeySameType() { + AttributesMap map = AttributesMap.create(1, Integer.MAX_VALUE); + AttributeKey key = stringKeys.get(0); + for (int i = 0; i < numAttributes; i++) { + map.put(key, values.get(i)); + } + return map; + } + + /** + * Same string key name, cycling through string → boolean → long → double → string → ... + * + *

Regression benchmark: pre-fix produces up to 4 entries (one per type); post-fix produces 1 + * entry (last-value-wins). Capacity=4 matches the number of distinct types in the cycle. + */ + @Benchmark + public AttributesMap sameKeyDifferentType() { + AttributesMap map = AttributesMap.create(4, Integer.MAX_VALUE); + for (int i = 0; i < numAttributes; i++) { + switch (i % 4) { + case 0: + map.put(stringKeys.get(0), values.get(0)); + break; + case 1: + map.put(boolKeys.get(0), true); + break; + case 2: + map.put(longKeys.get(0), 42L); + break; + default: + map.put(doubleKeys.get(0), 3.14); + break; + } + } + return map; + } + + /** + * Realistic mixed case: first {@code numAttributes/2} puts insert unique string keys, the + * remaining puts overwrite those same keys with boolean values (different type). Always performs + * exactly {@code numAttributes} put operations total. + */ + @Benchmark + public AttributesMap mixedUniqueAndOverwrite() { + int first = numAttributes / 2; + int second = numAttributes - first; + AttributesMap map = AttributesMap.create(numAttributes, Integer.MAX_VALUE); + for (int i = 0; i < first; i++) { + map.put(stringKeys.get(i), values.get(i)); + } + for (int i = 0; i < second; i++) { + map.put(boolKeys.get(i), i % 2 == 0); + } + return map; + } + + // ---- Read benchmarks (operate on pre-filled map) ---- + + /** + * Lookup with the exact stored key type — always a hit. Measures the cost of a successful + * {@code get()} for each entry in the map. + */ + @Benchmark + public void getHit(Blackhole bh) { + for (int i = 0; i < numAttributes; i++) { + bh.consume(filledMap.get(stringKeys.get(i))); + } + } + + /** + * Lookup with a different type for the same key name — always a type-miss (returns null). + * + *

Pre-fix: map contains N string entries; boolean keys simply miss the HashMap. Post-fix: map + * contains N string entries; boolean key finds the entry but type check returns null. Isolates + * the type-check overhead introduced by the fix. + */ + @Benchmark + public void getTypeMiss(Blackhole bh) { + for (int i = 0; i < numAttributes; i++) { + bh.consume(filledMap.get(boolKeys.get(i))); + } + } + + /** Full iteration over all entries via {@code forEach}. */ + @Benchmark + public void forEachAll(Blackhole bh) { + filledMap.forEach((k, v) -> bh.consume(v)); + } +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/common/internal/AttributesMap.java b/sdk/common/src/main/java/io/opentelemetry/sdk/common/internal/AttributesMap.java index 0ddf9599752..ed9b3bb3129 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/common/internal/AttributesMap.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/common/internal/AttributesMap.java @@ -10,6 +10,7 @@ import io.opentelemetry.api.common.AttributesBuilder; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.function.BiConsumer; import javax.annotation.Nullable; @@ -18,28 +19,35 @@ * A map with a fixed capacity that drops attributes when the map gets full, and which truncates * string and array string attribute values to the {@link #lengthLimit}. * - *

WARNING: In order to reduce memory allocation, this class extends {@link HashMap} when it - * would be more appropriate to delegate. The problem with extending is that we don't enforce that - * all {@link HashMap} methods for reading / writing data conform to the configured attribute - * limits. Therefore, it's easy to accidentally call something like {@link Map#putAll(Map)} and - * bypass the restrictions (see #7135). Callers MUST - * take care to only call methods from {@link AttributesMap}, and not {@link HashMap}. + *

Keyed internally by raw string attribute name, so that attributes with the same name but + * different types are treated as the same key (last-value-wins), consistent with the OpenTelemetry + * specification. * *

This class is internal and is hence not for public use. Its APIs are unstable and can change * at any time. */ -public final class AttributesMap extends HashMap, Object> implements Attributes { - - private static final long serialVersionUID = -5072696312123632376L; +public final class AttributesMap implements Attributes { private final long capacity; private final int lengthLimit; private int totalAddedValues = 0; + private final LinkedHashMap data; + + private static final class AttributeEntry { + AttributeKey key; + Object value; + + AttributeEntry(AttributeKey key, Object value) { + this.key = key; + this.value = value; + } + } + private AttributesMap(long capacity, int lengthLimit) { this.capacity = capacity; this.lengthLimit = lengthLimit; + this.data = new LinkedHashMap<>(); } /** @@ -55,18 +63,30 @@ public static AttributesMap create(long capacity, int lengthLimit) { /** * Add the attribute key value pair, applying capacity and length limits. Callers MUST ensure the * {@code value} type matches the type required by {@code key}. + * + *

If an attribute with the same string key name already exists (regardless of type), it is + * overwritten — last-value-wins, consistent with the OTel spec. */ - @Override @Nullable public Object put(AttributeKey key, @Nullable Object value) { if (value == null) { return null; } totalAddedValues++; - if (size() >= capacity && !containsKey(key)) { + String name = key.getKey(); + AttributeEntry entry = data.get(name); + if (entry == null && data.size() >= capacity) { + return null; + } + Object limited = AttributeUtil.applyAttributeLengthLimit(value, lengthLimit); + if (entry == null) { + data.put(name, new AttributeEntry(key, limited)); return null; } - return super.put(key, AttributeUtil.applyAttributeLengthLimit(value, lengthLimit)); + Object old = entry.value; + entry.key = key; + entry.value = limited; + return old; } /** Generic overload of {@link #put(AttributeKey, Object)}. */ @@ -83,17 +103,28 @@ public int getTotalAddedValues() { @Override @Nullable public T get(AttributeKey key) { - return (T) super.get(key); + AttributeEntry entry = data.get(key.getKey()); + if (entry == null || !entry.key.getType().equals(key.getType())) { + return null; + } + return (T) entry.value; + } + + @Override + public int size() { + return data.size(); + } + + @Override + public boolean isEmpty() { + return data.isEmpty(); } @Override public Map, Object> asMap() { - // 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); + Map, Object> snapshot = new HashMap<>(data.size()); + data.values().forEach(e -> snapshot.put(e.key, e.value)); + return Collections.unmodifiableMap(snapshot); } @Override @@ -103,17 +134,30 @@ public AttributesBuilder toBuilder() { @Override public void forEach(BiConsumer, ? super Object> action) { - // https://github.com/open-telemetry/opentelemetry-java/issues/4161 - // Help out android desugaring by having an explicit call to HashMap.forEach, when forEach is - // just called through Attributes.forEach desugaring is unable to correctly handle it. - super.forEach(action); + data.forEach((name, entry) -> action.accept(entry.key, entry.value)); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Attributes)) { + return false; + } + return asMap().equals(((Attributes) o).asMap()); + } + + @Override + public int hashCode() { + return asMap().hashCode(); } @Override public String toString() { return "AttributesMap{" + "data=" - + super.toString() + + asMap() + ", capacity=" + capacity + ", totalAddedValues=" diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/common/internal/AttributesMapTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/common/internal/AttributesMapTest.java index a7a1f8ecb2e..3e343d77690 100644 --- a/sdk/common/src/test/java/io/opentelemetry/sdk/common/internal/AttributesMapTest.java +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/common/internal/AttributesMapTest.java @@ -5,7 +5,9 @@ package io.opentelemetry.sdk.common.internal; +import static io.opentelemetry.api.common.AttributeKey.booleanKey; import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; @@ -22,4 +24,38 @@ void asMap() { assertThat(attributesMap.asMap()) .containsOnly(entry(longKey("one"), 1L), entry(longKey("two"), 2L)); } + + @Test + void putSameKeyDifferentType_lastWins() { + AttributesMap map = AttributesMap.create(128, Integer.MAX_VALUE); + map.put(stringKey("k"), "hello"); + map.put(booleanKey("k"), false); + + assertThat(map.size()).isEqualTo(1); + assertThat(map.get(booleanKey("k"))).isEqualTo(false); + assertThat(map.get(stringKey("k"))).isNull(); + } + + @Test + void putSameKeyDifferentType_doesNotConsumeExtraCapacity() { + AttributesMap map = AttributesMap.create(2, Integer.MAX_VALUE); + map.put(stringKey("a"), "v1"); + map.put(booleanKey("a"), false); // overwrite — must not consume a new capacity slot + map.put(longKey("b"), 42L); + + assertThat(map.size()).isEqualTo(2); + assertThat(map.get(booleanKey("a"))).isEqualTo(false); + assertThat(map.get(longKey("b"))).isEqualTo(42L); + } + + @Test + void putSameKeyDifferentType_getByOldTypeMissAfterOverwrite() { + AttributesMap map = AttributesMap.create(128, Integer.MAX_VALUE); + map.put(stringKey("k"), "hello"); + map.put(booleanKey("k"), true); + + assertThat(map.get(stringKey("k"))).isNull(); + assertThat(map.get(booleanKey("k"))).isEqualTo(true); + } + } diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java index 2a69ade6702..b8f662e2791 100644 --- a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java @@ -1172,4 +1172,25 @@ void doNotCrash() { }) .doesNotThrowAnyException(); } + + @Test + void setAttribute_sameKeyDifferentType_lastValueWins() { + // Regression test for https://github.com/open-telemetry/opentelemetry-java/issues/7897 + // Setting the same string key with different types must overwrite, not accumulate. + SdkSpan span = + (SdkSpan) + sdkTracer + .spanBuilder("test") + .setAttribute("key", "string_value") + .setAttribute("key", false) + .startSpan(); + try { + Attributes attributes = span.toSpanData().getAttributes(); + assertThat(attributes.size()).isEqualTo(1); + assertThat(attributes.get(booleanKey("key"))).isEqualTo(false); + assertThat(attributes.get(stringKey("key"))).isNull(); + } finally { + span.end(); + } + } } From c63f8d8dacce13e652915a1bf9b385d8f54fc0b9 Mon Sep 17 00:00:00 2001 From: EvgeniiR Date: Fri, 26 Jun 2026 21:53:58 +0200 Subject: [PATCH 2/7] perf: replace LinkedHashMap with parallel-array open-addressing map in AttributesMap --- .../internal/AttributesMapBenchmark.java | 109 ++++---------- .../sdk/common/internal/AttributesMap.java | 140 ++++++++++++++---- .../common/internal/AttributesMapTest.java | 134 ++++++++++++++++- 3 files changed, 259 insertions(+), 124 deletions(-) diff --git a/sdk/common/src/jmh/java/io/opentelemetry/sdk/common/internal/AttributesMapBenchmark.java b/sdk/common/src/jmh/java/io/opentelemetry/sdk/common/internal/AttributesMapBenchmark.java index ebf0764c1a4..6bf5b9dcfe8 100644 --- a/sdk/common/src/jmh/java/io/opentelemetry/sdk/common/internal/AttributesMapBenchmark.java +++ b/sdk/common/src/jmh/java/io/opentelemetry/sdk/common/internal/AttributesMapBenchmark.java @@ -6,15 +6,12 @@ package io.opentelemetry.sdk.common.internal; import static io.opentelemetry.api.common.AttributeKey.booleanKey; -import static io.opentelemetry.api.common.AttributeKey.doubleKey; -import static io.opentelemetry.api.common.AttributeKey.longKey; import static io.opentelemetry.api.common.AttributeKey.stringKey; import io.opentelemetry.api.common.AttributeKey; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; -import org.openjdk.jmh.infra.Blackhole; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; @@ -26,28 +23,24 @@ import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; /** - * Microbenchmark for {@link AttributesMap}. Covers writes ({@code put}) and reads ({@code get}, - * {@code forEach}), parametrized by number of attributes. + * Microbenchmark for {@link AttributesMap}. Parametrized by number of attributes. * *

Write scenarios: * *

    - *
  • {@code uniqueKeys} — normal case: each key name is distinct - *
  • {@code sameKeySameType} — repeated put with the same AttributeKey (pure overwrite) - *
  • {@code sameKeyDifferentType} — regression benchmark: same string name cycling through four - * types (pre-fix: produces N entries; post-fix: produces 1 entry) - *
  • {@code mixedUniqueAndOverwrite} — primary realistic case: unique keys followed by - * same-name overwrites with a different type, exactly {@code numAttributes} puts total + *
  • {@code uniqueKeys} — normal production case: each key name is distinct + *
  • {@code putThenForEach} — combined write + export cycle: N unique puts followed by one + * {@code forEach}, modeling the dominant span lifecycle *
* *

Read scenarios (run on a pre-filled map of {@code numAttributes} unique string entries): * *

    *
  • {@code getHit} — lookup with the exact stored key type (hit) - *
  • {@code getTypeMiss} — lookup with a different type for the same key name (miss by type; - * behaviour changes after the fix: post-fix only one entry exists per name) + *
  • {@code getTypeMiss} — lookup with a different type for the same key name (type miss) *
  • {@code forEachAll} — full iteration over all entries *
*/ @@ -64,8 +57,6 @@ public class AttributesMapBenchmark { private List> stringKeys; private List> boolKeys; - private List> longKeys; - private List> doubleKeys; private List values; // Pre-filled map used by read benchmarks — populated once in @Setup. @@ -75,14 +66,10 @@ public class AttributesMapBenchmark { public void setup() { stringKeys = new ArrayList<>(numAttributes); boolKeys = new ArrayList<>(numAttributes); - longKeys = new ArrayList<>(numAttributes); - doubleKeys = new ArrayList<>(numAttributes); values = new ArrayList<>(numAttributes); for (int i = 0; i < numAttributes; i++) { stringKeys.add(stringKey("key" + i)); boolKeys.add(booleanKey("key" + i)); - longKeys.add(longKey("key" + i)); - doubleKeys.add(doubleKey("key" + i)); values.add("value" + i); } filledMap = AttributesMap.create(numAttributes, Integer.MAX_VALUE); @@ -101,70 +88,11 @@ public AttributesMap uniqueKeys() { return map; } - /** Same AttributeKey reused — standard same-type overwrite path. Capacity=1 since only one - * entry ever occupies the map. */ - @Benchmark - public AttributesMap sameKeySameType() { - AttributesMap map = AttributesMap.create(1, Integer.MAX_VALUE); - AttributeKey key = stringKeys.get(0); - for (int i = 0; i < numAttributes; i++) { - map.put(key, values.get(i)); - } - return map; - } - - /** - * Same string key name, cycling through string → boolean → long → double → string → ... - * - *

Regression benchmark: pre-fix produces up to 4 entries (one per type); post-fix produces 1 - * entry (last-value-wins). Capacity=4 matches the number of distinct types in the cycle. - */ - @Benchmark - public AttributesMap sameKeyDifferentType() { - AttributesMap map = AttributesMap.create(4, Integer.MAX_VALUE); - for (int i = 0; i < numAttributes; i++) { - switch (i % 4) { - case 0: - map.put(stringKeys.get(0), values.get(0)); - break; - case 1: - map.put(boolKeys.get(0), true); - break; - case 2: - map.put(longKeys.get(0), 42L); - break; - default: - map.put(doubleKeys.get(0), 3.14); - break; - } - } - return map; - } - - /** - * Realistic mixed case: first {@code numAttributes/2} puts insert unique string keys, the - * remaining puts overwrite those same keys with boolean values (different type). Always performs - * exactly {@code numAttributes} put operations total. - */ - @Benchmark - public AttributesMap mixedUniqueAndOverwrite() { - int first = numAttributes / 2; - int second = numAttributes - first; - AttributesMap map = AttributesMap.create(numAttributes, Integer.MAX_VALUE); - for (int i = 0; i < first; i++) { - map.put(stringKeys.get(i), values.get(i)); - } - for (int i = 0; i < second; i++) { - map.put(boolKeys.get(i), i % 2 == 0); - } - return map; - } - // ---- Read benchmarks (operate on pre-filled map) ---- /** - * Lookup with the exact stored key type — always a hit. Measures the cost of a successful - * {@code get()} for each entry in the map. + * Lookup with the exact stored key type — always a hit. Measures the cost of a successful {@code + * get()} for each entry in the map. */ @Benchmark public void getHit(Blackhole bh) { @@ -174,11 +102,10 @@ public void getHit(Blackhole bh) { } /** - * Lookup with a different type for the same key name — always a type-miss (returns null). + * Lookup with a different type for the same key name — always returns null. * - *

Pre-fix: map contains N string entries; boolean keys simply miss the HashMap. Post-fix: map - * contains N string entries; boolean key finds the entry but type check returns null. Isolates - * the type-check overhead introduced by the fix. + *

The map holds N string-typed entries; boolean keys for the same names locate each entry by + * name but fail the type check. Isolates the cost of a name-hit / type-miss lookup. */ @Benchmark public void getTypeMiss(Blackhole bh) { @@ -192,4 +119,18 @@ public void getTypeMiss(Blackhole bh) { public void forEachAll(Blackhole bh) { filledMap.forEach((k, v) -> bh.consume(v)); } + + /** + * Combined write + read cycle: fill a fresh map with N unique string keys, then iterate all + * entries once. Models the dominant production path: N puts during span building, followed by one + * forEach at export time. + */ + @Benchmark + public void putThenForEach(Blackhole bh) { + AttributesMap map = AttributesMap.create(numAttributes, Integer.MAX_VALUE); + for (int i = 0; i < numAttributes; i++) { + map.put(stringKeys.get(i), values.get(i)); + } + map.forEach((k, v) -> bh.consume(v)); + } } diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/common/internal/AttributesMap.java b/sdk/common/src/main/java/io/opentelemetry/sdk/common/internal/AttributesMap.java index ed9b3bb3129..a827428405c 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/common/internal/AttributesMap.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/common/internal/AttributesMap.java @@ -8,9 +8,9 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.Map; import java.util.function.BiConsumer; import javax.annotation.Nullable; @@ -19,35 +19,50 @@ * A map with a fixed capacity that drops attributes when the map gets full, and which truncates * string and array string attribute values to the {@link #lengthLimit}. * - *

Keyed internally by raw string attribute name, so that attributes with the same name but - * different types are treated as the same key (last-value-wins), consistent with the OpenTelemetry - * specification. + *

Keyed internally by attribute name, so that attributes with the same name but different types + * are treated as the same key (last-value-wins), consistent with the OpenTelemetry specification. + * + *

Backed by parallel arrays and an open-addressing {@code int[]} hash table (linear probing, + * load factor ≤ 0.5). Avoids per-entry object allocation; {@code forEach} is a tight sequential + * array loop with no pointer chasing. * *

This class is internal and is hence not for public use. Its APIs are unstable and can change * at any time. */ public final class AttributesMap implements Attributes { + /** + * Sentinel value meaning "slot is empty" in the hash table. Using 0 lets {@code new int[n]} (JVM + * zero-initialization) serve as the initial fill, eliminating explicit {@code Arrays.fill} calls. + * Occupied slots store {@code entryIndex + 1} so that index 0 is distinguishable from EMPTY. + */ + private static final int EMPTY = 0; + private final long capacity; private final int lengthLimit; private int totalAddedValues = 0; + private int size = 0; - private final LinkedHashMap data; + /** + * Open-addressing hash table: {@code hashTable[slot]} = index into entry arrays, or {@link + * #EMPTY}. Length is always a power of 2 and ≥ 2× the entry array length (load factor ≤ 0.5). + */ + private int[] hashTable; - private static final class AttributeEntry { - AttributeKey key; - Object value; + /** Parallel entry arrays. Index {@code i} holds the i-th inserted entry. */ + private String[] entryNames; - AttributeEntry(AttributeKey key, Object value) { - this.key = key; - this.value = value; - } - } + private AttributeKey[] entryKeys; + private Object[] entryValues; private AttributesMap(long capacity, int lengthLimit) { this.capacity = capacity; this.lengthLimit = lengthLimit; - this.data = new LinkedHashMap<>(); + int init = (int) Math.min(capacity, 16L); + entryNames = new String[init]; + entryKeys = new AttributeKey[init]; + entryValues = new Object[init]; + hashTable = new int[tableSizeFor(init)]; // JVM zero-init == EMPTY } /** @@ -74,18 +89,28 @@ public Object put(AttributeKey key, @Nullable Object value) { } totalAddedValues++; String name = key.getKey(); - AttributeEntry entry = data.get(name); - if (entry == null && data.size() >= capacity) { - return null; - } - Object limited = AttributeUtil.applyAttributeLengthLimit(value, lengthLimit); - if (entry == null) { - data.put(name, new AttributeEntry(key, limited)); + int slot = findSlot(name); + int stored = hashTable[slot]; // EMPTY or 1-based index + if (stored == EMPTY) { + if (size >= capacity) { + return null; + } + Object limited = AttributeUtil.applyAttributeLengthLimit(value, lengthLimit); + if (size == entryNames.length) { + grow(); + slot = findSlot(name); // hashTable was rebuilt by grow() + } + entryNames[size] = name; + entryKeys[size] = key; + entryValues[size] = limited; + hashTable[slot] = size + 1; // 1-based + size++; return null; } - Object old = entry.value; - entry.key = key; - entry.value = limited; + int idx = stored - 1; + Object old = entryValues[idx]; + entryKeys[idx] = key; + entryValues[idx] = AttributeUtil.applyAttributeLengthLimit(value, lengthLimit); return old; } @@ -103,27 +128,33 @@ public int getTotalAddedValues() { @Override @Nullable public T get(AttributeKey key) { - AttributeEntry entry = data.get(key.getKey()); - if (entry == null || !entry.key.getType().equals(key.getType())) { + int stored = hashTable[findSlot(key.getKey())]; + if (stored == EMPTY) { return null; } - return (T) entry.value; + int idx = stored - 1; + if (!entryKeys[idx].getType().equals(key.getType())) { + return null; + } + return (T) entryValues[idx]; } @Override public int size() { - return data.size(); + return size; } @Override public boolean isEmpty() { - return data.isEmpty(); + return size == 0; } @Override public Map, Object> asMap() { - Map, Object> snapshot = new HashMap<>(data.size()); - data.values().forEach(e -> snapshot.put(e.key, e.value)); + Map, Object> snapshot = new HashMap<>(size); + for (int i = 0; i < size; i++) { + snapshot.put(entryKeys[i], entryValues[i]); + } return Collections.unmodifiableMap(snapshot); } @@ -134,7 +165,9 @@ public AttributesBuilder toBuilder() { @Override public void forEach(BiConsumer, ? super Object> action) { - data.forEach((name, entry) -> action.accept(entry.key, entry.value)); + for (int i = 0; i < size; i++) { + action.accept(entryKeys[i], entryValues[i]); + } } @Override @@ -169,4 +202,47 @@ public String toString() { public Attributes immutableCopy() { return Attributes.builder().putAll(this).build(); } + + /** + * Returns the hash table slot that either contains the entry for {@code name} or is the first + * empty slot available for insertion. Single shared probe loop used by {@code put}, {@code get}, + * and {@code grow}. Slots store {@code entryIndex + 1}; 0 ({@link #EMPTY}) means unoccupied. + */ + private int findSlot(String name) { + int mask = hashTable.length - 1; + int slot = name.hashCode() & mask; + int stored; + while ((stored = hashTable[slot]) != EMPTY && !entryNames[stored - 1].equals(name)) { + slot = (slot + 1) & mask; + } + return slot; + } + + private void grow() { + long maxLen = Math.min(capacity, (long) Integer.MAX_VALUE - 8); + int newLen = (int) Math.min((long) entryNames.length * 2, maxLen); + entryNames = Arrays.copyOf(entryNames, newLen); + entryKeys = Arrays.copyOf(entryKeys, newLen); + entryValues = Arrays.copyOf(entryValues, newLen); + hashTable = new int[tableSizeFor(newLen)]; // JVM zero-init == EMPTY + for (int i = 0; i < size; i++) { + int slot = findSlot(entryNames[i]); + hashTable[slot] = i + 1; // 1-based + } + } + + /** + * Returns the smallest power of 2 that is ≥ 2n, guaranteeing load factor ≤ 0.5. Using {@code + * (2n-1)} instead of {@code 2n} prevents doubling the result when {@code n} is itself a power of + * 2. + */ + private static int tableSizeFor(int n) { + if (n <= 2) { + return 4; + } + if (n >= (1 << 29)) { + return 1 << 30; + } + return Integer.highestOneBit(2 * n - 1) << 1; + } } diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/common/internal/AttributesMapTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/common/internal/AttributesMapTest.java index 3e343d77690..a13c2b6d58a 100644 --- a/sdk/common/src/test/java/io/opentelemetry/sdk/common/internal/AttributesMapTest.java +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/common/internal/AttributesMapTest.java @@ -11,22 +11,43 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; +import io.opentelemetry.api.common.Attributes; +import java.util.ArrayList; +import java.util.List; import org.junit.jupiter.api.Test; class AttributesMapTest { + // ---- put ---- + @Test - void asMap() { - AttributesMap attributesMap = AttributesMap.create(2, Integer.MAX_VALUE); - attributesMap.put(longKey("one"), 1L); - attributesMap.put(longKey("two"), 2L); + void put_returnsNullForNewEntry() { + AttributesMap map = AttributesMap.create(10, Integer.MAX_VALUE); - assertThat(attributesMap.asMap()) - .containsOnly(entry(longKey("one"), 1L), entry(longKey("two"), 2L)); + assertThat(map.put(stringKey("k"), "v")).isNull(); } @Test - void putSameKeyDifferentType_lastWins() { + void put_returnsOldValueOnOverwrite() { + AttributesMap map = AttributesMap.create(10, Integer.MAX_VALUE); + map.put(stringKey("k"), "first"); + + assertThat(map.put(stringKey("k"), "second")).isEqualTo("first"); + assertThat(map.get(stringKey("k"))).isEqualTo("second"); + } + + @Test + void put_ignoresNullValue() { + AttributesMap map = AttributesMap.create(10, Integer.MAX_VALUE); + map.put(stringKey("k"), null); + + assertThat(map.size()).isEqualTo(0); + assertThat(map.isEmpty()).isTrue(); + assertThat(map.getTotalAddedValues()).isEqualTo(0); + } + + @Test + void putSameKeyDifferentType_lastValueWins() { AttributesMap map = AttributesMap.create(128, Integer.MAX_VALUE); map.put(stringKey("k"), "hello"); map.put(booleanKey("k"), false); @@ -49,7 +70,7 @@ void putSameKeyDifferentType_doesNotConsumeExtraCapacity() { } @Test - void putSameKeyDifferentType_getByOldTypeMissAfterOverwrite() { + void putSameKeyDifferentType_previousTypeGetReturnsNull() { AttributesMap map = AttributesMap.create(128, Integer.MAX_VALUE); map.put(stringKey("k"), "hello"); map.put(booleanKey("k"), true); @@ -58,4 +79,101 @@ void putSameKeyDifferentType_getByOldTypeMissAfterOverwrite() { assertThat(map.get(booleanKey("k"))).isEqualTo(true); } + // ---- get ---- + + @Test + void get_returnsNullForAbsentKey() { + AttributesMap map = AttributesMap.create(10, Integer.MAX_VALUE); + + assertThat(map.get(stringKey("absent"))).isNull(); + } + + // ---- capacity ---- + + @Test + void capacity_dropsEntriesBeyondLimit() { + AttributesMap map = AttributesMap.create(2, Integer.MAX_VALUE); + map.put(stringKey("a"), "v1"); + map.put(stringKey("b"), "v2"); + map.put(stringKey("c"), "v3"); // dropped — capacity reached + + assertThat(map.size()).isEqualTo(2); + assertThat(map.getTotalAddedValues()).isEqualTo(3); + assertThat(map.get(stringKey("c"))).isNull(); + } + + @Test + void capacity_zeroDropsAllEntries() { + AttributesMap map = AttributesMap.create(0, Integer.MAX_VALUE); + map.put(stringKey("k"), "v"); + + assertThat(map.size()).isEqualTo(0); + assertThat(map.isEmpty()).isTrue(); + } + + // ---- grow ---- + + @Test + void grow_preservesAllEntriesWhenSizeExceedsInitialArrayLength() { + // init = min(capacity, 16) = 16; grow() is triggered when the 17th entry is inserted + int n = 20; + AttributesMap map = AttributesMap.create(n, Integer.MAX_VALUE); + for (int i = 0; i < n; i++) { + map.put(stringKey("key" + i), "val" + i); + } + + assertThat(map.size()).isEqualTo(n); + for (int i = 0; i < n; i++) { + assertThat(map.get(stringKey("key" + i))).isEqualTo("val" + i); + } + } + + // ---- lengthLimit ---- + + @Test + void lengthLimit_truncatesStringValues() { + AttributesMap map = AttributesMap.create(10, 3); + map.put(stringKey("k"), "hello"); + + assertThat(map.get(stringKey("k"))).isEqualTo("hel"); + } + + // ---- forEach ---- + + @Test + void forEach_iteratesInInsertionOrder() { + AttributesMap map = AttributesMap.create(10, Integer.MAX_VALUE); + map.put(stringKey("first"), "v1"); + map.put(stringKey("second"), "v2"); + map.put(stringKey("third"), "v3"); + + List keys = new ArrayList<>(); + map.forEach((k, v) -> keys.add(k.getKey())); + + assertThat(keys).containsExactly("first", "second", "third"); + } + + // ---- views ---- + + @Test + void asMap() { + AttributesMap attributesMap = AttributesMap.create(2, Integer.MAX_VALUE); + attributesMap.put(longKey("one"), 1L); + attributesMap.put(longKey("two"), 2L); + + assertThat(attributesMap.asMap()) + .containsOnly(entry(longKey("one"), 1L), entry(longKey("two"), 2L)); + } + + @Test + void immutableCopy_containsAllEntries() { + AttributesMap map = AttributesMap.create(10, Integer.MAX_VALUE); + map.put(stringKey("a"), "v1"); + map.put(longKey("b"), 42L); + + Attributes copy = map.immutableCopy(); + + assertThat(copy.get(stringKey("a"))).isEqualTo("v1"); + assertThat(copy.get(longKey("b"))).isEqualTo(42L); + } } From 6442ca12288c7bc76b8568612808d65eb16dbcce Mon Sep 17 00:00:00 2001 From: EvgeniiR Date: Sat, 27 Jun 2026 00:26:20 +0200 Subject: [PATCH 3/7] Add test for equals() --- .../sdk/common/internal/AttributesMapTest.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/common/internal/AttributesMapTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/common/internal/AttributesMapTest.java index a13c2b6d58a..78a1f653819 100644 --- a/sdk/common/src/test/java/io/opentelemetry/sdk/common/internal/AttributesMapTest.java +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/common/internal/AttributesMapTest.java @@ -11,6 +11,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; +import com.google.common.testing.EqualsTester; import io.opentelemetry.api.common.Attributes; import java.util.ArrayList; import java.util.List; @@ -176,4 +177,19 @@ void immutableCopy_containsAllEntries() { assertThat(copy.get(stringKey("a"))).isEqualTo("v1"); assertThat(copy.get(longKey("b"))).isEqualTo(42L); } + + @Test + void equals_andHashCode() { + AttributesMap withK_v1a = AttributesMap.create(10, Integer.MAX_VALUE); + withK_v1a.put(stringKey("k"), "v1"); + AttributesMap withK_v1b = AttributesMap.create(10, Integer.MAX_VALUE); + withK_v1b.put(stringKey("k"), "v1"); + AttributesMap withK_v2 = AttributesMap.create(10, Integer.MAX_VALUE); + withK_v2.put(stringKey("k"), "v2"); + + new EqualsTester() + .addEqualityGroup(withK_v1a, withK_v1b) + .addEqualityGroup(withK_v2) + .testEquals(); + } } From 73738073b2f40b45be84d600cae7856d097d7062 Mon Sep 17 00:00:00 2001 From: EvgeniiR Date: Sat, 27 Jun 2026 01:12:40 +0200 Subject: [PATCH 4/7] Fix codestyle in test --- .../common/internal/AttributesMapTest.java | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/common/internal/AttributesMapTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/common/internal/AttributesMapTest.java index 78a1f653819..10cc7a82e5f 100644 --- a/sdk/common/src/test/java/io/opentelemetry/sdk/common/internal/AttributesMapTest.java +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/common/internal/AttributesMapTest.java @@ -180,16 +180,13 @@ void immutableCopy_containsAllEntries() { @Test void equals_andHashCode() { - AttributesMap withK_v1a = AttributesMap.create(10, Integer.MAX_VALUE); - withK_v1a.put(stringKey("k"), "v1"); - AttributesMap withK_v1b = AttributesMap.create(10, Integer.MAX_VALUE); - withK_v1b.put(stringKey("k"), "v1"); - AttributesMap withK_v2 = AttributesMap.create(10, Integer.MAX_VALUE); - withK_v2.put(stringKey("k"), "v2"); - - new EqualsTester() - .addEqualityGroup(withK_v1a, withK_v1b) - .addEqualityGroup(withK_v2) - .testEquals(); + AttributesMap mapV1a = AttributesMap.create(10, Integer.MAX_VALUE); + mapV1a.put(stringKey("k"), "v1"); + AttributesMap mapV1b = AttributesMap.create(10, Integer.MAX_VALUE); + mapV1b.put(stringKey("k"), "v1"); + AttributesMap mapV2 = AttributesMap.create(10, Integer.MAX_VALUE); + mapV2.put(stringKey("k"), "v2"); + + new EqualsTester().addEqualityGroup(mapV1a, mapV1b).addEqualityGroup(mapV2).testEquals(); } } From 1f94821b466d032e85e10f0656821734a61adbb2 Mon Sep 17 00:00:00 2001 From: EvgeniiR Date: Sat, 27 Jun 2026 12:33:29 +0200 Subject: [PATCH 5/7] Remove extra guard for int overflow - it is ok to just fail with `IndexOutOfBounds` if someone put hundreds of millions elements into AttributesMap --- .../io/opentelemetry/sdk/common/internal/AttributesMap.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/common/internal/AttributesMap.java b/sdk/common/src/main/java/io/opentelemetry/sdk/common/internal/AttributesMap.java index a827428405c..3564c26e98e 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/common/internal/AttributesMap.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/common/internal/AttributesMap.java @@ -240,9 +240,6 @@ private static int tableSizeFor(int n) { if (n <= 2) { return 4; } - if (n >= (1 << 29)) { - return 1 << 30; - } return Integer.highestOneBit(2 * n - 1) << 1; } } From 5615eac916a6c823f3be427b485e7c646f411898 Mon Sep 17 00:00:00 2001 From: EvgeniiR Date: Sat, 27 Jun 2026 13:22:10 +0200 Subject: [PATCH 6/7] A few small optimizations for AttributesMap --- .../sdk/common/internal/AttributesMapBenchmark.java | 2 +- .../sdk/common/internal/AttributesMap.java | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/sdk/common/src/jmh/java/io/opentelemetry/sdk/common/internal/AttributesMapBenchmark.java b/sdk/common/src/jmh/java/io/opentelemetry/sdk/common/internal/AttributesMapBenchmark.java index 6bf5b9dcfe8..daa00c43756 100644 --- a/sdk/common/src/jmh/java/io/opentelemetry/sdk/common/internal/AttributesMapBenchmark.java +++ b/sdk/common/src/jmh/java/io/opentelemetry/sdk/common/internal/AttributesMapBenchmark.java @@ -52,7 +52,7 @@ @State(Scope.Thread) public class AttributesMapBenchmark { - @Param({"4", "16", "128"}) + @Param({"4", "16", "20", "32", "128"}) int numAttributes; private List> stringKeys; diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/common/internal/AttributesMap.java b/sdk/common/src/main/java/io/opentelemetry/sdk/common/internal/AttributesMap.java index 3564c26e98e..1f3283f672d 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/common/internal/AttributesMap.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/common/internal/AttributesMap.java @@ -38,7 +38,7 @@ public final class AttributesMap implements Attributes { */ private static final int EMPTY = 0; - private final long capacity; + private final int capacity; private final int lengthLimit; private int totalAddedValues = 0; private int size = 0; @@ -49,6 +49,9 @@ public final class AttributesMap implements Attributes { */ private int[] hashTable; + /** Cached {@code hashTable.length - 1}; kept in sync with {@link #hashTable}. */ + private int mask; + /** Parallel entry arrays. Index {@code i} holds the i-th inserted entry. */ private String[] entryNames; @@ -56,13 +59,14 @@ public final class AttributesMap implements Attributes { private Object[] entryValues; private AttributesMap(long capacity, int lengthLimit) { - this.capacity = capacity; + this.capacity = (int) Math.min(capacity, Integer.MAX_VALUE); this.lengthLimit = lengthLimit; int init = (int) Math.min(capacity, 16L); entryNames = new String[init]; entryKeys = new AttributeKey[init]; entryValues = new Object[init]; hashTable = new int[tableSizeFor(init)]; // JVM zero-init == EMPTY + mask = hashTable.length - 1; } /** @@ -209,7 +213,6 @@ public Attributes immutableCopy() { * and {@code grow}. Slots store {@code entryIndex + 1}; 0 ({@link #EMPTY}) means unoccupied. */ private int findSlot(String name) { - int mask = hashTable.length - 1; int slot = name.hashCode() & mask; int stored; while ((stored = hashTable[slot]) != EMPTY && !entryNames[stored - 1].equals(name)) { @@ -225,6 +228,7 @@ private void grow() { entryKeys = Arrays.copyOf(entryKeys, newLen); entryValues = Arrays.copyOf(entryValues, newLen); hashTable = new int[tableSizeFor(newLen)]; // JVM zero-init == EMPTY + mask = hashTable.length - 1; for (int i = 0; i < size; i++) { int slot = findSlot(entryNames[i]); hashTable[slot] = i + 1; // 1-based From 7140ae7c7f12898529ae3400cf39e0e2ff553704 Mon Sep 17 00:00:00 2001 From: EvgeniiR Date: Tue, 7 Jul 2026 21:47:19 +0200 Subject: [PATCH 7/7] Create AttributesMap with default SpanLimits capacity in benchmark --- .../sdk/common/internal/AttributesMapBenchmark.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sdk/common/src/jmh/java/io/opentelemetry/sdk/common/internal/AttributesMapBenchmark.java b/sdk/common/src/jmh/java/io/opentelemetry/sdk/common/internal/AttributesMapBenchmark.java index daa00c43756..e0baa45e405 100644 --- a/sdk/common/src/jmh/java/io/opentelemetry/sdk/common/internal/AttributesMapBenchmark.java +++ b/sdk/common/src/jmh/java/io/opentelemetry/sdk/common/internal/AttributesMapBenchmark.java @@ -52,6 +52,9 @@ @State(Scope.Thread) public class AttributesMapBenchmark { + // Default SpanLimits attribute count limit. + private static final int CAPACITY = 128; + @Param({"4", "16", "20", "32", "128"}) int numAttributes; @@ -72,7 +75,7 @@ public void setup() { boolKeys.add(booleanKey("key" + i)); values.add("value" + i); } - filledMap = AttributesMap.create(numAttributes, Integer.MAX_VALUE); + filledMap = AttributesMap.create(CAPACITY, Integer.MAX_VALUE); for (int i = 0; i < numAttributes; i++) { filledMap.put(stringKeys.get(i), values.get(i)); } @@ -81,7 +84,7 @@ public void setup() { /** Each key name is unique — the common production case. */ @Benchmark public AttributesMap uniqueKeys() { - AttributesMap map = AttributesMap.create(numAttributes, Integer.MAX_VALUE); + AttributesMap map = AttributesMap.create(CAPACITY, Integer.MAX_VALUE); for (int i = 0; i < numAttributes; i++) { map.put(stringKeys.get(i), values.get(i)); } @@ -127,7 +130,7 @@ public void forEachAll(Blackhole bh) { */ @Benchmark public void putThenForEach(Blackhole bh) { - AttributesMap map = AttributesMap.create(numAttributes, Integer.MAX_VALUE); + AttributesMap map = AttributesMap.create(CAPACITY, Integer.MAX_VALUE); for (int i = 0; i < numAttributes; i++) { map.put(stringKeys.get(i), values.get(i)); }