From 77b8ac8cb07df5d86ef59f53fc25a4f2cc269e73 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 23 Oct 2026 00:08:11 -0400 Subject: [PATCH] Make TagMap a facade over an id store and a name store TagMap keeps known tags in an IdStore (by id) and custom tags in a HashStore (by name), instead of one bucket array. A name is classified once, at the boundary, and routed to its store; known and custom tags never share a key, so read-through and precedence work per store and never cross between them. The name store is created on the first custom write; until then a map borrows its read-through parent's store read-only. Behavior is unchanged: the TagMap suites pass as they are, and Entry objects are still shared across copies. The span still holds a single TagMap; it starts owning the stores directly in the next change. TagMap.Internals stays for now, writing straight to the id store, and goes away with that change. Reads by id go straight to the id store. stream() and spliterator() yield stable Entries, and getEntryReader gives a momentary reader for typed reads. TagStoresTest covers the stores through TagMap, and IdStoreContractTest gains the view cases. Co-Authored-By: Claude Opus 5.5 --- .../forbiddenApiFilters/instrumentation.txt | 11 + .../java/datadog/trace/api/HashStore.java | 8 + .../java/datadog/trace/api/HashSupport.java | 18 - .../main/java/datadog/trace/api/IdStore.java | 5 + .../main/java/datadog/trace/api/TagMap.java | 1075 +++++++---------- .../trace/api/EntryReadingHelperTest.java | 169 --- .../trace/api/IdStoreContractTest.java | 102 ++ .../java/datadog/trace/api/TagStoresTest.java | 458 +++++++ 8 files changed, 1012 insertions(+), 834 deletions(-) delete mode 100644 internal-api/src/test/java/datadog/trace/api/EntryReadingHelperTest.java create mode 100644 internal-api/src/test/java/datadog/trace/api/TagStoresTest.java diff --git a/gradle/forbiddenApiFilters/instrumentation.txt b/gradle/forbiddenApiFilters/instrumentation.txt index 5ffb1f7868d..385cb45b0b2 100644 --- a/gradle/forbiddenApiFilters/instrumentation.txt +++ b/gradle/forbiddenApiFilters/instrumentation.txt @@ -7,3 +7,14 @@ datadog.trace.bootstrap.instrumentation.api.AgentSpan#setServiceName(java.lang.S # (see datadog.apicontract.Restricted). Instrumentation uses TagMap's checked methods. @defaultMessage TagMap.internals() is restricted to the tracer core; use TagMap's checked methods instead. datadog.trace.api.TagMap#internals() + +# A span's tag stores, how it gets them from a parent, and the span's view of them skip checks the tracer +# core has already made; they are @Restricted to the core (see datadog.apicontract.Restricted). +# Instrumentation uses TagMap's checked methods. +@defaultMessage Tag stores are restricted to the tracer core; use TagMap's checked methods instead. +datadog.trace.api.HashStore +datadog.trace.api.IdStore +datadog.trace.api.TagMap#readThroughParent(datadog.trace.api.TagMap) +datadog.trace.api.TagMap#viewOf(datadog.trace.api.IdStore,datadog.trace.api.HashStore,boolean) +datadog.trace.api.TagMap#customStore() +datadog.trace.api.TagMap#isCustomStoreBorrowed() diff --git a/internal-api/src/main/java/datadog/trace/api/HashStore.java b/internal-api/src/main/java/datadog/trace/api/HashStore.java index 3b746cb5244..1b0a38df6e1 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashStore.java @@ -38,6 +38,14 @@ public final class HashStore { */ private Set removedFromParent; + /** + * {@code parent}'s custom store, for an owner to borrow read-only until its first custom write, + * or null. + */ + public static HashStore inheritedFrom(TagMap parent) { + return parent == null ? null : parent.nameStore; + } + /** * A fresh custom store for an owner's first custom write, reading through to {@code borrowed}, * the store it borrowed until now (may be null). diff --git a/internal-api/src/main/java/datadog/trace/api/HashSupport.java b/internal-api/src/main/java/datadog/trace/api/HashSupport.java index 368244f3264..ee6749e1e4e 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashSupport.java +++ b/internal-api/src/main/java/datadog/trace/api/HashSupport.java @@ -414,24 +414,6 @@ static String toInternalString(Object[] buckets) { return ledger.toString(); } - /** Whether {@code buckets} holds no entry, checked bucket by bucket. */ - static boolean isEmpty(Object[] buckets) { - Object[] thisBuckets = buckets; - - for (int i = 0; i < thisBuckets.length; ++i) { - Object curBucket = thisBuckets[i]; - - if (curBucket instanceof Entry) { - return false; - } else if (curBucket instanceof BucketGroup) { - BucketGroup curGroup = (BucketGroup) curBucket; - if (!curGroup.isEmptyChain()) return false; - } - } - - return true; - } - /** Walks a bucket array's raw entries, tombstones included, for an iterator. */ static final class BucketWalker { private Object[] buckets; diff --git a/internal-api/src/main/java/datadog/trace/api/IdStore.java b/internal-api/src/main/java/datadog/trace/api/IdStore.java index d9e2f1bdf86..7681edef9e9 100644 --- a/internal-api/src/main/java/datadog/trace/api/IdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/IdStore.java @@ -27,6 +27,11 @@ interface Cursor { EntryReader next(); } + /** A fresh id store reading through to {@code parent}'s known tags (parent may be null). */ + public static IdStore create(TagMap parent) { + return IdStoreFactory.Default.INSTANCE.create(parent == null ? null : parent.knownStore); + } + /** Throws for an id that names no known tag. */ static IllegalArgumentException notKnown(long tagId) { return new IllegalArgumentException("not a known tag id: " + Long.toHexString(tagId)); diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index 2a476c41c4c..27873667bf0 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -9,11 +9,12 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; @@ -51,14 +52,9 @@ public final class TagMap implements Map, Iterable, SpanStorage { /** Immutable empty TagMap - similar to {@link Collections#emptyMap()} */ - // Frozen view over a length-1 array: bucket masking needs a power-of-two array length (size 0 - // would fail with ArrayIndexOutOfBoundsException, size 1 works), and the private constructor - // reads no statics, so this is safe to build directly during TagMap's . - public static final TagMap EMPTY = new TagMap(new Object[1], 0); - - // Sentinel for an ID not yet resolved by the reused EntryReadingHelper. Zero denotes an unknown - // tag, so it cannot represent "not computed". - static final long TAG_ID_NOT_COMPUTED = Long.MIN_VALUE; + // Built from the JVM's chosen store's createEmpty(); IdStoreFactory explains why that is safe + // during TagMap's , whichever class initializes first. + public static final TagMap EMPTY = new TagMap(IdStoreFactory.Default.INSTANCE.createEmpty()); /** Creates a new mutable TagMap that contains the contents of map */ public static final TagMap fromMap(@Nonnull Map map) { @@ -99,19 +95,7 @@ public static final TagMap create(int size) { * local miss/removal for no benefit. */ public static final TagMap createFromParent(TagMap parent) { - if (parent != null) { - if (!parent.frozen) { - throw new IllegalStateException("read-through parent must be frozen"); - } - // Exact, not isDefinitelyEmpty(): a multi-level parent can be observationally empty while a - // local level still holds entries (all of them tombstoned by a nearer level). Dropping such a - // parent keeps isEmpty()'s no-tombstone fast path valid -- an attached parent always - // contributes at least one visible entry. - if (parent.isEmpty()) { - parent = null; - } - } - return new TagMap(parent); + return new TagMap(readThroughParent(parent)); } /** Creates a new TagMap.Ledger */ @@ -1228,49 +1212,38 @@ public EntryChange next() { } /* - * For memory efficiency, TagMap uses a rather complicated bucket system. - *

- * When there is only a single Entry in a particular bucket, the Entry is stored into the bucket directly. - *

- * Because the Entry objects can be shared between multiple TagMaps, the Entry objects cannot - * directly form a linked list to handle collisions. - *

- * Instead when multiple entries collide in the same bucket, a BucketGroup is formed to hold multiple entries. - * But a BucketGroup is only formed when a collision occurs to keep allocation low in the common case of no collisions. - *

- * For efficiency, BucketGroups are a fixed size, so when a BucketGroup fills up another BucketGroup is formed - * to hold the additional Entry-s. And the BucketGroup-s are connected via a linked list instead of the Entry-s. - *

- * This does introduce some inefficiencies when Entry-s are removed. - * The assumption is that removals are rare, so BucketGroups are never consolidated. - * However as a precaution if a BucketGroup becomes completely empty, then that BucketGroup will be - * removed from the collision chain. + * A TagMap holds its tags in two stores: an IdStore with the known tags by id, and a HashStore + * with the custom tags by name. Their key spaces are disjoint, + * so each store reads through to its own parent chain and keeps its own tombstones, and + * precedence never crosses stores. + * + * The custom store is created on the first custom write. Until then, a map with a parent borrows + * the parent's custom store, read-only (nameStoreBorrowed): the parent is frozen, so its store is + * stable, and with no local custom state the borrowed store reads exactly as an own one would. */ - final Object[] buckets; - private int size; - private boolean frozen; + /** The known tags, by id. Not final only so {@link #clear()} can replace it. */ + IdStore knownStore; /** - * Optional frozen parent for read-through. When non-null, reads that miss the local buckets fall - * through to the parent chain, nearest-level-wins (a local entry shadows the parent's, a nearer - * ancestor shadows a farther one). Parents may themselves have parents, so this is a chain (e.g. - * baggage layered over trace tags). Must be frozen when attached, so it is safely shareable. Not - * final only so {@link #clear()} can detach it (to null); it is otherwise fixed at construction - * and never re-pointed. Package-visible so same-package tests can assert attach/detach directly. + * The custom tags, by name: null until the first custom write, or the parent's custom store, + * borrowed read-only while {@link #nameStoreBorrowed}. */ - @VisibleForTesting TagMap parent; + HashStore nameStore; + + /** True while {@link #nameStore} is the parent's custom store, borrowed until the first write. */ + boolean nameStoreBorrowed; + + private boolean frozen; /** - * Parent keys removed locally (read-through tombstones). Lazily allocated on the first such - * removal; {@code null} both means "no tombstones" and serves as the gate that keeps the hot - * paths untouched. Only meaningful when {@link #parent} != null. A tombstone stops read-through - * fall-through for its key, so a key removed from a child no longer reads through to the parent. - * Kept off the bucket structure deliberately — it is shape-agnostic (bare-Entry vs BucketGroup) - * and rare, so it costs a lazy allocation on removal rather than complicating the hot bucket - * code. + * Optional frozen parent for read-through. Reads that miss locally fall through to the parent + * chain (each store reads through to the parent's matching store). Must be frozen when attached, + * so it is safely shareable. Not final only so {@link #clear()} can detach it (to null); it is + * otherwise fixed at construction and never re-pointed. Package-visible so same-package tests can + * assert attach/detach directly. */ - private Set removedFromParent; + @VisibleForTesting TagMap parent; public TagMap() { this((TagMap) null); @@ -1282,92 +1255,149 @@ public TagMap() { * optimizations can treat it as fixed. */ private TagMap(TagMap parent) { - this.buckets = HashSupport.newBuckets(); - this.size = 0; + this.knownStore = IdStore.create(parent); + this.nameStore = HashStore.inheritedFrom(parent); + this.nameStoreBorrowed = this.nameStore != null; + this.frozen = false; + this.parent = parent; + } + + /** A map over {@code knownStore} and {@code nameStore}: a copy ({@link #copy()}) or a view. */ + private TagMap( + TagMap parent, IdStore knownStore, HashStore nameStore, boolean nameStoreBorrowed) { + this.knownStore = knownStore; + this.nameStore = nameStore; + this.nameStoreBorrowed = nameStoreBorrowed; this.frozen = false; this.parent = parent; } - /** Used for inexpensive immutable */ - private TagMap(Object[] buckets, int size) { - this.buckets = buckets; - this.size = size; + /** A frozen map over the empty parentless store {@code knownStore}: {@link #EMPTY}. */ + private TagMap(IdStore knownStore) { + this.knownStore = knownStore; + this.nameStore = null; + this.nameStoreBorrowed = false; this.frozen = true; this.parent = null; } - @Override - public int size() { - // Exact (Map contract). Under read-through resolves the union; prefer estimateSize() for hints. - TagMap parent = this.parent; - return parent == null ? this.size : this.size + this.visibleParentCount(); + /** + * A live view over a span's stores, for the span's internal readers (tag post-processors, + * serializers) and its snapshot copy; never on the span's own set/get paths. Not thread-safe: use + * it under the span's tag lock. + * + *

The span owns the stores, and the view keeps the ones it was created over. A span that later + * creates its own custom store drops its view and creates a new one. After using the view, the + * span adopts any custom store the view created ({@link #customStore}), since a reader may write + * a custom tag. + */ + @Restricted(allowedIn = {"datadog.trace.core"}) + public static TagMap viewOf(IdStore knownStore, HashStore nameStore, boolean nameStoreBorrowed) { + return new TagMap(null, knownStore, nameStore, nameStoreBorrowed); } /** - * Exact count of ancestor entries not shadowed by a nearer level or tombstoned (the read-through - * addition). Walks the ancestor chain iteratively, nearest-level-wins. + * The parent an owner reads through to: {@code parent}, or null when it is null or empty, as + * {@link #createFromParent} decides. + * + * @throws IllegalStateException if {@code parent} is not frozen */ - private int visibleParentCount() { - int count = 0; - for (TagMap ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { - Object[] parentBuckets = ancestor.buckets; - for (int i = 0; i < parentBuckets.length; ++i) { - Object parentBucket = parentBuckets[i]; - if (parentBucket instanceof Entry) { - if (parentEntryVisible((Entry) parentBucket, ancestor)) count++; - } else if (parentBucket instanceof BucketGroup) { - for (BucketGroup curGroup = (BucketGroup) parentBucket; - curGroup != null; - curGroup = curGroup.prev) { - for (int j = 0; j < BucketGroup.LEN; ++j) { - Entry parentEntry = curGroup._entryAt(j); - if (parentEntry != null && parentEntryVisible(parentEntry, ancestor)) count++; - } - } - } + @Restricted(allowedIn = {"datadog.trace.core"}) + public static TagMap readThroughParent(TagMap parent) { + if (parent == null) { + return null; + } + if (!parent.isFrozen()) { + throw new IllegalStateException("read-through parent must be frozen"); + } + // Exact, not isDefinitelyEmpty(): a multi-level parent can be observationally empty while a + // local level still holds entries (all of them tombstoned by a nearer level). Dropping such a + // parent keeps isEmpty()'s no-tombstone fast path valid -- an attached parent always + // contributes at least one visible entry. + return parent.isEmpty() ? null : parent; + } + + /** + * The Entry an id store's {@code reader} denotes, or null, for a caller that keeps it or hands it + * out. Free for a hash store's reader, which is an Entry; for a dense store's reused flyweight, + * this builds one. + */ + private static Entry entryOf(EntryReader reader) { + return reader == null ? null : reader.entry(); + } + + /** This map's custom store: null, borrowed ({@link #isCustomStoreBorrowed}), or its own. */ + @Restricted(allowedIn = {"datadog.trace.core"}) + public HashStore customStore() { + return this.nameStore; + } + + /** Whether {@link #customStore} is the parent's, borrowed read-only. */ + @Restricted(allowedIn = {"datadog.trace.core"}) + public boolean isCustomStoreBorrowed() { + return this.nameStoreBorrowed; + } + + /** The custom store to write to: created on the first write, ending any borrowing. */ + HashStore nameStoreForWrite() { + HashStore nameStore = this.nameStore; + if (nameStore == null || this.nameStoreBorrowed) { + // A borrowed store is the nearest ancestor's custom store: read through to it. + nameStore = HashStore.create(nameStore); + this.nameStore = nameStore; + this.nameStoreBorrowed = false; + } + return nameStore; + } + + /** Removes the custom tag {@code tag}, returning the prior visible entry. */ + Entry removeCustom(String tag) { + HashStore nameStore = this.nameStore; + if (nameStore == null) { + return null; + } + if (this.nameStoreBorrowed) { + if (nameStore.get(tag) == null) { + return null; // nothing to hide: keep borrowing } + nameStore = this.nameStoreForWrite(); // hiding an inherited tag needs a tombstone of our own } - return count; + return nameStore.remove(tag); + } + + /** Whether either store reads through to a parent. */ + private boolean readsThrough() { + HashStore nameStore = this.nameStore; + return this.knownStore.hasParent() + || this.nameStoreBorrowed + || (nameStore != null && nameStore.parent != null); + } + + @Override + public int size() { + // Exact (Map contract). Under read-through resolves the union; prefer estimateSize() for hints. + HashStore nameStore = this.nameStore; + return this.knownStore.size() + (nameStore == null ? 0 : nameStore.size()); } @Override public boolean isEmpty() { // Exact (Map contract). Under read-through resolves the parent; prefer isDefinitelyEmpty(). - if (this.size != 0) { - return false; - } - TagMap parent = this.parent; - if (parent == null) { - return true; - } - if (this.removedFromParent == null) { - // A live parent is never definitely-empty: createFromParent drops an empty parent (and copy() - // only forwards an already-vetted parent), and the parent is frozen so it can't become empty. - // So with no local entries and no tombstones, the parent contributes at least one visible - // entry -> this map is non-empty. - return false; - } - // size == 0 with tombstones (rare): empty iff every visible ancestor entry is tombstoned - return this.visibleParentCount() == 0; + HashStore nameStore = this.nameStore; + return this.knownStore.isEmpty() && (nameStore == null || nameStore.isEmpty()); } public boolean isDefinitelyEmpty() { // Cheap: empty iff no level in the chain holds a local entry (ignores shadowing/tombstones). - for (TagMap level = this; level != null; level = level.parent) { - if (level.size != 0) { - return false; - } - } - return true; + HashStore nameStore = this.nameStore; + return this.knownStore.isDefinitelyEmpty() + && (nameStore == null || nameStore.isDefinitelyEmpty()); } public int estimateSize() { // Upper bound: sum of every level's local size, ignoring read-through shadowing/removals. - int total = 0; - for (TagMap level = this; level != null; level = level.parent) { - total += level.size; - } - return total; + HashStore nameStore = this.nameStore; + return this.knownStore.estimateSize() + (nameStore == null ? 0 : nameStore.estimateSize()); } @Deprecated @@ -1378,15 +1408,31 @@ public Object get(Object tag) { return this.getObject((String) tag); } - /** Provides the corresponding entry value as an Object - boxing if necessary */ + /** + * Provides the corresponding entry value as an Object - boxing if necessary. A known tag is read + * straight from its store, so a store without an Entry per tag builds none. + */ public Object getObject(String tag) { - Entry entry = this.getEntry(tag); + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { + return this.knownStore.getValue(key); + } + HashStore nameStore = this.nameStore; + Entry entry = nameStore == null ? null : nameStore.get(tag); return entry == null ? null : entry.objectValue(); } - /** Provides the corresponding entry value as a String - calling toString if necessary */ + /** + * Provides the corresponding entry value as a String - calling toString if necessary. Like {@link + * #getObject}, a known tag is read straight from its store. + */ public String getString(String tag) { - Entry entry = this.getEntry(tag); + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { + return this.knownStore.getString(key); + } + HashStore nameStore = this.nameStore; + Entry entry = nameStore == null ? null : nameStore.get(tag); return entry == null ? null : entry.stringValue(); } @@ -1417,8 +1463,8 @@ public boolean getBoolean(String tag) { } public boolean getBooleanOrDefault(String tag, boolean defaultValue) { - Entry entry = this.getEntry(tag); - return entry == null ? defaultValue : entry.booleanValue(); + EntryReader reader = this.getEntryReader(tag); + return reader == null ? defaultValue : reader.booleanValue(); } public int getInt(String tag) { @@ -1426,8 +1472,8 @@ public int getInt(String tag) { } public int getIntOrDefault(String tag, int defaultValue) { - Entry entry = this.getEntry(tag); - return entry == null ? defaultValue : entry.intValue(); + EntryReader reader = this.getEntryReader(tag); + return reader == null ? defaultValue : reader.intValue(); } public long getLong(String tag) { @@ -1435,8 +1481,8 @@ public long getLong(String tag) { } public long getLongOrDefault(String tag, long defaultValue) { - Entry entry = this.getEntry(tag); - return entry == null ? defaultValue : entry.longValue(); + EntryReader reader = this.getEntryReader(tag); + return reader == null ? defaultValue : reader.longValue(); } public float getFloat(String tag) { @@ -1444,8 +1490,8 @@ public float getFloat(String tag) { } public float getFloatOrDefault(String tag, float defaultValue) { - Entry entry = this.getEntry(tag); - return entry == null ? defaultValue : entry.floatValue(); + EntryReader reader = this.getEntryReader(tag); + return reader == null ? defaultValue : reader.floatValue(); } public double getDouble(String tag) { @@ -1453,15 +1499,15 @@ public double getDouble(String tag) { } public double getDoubleOrDefault(String tag, double defaultValue) { - Entry entry = this.getEntry(tag); - return entry == null ? defaultValue : entry.doubleValue(); + EntryReader reader = this.getEntryReader(tag); + return reader == null ? defaultValue : reader.doubleValue(); } @Override public boolean containsKey(Object key) { if (!(key instanceof String)) return false; - return (this.getEntry((String) key) != null); + return (this.getEntryReader((String) key) != null); } @Override @@ -1498,69 +1544,37 @@ public Set> entrySet() { public Entry getEntry(String tag) { // Entries are stored under their canonical Datadog name (see Entry's constructor); a lookup by - // an OpenTelemetry rename must canonicalize the same way, or it would hash to the wrong bucket - // and silently miss the entry stored under the Datadog name. - long tagHash = Entry.tagHashOf(tag); - String canonicalTag = (tagHash >>> 32) != 0 ? KnownTagCodec.nameOf(tagHash) : tag; - return this.getEntry(canonicalTag, tagHash); + // an OpenTelemetry rename must resolve to the same id, or it would silently miss the entry. + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { + return this.knownStore.getEntry(key); + } + HashStore nameStore = this.nameStore; + return nameStore == null ? null : nameStore.get(tag); } /** The entry for a known tag id, found by the id itself rather than by resolving a name. */ public Entry getEntry(long tagId) { - return this.getEntry(Entry.requireKnownName(tagId), tagId); - } - - private Entry getEntry(String canonicalTag, long tagHash) { - Entry local = this.getLocalEntry(canonicalTag, tagHash); - if (local != null) { - // Local entry shadows the parent (local-wins) — unchanged hot path. - return local; - } - // Read-through: miss locally, defer to the frozen parent. Single-parent in phase 1. - // The tombstone check lives only here, on the cold miss+parent path — the hot local hit above - // never touches it. - TagMap parent = this.parent; - if (parent == null) { - return null; - } - if (this.removedFromParent != null && this.removedFromParent.contains(canonicalTag)) { - return null; // tombstoned: removed locally, do not read through - } - return parent.getEntry(canonicalTag, tagHash); - } - - /** Looks up an entry in this map's own buckets only — no read-through to the parent. */ - private Entry getLocalEntry(String tag, long tagHash) { - return HashSupport.find(this.buckets, Entry.bucketHash(tagHash), tag); + return this.knownStore.getEntry(tagId); } /** - * Whether an entry that lives at ancestor level {@code fromAncestor} is visible through this - * leaf: not shadowed and not tombstoned by any nearer level (the leaf, or a closer ancestor). - * Nearest-level-wins. This mirrors what {@link #getEntry(String)}'s recursion applies as it - * descends -- each nearer level contributes both its own local entries (shadowing) and its own - * read-through removals (tombstones). A non-leaf level can carry tombstones too: a map may remove - * an inherited key and then itself be frozen and reused as a parent. Exploits universal hashing — - * by {@code _hash} the only local entry that could shadow {@code parentEntry} at a level is in - * that level's same-index bucket, so each nearer level is probed at one bucket using {@code - * parentEntry}'s cached hash (no re-hash, no full-map probe). + * Like {@link #getEntry(String)}, but answers with a momentary {@link EntryReader}, so a store + * without an Entry per tag builds none. The reader MUST NOT be kept (see {@link EntryReader}); + * call {@link EntryReader#entry()} to keep one. Safe on a frozen map that several threads read. */ - boolean parentEntryVisible(Entry parentEntry, TagMap fromAncestor) { - int hash = parentEntry.hash(); - String tag = parentEntry.tag; - for (TagMap nearer = this; nearer != fromAncestor; nearer = nearer.parent) { - // tombstoned by a nearer level (its own read-through removal hides the deeper entry) - if (nearer.removedFromParent != null && nearer.removedFromParent.contains(tag)) { - return false; - } - // shadowed by a nearer level's local entry - Object[] nearerBuckets = nearer.buckets; - Object nearerBucket = nearerBuckets[hash & (nearerBuckets.length - 1)]; - if (HashSupport.findInBucket(nearerBucket, hash, tag) != null) { - return false; - } + public EntryReader getEntryReader(String tag) { + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { + return this.knownStore.getEntryReader(key); } - return true; + HashStore nameStore = this.nameStore; + return nameStore == null ? null : nameStore.get(tag); + } + + /** {@link #getEntryReader(String)} for a known tag id. */ + public EntryReader getEntryReader(long tagId) { + return this.knownStore.getEntryReader(tagId); } @Deprecated @@ -1588,38 +1602,38 @@ public void set(@Nullable TagMap.EntryReader newEntryReader) { * from its id or a custom tag from its name. (Entry's own name factories are custom-only.) */ static Entry anyEntryFor(String tag, Object value) { - long tagId = KnownTagCodec.keyOf(tag); - return tagId != 0 ? Entry.newAnyEntry(tagId, value) : Entry.newAnyEntry(tag, value); + long key = KnownTagCodec.keyOf(tag); + return key != 0 ? Entry.newAnyEntry(key, value) : Entry.newAnyEntry(tag, value); } static Entry objectEntryFor(String tag, CharSequence value) { - long tagId = KnownTagCodec.keyOf(tag); - return tagId != 0 ? Entry.newObjectEntry(tagId, value) : Entry.newObjectEntry(tag, value); + long key = KnownTagCodec.keyOf(tag); + return key != 0 ? Entry.newObjectEntry(key, value) : Entry.newObjectEntry(tag, value); } static Entry booleanEntryFor(String tag, boolean value) { - long tagId = KnownTagCodec.keyOf(tag); - return tagId != 0 ? Entry.newBooleanEntry(tagId, value) : Entry.newBooleanEntry(tag, value); + long key = KnownTagCodec.keyOf(tag); + return key != 0 ? Entry.newBooleanEntry(key, value) : Entry.newBooleanEntry(tag, value); } static Entry intEntryFor(String tag, int value) { - long tagId = KnownTagCodec.keyOf(tag); - return tagId != 0 ? Entry.newIntEntry(tagId, value) : Entry.newIntEntry(tag, value); + long key = KnownTagCodec.keyOf(tag); + return key != 0 ? Entry.newIntEntry(key, value) : Entry.newIntEntry(tag, value); } static Entry longEntryFor(String tag, long value) { - long tagId = KnownTagCodec.keyOf(tag); - return tagId != 0 ? Entry.newLongEntry(tagId, value) : Entry.newLongEntry(tag, value); + long key = KnownTagCodec.keyOf(tag); + return key != 0 ? Entry.newLongEntry(key, value) : Entry.newLongEntry(tag, value); } static Entry floatEntryFor(String tag, float value) { - long tagId = KnownTagCodec.keyOf(tag); - return tagId != 0 ? Entry.newFloatEntry(tagId, value) : Entry.newFloatEntry(tag, value); + long key = KnownTagCodec.keyOf(tag); + return key != 0 ? Entry.newFloatEntry(key, value) : Entry.newFloatEntry(tag, value); } static Entry doubleEntryFor(String tag, double value) { - long tagId = KnownTagCodec.keyOf(tag); - return tagId != 0 ? Entry.newDoubleEntry(tagId, value) : Entry.newDoubleEntry(tag, value); + long key = KnownTagCodec.keyOf(tag); + return key != 0 ? Entry.newDoubleEntry(key, value) : Entry.newDoubleEntry(tag, value); } /** A null value removes the tag, as {@code setTag(tag, null)} does on a span. */ @@ -1628,11 +1642,11 @@ public void set(@Nonnull String tag, @Nullable Object value) { this.getAndRemove(tag); return; } - long tagId = KnownTagCodec.keyOf(tag); - if (tagId != 0) { - this.set(tagId, value); + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { + this.putKnown(Entry.newAnyEntry(key, value)); } else { - this.putEntry(Entry.newAnyEntry(tag, value)); + this.putCustom(Entry.newAnyEntry(tag, value)); } } @@ -1642,56 +1656,56 @@ public void set(@Nonnull String tag, @Nullable CharSequence value) { this.getAndRemove(tag); return; } - long tagId = KnownTagCodec.keyOf(tag); - if (tagId != 0) { - this.set(tagId, value); + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { + this.putKnown(Entry.newObjectEntry(key, value)); } else { - this.putEntry(Entry.newObjectEntry(tag, value)); + this.putCustom(Entry.newObjectEntry(tag, value)); } } public void set(@Nonnull String tag, boolean value) { - long tagId = KnownTagCodec.keyOf(tag); - if (tagId != 0) { - this.set(tagId, value); + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { + this.putKnown(Entry.newBooleanEntry(key, value)); } else { - this.putEntry(Entry.newBooleanEntry(tag, value)); + this.putCustom(Entry.newBooleanEntry(tag, value)); } } public void set(@Nonnull String tag, int value) { - long tagId = KnownTagCodec.keyOf(tag); - if (tagId != 0) { - this.set(tagId, value); + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { + this.putKnown(Entry.newIntEntry(key, value)); } else { - this.putEntry(Entry.newIntEntry(tag, value)); + this.putCustom(Entry.newIntEntry(tag, value)); } } public void set(@Nonnull String tag, long value) { - long tagId = KnownTagCodec.keyOf(tag); - if (tagId != 0) { - this.set(tagId, value); + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { + this.putKnown(Entry.newLongEntry(key, value)); } else { - this.putEntry(Entry.newLongEntry(tag, value)); + this.putCustom(Entry.newLongEntry(tag, value)); } } public void set(@Nonnull String tag, float value) { - long tagId = KnownTagCodec.keyOf(tag); - if (tagId != 0) { - this.set(tagId, value); + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { + this.putKnown(Entry.newFloatEntry(key, value)); } else { - this.putEntry(Entry.newFloatEntry(tag, value)); + this.putCustom(Entry.newFloatEntry(tag, value)); } } public void set(@Nonnull String tag, double value) { - long tagId = KnownTagCodec.keyOf(tag); - if (tagId != 0) { - this.set(tagId, value); + long key = KnownTagCodec.keyOf(tag); + if (key != 0) { + this.putKnown(Entry.newDoubleEntry(key, value)); } else { - this.putEntry(Entry.newDoubleEntry(tag, value)); + this.putCustom(Entry.newDoubleEntry(tag, value)); } } @@ -1700,9 +1714,11 @@ public void set(@Nonnull String tag, double value) { * canonical name without the name lookup the String setters pay. The id must name a known tag; * custom tags have no id and use the String setters. */ + /** * The tracer core's trusted operations on this map, which skip checks the core has already made. - * Restricted to the core; instrumentation uses the checked methods. + * Restricted to the core; instrumentation uses the checked methods. Goes away once the span owns + * its stores and writes to them directly. */ @Restricted(allowedIn = {"datadog.trace.core"}) public Internals internals() { @@ -1726,33 +1742,33 @@ public static final class Internals { * already checked KnownTagCodec.isKnown(tagId). */ public void setKnown(long tagId, @Nonnull Object value) { - this.map.putEntry(new Entry(tagId, null, Entry.ANY, 0L, value)); + this.map.putKnown(new Entry(tagId, null, Entry.ANY, 0L, value)); } public void setKnown(long tagId, @Nonnull CharSequence value) { - this.map.putEntry(new Entry(tagId, null, EntryReader.OBJECT, 0L, value)); + this.map.putKnown(new Entry(tagId, null, EntryReader.OBJECT, 0L, value)); } public void setKnown(long tagId, boolean value) { - this.map.putEntry( + this.map.putKnown( new Entry( tagId, null, EntryReader.BOOLEAN, Entry.boolean2Prim(value), Boolean.valueOf(value))); } public void setKnown(long tagId, int value) { - this.map.putEntry(new Entry(tagId, null, EntryReader.INT, Entry.int2Prim(value), null)); + this.map.putKnown(new Entry(tagId, null, EntryReader.INT, Entry.int2Prim(value), null)); } public void setKnown(long tagId, long value) { - this.map.putEntry(new Entry(tagId, null, EntryReader.LONG, Entry.long2Prim(value), null)); + this.map.putKnown(new Entry(tagId, null, EntryReader.LONG, Entry.long2Prim(value), null)); } public void setKnown(long tagId, float value) { - this.map.putEntry(new Entry(tagId, null, EntryReader.FLOAT, Entry.float2Prim(value), null)); + this.map.putKnown(new Entry(tagId, null, EntryReader.FLOAT, Entry.float2Prim(value), null)); } public void setKnown(long tagId, double value) { - this.map.putEntry(new Entry(tagId, null, EntryReader.DOUBLE, Entry.double2Prim(value), null)); + this.map.putKnown(new Entry(tagId, null, EntryReader.DOUBLE, Entry.double2Prim(value), null)); } } @@ -1762,7 +1778,7 @@ public void set(long tagId, @Nullable Object value) { this.getAndRemove(tagId); return; } - this.putEntry(Entry.newAnyEntry(tagId, value)); + this.putKnown(Entry.newAnyEntry(tagId, value)); } /** A null value removes the tag, as {@code setTag(tag, null)} does on a span. */ @@ -1771,27 +1787,27 @@ public void set(long tagId, @Nullable CharSequence value) { this.getAndRemove(tagId); return; } - this.putEntry(Entry.newObjectEntry(tagId, value)); + this.putKnown(Entry.newObjectEntry(tagId, value)); } public void set(long tagId, boolean value) { - this.putEntry(Entry.newBooleanEntry(tagId, value)); + this.putKnown(Entry.newBooleanEntry(tagId, value)); } public void set(long tagId, int value) { - this.putEntry(Entry.newIntEntry(tagId, value)); + this.putKnown(Entry.newIntEntry(tagId, value)); } public void set(long tagId, long value) { - this.putEntry(Entry.newLongEntry(tagId, value)); + this.putKnown(Entry.newLongEntry(tagId, value)); } public void set(long tagId, float value) { - this.putEntry(Entry.newFloatEntry(tagId, value)); + this.putKnown(Entry.newFloatEntry(tagId, value)); } public void set(long tagId, double value) { - this.putEntry(Entry.newDoubleEntry(tagId, value)); + this.putKnown(Entry.newDoubleEntry(tagId, value)); } /** @@ -1804,22 +1820,11 @@ public Entry getAndSet(@Nullable Entry newEntry) { if (newEntry == null) { return null; } - // Capture whether the key was tombstoned BEFORE putEntry clears it: a tombstoned key had no - // visible prior value (it was removed), so getAndSet must report null rather than the parent's. - boolean wasTombstoned = - this.removedFromParent != null && this.removedFromParent.contains(newEntry.tag); + this.checkWriteAccess(); - Entry priorLocal = this.putEntry(newEntry); - if (priorLocal != null) { - return priorLocal; // replaced a local entry -> that is the prior value - } - // No local entry was replaced. The prior visible value, if any, was the parent's -- unless the - // key was tombstoned (then it was not visible). set(...) skips this via putEntry (no prior). - if (wasTombstoned || this.parent == null) { - return null; - } - // By the entry's own identity, not its name: the name would only resolve back to the same hash. - return this.parent.getEntry(newEntry.tag, newEntry.tagHash); + return newEntry.tagId() != 0 + ? entryOf(this.knownStore.getAndPut(newEntry)) + : this.nameStoreForWrite().getAndPut(newEntry); } /** @@ -1831,17 +1836,23 @@ public Entry getAndSet(@Nullable Entry newEntry) { Entry putEntry(@Nonnull Entry newEntry) { this.checkWriteAccess(); - // Re-setting a key clears any read-through tombstone for it (the new value overrides the - // removal). Gated on the lazy field, so this is a no-op for the common no-tombstone case. - if (this.removedFromParent != null) { - this.removedFromParent.remove(newEntry.tag); - } + return newEntry.tagId() != 0 + ? entryOf(this.knownStore.put(newEntry)) + : this.nameStoreForWrite().put(newEntry); + } - Entry replaced = HashSupport.put(this.buckets, newEntry); - if (replaced == null) { - this.size += 1; - } - return replaced; + /** {@link #putEntry} for an entry known to be a known tag's. */ + void putKnown(Entry knownEntry) { + this.checkWriteAccess(); + + this.knownStore.put(knownEntry); + } + + /** {@link #putEntry} for an entry known to be a custom tag's. */ + void putCustom(Entry customEntry) { + this.checkWriteAccess(); + + this.nameStoreForWrite().put(customEntry); } public Entry getAndSet(@Nonnull String tag, Object value) { @@ -1903,53 +1914,45 @@ public void putAll(TagMap that) { } private void putAllOptimizedMap(TagMap that) { - if (that.parent != null) { - // read-through source: the bucket-copy paths below only see that's local entries, so they - // would drop entries visible only through that's ancestor chain (and ignore its tombstones). + if (that.readsThrough()) { + // read-through source: the bucket-copy paths only see that's local entries, so they would + // drop entries visible only through that's ancestor chain (and ignore its tombstones). // Union-copy the full visible set instead -- still shares the source Entry objects. that.forEach(this, (self, entry) -> self.set(entry)); return; } - if (this.size == 0) { - this.putAllIntoEmptyMap(that); - } else { - this.putAllMerge(that); + this.knownStore.putAllLocal(that.knownStore); + HashStore thatNames = that.nameStore; + if (thatNames != null && thatNames.count != 0) { + this.nameStoreForWrite().putAllLocal(thatNames); } } - private void putAllMerge(TagMap that) { - this.size += HashSupport.merge(this.buckets, that.buckets); - } - - /* - * Specially optimized version of putAll for the common case of destination map being empty - */ - private void putAllIntoEmptyMap(TagMap that) { - HashSupport.copyInto(this.buckets, that.buckets); - this.size = that.size; - } - public void fillMap(Map map) { - if (this.parent != null) { + if (this.readsThrough()) { // read-through source: the bucket-only loop below sees just local entries, so it would drop // tags visible only through the parent chain (and ignore shadowing/tombstones). Walk the full // visible union instead -- mirrors putAllOptimizedMap. this.forEach(map, (m, entry) -> m.put(entry.tag(), entry.objectValue())); return; } - - HashSupport.fillMap(this.buckets, map); + this.knownStore.fillMapLocal(map); + if (this.nameStore != null) { + this.nameStore.fillMapLocal(map); + } } public void fillStringMap(Map stringMap) { - if (this.parent != null) { + if (this.readsThrough()) { // read-through source: walk the visible union so parent-only tags aren't dropped -- see // fillMap. this.forEach(stringMap, (m, entry) -> m.put(entry.tag(), entry.stringValue())); return; } - - HashSupport.fillStringMap(this.buckets, stringMap); + this.knownStore.fillStringMapLocal(stringMap); + if (this.nameStore != null) { + this.nameStore.fillStringMapLocal(stringMap); + } } @Override @@ -1968,65 +1971,29 @@ public Entry getAndRemove(String tag) { this.checkWriteAccess(); // See getEntry: entries are stored under their canonical Datadog name, so a removal by an - // OpenTelemetry rename must canonicalize first to find (and tombstone) the right entry. - long tagHash = Entry.tagHashOf(tag); - String canonicalTag = (tagHash >>> 32) != 0 ? KnownTagCodec.nameOf(tagHash) : tag; - return this.getAndRemove(canonicalTag, tagHash); + // OpenTelemetry rename must resolve to the id first to find (and tombstone) the right entry. + long key = KnownTagCodec.keyOf(tag); + return key != 0 ? entryOf(this.knownStore.remove(key)) : this.removeCustom(tag); } /** Removes the entry for a known tag id; see {@link #getEntry(long)}. */ public Entry getAndRemove(long tagId) { this.checkWriteAccess(); - return this.getAndRemove(Entry.requireKnownName(tagId), tagId); - } - - private Entry getAndRemove(String canonicalTag, long tagHash) { - Entry localRemoved = this.removeLocal(canonicalTag, tagHash); - - TagMap parent = this.parent; - if (parent != null) { - // Read-through: if the parent still exposes this key, removing it must also hide it from - // fall-through — install a tombstone. The prior *visible* value (Map.remove contract) is the - // local entry if there was one, otherwise the parent's (which we now hide). Single-parent in - // phase 1; rare path (only when removing a parent-exposed key). - boolean alreadyTombstoned = - this.removedFromParent != null && this.removedFromParent.contains(canonicalTag); - if (!alreadyTombstoned) { - Entry parentEntry = parent.getEntry(canonicalTag, tagHash); - if (parentEntry != null) { - if (this.removedFromParent == null) { - // Small initial capacity: this set is rare and almost always holds only a handful of - // tombstoned keys, so the default 16-bucket HashSet table would be wasteful. - this.removedFromParent = new HashSet<>(4); - } - this.removedFromParent.add(canonicalTag); - return localRemoved != null ? localRemoved : parentEntry; - } - } - } - return localRemoved; - } - - /** Removes an entry from this map's own buckets only — no parent/tombstone handling. */ - private Entry removeLocal(String tag, long tagHash) { - Entry removed = HashSupport.remove(this.buckets, Entry.bucketHash(tagHash), tag); - if (removed != null) { - this.size -= 1; - } - return removed; + return entryOf(this.knownStore.remove(tagId)); } public TagMap copy() { - // Construct with the same (frozen, shared) parent up front — the parent is fixed at - // construction. putAll then clones this map's own (local) buckets + size. The copy stays - // independently mutable (writes land on its local buckets, never the shared parent). - TagMap copy = new TagMap(this.parent); - copy.putAllIntoEmptyMap(this); - if (this.removedFromParent != null) { - copy.removedFromParent = new HashSet<>(this.removedFromParent); - } - return copy; + // The same (frozen, shared) parent, fixed at construction, over a copy of this map's own + // entries and tombstones. The copy stays independently mutable (writes land on its local + // store, never the shared parent). + HashStore nameStore = this.nameStore; + boolean borrowed = this.nameStoreBorrowed; + return new TagMap( + this.parent, + this.knownStore.copy(), + nameStore == null || borrowed ? nameStore : nameStore.copy(), + borrowed); } public TagMap immutableCopy() { @@ -2037,130 +2004,80 @@ public TagMap immutableCopy() { } } + /** The tags as momentary readers (see {@link EntryReader}): read each one, don't keep it. */ @Override public Iterator iterator() { return new EntryReaderIterator(this); } + /** + * The tags as stable {@link Entry} objects, unlike {@link #iterator()}: a spliterator usually + * feeds a stream, and a stream may collect what it gets. Free where the store already keeps an + * Entry per tag; a store that doesn't builds one per tag. + */ + @Override + public Spliterator spliterator() { + Iterator readers = this.iterator(); + Iterator entries = + new Iterator() { + @Override + public boolean hasNext() { + return readers.hasNext(); + } + + @Override + public EntryReader next() { + return readers.next().entry(); + } + }; + return Spliterators.spliterator( + entries, this.size(), Spliterator.DISTINCT | Spliterator.NONNULL); + } + + /** + * The tags as stable {@link Entry} objects, so collecting them is safe (see {@link + * #spliterator}). + */ public Stream stream() { return StreamSupport.stream(spliterator(), false); } @Override public void forEach(Consumer consumer) { - HashSupport.forEach(this.buckets, consumer); - - // read-through: parent entries not shadowed locally or tombstoned. Kept out of line so the - // common parent == null path stays byte-identical to before (small / inlinable). - if (this.parent != null) { - this.forEachParent(consumer); - } - } - - private void forEachParent(Consumer consumer) { - // Walk the ancestor chain, nearest first. Each entry is emitted once, by the nearest level that - // defines its key, when not shadowed by a nearer level and not tombstoned. - for (TagMap ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { - Object[] parentBuckets = ancestor.buckets; - for (int i = 0; i < parentBuckets.length; ++i) { - Object parentBucket = parentBuckets[i]; - if (parentBucket instanceof Entry) { - Entry parentEntry = (Entry) parentBucket; - if (parentEntryVisible(parentEntry, ancestor)) consumer.accept(parentEntry); - } else if (parentBucket instanceof BucketGroup) { - for (BucketGroup curGroup = (BucketGroup) parentBucket; - curGroup != null; - curGroup = curGroup.prev) { - for (int j = 0; j < BucketGroup.LEN; ++j) { - Entry parentEntry = curGroup._entryAt(j); - if (parentEntry != null && parentEntryVisible(parentEntry, ancestor)) - consumer.accept(parentEntry); - } - } - } - } + this.knownStore.forEach(consumer); + HashStore nameStore = this.nameStore; + if (nameStore != null) { + nameStore.forEach(consumer); } } public void forEach(T thisObj, BiConsumer consumer) { - HashSupport.forEach(this.buckets, thisObj, consumer); - - // read-through: parent entries not shadowed locally or tombstoned (kept out of line). - if (this.parent != null) { - this.forEachParent(thisObj, consumer); - } - } - - private void forEachParent(T thisObj, BiConsumer consumer) { - for (TagMap ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { - Object[] parentBuckets = ancestor.buckets; - for (int i = 0; i < parentBuckets.length; ++i) { - Object parentBucket = parentBuckets[i]; - if (parentBucket instanceof Entry) { - Entry parentEntry = (Entry) parentBucket; - if (parentEntryVisible(parentEntry, ancestor)) consumer.accept(thisObj, parentEntry); - } else if (parentBucket instanceof BucketGroup) { - for (BucketGroup curGroup = (BucketGroup) parentBucket; - curGroup != null; - curGroup = curGroup.prev) { - for (int j = 0; j < BucketGroup.LEN; ++j) { - Entry parentEntry = curGroup._entryAt(j); - if (parentEntry != null && parentEntryVisible(parentEntry, ancestor)) { - consumer.accept(thisObj, parentEntry); - } - } - } - } - } + this.knownStore.forEach(thisObj, consumer); + HashStore nameStore = this.nameStore; + if (nameStore != null) { + nameStore.forEach(thisObj, consumer); } } public void forEach( T thisObj, U otherObj, TriConsumer consumer) { - HashSupport.forEach(this.buckets, thisObj, otherObj, consumer); - - // read-through: parent entries not shadowed locally or tombstoned (kept out of line). - if (this.parent != null) { - this.forEachParent(thisObj, otherObj, consumer); - } - } - - private void forEachParent( - T thisObj, U otherObj, TriConsumer consumer) { - for (TagMap ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { - Object[] parentBuckets = ancestor.buckets; - for (int i = 0; i < parentBuckets.length; ++i) { - Object parentBucket = parentBuckets[i]; - if (parentBucket instanceof Entry) { - Entry parentEntry = (Entry) parentBucket; - if (parentEntryVisible(parentEntry, ancestor)) - consumer.accept(thisObj, otherObj, parentEntry); - } else if (parentBucket instanceof BucketGroup) { - for (BucketGroup curGroup = (BucketGroup) parentBucket; - curGroup != null; - curGroup = curGroup.prev) { - for (int j = 0; j < BucketGroup.LEN; ++j) { - Entry parentEntry = curGroup._entryAt(j); - if (parentEntry != null && parentEntryVisible(parentEntry, ancestor)) { - consumer.accept(thisObj, otherObj, parentEntry); - } - } - } - } - } + this.knownStore.forEach(thisObj, otherObj, consumer); + HashStore nameStore = this.nameStore; + if (nameStore != null) { + nameStore.forEach(thisObj, otherObj, consumer); } } public void clear() { this.checkWriteAccess(); - Arrays.fill(this.buckets, null); - this.size = 0; - // clear() removes ALL mappings, including any inherited through read-through. Detaching the - // parent (rather than tombstoning every inherited key) is simpler and cheaper, and leaves an + // clear() removes ALL mappings, including any inherited through read-through. Fresh parentless + // stores (rather than tombstoning every inherited key) are simpler and cheaper, and leave an // empty, parent-less map. Detach is one-way -- the parent is never re-pointed. + this.knownStore = IdStoreFactory.Default.INSTANCE.create(null); + this.nameStore = null; + this.nameStoreBorrowed = false; this.parent = null; - this.removedFromParent = null; } public TagMap freeze() { @@ -2178,28 +2095,21 @@ public void checkWriteAccess() { } void checkIntegrity() { - // Decided to use if ( cond ) throw new IllegalStateException rather than assert - // That was done to avoid the extra static initialization needed for an assertion - // While that's probably an unnecessary optimization, this method is only called in tests - - HashSupport.checkBuckets(this.buckets); - - if (this.size != this.computeSize()) { - throw new IllegalStateException("incorrect size"); - } - // Local-structure invariant: the size counter's emptiness must match the local buckets. Uses - // the local (this.size == 0), NOT isEmpty(), which under read-through resolves the parent too. - if ((this.size == 0) != this.checkIfEmpty()) { - throw new IllegalStateException("incorrect empty status"); + this.knownStore.checkIntegrity(); + if (this.nameStore != null && !this.nameStoreBorrowed) { + this.nameStore.checkIntegrity(); } } + /** The local entries, recomputed from both stores' structure. */ int computeSize() { - return HashSupport.count(this.buckets); + HashStore nameStore = this.nameStore; + return this.knownStore.computeSize() + + (nameStore == null || this.nameStoreBorrowed ? 0 : nameStore.computeSize()); } boolean checkIfEmpty() { - return HashSupport.isEmpty(this.buckets); + return this.computeSize() == 0; } @Override @@ -2237,19 +2147,19 @@ public String toString() { String toPrettyString() { boolean first = true; - StringBuilder ledger = new StringBuilder(128); - ledger.append('{'); + StringBuilder builder = new StringBuilder(128); + builder.append('{'); for (EntryReader entry : this) { if (first) { first = false; } else { - ledger.append(", "); + builder.append(", "); } - ledger.append(entry.tag()).append('=').append(entry.stringValue()); + builder.append(entry.tag()).append('=').append(entry.stringValue()); } - ledger.append('}'); - return ledger.toString(); + builder.append('}'); + return builder.toString(); } /** @@ -2257,28 +2167,26 @@ String toPrettyString() { * debugging */ String toInternalString() { - return HashSupport.toInternalString(this.buckets); + HashStore nameStore = this.nameStore; + return "knownStore:\n" + + this.knownStore.toInternalString() + + "nameStore:\n" + + (nameStore == null ? "none\n" : nameStore.toInternalString()); } abstract static class IteratorBase { - private final TagMap map; + // Two phases: the id store's visible entries, through its own cursor, then the custom store's + // chain -- the leaf's buckets first, then each ancestor's in turn (read-through union). + private IdStore.Cursor knownCursor; // null once the id phase is done + private final HashStore nameLeaf; + private HashStore nameLevel; // the custom level being walked; null before and after + private HashSupport.BucketWalker walker; - // the level whose buckets are currently being walked: the leaf (map) first, then each ancestor - // in turn (read-through union). map == level means we're on the leaf's own entries. - private TagMap level; - private Object[] buckets; - - private Entry nextEntry; - - private int bucketIndex = -1; - - private BucketGroup group = null; - private int groupIndex = 0; + private EntryReader nextEntry; IteratorBase(TagMap map) { - this.map = map; - this.level = map; - this.buckets = map.buckets; + this.knownCursor = map.knownStore.cursor(); + this.nameLeaf = map.nameStore; } public final boolean hasNext() { @@ -2288,9 +2196,9 @@ public final boolean hasNext() { return this.nextEntry != null; } - final Entry nextEntryOrThrowNoSuchElement() { + final EntryReader nextEntryOrThrowNoSuchElement() { if (this.nextEntry != null) { - Entry nextEntry = this.nextEntry; + EntryReader nextEntry = this.nextEntry; this.nextEntry = null; return nextEntry; } @@ -2302,72 +2210,38 @@ final Entry nextEntryOrThrowNoSuchElement() { } } - final Entry nextEntryOrNull() { - if (this.nextEntry != null) { - Entry nextEntry = this.nextEntry; - this.nextEntry = null; - return nextEntry; - } - - return this.hasNext() ? this.nextEntry : null; - } - - private final Entry advance() { - while (true) { - Entry tagEntry = this.rawAdvance(); - if (tagEntry != null) { - // leaf entries emit as-is; ancestor entries only if visible from the leaf -- not shadowed - // by a nearer level and not tombstoned. (parentEntryVisible walks the nearer levels.) - if (this.level == this.map || this.map.parentEntryVisible(tagEntry, this.level)) { - return tagEntry; - } - continue; // ancestor entry shadowed/tombstoned -> skip + private EntryReader advance() { + IdStore.Cursor knownCursor = this.knownCursor; + if (knownCursor != null) { + EntryReader reader = knownCursor.next(); + if (reader != null) { + return reader; } - - // current level exhausted; advance to the next ancestor's buckets (read-through union) - if (this.level.parent != null) { - this.level = this.level.parent; - this.buckets = this.level.buckets; - this.bucketIndex = -1; - this.group = null; - this.groupIndex = 0; - continue; + this.knownCursor = null; + if (this.nameLeaf == null) { + return null; } - return null; - } - } - - /** Next raw entry in the current bucket array, ignoring shadowing/tombstones. */ - private final Entry rawAdvance() { - while (this.bucketIndex < this.buckets.length) { - if (this.group != null) { - for (++this.groupIndex; this.groupIndex < BucketGroup.LEN; ++this.groupIndex) { - Entry tagEntry = this.group._entryAt(this.groupIndex); - if (tagEntry != null) return tagEntry; + this.nameLevel = this.nameLeaf; + this.walker = new HashSupport.BucketWalker(); + this.walker.reset(this.nameLeaf.buckets); + } + + HashStore level = this.nameLevel; + while (level != null) { + Entry entry = this.walker.next(); + if (entry != null) { + // leaf entries emit as-is; ancestor entries only if not shadowed by a nearer level + if (level == this.nameLeaf || this.nameLeaf.parentEntryVisible(entry, level)) { + return entry; } - - // done processing - that group, go to next group - this.group = this.group.prev; - this.groupIndex = -1; + continue; } - - // if the group is null, then we've finished the current bucket - so advance the bucket - if (this.group == null) { - for (++this.bucketIndex; this.bucketIndex < this.buckets.length; ++this.bucketIndex) { - Object bucket = this.buckets[this.bucketIndex]; - - if (bucket instanceof Entry) { - return (Entry) bucket; - } else if (bucket instanceof BucketGroup) { - this.group = (BucketGroup) bucket; - this.groupIndex = -1; - - break; - } - } + level = level.parent; + this.nameLevel = level; + if (level != null) { + this.walker.reset(level.buckets); } } - return null; } } @@ -2811,9 +2685,23 @@ public boolean isEmpty() { @Override public Iterator> iterator() { - @SuppressWarnings({"rawtypes", "unchecked"}) - Iterator> iter = (Iterator) this.map.iterator(); - return iter; + return new EntriesIterator(this.map); + } + } + + /** + * Map entries for the {@link Map} view: an Entry is its own, while a flyweight reader (a dense + * store's) builds one, since a caller may keep it. + */ + static final class EntriesIterator extends IteratorBase + implements Iterator> { + EntriesIterator(TagMap map) { + super(map); + } + + @Override + public Map.Entry next() { + return this.nextEntryOrThrowNoSuchElement().mapEntry(); } } @@ -2896,113 +2784,6 @@ public Object next() { } } -final class EntryReadingHelper implements TagMap.EntryReader { - private Map.Entry mapEntry; - private String tag; - private Object value; - private long tagId; - - void set(String tag, Object value) { - this.mapEntry = null; - this.tag = tag; - this.value = value; - this.tagId = TagMap.TAG_ID_NOT_COMPUTED; // resolve lazily via keyOf on first tagId() access - } - - void set(Map.Entry mapEntry) { - this.mapEntry = mapEntry; - this.tag = mapEntry.getKey(); - this.value = mapEntry.getValue(); - this.tagId = TagMap.TAG_ID_NOT_COMPUTED; // resolve lazily via keyOf on first tagId() access - } - - @Override - public String tag() { - return this.tag; - } - - @Override - public long tagId() { - long id = this.tagId; - if (id != TagMap.TAG_ID_NOT_COMPUTED) return id; - - id = KnownTagCodec.keyOf(this.tag); - this.tagId = id; - return id; - } - - @Override - public byte type() { - return TagValueConversions.typeOf(this.value); - } - - @Override - public boolean is(byte type) { - return TagValueConversions.isA(this.value, type); - } - - @Override - public boolean isNumber() { - return TagValueConversions.isNumber(this.value); - } - - @Override - public boolean isNumericPrimitive() { - return TagValueConversions.isNumericPrimitive(this.value); - } - - @Override - public boolean isObject() { - return TagValueConversions.isObject(this.value); - } - - @Override - public boolean booleanValue() { - return TagValueConversions.toBoolean(this.value); - } - - @Override - public int intValue() { - return TagValueConversions.toInt(this.value); - } - - @Override - public long longValue() { - return TagValueConversions.toLong(this.value); - } - - @Override - public float floatValue() { - return TagValueConversions.toFloat(this.value); - } - - @Override - public double doubleValue() { - return TagValueConversions.toDouble(this.value); - } - - @Override - public String stringValue() { - return TagValueConversions.toString(this.value); - } - - @Override - public Object objectValue() { - return this.value; - } - - @Override - public TagMap.Entry entry() { - return TagMap.anyEntryFor(this.tag, this.value); - } - - @Override - public Map.Entry mapEntry() { - Map.Entry mapEntry = this.mapEntry; - return (mapEntry != null) ? mapEntry : this.entry(); - } -} - final class TagValueConversions { TagValueConversions() {} diff --git a/internal-api/src/test/java/datadog/trace/api/EntryReadingHelperTest.java b/internal-api/src/test/java/datadog/trace/api/EntryReadingHelperTest.java deleted file mode 100644 index 5da8564cb87..00000000000 --- a/internal-api/src/test/java/datadog/trace/api/EntryReadingHelperTest.java +++ /dev/null @@ -1,169 +0,0 @@ -package datadog.trace.api; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.AbstractMap; -import java.util.Map; -import org.junit.jupiter.api.Test; - -class EntryReadingHelperTest { - - @Test - void setTagValueExposesIntValue() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("my.tag", 42); - - assertEquals("my.tag", helper.tag()); - assertEquals(42, helper.intValue()); - assertEquals(42L, helper.longValue()); - assertEquals(TagMap.EntryReader.INT, helper.type()); - assertTrue(helper.isNumber()); - assertTrue(helper.isNumericPrimitive()); - assertFalse(helper.isObject()); - assertTrue(helper.is(TagMap.EntryReader.INT)); - } - - @Test - void setTagValueExposesLongValue() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("id", 123456789L); - - assertEquals(123456789L, helper.longValue()); - assertEquals(TagMap.EntryReader.LONG, helper.type()); - assertTrue(helper.isNumber()); - assertTrue(helper.isNumericPrimitive()); - } - - @Test - void setTagValueExposesBooleanValue() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("flag", true); - - assertTrue(helper.booleanValue()); - assertEquals(TagMap.EntryReader.BOOLEAN, helper.type()); - assertFalse(helper.isNumber()); - assertFalse(helper.isObject()); - } - - @Test - void setTagValueExposesFloatValue() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("rate", 1.5f); - - assertEquals(1.5f, helper.floatValue()); - assertEquals(TagMap.EntryReader.FLOAT, helper.type()); - assertTrue(helper.isNumericPrimitive()); - } - - @Test - void setTagValueExposesDoubleValue() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("pi", 3.14); - - assertEquals(3.14, helper.doubleValue()); - assertEquals(TagMap.EntryReader.DOUBLE, helper.type()); - assertTrue(helper.isNumericPrimitive()); - } - - @Test - void setTagValueExposesStringAsObject() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("name", "foo"); - - assertEquals("foo", helper.stringValue()); - assertEquals("foo", helper.objectValue()); - assertEquals(TagMap.EntryReader.OBJECT, helper.type()); - assertTrue(helper.isObject()); - assertFalse(helper.isNumber()); - assertFalse(helper.isNumericPrimitive()); - } - - @Test - void setTagValueCreatesEntry() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("tag", "value"); - - TagMap.Entry entry = helper.entry(); - assertNotNull(entry); - assertEquals("tag", entry.tag()); - assertEquals("value", entry.objectValue()); - } - - @Test - void mapEntryFallsBackToNewEntryWhenSetWithTagValue() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("tag", "value"); - - Map.Entry mapEntry = helper.mapEntry(); - assertEquals("tag", mapEntry.getKey()); - assertEquals("value", mapEntry.getValue()); - } - - @Test - void setMapEntryExposesReadMethods() { - EntryReadingHelper helper = new EntryReadingHelper(); - Map.Entry original = new AbstractMap.SimpleEntry<>("key", "hello"); - helper.set(original); - - assertEquals("key", helper.tag()); - assertEquals("hello", helper.stringValue()); - assertEquals("hello", helper.objectValue()); - assertEquals(TagMap.EntryReader.OBJECT, helper.type()); - assertTrue(helper.isObject()); - assertFalse(helper.isNumber()); - } - - @Test - void mapEntryReturnsSameInstanceWhenSetWithMapEntry() { - EntryReadingHelper helper = new EntryReadingHelper(); - Map.Entry original = new AbstractMap.SimpleEntry<>("key", "value"); - helper.set(original); - - assertSame(original, helper.mapEntry()); - } - - @Test - void setMapEntryCreatesEntry() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set(new AbstractMap.SimpleEntry<>("k", 99)); - - TagMap.Entry entry = helper.entry(); - assertNotNull(entry); - assertEquals("k", entry.tag()); - assertEquals(99, entry.objectValue()); - } - - @Test - void tagIdResolvesLazilyAndCaches() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("http.method", "GET"); - - long expectedId = KnownTagCodec.keyOf("http.method"); - assertEquals(expectedId, helper.tagId()); - // second call hits the already-resolved cache rather than re-resolving - assertEquals(expectedId, helper.tagId()); - } - - @Test - void tagIdOfUnknownTagResolvesToZeroAndCaches() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("my.custom.tag", "value"); - - assertEquals(0L, helper.tagId()); - assertEquals(0L, helper.tagId()); - } - - @Test - void tagIdIsRecomputedAfterSet() { - EntryReadingHelper helper = new EntryReadingHelper(); - helper.set("http.method", "GET"); - assertEquals(KnownTagCodec.keyOf("http.method"), helper.tagId()); - - helper.set(new AbstractMap.SimpleEntry<>("db.type", "mysql")); - assertEquals(KnownTagCodec.keyOf("db.type"), helper.tagId()); - } -} diff --git a/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java b/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java index 9662c577240..f22d64c0b63 100644 --- a/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java +++ b/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.Map; import java.util.function.Function; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -386,6 +387,82 @@ void getStringFormatsTheVisibleValue(Function newStore) { IllegalArgumentException.class, () -> store.getString(KnownTagCodec.makeTagId(9999))); } + @ParameterizedTest + @MethodSource("stores") + void aViewReadsValuesFromEitherStore(Function newStore) { + IdStore store = newStore.apply(null); + store.set(KnownTags.HTTP_ROUTE_ID, "/a"); + store.set(KnownTags.PEER_PORT_ID, 5432); + HashStore names = HashStore.create(null); + names.set("my.tag", 7); + + TagMap view = TagMap.viewOf(store, names, false); + assertEquals("/a", view.getObject(KnownTags.HTTP_ROUTE_NAME)); + assertEquals(5432, view.getObject("peer.port")); + assertEquals("5432", view.getString("peer.port")); + assertEquals(7, view.getObject("my.tag")); + assertEquals("7", view.getString("my.tag")); + assertNull(view.getObject(KnownTags.HTTP_METHOD_NAME)); + assertNull(view.getString("other.tag")); + assertNull(TagMap.viewOf(store, null, false).getString("my.tag"), "no custom store"); + } + + @ParameterizedTest + @MethodSource("stores") + void aViewReadsPrimitivesThroughAReader(Function newStore) { + IdStore store = newStore.apply(null); + store.set(KnownTags.PEER_PORT_ID, 5432); + store.set(KnownTags.DB_INSTANCE_ID, 1.5d); + store.set(KnownTags.DD_MEASURED_ID, true); + HashStore names = HashStore.create(null); + names.set("my.count", 7L); + + TagMap view = TagMap.viewOf(store, names, false); + assertEquals(5432, view.getInt("peer.port")); + assertEquals(5432L, view.getLong("peer.port")); + assertEquals(1.5d, view.getDouble("db.instance")); + assertEquals(1.5f, view.getFloat("db.instance")); + assertTrue(view.getBoolean(KnownTags.DD_MEASURED_NAME)); + assertEquals(7L, view.getLong("my.count")); + assertEquals(-1, view.getIntOrDefault(KnownTags.HTTP_METHOD_NAME, -1)); + assertEquals(-1L, view.getLongOrDefault("other.tag", -1L)); + assertTrue(view.containsKey("peer.port")); + assertTrue(view.containsKey("my.count")); + assertFalse(view.containsKey("other.tag")); + + EntryReader port = view.getEntryReader(KnownTags.PEER_PORT_ID); + assertEquals(5432, port.intValue()); + assertEquals(KnownTags.PEER_PORT_ID, port.tagId()); + assertEquals(7L, view.getEntryReader("my.count").longValue()); + assertNull(view.getEntryReader(KnownTags.HTTP_METHOD_ID)); + assertNull(TagMap.viewOf(store, null, false).getEntryReader("my.count"), "no custom store"); + } + + @ParameterizedTest + @MethodSource("stores") + void aStreamYieldsEntriesThatStayValidOnceCollected(Function newStore) { + IdStore store = newStore.apply(null); + store.set(KnownTags.HTTP_ROUTE_ID, "/a"); + store.set(KnownTags.HTTP_METHOD_ID, "GET"); + store.set(KnownTags.PEER_PORT_ID, 5432); + HashStore names = HashStore.create(null); + names.set("my.tag", "custom"); + TagMap view = TagMap.viewOf(store, names, false); + + // collected, then read: each element must still describe its own tag + List collected = view.stream().collect(Collectors.toList()); + Map byName = new HashMap<>(); + for (EntryReader entryReader : collected) { + assertNull(byName.put(entryReader.tag(), entryReader.objectValue()), entryReader.tag()); + } + Map expected = new HashMap<>(); + expected.put("http.route", "/a"); + expected.put("http.method", "GET"); + expected.put("peer.port", 5432); + expected.put("my.tag", "custom"); + assertEquals(expected, byName); + } + @ParameterizedTest @MethodSource("stores") void settingANullValueRemovesTheTag(Function newStore) { @@ -402,6 +479,31 @@ void settingANullValueRemovesTheTag(Function newStore) { assertEquals("/inherited", parent.getValue(KnownTags.HTTP_ROUTE_ID), "the parent keeps it"); } + @ParameterizedTest + @MethodSource("stores") + void aNullValueThroughTheMapApiRemovesTheTag(Function newStore) { + TagMap view = TagMap.viewOf(newStore.apply(null), HashStore.create(null), false); + view.put("http.route", "/a"); + view.put("my.tag", "custom"); + view.set("http.method", "GET"); + view.set("peer.port", 5432); + + assertEquals("/a", view.put("http.route", null), "returns the prior value"); + assertEquals("custom", view.put("my.tag", null)); + view.set("http.method", (Object) null); + assertEquals(5432, view.getAndSet("peer.port", (Object) null).objectValue()); + Map withNull = new HashMap<>(); + withNull.put("http.route", null); + view.putAll(withNull); // already absent: still absent + + for (String tag : new String[] {"http.route", "my.tag", "http.method", "peer.port"}) { + assertFalse(view.containsKey(tag), tag); + assertNull(view.get(tag), tag); + } + assertEquals(0, view.size()); + assertTrue(view.isEmpty()); + } + @ParameterizedTest @MethodSource("stores") void aReaderWithNoEntryIsANoOp(Function newStore) { diff --git a/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java new file mode 100644 index 00000000000..1d71f09aa56 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagStoresTest.java @@ -0,0 +1,458 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * A TagMap's two stores -- an {@link IdStore} for known tags ({@link HashIdStore} unless dense mode + * is on), a {@link HashStore} for custom tags, created lazily -- the name-or-id routing over them, + * and the span's view ({@link TagMap#viewOf}). + */ +class TagStoresTest { + private static TagMap frozenParent() { + TagMap parent = TagMap.create(); + parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + parent.set(KnownTags.HTTP_STATUS_CODE_ID, 200); + parent.set("inherited.custom", "c"); + return parent.freeze(); + } + + @Test + void aMapWithNoCustomTagsHasNoCustomStore() { + TagMap tags = TagMap.create(); + tags.set(KnownTags.HTTP_ROUTE_ID, "/a"); + tags.set(KnownTags.COMPONENT_NAME, "jdbc"); + assertNull(tags.nameStore); + + tags.set("my.tag", "x"); + assertNotNull(tags.nameStore); + assertEquals(3, tags.size()); + } + + @Test + void aChildBorrowsItsParentsCustomStoreUntilItWritesOne() { + TagMap parent = frozenParent(); + TagMap child = TagMap.createFromParent(parent); + assertSame(parent.nameStore, child.nameStore); + assertTrue(child.nameStoreBorrowed); + assertEquals("c", child.getString("inherited.custom")); + + child.set(KnownTags.HTTP_METHOD_ID, "GET"); // a known write keeps borrowing + assertTrue(child.nameStoreBorrowed); + assertFalse(child.remove("not.there")); // nothing to hide keeps borrowing + assertTrue(child.nameStoreBorrowed); + + child.set("own.custom", "o"); + assertFalse(child.nameStoreBorrowed); + assertNotSame(parent.nameStore, child.nameStore); + assertEquals("c", child.getString("inherited.custom")); // still reads through + assertEquals(5, child.size()); + assertEquals("c", parent.getString("inherited.custom")); + assertNull(parent.getString("own.custom")); + } + + @Test + void removingABorrowedCustomTagHidesItLocally() { + TagMap parent = frozenParent(); + TagMap child = TagMap.createFromParent(parent); + + assertEquals("c", child.getAndRemove("inherited.custom").stringValue()); + + assertFalse(child.nameStoreBorrowed); + assertNull(child.getString("inherited.custom")); + assertEquals(2, child.size()); + assertEquals("c", parent.getString("inherited.custom")); + } + + @Test + void removingAnInheritedKnownTagTombstonesItWithoutCountingIt() { + TagMap parent = frozenParent(); + TagMap child = TagMap.createFromParent(parent); + + assertEquals("/inherited", child.getAndRemove(KnownTags.HTTP_ROUTE_ID).stringValue()); + assertNull(child.getAndRemove(KnownTags.HTTP_ROUTE_ID)); // already removed + + assertNull(child.getEntry(KnownTags.HTTP_ROUTE_ID)); + assertEquals(2, child.size()); + assertFalse(child.containsKey(KnownTags.HTTP_ROUTE_NAME)); + for (TagMap.EntryReader entry : child) { + assertNotEquals(KnownTags.HTTP_ROUTE_ID, entry.tagId()); + } + IdStore ids = child.knownStore; + assertEquals(0, ids.computeSize()); + assertTrue(((HashIdStore) ids).hasTombstones); + child.checkIntegrity(); + + // a re-set undoes the removal: no prior visible value + assertNull(child.getAndSet(KnownTags.HTTP_ROUTE_NAME, "/again")); + assertEquals("/again", child.getString(KnownTags.HTTP_ROUTE_NAME)); + assertEquals(3, child.size()); + } + + @Test + void tombstonesAreSharedPerTag() { + TagMap.Entry first = TagMap.Entry.tombstoneOf(KnownTags.HTTP_ROUTE_ID); + assertSame(first, TagMap.Entry.tombstoneOf(KnownTags.HTTP_ROUTE_ID)); + assertTrue(first.tombstone); + assertEquals(KnownTags.HTTP_ROUTE_NAME, first.tag()); + } + + /** A span-level known tag colored into {@code traceLevelId}'s slot, or 0 if there is none. */ + private static long spanLevelTagInSlotOf(long traceLevelId) throws IllegalAccessException { + for (java.lang.reflect.Field field : KnownTags.class.getFields()) { + if (field.getType() != long.class) continue; + long id = field.getLong(null); + if (KnownTagCodec.isKnown(id) + && !KnownTagCodec.isTraceLevel(id) + && KnownTagCodec.slotOf(id) == KnownTagCodec.slotOf(traceLevelId)) { + return id; + } + } + return 0L; + } + + @Test + void aSpanLevelReadMissesAParentHoldingATraceLevelTagInItsSlot() throws Exception { + // Span-level and trace-level tags are colored independently, so their slots overlap: find a + // span-level tag that shares runtime-id's slot (the coloring decides which one). + long spanLevelId = spanLevelTagInSlotOf(KnownTags.RUNTIME_ID); + assumeTrue(spanLevelId != 0L, "no span-level tag shares runtime-id's slot"); + assertTrue(KnownTagCodec.isTraceLevel(KnownTags.RUNTIME_ID)); + + HashIdStore traceLevel = new HashIdStore(null); + traceLevel.set(KnownTags.RUNTIME_ID, "abc"); + HashIdStore span = new HashIdStore(traceLevel); + long other = + spanLevelId == KnownTags.HTTP_ROUTE_ID ? KnownTags.COMPONENT_ID : KnownTags.HTTP_ROUTE_ID; + span.set(other, "/a"); + + assertNull(span.getEntryReader(spanLevelId)); + assertEquals("abc", span.getValue(KnownTags.RUNTIME_ID)); + } + + @Test + void anUnknownIdIsRejected() { + HashIdStore ids = new HashIdStore(null); + assertThrows(IllegalArgumentException.class, () -> ids.getEntryReader(0L)); + assertThrows(IllegalArgumentException.class, () -> ids.remove(0L)); + } + + @Test + void theIdStoreSetsEveryValueShape() { + HashIdStore ids = new HashIdStore(null); + ids.set(KnownTags.COMPONENT_ID, (Object) new StringBuilder("jdbc")); + ids.set(KnownTags.HTTP_ROUTE_ID, "/a"); + ids.set(KnownTags.DD_PROFILING_ENABLED_ID, true); + ids.set(KnownTags.HTTP_STATUS_CODE_ID, 200); + ids.set(KnownTags.HTTP_RESEND_COUNT_ID, 2L); + ids.set(KnownTags.DB_POOL_NAME_ID, 1.5f); + ids.set(KnownTags.DB_INSTANCE_ID, 2.25d); + + TagMap checked = TagMap.create(); + checked.set(KnownTags.COMPONENT_ID, ids.getEntryReader(KnownTags.COMPONENT_ID).objectValue()); + checked.set(KnownTags.HTTP_ROUTE_ID, "/a"); + checked.set(KnownTags.DD_PROFILING_ENABLED_ID, true); + checked.set(KnownTags.HTTP_STATUS_CODE_ID, 200); + checked.set(KnownTags.HTTP_RESEND_COUNT_ID, 2L); + checked.set(KnownTags.DB_POOL_NAME_ID, 1.5f); + checked.set(KnownTags.DB_INSTANCE_ID, 2.25d); + + Map actual = new HashMap<>(); + ids.fillMapLocal(actual); + assertEquals(new HashMap<>(checked), actual); + assertEquals(7, ids.size()); + assertEquals(7, ids.computeSize()); + assertTrue(ids.toInternalString().contains(KnownTags.HTTP_ROUTE_NAME)); + } + + @Test + void putAllIntoAStoreWithTombstonesPutsOneAtATime() { + TagMap child = TagMap.createFromParent(frozenParent()); + child.remove(KnownTags.HTTP_ROUTE_NAME); // a tombstone + + TagMap source = TagMap.create(); + source.set(KnownTags.HTTP_ROUTE_ID, "/from-source"); + source.set(KnownTags.HTTP_METHOD_ID, "GET"); + child.putAll(source); + + assertEquals("/from-source", child.getString(KnownTags.HTTP_ROUTE_NAME)); + assertEquals(4, child.size()); // route, method, status code, inherited custom + child.checkIntegrity(); + } + + @Test + void aCopyKeepsTombstonesAndBorrowing() { + TagMap child = TagMap.createFromParent(frozenParent()); + child.remove(KnownTags.HTTP_ROUTE_NAME); + + TagMap copy = child.copy(); + assertNull(copy.getString(KnownTags.HTTP_ROUTE_NAME)); + assertTrue(copy.nameStoreBorrowed); + assertEquals(child.size(), copy.size()); + + copy.set(KnownTags.HTTP_ROUTE_NAME, "/copy"); + assertNull(child.getString(KnownTags.HTTP_ROUTE_NAME)); // independent + } + + @Test + void anIteratorWalksBothStoresAndTheirParents() { + TagMap child = TagMap.createFromParent(frozenParent()); + child.set(KnownTags.HTTP_METHOD_ID, "GET"); + child.set("own.custom", "o"); + child.remove("inherited.custom"); + + Map seen = new HashMap<>(); + for (TagMap.EntryReader entry : child) { + seen.put(entry.tag(), entry.objectValue()); + } + Map expected = new HashMap<>(); + expected.put(KnownTags.HTTP_ROUTE_NAME, "/inherited"); + expected.put(KnownTags.HTTP_STATUS_CODE_NAME, 200); + expected.put(KnownTags.HTTP_METHOD_NAME, "GET"); + expected.put("own.custom", "o"); + assertEquals(expected, seen); + assertEquals(expected, new HashMap<>(child)); + } + + @Test + void routingReadsKnownTagsByIdAndEverythingElseByName() { + IdStore ids = IdStore.create(null); + HashStore names = HashStore.create(null); + // a direction-scoped OpenTelemetry name is resolved at the OpenTelemetry boundary, not here + assertEquals(0L, KnownTagCodec.keyOf("server.address")); + + ids.set(KnownTags.HTTP_ROUTE_ID, "/a"); + names.set("my.tag", "custom"); + + TagMap view = TagMap.viewOf(ids, names, false); + assertEquals("/a", view.getEntry(KnownTags.HTTP_ROUTE_NAME).stringValue()); + assertEquals("custom", view.getEntry("my.tag").stringValue()); + assertNull(TagMap.viewOf(ids, null, false).getEntry("my.tag")); // no custom store yet + } + + @Test + void anOwnerReadsThroughItsParentsStores() { + assertNull(TagMap.readThroughParent(null)); + assertThrows(IllegalStateException.class, () -> TagMap.readThroughParent(TagMap.create())); + assertNull(TagMap.readThroughParent(TagMap.create().freeze())); // empty parent dropped + + TagMap parent = TagMap.readThroughParent(frozenParent()); + IdStore ids = IdStore.create(parent); + HashStore borrowed = HashStore.inheritedFrom(parent); + assertSame(parent.nameStore, borrowed); + assertEquals("/inherited", ids.getValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals("c", borrowed.get("inherited.custom").stringValue()); + + HashStore own = HashStore.create(borrowed); + own.set("own", "o"); + assertEquals("c", own.get("inherited.custom").stringValue()); + assertNull(borrowed.get("own")); + } + + @Test + void theCustomStoreSetsEveryValueShape() { + HashStore names = HashStore.create(null); + names.set("o", (Object) "obj"); + names.set("s", "v"); + names.set("b", true); + names.set("i", 1); + names.set("l", 2L); + names.set("f", 3f); + names.set("d", 4d); + assertNull(names.getAndPut(TagMap.Entry.create("p", "x"))); + assertEquals("x", names.put(TagMap.Entry.create("p", "y")).stringValue()); + + assertEquals(true, names.get("b").objectValue()); + assertEquals(4d, names.get("d").objectValue()); + assertEquals(8, names.size()); + } + + @Test + void aViewWritesThroughToItsOwnersStoresAndHandsBackANewCustomStore() { + TagMap parent = TagMap.readThroughParent(frozenParent()); + IdStore ids = IdStore.create(parent); + HashStore borrowed = HashStore.inheritedFrom(parent); + TagMap view = TagMap.viewOf(ids, borrowed, true); + assertSame(borrowed, view.customStore()); + assertTrue(view.isCustomStoreBorrowed()); + + view.set(KnownTags.HTTP_METHOD_NAME, "GET"); + view.set("own", "o"); + assertEquals("GET", ids.getValue(KnownTags.HTTP_METHOD_ID)); + assertFalse(view.isCustomStoreBorrowed()); + assertNotSame(borrowed, view.customStore()); + assertEquals("o", view.customStore().get("own").stringValue()); + } + + @Test + void clearDetachesBothStores() { + TagMap child = TagMap.createFromParent(frozenParent()); + child.set("own", "o"); + child.clear(); + assertTrue(child.isEmpty()); + assertNull(child.nameStore); + assertNull(child.getString("inherited.custom")); + assertTrue(child.toInternalString().contains("nameStore:")); + } + + /** Every known tag's real id, from the registry. */ + private static java.util.List knownIds() { + java.util.List ids = new java.util.ArrayList<>(); + for (int serial = 1; serial < KnownTagCodec.serialLimit(); serial++) { + long id = KnownTagCodec.keyOf(KnownTagCodec.nameOf(KnownTagCodec.makeTagId(serial))); + if (id > 0) { + ids.add(id); + } + } + return ids; + } + + /** + * A three-level chain with enough tags in each store to fill bucket groups, checked against a + * HashMap model through every read path: lookups, size, emptiness, iteration and forEach. + */ + @Test + void aDeepChainReadsLikeItsModel() { + java.util.List ids = knownIds(); + Map model = new HashMap<>(); + + TagMap grandparent = TagMap.create(); + for (int i = 0; i < ids.size(); i += 2) { + grandparent.set(ids.get(i), "gp" + i); + model.put(KnownTagCodec.nameOf(ids.get(i)), "gp" + i); + } + for (int i = 0; i < 40; i++) { + grandparent.set("custom." + i, "gp" + i); + model.put("custom." + i, "gp" + i); + } + grandparent.freeze(); + + TagMap parent = TagMap.createFromParent(grandparent); + for (int i = 1; i < ids.size(); i += 4) { + parent.set(ids.get(i), "p" + i); + model.put(KnownTagCodec.nameOf(ids.get(i)), "p" + i); + } + for (int i = 0; i < 40; i += 3) { + parent.set("custom." + i, "p" + i); + model.put("custom." + i, "p" + i); + } + parent.remove(KnownTagCodec.nameOf(ids.get(0))); // tombstone a grandparent known tag + model.remove(KnownTagCodec.nameOf(ids.get(0))); + parent.remove("custom.1"); + model.remove("custom.1"); + parent.freeze(); + + TagMap child = TagMap.createFromParent(parent); + for (int i = 0; i < ids.size(); i += 3) { + child.set(ids.get(i), "c" + i); + model.put(KnownTagCodec.nameOf(ids.get(i)), "c" + i); + } + for (int i = 2; i < ids.size(); i += 5) { + String name = KnownTagCodec.nameOf(ids.get(i)); + child.remove(name); + model.remove(name); + } + for (int i = 0; i < 40; i += 4) { + child.set("custom." + i, "c" + i); + model.put("custom." + i, "c" + i); + } + child.remove("custom.2"); + model.remove("custom.2"); + + assertEquals(model.size(), child.size()); + assertFalse(child.isEmpty()); + assertFalse(child.isDefinitelyEmpty()); + assertTrue(child.estimateSize() >= model.size()); + assertEquals(model, new HashMap<>(child)); + for (Map.Entry entry : model.entrySet()) { + assertEquals(entry.getValue(), child.getObject(entry.getKey()), entry.getKey()); + } + + Map viaConsumer = new HashMap<>(); + child.forEach(entry -> viaConsumer.put(entry.tag(), entry.objectValue())); + assertEquals(model, viaConsumer); + Map viaBi = new HashMap<>(); + child.forEach(viaBi, (m, entry) -> m.put(entry.tag(), entry.objectValue())); + assertEquals(model, viaBi); + Map viaTri = new HashMap<>(); + child.forEach(viaTri, "", (m, unused, entry) -> m.put(entry.tag(), entry.objectValue())); + assertEquals(model, viaTri); + Map filled = new HashMap<>(); + child.fillMap(filled); + assertEquals(model, filled); + + TagMap flat = TagMap.create(); + flat.set("custom.0", "flat"); + flat.set(ids.get(0), "flat"); + flat.putAll(child); // a read-through source: copied as its visible union + assertEquals(model, new HashMap<>(flat)); + flat.checkIntegrity(); + } + + @Test + void aChildThatHidesEverythingInheritedIsEmpty() { + TagMap parent = TagMap.create(); + parent.set(KnownTags.HTTP_ROUTE_ID, "/a"); + parent.set("c", "x"); + TagMap child = TagMap.createFromParent(parent.freeze()); + assertFalse(child.isEmpty()); + + child.remove(KnownTags.HTTP_ROUTE_NAME); + assertFalse(child.isEmpty()); + child.remove("c"); + assertTrue(child.isEmpty()); + assertEquals(0, child.size()); + assertTrue(child.isDefinitelyEmpty() == false); // the parent still holds local entries + } + + @Test + void removingALocalOverrideOfAnInheritedTagReturnsTheLocalValue() { + TagMap child = TagMap.createFromParent(frozenParent()); + child.set(KnownTags.HTTP_ROUTE_ID, "/local"); + child.set("inherited.custom", "local"); + + assertEquals("/local", child.getAndRemove(KnownTags.HTTP_ROUTE_ID).stringValue()); + assertEquals("local", child.getAndRemove("inherited.custom").stringValue()); + assertNull(child.getString(KnownTags.HTTP_ROUTE_NAME)); + assertNull(child.getString("inherited.custom")); + } + + @Test + void putAllMergesIntoNonEmptyStores() { + TagMap dest = TagMap.create(); + dest.set(KnownTags.HTTP_ROUTE_ID, "/dest"); + dest.set("d", "dest"); + TagMap source = TagMap.create(); + source.set(KnownTags.HTTP_METHOD_ID, "GET"); + source.set(KnownTags.HTTP_ROUTE_ID, "/source"); + source.set("s", "source"); + + dest.putAll(source); + + assertEquals("/source", dest.getString(KnownTags.HTTP_ROUTE_NAME)); + assertEquals("GET", dest.getString(KnownTags.HTTP_METHOD_NAME)); + assertEquals("source", dest.getString("s")); + assertEquals("dest", dest.getString("d")); + assertEquals(4, dest.size()); + dest.checkIntegrity(); + } + + @Test + void aCustomStoreRejectsAKnownTagsName() { + HashStore names = new HashStore(null); + assertThrows(AssertionError.class, () -> names.get(KnownTags.HTTP_ROUTE_NAME)); + assertThrows(AssertionError.class, () -> names.remove(KnownTags.HTTP_ROUTE_NAME)); + } +}