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)); + } +}