From e411decceb14a9ae4b39a4d46c608a885ab3c4a1 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 16:04:02 -0400 Subject: [PATCH 01/29] Color known tags into dense-store slots in the tag registry The generator now assigns each required/conditional/recommended tag a slot in id bits [47-32] by graph coloring: tags resolved onto one concrete span type, or both trace-level, get distinct slots; tags that never co-occur -- such as a tag declared per direction -- may share one. The rest get NO_SLOT and stay hash-stored. The current conventions need 18 slots, within the one occupancy long the dense store will use. KnownTagCodec gains slotOf, hasSlot, slotCount, and the slot layout constants. Nothing reads the slot yet; the dense store does. Co-Authored-By: Claude Opus 5.5 --- .../tagRegistry/KnownTagsEmitter.kt | 4 + .../buildlogic/tagRegistry/TagRegistry.kt | 86 ++++++++++++++++--- .../tagRegistry/TagRegistryGenerator.kt | 5 +- .../tagRegistry/TagRegistryGeneratorTest.kt | 52 +++++++++-- .../java/datadog/trace/api/KnownTagCodec.java | 46 +++++++--- .../trace/api/KnownTagCodecSlotTest.java | 62 +++++++++++++ tag-conventions.yaml | 15 ++-- 7 files changed, 231 insertions(+), 39 deletions(-) create mode 100644 internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt index 0632b636145..aa8ed7559fa 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt @@ -118,6 +118,7 @@ object KnownTagsEmitter { appendLine(" public static final String ${otelNameC(t.identity)} = \"${escape(t.otelName)}\";") } append("// makeTagId(serial=${t.serial})") + append(if (t.slot == TagRegistry.NO_SLOT) " + no slot" else " + slot=${t.slot}") if (t.traceLevel) append(" + trace-level") if (t.intercepted) append(" + intercepted") if (t.sharedNameDirection != null) append(" + shared-name") @@ -135,6 +136,9 @@ object KnownTagsEmitter { // Every known tag's serial is below SERIAL_LIMIT; serial 0 is no tag. val serialLimit = (reg.tags.maxOfOrNull { it.serial } ?: 0) + 1 appendLine(" public static final int SERIAL_LIMIT = $serialLimit;") + // Dense-store slots in use: every slotted tag's slot is below SLOT_COUNT. + val slotCount = (reg.tags.filter { it.slot != TagRegistry.NO_SLOT }.maxOfOrNull { it.slot } ?: -1) + 1 + appendLine(" public static final int SLOT_COUNT = $slotCount;") // OpenTelemetry name -> canonical tag name. Validation ensures aliases are distinct from all // canonical names. Sort by OTel name to keep output deterministic. diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt index 6b96a1f5283..c6961904735 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt @@ -2,12 +2,18 @@ package datadog.buildlogic.tagRegistry /** * Assigns tag ids from a parsed [TagConventions]. The id encoding mirrors KnownTagCodec: [63-48 - * serial][47-32 reserved][31-0 flags]. + * serial][47-32 slot][31-0 flags]. * - *

An id is a globally unique serial plus two classification bits: trace-level and intercepted. It - * carries no storage-layout coordinate -- bits [47-32] are held vacant for the co-occurrence slot - * that the dense tag store assigns by graph coloring, which lands with the dense store itself. - * Nothing here needs to know how (or whether) a tag is stored. + *

An id is a globally unique serial, a dense-store slot, and two classification bits: + * trace-level and intercepted. + * + *

The slot comes from GRAPH COLORING the tag co-occurrence graph. Each concrete span type's + * resolved tags appear together on one span, so they form a clique and need distinct slots; the + * trace-level tier is its own clique (it lives in a separate TagMap). Tags that never share a clique + * may share a slot -- a tag declared per direction (peer.port inbound and outbound) typically does -- + * so the slot count stays bounded by the largest clique, small enough for one occupancy `long` in + * the dense store. Only tags at a required/conditional/recommended level are slotted; the rest get + * [NO_SLOT] and are stored by hash. * *

The intercepted bit marks a tag the tracer may route on the set-path (to a span field or a * sampling directive instead of tag storage), as listed by the tracer overlay. A setter called with @@ -22,6 +28,8 @@ class TagRegistry private constructor(val tags: List) { val serial: Int, val traceLevel: Boolean, val intercepted: Boolean, + /** The dense-store slot, or [NO_SLOT] when the tag is stored by hash. */ + val slot: Int, val id: Long, /** The tag's OpenTelemetry name, in [otelDirection] or every direction; null when not renamed. */ val declaredOtelName: String? = null, @@ -49,6 +57,13 @@ class TagRegistry private constructor(val tags: List) { const val INTERCEPTED = 1L shl 1 // low-32 carve bit 1; mirrors KnownTagCodec.INTERCEPTED const val SHARED_NAME = 1L shl 0 // low-32 carve bit 0; mirrors KnownTagCodec.SHARED_NAME + const val SLOT_SHIFT = 32 // slot window [47-32]; mirrors KnownTagCodec.SLOT_SHIFT + const val NO_SLOT = 0xFFFF // all-ones slot: stored by hash; mirrors KnownTagCodec.NO_SLOT + const val MAX_SLOTS = 64 // one occupancy long in the dense store + + /** Requirement levels whose tags get a dense-store slot. */ + val SLOTTED_LEVELS = setOf("required", "conditional", "recommended") + /** * Serials stay below 2^15, so bit 63 of an id is never set: every id is positive, clear of 0 * (unknown) and of KnownTagCodec's negative lookup sentinels. @@ -56,17 +71,23 @@ class TagRegistry private constructor(val tags: List) { const val SERIAL_CAPACITY = 1 shl 15 /** - * Mirrors KnownTagCodec.makeTagId(serial) + traceLevel() + intercepted() -- must stay in sync. - * LEVEL_TRACE at bit 2, INTERCEPTED at bit 1, SHARED_NAME at bit 0, other low bits and the - * reserved [47-32] window zero. Every id is made here, and the range check makes each one - * positive by construction. + * Mirrors KnownTagCodec's layout -- must stay in sync: serial at [63-48], slot at [47-32], + * LEVEL_TRACE at bit 2, INTERCEPTED at bit 1, SHARED_NAME at bit 0, other low bits zero. Every id + * is made here, and the range check makes each one positive by construction. */ - fun encode(serial: Int, traceLevel: Boolean, intercepted: Boolean, sharedName: Boolean = false): Long { + fun encode( + serial: Int, + traceLevel: Boolean, + intercepted: Boolean, + sharedName: Boolean = false, + slot: Int, + ): Long { + require(slot == NO_SLOT || slot in 0 until MAX_SLOTS) { "slot $slot is outside [0, $MAX_SLOTS)" } require(serial in FIRST_SERIAL until SERIAL_CAPACITY) { "serial $serial is outside [$FIRST_SERIAL, $SERIAL_CAPACITY): more tags would make tag ids " + "negative, where KnownTagCodec's lookup sentinels live" } - var id = serial.toLong() shl 48 + var id = (serial.toLong() shl 48) or (slot.toLong() shl SLOT_SHIFT) if (traceLevel) id = id or LEVEL_TRACE if (intercepted) id = id or INTERCEPTED if (sharedName) id = id or SHARED_NAME @@ -77,6 +98,8 @@ class TagRegistry private constructor(val tags: List) { val traceLevel = conv.traceLevelTags().map { it.identity }.toSet() val renames = conv.otelMappings().associateBy { it.tag } + val slots = colorSlots(conv) + // Stable order (by name) so serials -- and therefore ids -- are a pure function of the input. val tags = conv.allDeclaredTags().sortedBy { it.name }.mapIndexed { i, t -> @@ -90,7 +113,8 @@ class TagRegistry private constructor(val tags: List) { serial, isTraceLevel, isIntercepted, - id = encode(serial, isTraceLevel, isIntercepted, t.identity.direction != null), + slot = slots[t.identity] ?: NO_SLOT, + id = encode(serial, isTraceLevel, isIntercepted, t.identity.direction != null, slots[t.identity] ?: NO_SLOT), declaredOtelName = renames[t.identity]?.otelName, otelDirection = renames[t.identity]?.direction, ) @@ -100,6 +124,44 @@ class TagRegistry private constructor(val tags: List) { return TagRegistry(tags) } + /** + * Colors the co-occurrence graph of slotted tags: tags resolved onto one concrete span type, or + * both trace-level, must get distinct slots. Greedy, most-constrained first (by clique count, + * then label), so the result is a pure function of the conventions. + */ + private fun colorSlots(conv: TagConventions): Map { + val cliques = ArrayList>() + for (type in conv.concreteTypes()) { + cliques.add(conv.resolve(type).filter { it.required in SLOTTED_LEVELS }.map { it.identity }.toSet()) + } + cliques.add(conv.traceLevelTags().filter { it.required in SLOTTED_LEVELS }.map { it.identity }.toSet()) + + val neighbors = HashMap>() + for (clique in cliques) { + for (a in clique) { + val adjacent = neighbors.getOrPut(a) { HashSet() } + for (b in clique) if (a != b) adjacent.add(b) + } + } + val cliqueCount = neighbors.keys.associateWith { tag -> cliques.count { tag in it } } + val order = + neighbors.keys.sortedWith( + compareByDescending { cliqueCount.getValue(it) }.thenBy { it.label } + ) + val slots = HashMap() + for (tag in order) { + val taken = neighbors.getValue(tag).mapNotNull { slots[it] }.toSet() + var slot = 0 + while (slot in taken) slot++ + require(slot < MAX_SLOTS) { + "coloring needs more than $MAX_SLOTS slots at '${tag.label}'; the dense store's occupancy " + + "long holds $MAX_SLOTS" + } + slots[tag] = slot + } + return slots + } + /** * Rejects OpenTelemetry names that collide with any Datadog name. Within each direction, * a rename must also belong to a single tag. Direction-free renames reserve their name diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt index 50960175cc0..c92af8539c4 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt @@ -71,16 +71,17 @@ object TagRegistryGenerator { """ # Tag id assignment. tags=${reg.tags.size} - # TAGS serial lvl int id required name + # TAGS serial lvl int slot id required name """.trimIndent() ) for (t in reg.tags) { appendLine( - " %6d %s %s %-18s %-12s %s".format( + " %6d %s %s %4s %-18s %-12s %s".format( Locale.ROOT, t.serial, if (t.traceLevel) "T" else "-", if (t.intercepted) "I" else "-", + if (t.slot == TagRegistry.NO_SLOT) "-" else t.slot.toString(), "0x%016X".format(Locale.ROOT, t.id), t.required, t.name diff --git a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt index b47fe5f0cf4..1ab58c355b7 100644 --- a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt +++ b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt @@ -60,7 +60,7 @@ class TagRegistryGeneratorTest { assertThat(first.getValue("tag-assignment.txt")) .contains("tags=6", "test.name", "service.name") assertThat(first.getValue("java/datadog/trace/api/KnownTags.java")) - .contains("ENV_ID = 0x0001000000000004L", "TEST_NAME", "http.request.method") + .contains("ENV_ID = 0x0001FFFF00000004L", "TEST_NAME", "http.request.method") } @TableTest( @@ -461,8 +461,8 @@ class TagRegistryGeneratorTest { @Test fun `every tag id is positive, at both ends of the serial range`() { for (serial in listOf(TagRegistry.FIRST_SERIAL, TagRegistry.SERIAL_CAPACITY - 1)) { - assertThat(TagRegistry.encode(serial, traceLevel = false, intercepted = false)).isPositive() - assertThat(TagRegistry.encode(serial, traceLevel = true, intercepted = false)).isPositive() + assertThat(TagRegistry.encode(serial, traceLevel = false, intercepted = false, slot = TagRegistry.NO_SLOT)).isPositive() + assertThat(TagRegistry.encode(serial, traceLevel = true, intercepted = false, slot = TagRegistry.NO_SLOT)).isPositive() } } @@ -470,7 +470,7 @@ class TagRegistryGeneratorTest { fun `a serial outside the range that keeps ids positive is rejected`() { for (serial in listOf(0, TagRegistry.SERIAL_CAPACITY, Short.MAX_VALUE + 1)) { assertThatIllegalArgumentException() - .isThrownBy { TagRegistry.encode(serial, traceLevel = false, intercepted = false) } + .isThrownBy { TagRegistry.encode(serial, traceLevel = false, intercepted = false, slot = TagRegistry.NO_SLOT) } .withMessageContaining("serial $serial is outside") } } @@ -554,9 +554,9 @@ class TagRegistryGeneratorTest { val knownTags = contents(output).getValue("java/datadog/trace/api/KnownTags.java") assertThat(knownTags) .contains( - "COMPONENT_ID = 0x0001000000000000L", - "RESOURCE_NAME_ID = 0x0002000000000002L", - "SERVICE_ID = 0x0003000000000002L", + "COMPONENT_ID = 0x0001FFFF00000000L", + "RESOURCE_NAME_ID = 0x0002FFFF00000002L", + "SERVICE_ID = 0x0003FFFF00000002L", "public static final int SERVICE_SERIAL_NUM = 3;", "public static final int SERIAL_LIMIT = 4;" ) @@ -580,6 +580,44 @@ class TagRegistryGeneratorTest { .withMessageContaining(message) } + @Test + fun `tags on one span type get distinct slots and tags that never co-occur share one`() { + val yaml = directory.conventionsFile( + """ + trace_level: + tags: [{dd-name: env, required: required}] + span_types: + base: + abstract: true + tags: [{dd-name: service, required: required}] + http.server: + extends: base + span-kind: server + tags: [{dd-name: http.route, required: required}] + db.client: + extends: base + span-kind: client + tags: [{dd-name: db.type, required: required}, {dd-name: db.user}] + """ + ) + val output = File(directory, "generated") + + TagRegistryGenerator.generate(yaml, output) + + // service is on both span types, so it takes slot 0; http.route and db.type never share a span + // type, so both take slot 1; env is trace-level (its own TagMap) and reuses slot 0; db.user is + // optional, so it is stored by hash (no slot, 0xFFFF). + assertThat(contents(output).getValue("java/datadog/trace/api/KnownTags.java")) + .contains( + "DB_TYPE_ID = 0x0001000100000000L", + "DB_USER_ID = 0x0002FFFF00000000L", + "ENV_ID = 0x0003000000000004L", + "HTTP_ROUTE_ID = 0x0004000100000000L", + "SERVICE_ID = 0x0005000000000000L", + "public static final int SLOT_COUNT = 2;" + ) + } + private fun tagConventions(@Language("yaml") yamlText: String) = TagConventions.parse( ObjectMapper(YAMLFactory()).readValue( directory.conventionsFile(yamlText), diff --git a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java index ec49d4619bf..6f2cfe2d188 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -10,13 +10,13 @@ */ public final class KnownTagCodec { /* - * tagId bit layout: [63-48 serialNum (16 bits, 15 used: bit 63 is kept clear)] [47-32 reserved, - * zero] [31-0 flags]. Keeping bit 63 clear makes every id positive, so 0 (unknown) and the - * negative lookup sentinels below can never be ids; the generator enforces it. serialNum is - * globally unique per known tag and is the whole of the tag's identity — nameOf/ - * openTelemetryNameOf switch on it, and the generator emits each id as a literal. Bits [47-32] - * are RESERVED and always zero here: they are the window the dense tag store uses for its - * co-occurrence slot coordinate, which arrives with that store. Of the low 32 flag bits, bit 2 is + * tagId bit layout: [63-48 serialNum (16 bits, 15 used: bit 63 is kept clear)] [47-32 slot] + * [31-0 flags]. Keeping bit 63 clear makes every id positive, so 0 (unknown) and the negative + * lookup sentinels below can never be ids; the generator enforces it. serialNum is globally + * unique per known tag and is the whole of the tag's identity — nameOf/openTelemetryNameOf key on + * it, and the generator emits each id as a literal. Bits [47-32] hold the tag's dense-store slot + * ({@link #slotOf}), assigned by graph coloring so tags on one span type never share a slot, or + * {@link #NO_SLOT} for a tag stored by hash. Of the low 32 flag bits, bit 2 is * the trace/span LEVEL bit (set ⟹ trace-level), bit 1 is the INTERCEPTED bit, and bit 0 is the * SHARED_NAME bit. Unknown (string-only) custom tags are NOT known ids — {@code keyOf} returns 0 for * them. @@ -73,16 +73,40 @@ public static boolean hasSharedName(long tagId) { return (tagId & SHARED_NAME) != 0L; } + /** Bit position of the dense-store slot: bits [47-32] of an id. */ + public static final int SLOT_SHIFT = 32; + + /** The slot of a tag stored by hash rather than in a dense-store slot. */ + public static final int NO_SLOT = 0xFFFF; + + /** + * The tag's dense-store slot, or {@link #NO_SLOT}. The generator colors tags that appear together + * on a span type with distinct slots, so a span's slotted tags never collide. + */ + public static int slotOf(long tagId) { + return (int) (tagId >>> SLOT_SHIFT) & 0xFFFF; + } + + /** True if the tag has a dense-store slot, below {@link #slotCount()}. */ + public static boolean hasSlot(long tagId) { + return slotOf(tagId) != NO_SLOT; + } + + /** Dense-store slots in use: every slotted tag's slot is below this. */ + public static int slotCount() { + return KnownTags.SLOT_COUNT; + } + /** Returns the tagId with the {@link #LEVEL_TRACE} flag set. */ public static long traceLevel(long tagId) { return tagId | LEVEL_TRACE; } /** - * Builds a tagId from its {@code serialNum} (globally unique per known tag). The reserved [47-32] - * window and the low 32 bits are zero, so the id is fully determined by the serial — the - * generator emits it as a literal. Inverse of {@link #serialNum}. Intended for the code generator - * and tests. + * Builds an id carrying only {@code serialNum}: slot 0 and no flags, so it equals a tag's real id + * only when that tag has neither. Enough for serial-keyed lookups such as {@link #nameOf}; + * compare against a {@code KnownTags} constant, never against this. Inverse of {@link + * #serialNum}. Intended for tests and serial-keyed lookups. */ public static long makeTagId(int serialNum) { return (long) serialNum << 48; diff --git a/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java b/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java new file mode 100644 index 00000000000..7e4b8fa828c --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java @@ -0,0 +1,62 @@ +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.assertTrue; + +import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; + +/** {@link KnownTagCodec}'s dense-store slot: bits [47-32] of an id, assigned by graph coloring. */ +class KnownTagCodecSlotTest { + @Test + void everySlotIsBelowTheSlotCount() throws IllegalAccessException { + for (Field field : KnownTags.class.getFields()) { + if (field.getType() != long.class) { + continue; + } + long tagId = field.getLong(null); + if (KnownTagCodec.hasSlot(tagId)) { + assertTrue(KnownTagCodec.slotOf(tagId) < KnownTagCodec.slotCount(), field.getName()); + } else { + assertEquals(KnownTagCodec.NO_SLOT, KnownTagCodec.slotOf(tagId), field.getName()); + } + } + } + + @Test + void tagsOnAnHttpServerSpanHaveDistinctSlots() { + long[] httpServerTags = { + KnownTags.SERVICE_ID, + KnownTags.COMPONENT_ID, + KnownTags.SPAN_KIND_ID, + KnownTags.HTTP_METHOD_ID, + KnownTags.HTTP_URL_ID, + KnownTags.HTTP_STATUS_CODE_ID, + KnownTags.HTTP_ROUTE_ID, + KnownTags.HTTP_HOSTNAME_ID, + }; + Set slots = new HashSet<>(); + for (long tagId : httpServerTags) { + assertTrue(KnownTagCodec.hasSlot(tagId), Long.toHexString(tagId)); + assertTrue(slots.add(KnownTagCodec.slotOf(tagId)), Long.toHexString(tagId)); + } + } + + @Test + void anOptionalTagIsStoredByHash() { + assertFalse(KnownTagCodec.hasSlot(KnownTags.PEER_PORT_OUTBOUND_ID)); + } + + @Test + void theSlotDoesNotChangeTheSerialOrFlags() { + long tagId = KnownTags.HTTP_METHOD_ID; + + assertTrue(KnownTagCodec.hasSlot(tagId)); + assertEquals(KnownTags.HTTP_METHOD_NAME, KnownTagCodec.nameOf(tagId)); + assertTrue(KnownTagCodec.isIntercepted(tagId)); + assertFalse(KnownTagCodec.isTraceLevel(tagId)); + } +} diff --git a/tag-conventions.yaml b/tag-conventions.yaml index c4a5f9f9b70..71b2c85172d 100644 --- a/tag-conventions.yaml +++ b/tag-conventions.yaml @@ -2,9 +2,10 @@ # --------------------------------------------------------------------------- # The code generator consumes THIS file to emit each language's tag-id constants and its # id<->name resolver. A tag id is IDENTITY (a globally unique serial + the trace-level bit); -# storage layout (the dense store's slot assignment) and set-path routing (which keys the tracer -# intercepts into span fields or sampling directives) are per-language concerns that arrive with -# the code that consumes them, via a per-language overlay alongside this file. +# storage layout and set-path routing (which keys the tracer intercepts into span fields or +# sampling directives) are per-language concerns. A language derives its layout from the span types +# below -- dd-trace-java colors them into dense-store slots -- and declares its routing in a +# per-language overlay alongside this file. # # TRACE-LEVEL is its own thing (its own TagMap "type" on the TraceSegment) — the process/trace # constants + product flags that are set once per trace, NOT per span. Declared explicitly in the @@ -53,10 +54,10 @@ # that OTel only uses that name for this tag (db.system, say, only ever names a database). It is # interim, and goes away once resolution is direction-aware. A rename on a concrete type with no # span-kind still requires it. -# The id coordinate (group-decl / field-decl) is NOT authored here — the generator assigns it: each -# declaration source (the trace-level tier, each span type, each mixin) is a group, and within a -# group `field-decl` numbers the dense (required/conditional/recommended) tags; the rest are -# bucketed. See the design doc. +# Storage layout is NOT authored here. dd-trace-java's generator derives a dense-store slot for each +# required/conditional/recommended tag by graph coloring: tags resolved onto one concrete span type +# (or both trace-level) get distinct slots, and tags that never appear together may share one. The +# rest are stored by hash. # --------------------------------------------------------------------------- # Trace-level tier: its own TagMap on the TraceSegment. Set once per trace, not per span. From b1f191f2de4e6a24ae509efadaf910cdc1e1b43c Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 16:29:27 -0400 Subject: [PATCH 02/29] Add trace.experimental.dense.tags.enabled (default off) Selects TagMap's dense store for slotted known tags. Read once into a static final so the JIT folds the dense branches away when it is off. Co-Authored-By: Claude Opus 5.5 --- .../main/java/datadog/trace/api/config/TracerConfig.java | 6 ++++++ internal-api/src/main/java/datadog/trace/api/Config.java | 7 +++++++ metadata/supported-configurations.json | 8 ++++++++ 3 files changed, 21 insertions(+) diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java index d31137c3aef..2fa5365f935 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java @@ -190,5 +190,11 @@ public final class TracerConfig { public static final String TRACE_ORG_GUARD_STRICT = "trace.org.guard.strict"; public static final String TRACE_ORG_GUARD_TRUSTED_OPMS = "trace.org.guard.trusted.opms"; + /** + * Stores known tags that have a dense-store slot in TagMap's dense store (no per-tag entry) + * instead of its hash buckets. Experimental, off by default. + */ + public static final String TRACE_DENSE_TAGS_ENABLED = "trace.experimental.dense.tags.enabled"; + private TracerConfig() {} } diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index b1707b77a71..bbfe0dae8d7 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -703,6 +703,7 @@ import static datadog.trace.api.config.TracerConfig.TRACE_CLOUD_PAYLOAD_TAGGING_SERVICES; import static datadog.trace.api.config.TracerConfig.TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING; import static datadog.trace.api.config.TracerConfig.TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING; +import static datadog.trace.api.config.TracerConfig.TRACE_DENSE_TAGS_ENABLED; import static datadog.trace.api.config.TracerConfig.TRACE_EXPERIMENTAL_FEATURES_ENABLED; import static datadog.trace.api.config.TracerConfig.TRACE_GIT_METADATA_ENABLED; import static datadog.trace.api.config.TracerConfig.TRACE_HTTP_CLIENT_ERROR_STATUSES; @@ -1467,6 +1468,7 @@ public static String getHostName() { private final boolean jdkSocketEnabled; private final boolean spanBuilderReuseEnabled; + private final boolean traceDenseTagsEnabled; private final int tagNameUtf8CacheSize; private final int tagValueUtf8CacheSize; private final int stackTraceLengthLimit; @@ -3472,6 +3474,7 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment()) this.spanBuilderReuseEnabled = configProvider.getBoolean(GeneralConfig.SPAN_BUILDER_REUSE_ENABLED, true); + this.traceDenseTagsEnabled = configProvider.getBoolean(TRACE_DENSE_TAGS_ENABLED, false); this.tagNameUtf8CacheSize = Math.max(configProvider.getInteger(GeneralConfig.TAG_NAME_UTF8_CACHE_SIZE, 128), 0); this.tagValueUtf8CacheSize = @@ -5448,6 +5451,10 @@ public boolean isJdkSocketEnabled() { return jdkSocketEnabled; } + public boolean isTraceDenseTagsEnabled() { + return traceDenseTagsEnabled; + } + public boolean isSpanBuilderReuseEnabled() { return spanBuilderReuseEnabled; } diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 84ee1174006..d548e3b2a1e 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -5836,6 +5836,14 @@ "aliases": [] } ], + "DD_TRACE_EXPERIMENTAL_DENSE_TAGS_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": [] + } + ], "DD_TRACE_EXPERIMENTAL_FEATURES_ENABLED": [ { "version": "A", From ba98cee1a5817d34faff2f2d3e9467c535b79c69 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 16:29:28 -0400 Subject: [PATCH 03/29] Store slotted known tags densely in TagMap, behind a flag A known tag with a dense-store slot is held in insertion-ordered parallel arrays (knownIds/knownValues) with no per-tag Entry; custom tags and known tags without a slot stay in the hash buckets. One occupancy long (1L << slot) lets a definitely-absent tag skip the scan; the scan stays authoritative, so two tags sharing a slot in one map still work. Whether a tag routes dense depends only on its id and the flag, so the two regions are disjoint and read-through shadows within a region (parentDenseVisible mirrors parentEntryVisible). Iteration and forEach emit dense values through a reused flyweight; point reads materialize an Entry. Buckets start on a shared empty array and are copied on the first bucket write. Ported from #12045 and #12046 onto today's TagMap: name canonicalization, id-keyed setters, and per-direction shared-name lookups all route through the same chokepoints. Co-Authored-By: Claude Opus 5.5 --- .../main/java/datadog/trace/api/TagMap.java | 438 ++++++++++++++++-- 1 file changed, 412 insertions(+), 26 deletions(-) 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 0cd1dae06e9..203418845aa 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1203,10 +1203,59 @@ public EntryChange next() { * removed from the collision chain. */ - private final Object[] buckets; + /* + * Dense known-tag store (experimental; see DenseStore.ENABLED, default off). + *

+ * With the flag on, a known tag that has a dense-store slot (KnownTagCodec.hasSlot -- the + * registry colors required/conditional/recommended tags into slots so tags on one span type never + * share one) is NOT stored as a bucket Entry. Its value lives in the insertion-ordered parallel + * arrays knownIds/knownValues instead, with no per-tag Entry object: that is the allocation win. + * Known tags without a slot (NO_SLOT) and custom tags stay in the buckets, exactly as before. + *

+ * Presence: knownOccupancy holds 1L << slot for every dense id ever added. A clear bit means the + * tag is definitely absent, so an append or a miss skips the scan. A set bit only means "maybe": + * the knownIds scan is authoritative, so two tags sharing a slot in one map (legal -- tags from + * span types that never co-occur can still both be set) still work. Bits are never cleared on + * remove; a stale bit costs a scan, never a wrong answer. knownTraceLevel records whether any + * trace-level id was added (trace and span tags reuse slot numbers), with the same superset rule. + *

+ * Disjointness: whether an id routes dense depends only on the id and the global flag, so in a + * given JVM a tag is always dense or always bucketed -- never both, in any map. Read-through + * therefore shadows within a region: an ancestor's dense value can only be shadowed by a nearer + * level's dense value of the same id (parentDenseVisible), an ancestor's bucket entry only by a + * nearer bucket entry (parentEntryVisible, unchanged). Tombstones (removedFromParent) are keyed by + * canonical name and apply to both regions. + *

+ * size counts bucket entries only; knownCount counts dense entries; the local total is + * size + knownCount. Buckets start on the shared EMPTY_BUCKETS (when the flag is on) and are + * copied on the first bucket write, so an all-dense map allocates no bucket array. + *

+ * Iteration (iterator(), forEach) emits a dense entry through a reused EntryReadingHelper + * flyweight: valid only until the next element, so call .entry() to retain one. Point reads + * (getEntry) materialize a fresh Entry. + */ + + /** Holder for the dense-store flag, read once from Config so the JIT folds it. */ + private static final class DenseStore { + static final boolean ENABLED = Config.get().isTraceDenseTagsEnabled(); + } + + /** Shared all-null buckets (length 16) that maps start on when the dense store is enabled. */ + private static final Object[] EMPTY_BUCKETS = new Object[1 << 4]; + + /** Initial capacity of the dense arrays; the largest span type resolves about this many tags. */ + private static final int KNOWN_INIT_CAP = 12; + + private Object[] buckets; private int size; private boolean frozen; + private long[] knownIds; + private Object[] knownValues; + private int knownCount; + private long knownOccupancy; + private boolean knownTraceLevel; + /** * 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 @@ -1238,8 +1287,9 @@ public TagMap() { * optimizations can treat it as fixed. */ private TagMap(TagMap parent) { - // needs to be a power of 2 for bucket masking calculation to work as intended - this.buckets = new Object[1 << 4]; + // needs to be a power of 2 for bucket masking calculation to work as intended. With the dense + // store on, start on the shared empty buckets; the first bucket write copies them. + this.buckets = DenseStore.ENABLED ? EMPTY_BUCKETS : new Object[1 << 4]; this.size = 0; this.frozen = false; this.parent = parent; @@ -1256,8 +1306,9 @@ private TagMap(Object[] buckets, int size) { @Override public int size() { // Exact (Map contract). Under read-through resolves the union; prefer estimateSize() for hints. + int local = this.size + this.knownCount; // buckets + dense TagMap parent = this.parent; - return parent == null ? this.size : this.size + this.visibleParentCount(); + return parent == null ? local : local + this.visibleParentCount(); } /** @@ -1267,6 +1318,10 @@ public int size() { private int visibleParentCount() { int count = 0; for (TagMap ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { + long[] ancestorIds = ancestor.knownIds; + for (int i = 0; i < ancestor.knownCount; ++i) { + if (this.parentDenseVisible(ancestorIds[i], ancestor)) count++; + } Object[] parentBuckets = ancestor.buckets; for (int i = 0; i < parentBuckets.length; ++i) { Object parentBucket = parentBuckets[i]; @@ -1290,7 +1345,7 @@ private int visibleParentCount() { @Override public boolean isEmpty() { // Exact (Map contract). Under read-through resolves the parent; prefer isDefinitelyEmpty(). - if (this.size != 0) { + if (this.size != 0 || this.knownCount != 0) { return false; } TagMap parent = this.parent; @@ -1304,14 +1359,14 @@ public boolean isEmpty() { // entry -> this map is non-empty. return false; } - // size == 0 with tombstones (rare): empty iff every visible ancestor entry is tombstoned + // no local entries but tombstones (rare): empty iff every visible ancestor entry is tombstoned return this.visibleParentCount() == 0; } 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) { + if (level.size != 0 || level.knownCount != 0) { return false; } } @@ -1319,10 +1374,11 @@ public boolean isDefinitelyEmpty() { } public int estimateSize() { - // Upper bound: sum of every level's local size, ignoring read-through shadowing/removals. + // Upper bound: sum of every level's local size (buckets + dense), ignoring read-through + // shadowing/removals. int total = 0; for (TagMap level = this; level != null; level = level.parent) { - total += level.size; + total += level.size + level.knownCount; } return total; } @@ -1483,8 +1539,15 @@ private Entry getEntry(String canonicalTag, long tagHash) { return parent.getEntry(canonicalTag, tagHash); } - /** Looks up an entry in this map's own buckets only — no read-through to the parent. */ + /** Looks up an entry in this map's own storage only — no read-through to the parent. */ private Entry getLocalEntry(String tag, long tagHash) { + if (DenseStore.ENABLED) { + long denseId = denseIdOf(tagHash); + if (denseId != 0) { + int i = this.knownIndexOf(denseId); + return i < 0 ? null : Entry.newAnyEntry(denseId, this.knownValues[i]); + } + } Object[] thisBuckets = this.buckets; int hash = Entry.bucketHash(tagHash); return findInBucket(thisBuckets[hash & (thisBuckets.length - 1)], hash, tag); @@ -1532,6 +1595,116 @@ private boolean parentEntryVisible(Entry parentEntry, TagMap fromAncestor) { return true; } + // ---- dense known-tag store (see the class-level note above the fields) ---- + + /** + * The id to store densely for {@code tagHash} (an Entry's tag hash, or a tag id), or 0 when it + * stays in the buckets: a custom tag, a known tag with no slot, or the dense store disabled. + */ + private static long denseIdOf(long tagHash) { + // a known tag's hash is its id (upper 32 bits nonzero); a custom tag's upper 32 bits are zero + return (tagHash >>> 32) != 0 && KnownTagCodec.hasSlot(tagHash) ? tagHash : 0L; + } + + private static long slotBit(long denseId) { + return 1L << KnownTagCodec.slotOf(denseId); + } + + /** + * Index of {@code denseId} in the dense arrays, or -1; skips the scan when its slot bit is clear. + */ + private int knownIndexOf(long denseId) { + if ((this.knownOccupancy & slotBit(denseId)) == 0) { + return -1; // definitely absent + } + long[] ids = this.knownIds; + for (int i = 0; i < this.knownCount; ++i) { + if (ids[i] == denseId) return i; + } + return -1; + } + + /** + * Stores a dense value, replacing it in place when present (returning the prior value as an + * Entry, per the Map contract) or appending it. No Entry is retained. + */ + private Entry putKnownValue(long denseId, Object value) { + int i = this.knownIndexOf(denseId); + if (i >= 0) { + Object prior = this.knownValues[i]; + this.knownValues[i] = value; + return Entry.newAnyEntry(denseId, prior); + } + if (this.knownIds == null) { + this.knownIds = new long[KNOWN_INIT_CAP]; + this.knownValues = new Object[KNOWN_INIT_CAP]; + } else if (this.knownCount == this.knownIds.length) { + int newCap = this.knownIds.length << 1; + this.knownIds = Arrays.copyOf(this.knownIds, newCap); + this.knownValues = Arrays.copyOf(this.knownValues, newCap); + } + int idx = this.knownCount++; + this.knownIds[idx] = denseId; + this.knownValues[idx] = value; + this.knownOccupancy |= slotBit(denseId); + this.knownTraceLevel |= KnownTagCodec.isTraceLevel(denseId); + return null; + } + + /** Local dense write: clears any tombstone for the tag, then stores the value. */ + private Entry putDenseLocal(long denseId, Object value) { + this.checkWriteAccess(); + if (this.removedFromParent != null) { + this.removedFromParent.remove(KnownTagCodec.nameOf(denseId)); + } + return this.putKnownValue(denseId, value); + } + + /** Removes a dense value (swap with the last), returning it as an Entry, or null. */ + private Entry removeKnown(long denseId) { + int i = this.knownIndexOf(denseId); + if (i < 0) return null; + Object prior = this.knownValues[i]; + int last = --this.knownCount; + this.knownIds[i] = this.knownIds[last]; + this.knownValues[i] = this.knownValues[last]; + this.knownIds[last] = 0L; + this.knownValues[last] = null; + // knownOccupancy is not cleared: another present id may share the slot bit. + return Entry.newAnyEntry(denseId, prior); + } + + /** + * Whether an ancestor's dense value for {@code denseId} (held at {@code fromAncestor}) is visible + * from this leaf: not shadowed by a nearer level's dense value of the same id, and not tombstoned + * by a nearer level. The dense counterpart of {@link #parentEntryVisible}; by disjointness no + * bucket check is needed. A nearer level with no trace-level ids cannot shadow a trace-level one. + */ + private boolean parentDenseVisible(long denseId, TagMap fromAncestor) { + boolean traceLevel = KnownTagCodec.isTraceLevel(denseId); + String tag = null; // resolved lazily, only if a nearer level carries tombstones + for (TagMap nearer = this; nearer != fromAncestor; nearer = nearer.parent) { + if ((!traceLevel || nearer.knownTraceLevel) && nearer.knownIndexOf(denseId) >= 0) { + return false; // shadowed by a nearer dense value + } + if (nearer.removedFromParent != null) { + if (tag == null) tag = KnownTagCodec.nameOf(denseId); + if (nearer.removedFromParent.contains(tag)) return false; // tombstoned by a nearer level + } + } + return true; + } + + /** The buckets to write to, copying the shared empty buckets on the first write. */ + private Object[] writableBuckets() { + Object[] b = this.buckets; + if (DenseStore.ENABLED && b == EMPTY_BUCKETS) { + b = new Object[1 << 4]; + this.buckets = b; + } + return b; + } + @Deprecated @Override public Object put(@Nonnull String tag, Object value) { @@ -1739,30 +1912,58 @@ public void setKnown(long tagId, double value) { } public void set(long tagId, @Nonnull Object value) { + if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { + this.putDenseLocal(tagId, value); + return; + } this.putEntry(Entry.newAnyEntry(tagId, value)); } public void set(long tagId, @Nonnull CharSequence value) { + if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { + this.putDenseLocal(tagId, value); + return; + } this.putEntry(Entry.newObjectEntry(tagId, value)); } public void set(long tagId, boolean value) { + if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { + this.putDenseLocal(tagId, Boolean.valueOf(value)); + return; + } this.putEntry(Entry.newBooleanEntry(tagId, value)); } public void set(long tagId, int value) { + if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { + this.putDenseLocal(tagId, Integer.valueOf(value)); + return; + } this.putEntry(Entry.newIntEntry(tagId, value)); } public void set(long tagId, long value) { + if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { + this.putDenseLocal(tagId, Long.valueOf(value)); + return; + } this.putEntry(Entry.newLongEntry(tagId, value)); } public void set(long tagId, float value) { + if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { + this.putDenseLocal(tagId, Float.valueOf(value)); + return; + } this.putEntry(Entry.newFloatEntry(tagId, value)); } public void set(long tagId, double value) { + if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { + this.putDenseLocal(tagId, Double.valueOf(value)); + return; + } this.putEntry(Entry.newDoubleEntry(tagId, value)); } @@ -1800,6 +2001,12 @@ public Entry getAndSet(@Nullable Entry newEntry) { * top. */ private Entry putEntry(@Nonnull Entry newEntry) { + if (DenseStore.ENABLED) { + long denseId = denseIdOf(newEntry.tagHash); + if (denseId != 0) { + return this.putDenseLocal(denseId, newEntry.objectValue()); + } + } this.checkWriteAccess(); long newTagHash = newEntry.tagHash; @@ -1816,7 +2023,7 @@ private Entry putEntry(@Nonnull Entry newEntry) { this.removedFromParent.remove(newEntry.tag); } - Object[] thisBuckets = this.buckets; + Object[] thisBuckets = this.writableBuckets(); int newHash = newEntry.hash(); int bucketIndex = newHash & (thisBuckets.length - 1); @@ -1941,7 +2148,8 @@ private void putAllOptimizedMap(TagMap that) { that.forEach(this, (self, entry) -> self.set(entry)); return; } - if (this.size == 0) { + // empty must consider both regions: a map holding only dense values has size == 0 + if (this.size == 0 && this.knownCount == 0) { this.putAllIntoEmptyMap(that); } else { this.putAllMerge(that); @@ -1949,7 +2157,15 @@ private void putAllOptimizedMap(TagMap that) { } private void putAllMerge(TagMap that) { - Object[] thisBuckets = this.buckets; + // incoming dense values clobber existing ones, as incoming bucket entries do below + for (int i = 0; i < that.knownCount; ++i) { + this.putKnownValue(that.knownIds[i], that.knownValues[i]); + } + if (that.size == 0) { + return; // no bucket entries to merge; leaves shared empty buckets shared + } + + Object[] thisBuckets = this.writableBuckets(); Object[] thatBuckets = that.buckets; // Since TagMap-s don't support expansion, buckets are perfectly aligned @@ -2066,7 +2282,19 @@ private void putAllMerge(TagMap that) { * Specially optimized version of putAll for the common case of destination map being empty */ private void putAllIntoEmptyMap(TagMap that) { - Object[] thisBuckets = this.buckets; + if (that.knownCount > 0) { + // dense values are immutable objects, so the copy can share them + this.knownIds = Arrays.copyOf(that.knownIds, that.knownIds.length); + this.knownValues = Arrays.copyOf(that.knownValues, that.knownValues.length); + this.knownCount = that.knownCount; + this.knownOccupancy = that.knownOccupancy; + this.knownTraceLevel = that.knownTraceLevel; + } + if (DenseStore.ENABLED && that.size == 0) { + return; // no bucket entries; stay on the shared empty buckets + } + + Object[] thisBuckets = this.writableBuckets(); Object[] thatBuckets = that.buckets; // Check against both thisBuckets.length && thatBuckets.length is to help the JIT do bound check @@ -2113,6 +2341,9 @@ public void fillMap(Map map) { thisGroup.fillMapFromChain(map); } } + for (int i = 0; i < this.knownCount; ++i) { + map.put(KnownTagCodec.nameOf(this.knownIds[i]), this.knownValues[i]); + } } public void fillStringMap(Map stringMap) { @@ -2138,6 +2369,11 @@ public void fillStringMap(Map stringMap) { thisGroup.fillStringMapFromChain(stringMap); } } + for (int i = 0; i < this.knownCount; ++i) { + stringMap.put( + KnownTagCodec.nameOf(this.knownIds[i]), + TagValueConversions.toString(this.knownValues[i])); + } } @Override @@ -2211,8 +2447,14 @@ private Entry getAndRemove(String canonicalTag, long tagHash) { return localRemoved; } - /** Removes an entry from this map's own buckets only — no parent/tombstone handling. */ + /** Removes an entry from this map's own storage only — no parent/tombstone handling. */ private Entry removeLocal(String tag, long tagHash) { + if (DenseStore.ENABLED) { + long denseId = denseIdOf(tagHash); + if (denseId != 0) { + return this.removeKnown(denseId); + } + } Object[] thisBuckets = this.buckets; int hash = Entry.bucketHash(tagHash); @@ -2280,6 +2522,14 @@ public Stream stream() { @Override public void forEach(Consumer consumer) { + if (this.knownCount > 0) { + // dense values go through one reused flyweight -- no Entry per tag + EntryReadingHelper reader = new EntryReadingHelper(); + for (int i = 0; i < this.knownCount; ++i) { + reader.set(this.knownIds[i], this.knownValues[i]); + consumer.accept(reader); + } + } Object[] thisBuckets = this.buckets; for (int i = 0; i < thisBuckets.length; ++i) { @@ -2306,7 +2556,16 @@ public void forEach(Consumer 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. + EntryReadingHelper reader = null; for (TagMap ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { + for (int i = 0; i < ancestor.knownCount; ++i) { + long denseId = ancestor.knownIds[i]; + if (this.parentDenseVisible(denseId, ancestor)) { + if (reader == null) reader = new EntryReadingHelper(); + reader.set(denseId, ancestor.knownValues[i]); + consumer.accept(reader); + } + } Object[] parentBuckets = ancestor.buckets; for (int i = 0; i < parentBuckets.length; ++i) { Object parentBucket = parentBuckets[i]; @@ -2329,6 +2588,13 @@ private void forEachParent(Consumer consumer) { } public void forEach(T thisObj, BiConsumer consumer) { + if (this.knownCount > 0) { + EntryReadingHelper reader = new EntryReadingHelper(); + for (int i = 0; i < this.knownCount; ++i) { + reader.set(this.knownIds[i], this.knownValues[i]); + consumer.accept(thisObj, reader); + } + } Object[] thisBuckets = this.buckets; for (int i = 0; i < thisBuckets.length; ++i) { @@ -2352,7 +2618,16 @@ public void forEach(T thisObj, BiConsumer con } private void forEachParent(T thisObj, BiConsumer consumer) { + EntryReadingHelper reader = null; for (TagMap ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { + for (int i = 0; i < ancestor.knownCount; ++i) { + long denseId = ancestor.knownIds[i]; + if (this.parentDenseVisible(denseId, ancestor)) { + if (reader == null) reader = new EntryReadingHelper(); + reader.set(denseId, ancestor.knownValues[i]); + consumer.accept(thisObj, reader); + } + } Object[] parentBuckets = ancestor.buckets; for (int i = 0; i < parentBuckets.length; ++i) { Object parentBucket = parentBuckets[i]; @@ -2377,6 +2652,13 @@ private void forEachParent(T thisObj, BiConsumer void forEach( T thisObj, U otherObj, TriConsumer consumer) { + if (this.knownCount > 0) { + EntryReadingHelper reader = new EntryReadingHelper(); + for (int i = 0; i < this.knownCount; ++i) { + reader.set(this.knownIds[i], this.knownValues[i]); + consumer.accept(thisObj, otherObj, reader); + } + } Object[] thisBuckets = this.buckets; for (int i = 0; i < thisBuckets.length; ++i) { @@ -2401,7 +2683,16 @@ public void forEach( private void forEachParent( T thisObj, U otherObj, TriConsumer consumer) { + EntryReadingHelper reader = null; for (TagMap ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { + for (int i = 0; i < ancestor.knownCount; ++i) { + long denseId = ancestor.knownIds[i]; + if (this.parentDenseVisible(denseId, ancestor)) { + if (reader == null) reader = new EntryReadingHelper(); + reader.set(denseId, ancestor.knownValues[i]); + consumer.accept(thisObj, otherObj, reader); + } + } Object[] parentBuckets = ancestor.buckets; for (int i = 0; i < parentBuckets.length; ++i) { Object parentBucket = parentBuckets[i]; @@ -2428,8 +2719,17 @@ private void forEachParent( public void clear() { this.checkWriteAccess(); - Arrays.fill(this.buckets, null); + if (DenseStore.ENABLED) { + this.buckets = EMPTY_BUCKETS; // never write to the shared empty buckets + } else { + Arrays.fill(this.buckets, null); + } this.size = 0; + this.knownIds = null; + this.knownValues = null; + this.knownCount = 0; + this.knownOccupancy = 0L; + this.knownTraceLevel = false; // 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 // empty, parent-less map. Detach is one-way -- the parent is never re-pointed. @@ -2489,6 +2789,35 @@ void checkIntegrity() { } } + // dense store: every id slotted and unique, its slot bit set, the count within bounds + if (this.knownCount > 0) { + if (this.knownIds == null || this.knownCount > this.knownIds.length) { + throw new IllegalStateException("incorrect known count"); + } + for (int i = 0; i < this.knownCount; ++i) { + long denseId = this.knownIds[i]; + if (denseIdOf(denseId) == 0) { + throw new IllegalStateException("dense id without a slot"); + } + if ((this.knownOccupancy & slotBit(denseId)) == 0) { + throw new IllegalStateException("dense id missing from occupancy"); + } + for (int j = i + 1; j < this.knownCount; ++j) { + if (this.knownIds[j] == denseId) { + throw new IllegalStateException("duplicate dense id"); + } + } + } + } + // disjointness: with the dense store on, no bucket entry may be one that routes dense + if (DenseStore.ENABLED) { + for (EntryReader entry : this) { + if (entry instanceof Entry && denseIdOf(((Entry) entry).tagHash) != 0) { + throw new IllegalStateException("dense-routed tag stored in a bucket: " + entry.tag()); + } + } + } + if (this.size != this.computeSize()) { throw new IllegalStateException("incorrect size"); } @@ -2608,6 +2937,16 @@ String toInternalString() { } ledger.append('\n'); } + for (int i = 0; i < this.knownCount; ++i) { + ledger + .append("[dense ") + .append(i) + .append("] = {") + .append(KnownTagCodec.nameOf(this.knownIds[i])) + .append('=') + .append(this.knownValues[i]) + .append("}\n"); + } return ledger.toString(); } @@ -2619,7 +2958,13 @@ abstract static class IteratorBase { private TagMap level; private Object[] buckets; - private Entry nextEntry; + // A bucket entry is its own retain-safe Entry; a dense value is emitted through the reused + // denseReader flyweight, valid only until the next element (call .entry() to retain one). + private EntryReader nextEntry; + private EntryReadingHelper denseReader; + + // dense cursor for the current level; reset when moving to the next ancestor + private int knownIndex = 0; private int bucketIndex = -1; @@ -2639,9 +2984,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; } @@ -2653,9 +2998,9 @@ final Entry nextEntryOrThrowNoSuchElement() { } } - final Entry nextEntryOrNull() { + final EntryReader nextEntryOrNull() { if (this.nextEntry != null) { - Entry nextEntry = this.nextEntry; + EntryReader nextEntry = this.nextEntry; this.nextEntry = null; return nextEntry; } @@ -2663,8 +3008,23 @@ final Entry nextEntryOrNull() { return this.hasNext() ? this.nextEntry : null; } - private final Entry advance() { + private final EntryReader advance() { while (true) { + // the current level's dense values first, then its buckets + if (this.knownIndex < this.level.knownCount) { + int i = this.knownIndex++; + long denseId = this.level.knownIds[i]; + if (this.level == this.map || this.map.parentDenseVisible(denseId, this.level)) { + EntryReadingHelper reader = this.denseReader; + if (reader == null) { + reader = this.denseReader = new EntryReadingHelper(); + } + reader.set(denseId, this.level.knownValues[i]); + return reader; + } + continue; // ancestor dense value shadowed/tombstoned -> skip + } + Entry tagEntry = this.rawAdvance(); if (tagEntry != null) { // leaf entries emit as-is; ancestor entries only if visible from the leaf -- not shadowed @@ -2678,6 +3038,7 @@ private final Entry advance() { // current level exhausted; advance to the next ancestor's buckets (read-through union) if (this.level.parent != null) { this.level = this.level.parent; + this.knownIndex = 0; this.buckets = this.level.buckets; this.bucketIndex = -1; this.group = null; @@ -3217,9 +3578,23 @@ public boolean isEmpty() { @Override public Iterator> iterator() { - @SuppressWarnings({"rawtypes", "unchecked"}) - Iterator> iter = (Iterator) this.map.iterator(); - return iter; + return new EntriesIterator(this.map); + } + } + + /** + * entrySet() yields retain-safe Map.Entry objects: a bucket entry is returned as is, a dense + * value is materialized into a fresh Entry. + */ + static final class EntriesIterator extends IteratorBase + implements Iterator> { + EntriesIterator(TagMap map) { + super(map); + } + + @Override + public Map.Entry next() { + return this.nextEntryOrThrowNoSuchElement().entry(); } } @@ -3315,6 +3690,14 @@ void set(String tag, Object value) { this.tagId = TagMap.TAG_ID_NOT_COMPUTED; // resolve lazily via keyOf on first tagId() access } + /** A dense value: the id is already known, so it needs no lookup (and may name a shared name). */ + void set(long tagId, Object value) { + this.mapEntry = null; + this.tag = KnownTagCodec.nameOf(tagId); + this.value = value; + this.tagId = tagId; + } + void set(Map.Entry mapEntry) { this.mapEntry = mapEntry; this.tag = mapEntry.getKey(); @@ -3399,7 +3782,10 @@ public Object objectValue() { @Override public TagMap.Entry entry() { - return TagMap.anyEntryFor(this.tag, this.value); + long id = this.tagId; + return id != TagMap.TAG_ID_NOT_COMPUTED && id != 0 + ? TagMap.Entry.newAnyEntry(id, this.value) + : TagMap.anyEntryFor(this.tag, this.value); } @Override From a842f72e6f5c5d6542e1f956d7dfa56c0929ff04 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 16:49:56 -0400 Subject: [PATCH 04/29] Test and benchmark the dense tag store TagMapDenseForkedTest covers dense routing against the generated registry: slotted vs unslotted vs custom tags, id-keyed and OpenTelemetry-name writes, two tags sharing a slot in one map, iteration/forEach/entrySet (retain-safe entries), the shared empty buckets, copy/clear/putAll/ledger/freeze, and read-through (inheritance, shadowing, tombstones, multi-level chains). TagMapDenseFuzzForkedTest replays random actions against a HashMap oracle over slotted, unslotted, and custom keys. Both fork, since the flag is read once per JVM. DenseStoreAllocBenchmark: with the store on, a 12-tag map of slotted tags allocates 232 B/op instead of 856; custom tags are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../trace/api/DenseStoreAllocBenchmark.java | 150 ++++++ .../trace/api/TagMapDenseForkedTest.java | 487 ++++++++++++++++++ .../trace/api/TagMapDenseFuzzForkedTest.java | 180 +++++++ 3 files changed, 817 insertions(+) create mode 100644 internal-api/src/jmh/java/datadog/trace/api/DenseStoreAllocBenchmark.java create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapDenseFuzzForkedTest.java diff --git a/internal-api/src/jmh/java/datadog/trace/api/DenseStoreAllocBenchmark.java b/internal-api/src/jmh/java/datadog/trace/api/DenseStoreAllocBenchmark.java new file mode 100644 index 00000000000..d9906a27606 --- /dev/null +++ b/internal-api/src/jmh/java/datadog/trace/api/DenseStoreAllocBenchmark.java @@ -0,0 +1,150 @@ +package datadog.trace.api; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** + * Allocation per span-sized TagMap with the dense store enabled. Run with {@code -prof gc} and read + * {@code gc.alloc.rate.norm} (B/op): allocation is deterministic, throughput here is not tuned. + * + *

Inputs: {@code tagCount} tags per map, drawn from the slotted known tags of an HTTP server + * span (dense) and custom names (bucketed). {@code scenario} sets the mix: {@code custom} (all + * bucketed -- the baseline), {@code mixed} (about 70% slotted), {@code allSlotted}. {@code + * buildMapById} sets the slotted tags by id, the path instrumentation is moving to. + * + *

The flag is per JVM, so this class forks with it on. For the store-off baseline, run with + * {@code -jvmArgsAppend -Ddd.trace.experimental.dense.tags.enabled=false}. + * + *

Results, B/op ({@code buildMap}; {@code buildMapById} matched it in every case), Zulu 17.0.7, + * Apple Silicon, {@code -prof gc -f 1 -wi 1 -i 2}, 2026-10-09: + * + *

+ * scenario     tagCount   store off   store on
+ * custom              7         472        472
+ * custom             12         808        808
+ * mixed               7         520        456
+ * mixed              12         856        504
+ * allSlotted          7         568        232
+ * allSlotted         12         856        232
+ * 
+ * + *

Custom tags are unchanged. With the store on, a map of up to 12 slotted tags costs the same + * (the dense arrays start at 12), and {@code buildAndForEach} adds a flat 16 B/op (one reused + * flyweight), not a per-tag Entry. + */ +@State(Scope.Benchmark) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@Warmup(iterations = 2, time = 2) +@Measurement(iterations = 3, time = 2) +@Fork( + value = 1, + jvmArgsAppend = {"-Ddd.trace.experimental.dense.tags.enabled=true"}) +@Threads(1) +public class DenseStoreAllocBenchmark { + static final long[] SLOTTED_IDS = { + KnownTags.COMPONENT_ID, + KnownTags.SPAN_KIND_ID, + KnownTags.SERVICE_ID, + KnownTags.HTTP_METHOD_ID, + KnownTags.HTTP_URL_ID, + KnownTags.HTTP_ROUTE_ID, + KnownTags.HTTP_STATUS_CODE_ID, + KnownTags.HTTP_HOSTNAME_ID, + KnownTags.HTTP_USERAGENT_ID, + KnownTags.NETWORK_PROTOCOL_VERSION_ID, + KnownTags.DD_INTEGRATION_ID, + KnownTags.ERROR_TYPE_ID, + }; + + @Param({"custom", "mixed", "allSlotted"}) + String scenario; + + @Param({"7", "12"}) + int tagCount; + + private String[] names; + private long[] ids; // 0 for a custom tag + private String[] values; + + @Setup(Level.Trial) + public void setup() { + for (long id : SLOTTED_IDS) { + if (!KnownTagCodec.hasSlot(id)) { + throw new IllegalStateException(KnownTagCodec.nameOf(id) + " has no dense-store slot"); + } + } + int slotted; + if ("allSlotted".equals(scenario)) { + slotted = tagCount; + } else if ("mixed".equals(scenario)) { + slotted = (tagCount * 7) / 10; + } else { + slotted = 0; + } + this.names = new String[tagCount]; + this.ids = new long[tagCount]; + this.values = new String[tagCount]; + for (int i = 0; i < tagCount; i++) { + if (i < slotted) { + this.ids[i] = SLOTTED_IDS[i]; + this.names[i] = KnownTagCodec.nameOf(SLOTTED_IDS[i]); + } else { + this.names[i] = "custom.tag." + i; + } + this.values[i] = "value-" + i; + } + } + + @Benchmark + public TagMap buildMap() { + TagMap m = TagMap.create(); + for (int i = 0; i < tagCount; i++) { + m.set(names[i], values[i]); + } + return m; + } + + @Benchmark + public TagMap buildMapById() { + TagMap m = TagMap.create(); + for (int i = 0; i < tagCount; i++) { + long id = ids[i]; + if (id != 0) { + m.set(id, values[i]); + } else { + m.set(names[i], values[i]); + } + } + return m; + } + + @Benchmark + public void buildAndForEach(Blackhole bh) { + TagMap m = buildMap(); + m.forEach(reader -> bh.consume(reader.objectValue())); + bh.consume(m); + } + + @Benchmark + public void buildAndIterate(Blackhole bh) { + TagMap m = buildMap(); + for (TagMap.EntryReader reader : m) { + bh.consume(reader.objectValue()); + } + bh.consume(m); + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java new file mode 100644 index 00000000000..27b85472e53 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java @@ -0,0 +1,487 @@ +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.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 java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * TagMap's dense store, enabled. The flag is read once into a static final, so this runs in its own + * JVM ({@code ForkedTest}) with the flag set before TagMap first checks it. + * + *

A known tag with a dense-store slot is stored densely; a known tag without a slot and a custom + * tag stay in the buckets. Behavior must match the bucket-only store. + */ +class TagMapDenseForkedTest { + static { + System.setProperty("dd.trace.experimental.dense.tags.enabled", "true"); + } + + // slotted, span-level + static final String COMPONENT = KnownTags.COMPONENT_NAME; + static final String DB_TYPE = KnownTags.DB_TYPE_NAME; + static final String HTTP_ROUTE = KnownTags.HTTP_ROUTE_NAME; + // slotted, trace-level + static final String BASE_SERVICE = KnownTags.DD_BASE_SERVICE_NAME; + // known, no slot + static final String DB_POOL_NAME = KnownTags.DB_POOL_NAME; + // custom + static final String CUSTOM_A = "custom.tag.a"; + static final String CUSTOM_B = "custom.tag.b"; + + /** Two slotted span-level tags that the registry gave the same slot. */ + static long sharedSlotA; + + static long sharedSlotB; + + @BeforeAll + static void checkSetup() throws IllegalAccessException { + assertTrue(Config.get().isTraceDenseTagsEnabled(), "dense store must be on in this JVM"); + assertTrue(KnownTagCodec.hasSlot(KnownTags.COMPONENT_ID)); + assertTrue(KnownTagCodec.hasSlot(KnownTags.DD_BASE_SERVICE_ID)); + assertFalse(KnownTagCodec.hasSlot(KnownTags.DB_POOL_NAME_ID), "db.pool.name has no slot"); + + Map bySlot = new HashMap<>(); + for (Field field : KnownTags.class.getFields()) { + if (field.getType() != long.class) continue; + long id = field.getLong(null); + if (!KnownTagCodec.hasSlot(id) || KnownTagCodec.isTraceLevel(id)) continue; + if (KnownTagCodec.keyOf(KnownTagCodec.nameOf(id)) != id) continue; // skip shared names + Long other = bySlot.putIfAbsent(KnownTagCodec.slotOf(id), id); + if (other != null) { + sharedSlotA = other; + sharedSlotB = id; + return; + } + } + throw new AssertionError("no two slotted tags share a slot"); + } + + private static int knownCount(TagMap map) { + try { + Field f = TagMap.class.getDeclaredField("knownCount"); + f.setAccessible(true); + return f.getInt(map); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } + + private static Object field(Object target, String name) { + try { + Field f = TagMap.class.getDeclaredField(name); + f.setAccessible(true); + return f.get(target); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } + + @Test + void aSlottedKnownTagIsStoredDenselyAndRoundTrips() { + TagMap map = TagMap.create(); + map.set(COMPONENT, "spring-web"); + map.set(BASE_SERVICE, "billing"); + + assertEquals(2, knownCount(map)); + assertEquals("spring-web", map.getObject(COMPONENT)); + assertEquals("billing", map.getString(BASE_SERVICE)); + assertEquals("spring-web", map.getEntry(COMPONENT).objectValue()); + assertEquals(KnownTags.COMPONENT_ID, map.getEntry(COMPONENT).tagId()); + assertTrue(map.containsKey(COMPONENT)); + assertEquals(2, map.size()); + map.checkIntegrity(); + } + + @Test + void customAndUnslottedTagsStayInBuckets() { + TagMap map = TagMap.create(); + map.set(CUSTOM_A, "alpha"); + map.set(DB_POOL_NAME, "pool-1"); + + assertEquals(0, knownCount(map)); + assertEquals("alpha", map.getObject(CUSTOM_A)); + assertEquals("pool-1", map.getObject(DB_POOL_NAME)); + assertEquals(2, map.size()); + map.checkIntegrity(); + } + + @Test + void typedValuesRoundTrip() { + TagMap map = TagMap.create(); + map.set(KnownTags.HTTP_STATUS_CODE_NAME, 404); + map.set(KnownTags.HTTP_RESEND_COUNT_NAME, 3L); + map.set(KnownTags.DD_PROFILING_ENABLED_NAME, true); + map.set(KnownTags.HTTP_ROUTE_ID, (CharSequence) "/users/{id}"); + + assertEquals(404, map.getInt(KnownTags.HTTP_STATUS_CODE_NAME)); + assertEquals(3L, map.getLong(KnownTags.HTTP_RESEND_COUNT_NAME)); + assertTrue(map.getBoolean(KnownTags.DD_PROFILING_ENABLED_NAME)); + assertEquals("/users/{id}", map.getString(HTTP_ROUTE)); + assertEquals(4, knownCount(map)); + map.checkIntegrity(); + } + + @Test + void idKeyedSetGetAndRemove() { + TagMap map = TagMap.create(); + map.set(KnownTags.DB_TYPE_ID, "postgresql"); + + assertEquals("postgresql", map.getEntry(KnownTags.DB_TYPE_ID).objectValue()); + assertEquals("postgresql", map.getObject(DB_TYPE)); + assertEquals("postgresql", map.getAndRemove(KnownTags.DB_TYPE_ID).objectValue()); + assertNull(map.getEntry(KnownTags.DB_TYPE_ID)); + assertEquals(0, map.size()); + map.checkIntegrity(); + } + + @Test + void anOpenTelemetryNameLandsOnTheSameDenseValue() { + TagMap map = TagMap.create(); + map.set(KnownTags.HTTP_METHOD_OTEL_NAME, "GET"); + + assertEquals(1, knownCount(map)); + assertEquals("GET", map.getObject(KnownTags.HTTP_METHOD_NAME)); + assertEquals("GET", map.getObject(KnownTags.HTTP_METHOD_OTEL_NAME)); + map.set(KnownTags.HTTP_METHOD_NAME, "POST"); + assertEquals(1, map.size()); + assertEquals("POST", map.getObject(KnownTags.HTTP_METHOD_OTEL_NAME)); + } + + @Test + void twoTagsSharingASlotInOneMapBothWork() { + assertEquals(KnownTagCodec.slotOf(sharedSlotA), KnownTagCodec.slotOf(sharedSlotB)); + TagMap map = TagMap.create(); + map.set(sharedSlotA, "a"); + map.set(sharedSlotB, "b"); + + assertEquals(2, knownCount(map)); + assertEquals("a", map.getEntry(sharedSlotA).objectValue()); + assertEquals("b", map.getEntry(sharedSlotB).objectValue()); + + map.set(sharedSlotA, "a2"); + assertEquals(2, map.size()); + assertEquals("a2", map.getEntry(sharedSlotA).objectValue()); + + assertEquals("a2", map.getAndRemove(sharedSlotA).objectValue()); + assertNull(map.getEntry(sharedSlotA)); + assertEquals("b", map.getEntry(sharedSlotB).objectValue()); + map.checkIntegrity(); + } + + @Test + void overwriteReplacesInPlaceAndGetAndSetReturnsThePrior() { + TagMap map = TagMap.create(); + map.set(COMPONENT, "first"); + TagMap.Entry prior = map.getAndSet(COMPONENT, "second"); + + assertEquals("first", prior.objectValue()); + assertEquals("second", map.getObject(COMPONENT)); + assertEquals(1, map.size()); + map.checkIntegrity(); + } + + @Test + void removeClearsADenseValue() { + TagMap map = TagMap.create(); + map.set(COMPONENT, "web"); + map.set(CUSTOM_A, "alpha"); + + assertEquals("web", map.remove((Object) COMPONENT)); + assertNull(map.getObject(COMPONENT)); + assertFalse(map.remove(COMPONENT)); + assertEquals(1, map.size()); + map.checkIntegrity(); + } + + @Test + void forEachIteratorAndEntrySetEmitBothRegions() { + TagMap map = TagMap.create(); + map.set(COMPONENT, "web"); + map.set(BASE_SERVICE, "billing"); + map.set(CUSTOM_A, "alpha"); + map.set(DB_POOL_NAME, "pool-1"); + + Map viaForEach = new HashMap<>(); + map.forEach(reader -> viaForEach.put(reader.tag(), reader.objectValue())); + Map viaBiForEach = new HashMap<>(); + map.forEach(viaBiForEach, (m, reader) -> m.put(reader.tag(), reader.objectValue())); + Map viaIterator = new HashMap<>(); + for (TagMap.EntryReader reader : map) { + viaIterator.put(reader.tag(), reader.objectValue()); + } + Map viaEntrySet = new HashMap<>(map); + Map viaFill = new HashMap<>(); + map.fillMap(viaFill); + + assertEquals(4, viaForEach.size()); + assertEquals("web", viaForEach.get(COMPONENT)); + assertEquals(viaForEach, viaBiForEach); + assertEquals(viaForEach, viaIterator); + assertEquals(viaForEach, viaEntrySet); + assertEquals(viaForEach, viaFill); + assertEquals(4, map.keySet().size()); + assertTrue(map.values().contains("billing")); + } + + @Test + void entrySetEntriesAndEntryCopiesAreRetainSafe() { + TagMap map = TagMap.create(); + map.set(COMPONENT, "web"); + map.set(DB_TYPE, "h2"); + + List> entries = new ArrayList<>(map.entrySet()); + List copies = new ArrayList<>(); + for (TagMap.EntryReader reader : map) { + copies.add(reader.entry()); + } + + Map fromEntries = new HashMap<>(); + for (Map.Entry e : entries) fromEntries.put(e.getKey(), e.getValue()); + Map fromCopies = new HashMap<>(); + for (TagMap.Entry e : copies) fromCopies.put(e.tag(), e.objectValue()); + + assertEquals(2, fromEntries.size()); + assertEquals("web", fromEntries.get(COMPONENT)); + assertEquals("h2", fromEntries.get(DB_TYPE)); + assertEquals(fromEntries, fromCopies); + } + + @Test + void anAllDenseMapAllocatesNoBuckets() { + TagMap map = TagMap.create(); + map.set(COMPONENT, "web"); + map.set(DB_TYPE, "h2"); + Object emptyBuckets = field(null, "EMPTY_BUCKETS"); + + assertSame(emptyBuckets, field(map, "buckets")); + TagMap copy = map.copy(); + assertSame(emptyBuckets, field(copy, "buckets")); + + map.set(CUSTOM_A, "alpha"); + assertNotEquals(emptyBuckets, field(map, "buckets")); + for (Object bucket : (Object[]) emptyBuckets) { + assertNull(bucket, "the shared empty buckets were written"); + } + map.clear(); + assertSame(emptyBuckets, field(map, "buckets")); + } + + @Test + void copyIsIndependent() { + TagMap map = TagMap.create(); + map.set(COMPONENT, "web"); + map.set(CUSTOM_A, "alpha"); + + TagMap copy = map.copy(); + copy.set(COMPONENT, "grpc"); + copy.remove(CUSTOM_A); + + assertEquals("web", map.getObject(COMPONENT)); + assertEquals("alpha", map.getObject(CUSTOM_A)); + assertEquals("grpc", copy.getObject(COMPONENT)); + assertEquals(1, copy.size()); + map.checkIntegrity(); + copy.checkIntegrity(); + } + + @Test + void clearEmptiesTheDenseStore() { + TagMap map = TagMap.create(); + map.set(COMPONENT, "web"); + map.set(CUSTOM_A, "alpha"); + map.clear(); + + assertEquals(0, map.size()); + assertTrue(map.isEmpty()); + assertTrue(map.isDefinitelyEmpty()); + assertNull(map.getObject(COMPONENT)); + map.set(COMPONENT, "again"); + assertEquals("again", map.getObject(COMPONENT)); + map.checkIntegrity(); + } + + @Test + void putAllMergesTheDenseStore() { + TagMap src = TagMap.create(); + src.set(BASE_SERVICE, "billing"); + src.set(DB_TYPE, "h2"); + src.set(CUSTOM_A, "alpha"); + + TagMap dst = TagMap.create(); + dst.set(COMPONENT, "web"); + dst.set(BASE_SERVICE, "old"); + dst.putAll(src); + + assertEquals("billing", dst.getObject(BASE_SERVICE)); + assertEquals("h2", dst.getObject(DB_TYPE)); + assertEquals("web", dst.getObject(COMPONENT)); + assertEquals("alpha", dst.getObject(CUSTOM_A)); + assertEquals(4, dst.size()); + dst.checkIntegrity(); + + TagMap empty = TagMap.create(); + empty.putAll(src); + assertEquals(3, empty.size()); + assertEquals("h2", empty.getObject(DB_TYPE)); + empty.checkIntegrity(); + } + + @Test + void aLedgerBuildsADenseMap() { + TagMap map = TagMap.ledger().set(COMPONENT, "web").set(CUSTOM_A, "alpha").buildImmutable(); + + assertEquals(1, knownCount(map)); + assertEquals("web", map.getObject(COMPONENT)); + assertEquals("alpha", map.getObject(CUSTOM_A)); + assertThrows(IllegalStateException.class, () -> map.set(COMPONENT, "x")); + } + + @Test + void aFrozenDenseMapRejectsWrites() { + TagMap map = TagMap.create(); + map.set(COMPONENT, "web"); + TagMap frozen = map.immutableCopy(); + + assertThrows(IllegalStateException.class, () -> frozen.set(COMPONENT, "x")); + assertThrows(IllegalStateException.class, () -> frozen.set(KnownTags.DB_TYPE_ID, "x")); + assertThrows(IllegalStateException.class, () -> frozen.remove(COMPONENT)); + assertEquals("web", frozen.getObject(COMPONENT)); + } + + // ---- read-through: dense values in a frozen parent ---- + + private static TagMap frozenParent() { + TagMap parent = TagMap.create(); + parent.set(BASE_SERVICE, "billing"); // dense, trace-level + parent.set(COMPONENT, "web"); // dense + parent.set(CUSTOM_A, "alpha"); // bucket + return parent.freeze(); + } + + @Test + void aChildReadsThroughToItsParentsDenseValues() { + TagMap child = TagMap.createFromParent(frozenParent()); + child.set(DB_TYPE, "h2"); + child.set(CUSTOM_B, "beta"); + + assertEquals("billing", child.getObject(BASE_SERVICE)); + assertEquals("web", child.getObject(COMPONENT)); + assertEquals("alpha", child.getObject(CUSTOM_A)); + assertEquals("h2", child.getObject(DB_TYPE)); + assertEquals(5, child.size()); + assertEquals(5, child.estimateSize()); + + Map viaForEach = new HashMap<>(); + child.forEach(reader -> viaForEach.put(reader.tag(), reader.objectValue())); + Map viaIterator = new HashMap<>(); + for (TagMap.EntryReader reader : child) viaIterator.put(reader.tag(), reader.objectValue()); + Map viaFill = new HashMap<>(); + child.fillMap(viaFill); + + assertEquals(5, viaForEach.size()); + assertEquals(viaForEach, viaIterator); + assertEquals(viaForEach, viaFill); + child.checkIntegrity(); + } + + @Test + void aChildsDenseValueShadowsItsParents() { + TagMap child = TagMap.createFromParent(frozenParent()); + child.set(BASE_SERVICE, "shipping"); + child.set(COMPONENT, "grpc"); + + assertEquals("shipping", child.getObject(BASE_SERVICE)); + assertEquals("grpc", child.getObject(COMPONENT)); + assertEquals(3, child.size()); + + Map union = new HashMap<>(); + child.forEach(reader -> union.put(reader.tag(), reader.objectValue())); + assertEquals(3, union.size()); + assertEquals("shipping", union.get(BASE_SERVICE)); + assertEquals("grpc", union.get(COMPONENT)); + + List iterated = new ArrayList<>(); + for (TagMap.EntryReader reader : child) iterated.add(reader.tag()); + assertEquals(3, iterated.size()); + } + + @Test + void removingAParentsDenseValueTombstonesIt() { + TagMap child = TagMap.createFromParent(frozenParent()); + + TagMap.Entry removed = child.getAndRemove(BASE_SERVICE); + assertNotNull(removed); + assertEquals("billing", removed.objectValue()); + assertNull(child.getObject(BASE_SERVICE)); + assertEquals("web", child.getObject(COMPONENT)); + assertEquals(2, child.size()); + + Map union = new HashMap<>(); + child.forEach(reader -> union.put(reader.tag(), reader.objectValue())); + assertEquals(2, union.size()); + assertFalse(union.containsKey(BASE_SERVICE)); + + child.set(BASE_SERVICE, "restored"); // re-setting clears the tombstone + assertEquals("restored", child.getObject(BASE_SERVICE)); + assertEquals(3, child.size()); + child.checkIntegrity(); + } + + @Test + void getAndSetReturnsAParentsDenseValueAsThePrior() { + TagMap child = TagMap.createFromParent(frozenParent()); + + TagMap.Entry prior = child.getAndSet(COMPONENT, "grpc"); + assertNotNull(prior); + assertEquals("web", prior.objectValue()); + assertEquals("grpc", child.getObject(COMPONENT)); + } + + @Test + void putAllFromAReadThroughSourceCopiesTheUnion() { + TagMap child = TagMap.createFromParent(frozenParent()); + child.set(DB_TYPE, "h2"); + + TagMap dst = TagMap.create(); + dst.putAll(child); + + assertEquals(4, dst.size()); + assertEquals("billing", dst.getObject(BASE_SERVICE)); + assertEquals("h2", dst.getObject(DB_TYPE)); + assertEquals("alpha", dst.getObject(CUSTOM_A)); + dst.checkIntegrity(); + } + + @Test + void aMultiLevelChainResolvesNearestFirst() { + TagMap middle = TagMap.createFromParent(frozenParent()); + middle.set(COMPONENT, "middle"); + middle.remove(BASE_SERVICE); + TagMap leaf = TagMap.createFromParent(middle.freeze()); + leaf.set(DB_TYPE, "leaf"); + + assertEquals("middle", leaf.getObject(COMPONENT)); + assertNull(leaf.getObject(BASE_SERVICE)); + assertEquals("leaf", leaf.getObject(DB_TYPE)); + assertEquals(3, leaf.size()); + + Map union = new HashMap<>(); + leaf.forEach(reader -> union.put(reader.tag(), reader.objectValue())); + assertEquals(3, union.size()); + assertEquals("middle", union.get(COMPONENT)); + assertFalse(union.containsKey(BASE_SERVICE)); + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapDenseFuzzForkedTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapDenseFuzzForkedTest.java new file mode 100644 index 00000000000..0f3762a3908 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapDenseFuzzForkedTest.java @@ -0,0 +1,180 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.api.TagMapFuzzTest.MapAction; +import datadog.trace.api.TagMapFuzzTest.TestCase; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Supplier; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Fuzzes TagMap with the dense store enabled, replaying random action sequences against a {@code + * HashMap} oracle through {@link TagMapFuzzTest}'s machinery (which also runs {@code + * checkIntegrity} after each step). + * + *

Keys come from three pools: slotted known tags (dense), known tags without a slot, and custom + * tags (both bucketed). Known tags are drawn by their canonical Datadog name, excluding names + * shared by a tag per direction, so the oracle's keys match TagMap's. The slotted pool is larger + * than the dense arrays' initial capacity, so growth is exercised, and includes tags that share a + * slot. Forked because the flag is read once per JVM. + */ +class TagMapDenseFuzzForkedTest { + static { + System.setProperty("dd.trace.experimental.dense.tags.enabled", "true"); + } + + static final int SINGLE_MAP_CASES = 1500; + static final int MERGE_CASES = 400; + static final int MAX_ACTIONS = 40; + static final int MIN_ACTIONS = 8; + static final int CUSTOM_SPACE = 48; + + static final List SLOTTED = new ArrayList<>(); + static final List UNSLOTTED = new ArrayList<>(); + + enum Regime { + DENSE_ONLY, + BUCKET_ONLY, + MIXED + } + + @BeforeAll + static void collectKeys() throws IllegalAccessException { + assertTrue(Config.get().isTraceDenseTagsEnabled(), "dense store must be on in this JVM"); + Set slotted = new LinkedHashSet<>(); + Set unslotted = new LinkedHashSet<>(); + for (Field field : KnownTags.class.getFields()) { + if (field.getType() != long.class) continue; + long id = field.getLong(null); + String name = KnownTagCodec.nameOf(id); + if (name == null || KnownTagCodec.keyOf(name) != id) continue; // shared per-direction name + (KnownTagCodec.hasSlot(id) ? slotted : unslotted).add(name); + } + SLOTTED.addAll(slotted); + UNSLOTTED.addAll(unslotted); + assertTrue(SLOTTED.size() > 12, "slotted pool should outgrow the dense arrays' initial size"); + assertTrue(!UNSLOTTED.isEmpty(), "need known tags without a slot"); + } + + @Test + void denseOnlyFuzz() { + runRegime(Regime.DENSE_ONLY); + } + + @Test + void bucketOnlyFuzz() { + runRegime(Regime.BUCKET_ONLY); + } + + @Test + void mixedFuzz() { + runRegime(Regime.MIXED); + } + + private static void runRegime(Regime regime) { + for (int i = 0; i < SINGLE_MAP_CASES; ++i) { + TagMapFuzzTest.test(generateTest(regime)); + } + for (int i = 0; i < MERGE_CASES; ++i) { + TagMap mapA = TagMapFuzzTest.test(generateTest(regime)); + TagMap mapB = TagMapFuzzTest.test(generateTest(regime)); + + HashMap hashA = new HashMap<>(mapA); + HashMap hashB = new HashMap<>(mapB); + + mapA.putAll(mapB); + hashA.putAll(hashB); + + TagMapFuzzTest.assertMapEquals(hashA, mapA); + } + } + + private static TestCase generateTest(Regime regime) { + ThreadLocalRandom r = ThreadLocalRandom.current(); + int numActions = r.nextInt(MAX_ACTIONS - MIN_ACTIONS) + MIN_ACTIONS; + List actions = new ArrayList<>(numActions); + for (int i = 0; i < numActions; ++i) { + actions.add(randomAction(regime)); + } + return new TestCase(actions); + } + + private static MapAction randomAction(Regime regime) { + switch (randomChoice(0.02, 0.1, 0.2)) { + case 0: + return TagMapFuzzTest.clear(); + case 1: + return choose( + () -> TagMapFuzzTest.putAll(randomKeysAndValues(regime)), + () -> TagMapFuzzTest.putAllTagMap(randomKeysAndValues(regime)), + () -> TagMapFuzzTest.putAllLedger(randomKeysAndValues(regime))); + case 2: + return choose( + () -> TagMapFuzzTest.remove(randomKey(regime)), + () -> TagMapFuzzTest.removeLight(randomKey(regime)), + () -> TagMapFuzzTest.getAndRemove(randomKey(regime))); + default: + return choose( + () -> TagMapFuzzTest.put(randomKey(regime), randomValue()), + () -> TagMapFuzzTest.set(randomKey(regime), randomValue()), + () -> TagMapFuzzTest.getAndSet(randomKey(regime), randomValue())); + } + } + + private static String randomKey(Regime regime) { + ThreadLocalRandom r = ThreadLocalRandom.current(); + boolean dense; + switch (regime) { + case DENSE_ONLY: + dense = true; + break; + case BUCKET_ONLY: + dense = false; + break; + default: + dense = r.nextBoolean(); + } + if (dense) { + return SLOTTED.get(r.nextInt(SLOTTED.size())); + } + return r.nextInt(4) == 0 + ? UNSLOTTED.get(r.nextInt(UNSLOTTED.size())) + : "custom-" + r.nextInt(CUSTOM_SPACE); + } + + private static String randomValue() { + return "values-" + ThreadLocalRandom.current().nextInt(); + } + + private static String[] randomKeysAndValues(Regime regime) { + int numEntries = ThreadLocalRandom.current().nextInt(SLOTTED.size() + CUSTOM_SPACE); + String[] keysAndValues = new String[numEntries << 1]; + for (int i = 0; i < keysAndValues.length; i += 2) { + keysAndValues[i] = randomKey(regime); + keysAndValues[i + 1] = randomValue(); + } + return keysAndValues; + } + + private static int randomChoice(double... proportions) { + double selector = ThreadLocalRandom.current().nextDouble(); + for (int i = 0; i < proportions.length; ++i) { + if (selector < proportions[i]) return i; + selector -= proportions[i]; + } + return proportions.length; + } + + @SafeVarargs + private static MapAction choose(Supplier... choices) { + return choices[ThreadLocalRandom.current().nextInt(choices.length)].get(); + } +} From 2fbe55b568a1e98767787de077959db66c2dd6a3 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 9 Oct 2026 22:34:28 -0400 Subject: [PATCH 05/29] Store dense tags without building an Entry for the replaced value putKnownValue built an Entry for the value it replaced, which set(long, ...) then discarded; the 0 B/op on overwrite held only because C2's escape analysis removed it. Split the store: storeKnownValue/storeDenseLocal return the raw prior value (dense values are never null) and build nothing, and only putDenseLocal -- the getAndSet path, which must return the prior entry -- materializes one. With -XX:-DoEscapeAnalysis, SetTagBenchmark's dense overwrite drops from 48 to 0 B/op. Co-Authored-By: Claude Opus 5.5 --- .../main/java/datadog/trace/api/TagMap.java | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) 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 203418845aa..379928e4002 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1625,15 +1625,16 @@ private int knownIndexOf(long denseId) { } /** - * Stores a dense value, replacing it in place when present (returning the prior value as an - * Entry, per the Map contract) or appending it. No Entry is retained. + * Stores a dense value, replacing it in place when present or appending it, and returns the prior + * value, or null when there was none. Dense values are never null. Builds no Entry, so a caller + * that discards the result allocates nothing, whether or not the JIT inlines this. */ - private Entry putKnownValue(long denseId, Object value) { + private Object storeKnownValue(long denseId, Object value) { int i = this.knownIndexOf(denseId); if (i >= 0) { Object prior = this.knownValues[i]; this.knownValues[i] = value; - return Entry.newAnyEntry(denseId, prior); + return prior; } if (this.knownIds == null) { this.knownIds = new long[KNOWN_INIT_CAP]; @@ -1651,13 +1652,22 @@ private Entry putKnownValue(long denseId, Object value) { return null; } - /** Local dense write: clears any tombstone for the tag, then stores the value. */ - private Entry putDenseLocal(long denseId, Object value) { + /** + * Local dense write: clears any tombstone for the tag, then stores the value, returning the prior + * value or null. The {@code set(long, ...)} methods use this; it builds no Entry. + */ + private Object storeDenseLocal(long denseId, Object value) { this.checkWriteAccess(); if (this.removedFromParent != null) { this.removedFromParent.remove(KnownTagCodec.nameOf(denseId)); } - return this.putKnownValue(denseId, value); + return this.storeKnownValue(denseId, value); + } + + /** {@link #storeDenseLocal}, returning the prior value as an Entry, per the Map contract. */ + private Entry putDenseLocal(long denseId, Object value) { + Object prior = this.storeDenseLocal(denseId, value); + return prior == null ? null : Entry.newAnyEntry(denseId, prior); } /** Removes a dense value (swap with the last), returning it as an Entry, or null. */ @@ -1913,7 +1923,7 @@ public void setKnown(long tagId, double value) { public void set(long tagId, @Nonnull Object value) { if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { - this.putDenseLocal(tagId, value); + this.storeDenseLocal(tagId, value); return; } this.putEntry(Entry.newAnyEntry(tagId, value)); @@ -1921,7 +1931,7 @@ public void set(long tagId, @Nonnull Object value) { public void set(long tagId, @Nonnull CharSequence value) { if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { - this.putDenseLocal(tagId, value); + this.storeDenseLocal(tagId, value); return; } this.putEntry(Entry.newObjectEntry(tagId, value)); @@ -1929,7 +1939,7 @@ public void set(long tagId, @Nonnull CharSequence value) { public void set(long tagId, boolean value) { if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { - this.putDenseLocal(tagId, Boolean.valueOf(value)); + this.storeDenseLocal(tagId, Boolean.valueOf(value)); return; } this.putEntry(Entry.newBooleanEntry(tagId, value)); @@ -1937,7 +1947,7 @@ public void set(long tagId, boolean value) { public void set(long tagId, int value) { if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { - this.putDenseLocal(tagId, Integer.valueOf(value)); + this.storeDenseLocal(tagId, Integer.valueOf(value)); return; } this.putEntry(Entry.newIntEntry(tagId, value)); @@ -1945,7 +1955,7 @@ public void set(long tagId, int value) { public void set(long tagId, long value) { if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { - this.putDenseLocal(tagId, Long.valueOf(value)); + this.storeDenseLocal(tagId, Long.valueOf(value)); return; } this.putEntry(Entry.newLongEntry(tagId, value)); @@ -1953,7 +1963,7 @@ public void set(long tagId, long value) { public void set(long tagId, float value) { if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { - this.putDenseLocal(tagId, Float.valueOf(value)); + this.storeDenseLocal(tagId, Float.valueOf(value)); return; } this.putEntry(Entry.newFloatEntry(tagId, value)); @@ -1961,7 +1971,7 @@ public void set(long tagId, float value) { public void set(long tagId, double value) { if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { - this.putDenseLocal(tagId, Double.valueOf(value)); + this.storeDenseLocal(tagId, Double.valueOf(value)); return; } this.putEntry(Entry.newDoubleEntry(tagId, value)); @@ -2159,7 +2169,7 @@ private void putAllOptimizedMap(TagMap that) { private void putAllMerge(TagMap that) { // incoming dense values clobber existing ones, as incoming bucket entries do below for (int i = 0; i < that.knownCount; ++i) { - this.putKnownValue(that.knownIds[i], that.knownValues[i]); + this.storeKnownValue(that.knownIds[i], that.knownValues[i]); } if (that.size == 0) { return; // no bucket entries to merge; leaves shared empty buckets shared From 30869a29203323fe7c085f814b0a3816934dea81 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sat, 10 Oct 2026 16:04:19 -0400 Subject: [PATCH 06/29] Route the core's trusted id writes to the dense store Internals.setKnown built an Entry and sent it through putEntry, whose dense routing then built another for the replaced value: two throwaway allocations on every span id write once the span moved to internals(). Check for a dense id first and store straight into the dense arrays, as set(long, ...) does. SetTagBenchmark with the store on: 0 B/op, and 16 B/op (just the Internals view) with -XX:-DoEscapeAnalysis. Co-Authored-By: Claude Opus 5.5 --- .../main/java/datadog/trace/api/TagMap.java | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) 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 379928e4002..d8c02b6bd34 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1888,35 +1888,64 @@ private Internals(TagMap map) { /* * Sets a known tag by id without validating it, unlike TagMap.set(long, ...): the caller has - * already checked KnownTagCodec.isKnown(tagId). + * already checked KnownTagCodec.isKnown(tagId). A slotted tag goes straight to the dense store, + * as in set(long, ...), so no Entry is built. */ public void setKnown(long tagId, @Nonnull Object value) { + if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { + this.map.storeDenseLocal(tagId, value); + return; + } this.map.putEntry(new Entry(tagId, null, Entry.ANY, 0L, value)); } public void setKnown(long tagId, @Nonnull CharSequence value) { + if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { + this.map.storeDenseLocal(tagId, value); + return; + } this.map.putEntry(new Entry(tagId, null, EntryReader.OBJECT, 0L, value)); } public void setKnown(long tagId, boolean value) { + if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { + this.map.storeDenseLocal(tagId, Boolean.valueOf(value)); + return; + } this.map.putEntry( new Entry( tagId, null, EntryReader.BOOLEAN, Entry.boolean2Prim(value), Boolean.valueOf(value))); } public void setKnown(long tagId, int value) { + if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { + this.map.storeDenseLocal(tagId, Integer.valueOf(value)); + return; + } this.map.putEntry(new Entry(tagId, null, EntryReader.INT, Entry.int2Prim(value), null)); } public void setKnown(long tagId, long value) { + if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { + this.map.storeDenseLocal(tagId, Long.valueOf(value)); + return; + } this.map.putEntry(new Entry(tagId, null, EntryReader.LONG, Entry.long2Prim(value), null)); } public void setKnown(long tagId, float value) { + if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { + this.map.storeDenseLocal(tagId, Float.valueOf(value)); + return; + } this.map.putEntry(new Entry(tagId, null, EntryReader.FLOAT, Entry.float2Prim(value), null)); } public void setKnown(long tagId, double value) { + if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { + this.map.storeDenseLocal(tagId, Double.valueOf(value)); + return; + } this.map.putEntry(new Entry(tagId, null, EntryReader.DOUBLE, Entry.double2Prim(value), null)); } } From c464afa27c56ea93bcb08278e632e2eb8b7f24a7 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sat, 10 Oct 2026 16:32:45 -0400 Subject: [PATCH 07/29] Store every known tag densely, and give every modeled tag a slot With the dense store on, every known tag now lives in the dense arrays -- denseIdOf means 'is a known id' -- so the id-keyed setters (set(long, ...) and Internals.setKnown) never reach the hash path: with the flag a static final, putEntry isn't compiled into them at all. The public set(long, ...) checks isKnown itself, since the dense path skips requireKnownName, and the dense write now applies the shared-name rule (an id write supersedes a value held under the shared name). The generator colors every tag resolved onto a modeled span type, whatever its requirement level (24 slots today). Only tags on no span type -- the overlay's routing-only keys and the CI tags of the unmodeled test type -- keep NO_SLOT; a long shift maps NO_SLOT to bit 63, which slots now stay below, so slotBit needs no branch and those tags share that presence bit. Co-Authored-By: Claude Opus 5.5 --- .../buildlogic/tagRegistry/TagRegistry.kt | 13 ++-- .../tagRegistry/TagRegistryGeneratorTest.kt | 12 +-- .../main/java/datadog/trace/api/TagMap.java | 74 ++++++++++++------- .../trace/api/KnownTagCodecSlotTest.java | 18 ++++- .../trace/api/TagMapDenseForkedTest.java | 11 +-- .../trace/api/TagMapDenseFuzzForkedTest.java | 9 ++- tag-conventions.yaml | 5 +- 7 files changed, 90 insertions(+), 52 deletions(-) diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt index c6961904735..d0d0b3eda02 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt @@ -12,8 +12,9 @@ package datadog.buildlogic.tagRegistry * trace-level tier is its own clique (it lives in a separate TagMap). Tags that never share a clique * may share a slot -- a tag declared per direction (peer.port inbound and outbound) typically does -- * so the slot count stays bounded by the largest clique, small enough for one occupancy `long` in - * the dense store. Only tags at a required/conditional/recommended level are slotted; the rest get - * [NO_SLOT] and are stored by hash. + * the dense store. Every tag resolved onto a span type, or trace-level, is slotted whatever its + * requirement level; a tag on no modeled span type (the tracer overlay's routing-only keys, tags of + * unmodeled span types) gets [NO_SLOT], and the dense store gives those one shared presence bit. * *

The intercepted bit marks a tag the tracer may route on the set-path (to a span field or a * sampling directive instead of tag storage), as listed by the tracer overlay. A setter called with @@ -59,10 +60,8 @@ class TagRegistry private constructor(val tags: List) { const val SLOT_SHIFT = 32 // slot window [47-32]; mirrors KnownTagCodec.SLOT_SHIFT const val NO_SLOT = 0xFFFF // all-ones slot: stored by hash; mirrors KnownTagCodec.NO_SLOT - const val MAX_SLOTS = 64 // one occupancy long in the dense store + const val MAX_SLOTS = 63 // one occupancy long in the dense store; bit 63 is shared by unslotted tags - /** Requirement levels whose tags get a dense-store slot. */ - val SLOTTED_LEVELS = setOf("required", "conditional", "recommended") /** * Serials stay below 2^15, so bit 63 of an id is never set: every id is positive, clear of 0 @@ -132,9 +131,9 @@ class TagRegistry private constructor(val tags: List) { private fun colorSlots(conv: TagConventions): Map { val cliques = ArrayList>() for (type in conv.concreteTypes()) { - cliques.add(conv.resolve(type).filter { it.required in SLOTTED_LEVELS }.map { it.identity }.toSet()) + cliques.add(conv.resolve(type).map { it.identity }.toSet()) } - cliques.add(conv.traceLevelTags().filter { it.required in SLOTTED_LEVELS }.map { it.identity }.toSet()) + cliques.add(conv.traceLevelTags().map { it.identity }.toSet()) val neighbors = HashMap>() for (clique in cliques) { diff --git a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt index 1ab58c355b7..e5cdaa68b72 100644 --- a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt +++ b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt @@ -60,7 +60,7 @@ class TagRegistryGeneratorTest { assertThat(first.getValue("tag-assignment.txt")) .contains("tags=6", "test.name", "service.name") assertThat(first.getValue("java/datadog/trace/api/KnownTags.java")) - .contains("ENV_ID = 0x0001FFFF00000004L", "TEST_NAME", "http.request.method") + .contains("ENV_ID = 0x0001000000000004L", "TEST_NAME", "http.request.method") } @TableTest( @@ -604,17 +604,17 @@ class TagRegistryGeneratorTest { TagRegistryGenerator.generate(yaml, output) - // service is on both span types, so it takes slot 0; http.route and db.type never share a span - // type, so both take slot 1; env is trace-level (its own TagMap) and reuses slot 0; db.user is - // optional, so it is stored by hash (no slot, 0xFFFF). + // service is on both span types, so it takes slot 0; db.type and db.user share db.client, so + // they take distinct slots, and http.route -- never on a span type with either -- reuses one of + // them; env is trace-level (its own TagMap) and reuses slot 0. Every level is slotted. assertThat(contents(output).getValue("java/datadog/trace/api/KnownTags.java")) .contains( "DB_TYPE_ID = 0x0001000100000000L", - "DB_USER_ID = 0x0002FFFF00000000L", + "DB_USER_ID = 0x0002000200000000L", "ENV_ID = 0x0003000000000004L", "HTTP_ROUTE_ID = 0x0004000100000000L", "SERVICE_ID = 0x0005000000000000L", - "public static final int SLOT_COUNT = 2;" + "public static final int SLOT_COUNT = 3;" ) } 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 d8c02b6bd34..4fdfc13d1c1 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1598,15 +1598,22 @@ private boolean parentEntryVisible(Entry parentEntry, TagMap fromAncestor) { // ---- dense known-tag store (see the class-level note above the fields) ---- /** - * The id to store densely for {@code tagHash} (an Entry's tag hash, or a tag id), or 0 when it - * stays in the buckets: a custom tag, a known tag with no slot, or the dense store disabled. + * The id to store densely for {@code tagHash} (an Entry's tag hash, or a tag id), or 0 for a + * custom tag, which stays in the buckets. Every known tag is dense, slotted or not, so an + * id-keyed write never reaches the hash path. */ private static long denseIdOf(long tagHash) { // a known tag's hash is its id (upper 32 bits nonzero); a custom tag's upper 32 bits are zero - return (tagHash >>> 32) != 0 && KnownTagCodec.hasSlot(tagHash) ? tagHash : 0L; + return (tagHash >>> 32) != 0 ? tagHash : 0L; } - private static long slotBit(long denseId) { + /** + * A dense id's occupancy bit. The bit only rules a tag out quickly; the scan decides, so ids may + * share one. No branch for a tag without a slot: a long shift uses only the low 6 bits of its + * count, so {@code NO_SLOT} (0xFFFF) lands on bit 63, which the generator reserves (real slots + * stay below 63) and every unslotted tag shares. + */ + static long slotBit(long denseId) { return 1L << KnownTagCodec.slotOf(denseId); } @@ -1656,8 +1663,25 @@ private Object storeKnownValue(long denseId, Object value) { * Local dense write: clears any tombstone for the tag, then stores the value, returning the prior * value or null. The {@code set(long, ...)} methods use this; it builds no Entry. */ + /** + * {@code tagId}, when it names a known tag; the checked id-keyed setters reject anything else. + */ + private static long requireKnownId(long tagId) { + if (!KnownTagCodec.isKnown(tagId)) { + throw new IllegalArgumentException("not a known tag id: " + Long.toHexString(tagId)); + } + return tagId; + } + private Object storeDenseLocal(long denseId, Object value) { this.checkWriteAccess(); + if (KnownTagCodec.hasSharedName(denseId)) { + // One direction of a tag declared per direction, written by id, supersedes a value written + // under the shared name alone (before a span knew its direction), so the map never holds + // both. Folds away for a constant id. + String sharedName = KnownTagCodec.nameOf(denseId); + this.removeLocal(sharedName, Entry.customHash(sharedName)); + } if (this.removedFromParent != null) { this.removedFromParent.remove(KnownTagCodec.nameOf(denseId)); } @@ -1892,7 +1916,7 @@ private Internals(TagMap map) { * as in set(long, ...), so no Entry is built. */ public void setKnown(long tagId, @Nonnull Object value) { - if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { + if (DenseStore.ENABLED) { this.map.storeDenseLocal(tagId, value); return; } @@ -1900,7 +1924,7 @@ public void setKnown(long tagId, @Nonnull Object value) { } public void setKnown(long tagId, @Nonnull CharSequence value) { - if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { + if (DenseStore.ENABLED) { this.map.storeDenseLocal(tagId, value); return; } @@ -1908,7 +1932,7 @@ public void setKnown(long tagId, @Nonnull CharSequence value) { } public void setKnown(long tagId, boolean value) { - if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { + if (DenseStore.ENABLED) { this.map.storeDenseLocal(tagId, Boolean.valueOf(value)); return; } @@ -1918,7 +1942,7 @@ public void setKnown(long tagId, boolean value) { } public void setKnown(long tagId, int value) { - if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { + if (DenseStore.ENABLED) { this.map.storeDenseLocal(tagId, Integer.valueOf(value)); return; } @@ -1926,7 +1950,7 @@ public void setKnown(long tagId, int value) { } public void setKnown(long tagId, long value) { - if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { + if (DenseStore.ENABLED) { this.map.storeDenseLocal(tagId, Long.valueOf(value)); return; } @@ -1934,7 +1958,7 @@ public void setKnown(long tagId, long value) { } public void setKnown(long tagId, float value) { - if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { + if (DenseStore.ENABLED) { this.map.storeDenseLocal(tagId, Float.valueOf(value)); return; } @@ -1942,7 +1966,7 @@ public void setKnown(long tagId, float value) { } public void setKnown(long tagId, double value) { - if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { + if (DenseStore.ENABLED) { this.map.storeDenseLocal(tagId, Double.valueOf(value)); return; } @@ -1951,56 +1975,56 @@ public void setKnown(long tagId, double value) { } public void set(long tagId, @Nonnull Object value) { - if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { - this.storeDenseLocal(tagId, value); + if (DenseStore.ENABLED) { + this.storeDenseLocal(requireKnownId(tagId), value); return; } this.putEntry(Entry.newAnyEntry(tagId, value)); } public void set(long tagId, @Nonnull CharSequence value) { - if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { - this.storeDenseLocal(tagId, value); + if (DenseStore.ENABLED) { + this.storeDenseLocal(requireKnownId(tagId), value); return; } this.putEntry(Entry.newObjectEntry(tagId, value)); } public void set(long tagId, boolean value) { - if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { - this.storeDenseLocal(tagId, Boolean.valueOf(value)); + if (DenseStore.ENABLED) { + this.storeDenseLocal(requireKnownId(tagId), Boolean.valueOf(value)); return; } this.putEntry(Entry.newBooleanEntry(tagId, value)); } public void set(long tagId, int value) { - if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { - this.storeDenseLocal(tagId, Integer.valueOf(value)); + if (DenseStore.ENABLED) { + this.storeDenseLocal(requireKnownId(tagId), Integer.valueOf(value)); return; } this.putEntry(Entry.newIntEntry(tagId, value)); } public void set(long tagId, long value) { - if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { - this.storeDenseLocal(tagId, Long.valueOf(value)); + if (DenseStore.ENABLED) { + this.storeDenseLocal(requireKnownId(tagId), Long.valueOf(value)); return; } this.putEntry(Entry.newLongEntry(tagId, value)); } public void set(long tagId, float value) { - if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { - this.storeDenseLocal(tagId, Float.valueOf(value)); + if (DenseStore.ENABLED) { + this.storeDenseLocal(requireKnownId(tagId), Float.valueOf(value)); return; } this.putEntry(Entry.newFloatEntry(tagId, value)); } public void set(long tagId, double value) { - if (DenseStore.ENABLED && denseIdOf(tagId) != 0) { - this.storeDenseLocal(tagId, Double.valueOf(value)); + if (DenseStore.ENABLED) { + this.storeDenseLocal(requireKnownId(tagId), Double.valueOf(value)); return; } this.putEntry(Entry.newDoubleEntry(tagId, value)); diff --git a/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java b/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java index 7e4b8fa828c..030f29b2b1c 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java @@ -46,8 +46,15 @@ void tagsOnAnHttpServerSpanHaveDistinctSlots() { } @Test - void anOptionalTagIsStoredByHash() { - assertFalse(KnownTagCodec.hasSlot(KnownTags.PEER_PORT_OUTBOUND_ID)); + void anOptionalTagOnASpanTypeHasASlot() { + assertTrue(KnownTagCodec.hasSlot(KnownTags.PEER_PORT_OUTBOUND_ID)); + assertTrue(KnownTagCodec.hasSlot(KnownTags.DB_POOL_NAME_ID)); + } + + @Test + void aTagOnNoSpanTypeHasNoSlot() { + // the tracer overlay's routing-only keys appear on no modeled span type + assertFalse(KnownTagCodec.hasSlot(KnownTags.RESOURCE_NAME_ID)); } @Test @@ -59,4 +66,11 @@ void theSlotDoesNotChangeTheSerialOrFlags() { assertTrue(KnownTagCodec.isIntercepted(tagId)); assertFalse(KnownTagCodec.isTraceLevel(tagId)); } + + @Test + void anUnslottedTagSharesTheReservedTopBit() { + // TagMap.slotBit relies on the long shift masking NO_SLOT (0xFFFF) down to 63 + assertEquals(1L << 63, TagMap.slotBit(KnownTags.RESOURCE_NAME_ID)); + assertTrue(KnownTagCodec.slotCount() < 63, "real slots must stay below the shared bit 63"); + } } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java index 27b85472e53..7a89e6b7af2 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java @@ -37,6 +37,7 @@ class TagMapDenseForkedTest { static final String BASE_SERVICE = KnownTags.DD_BASE_SERVICE_NAME; // known, no slot static final String DB_POOL_NAME = KnownTags.DB_POOL_NAME; + static final String UNSLOTTED_KNOWN = KnownTags.RESOURCE_NAME; // on no span type: no slot // custom static final String CUSTOM_A = "custom.tag.a"; static final String CUSTOM_B = "custom.tag.b"; @@ -51,7 +52,7 @@ static void checkSetup() throws IllegalAccessException { assertTrue(Config.get().isTraceDenseTagsEnabled(), "dense store must be on in this JVM"); assertTrue(KnownTagCodec.hasSlot(KnownTags.COMPONENT_ID)); assertTrue(KnownTagCodec.hasSlot(KnownTags.DD_BASE_SERVICE_ID)); - assertFalse(KnownTagCodec.hasSlot(KnownTags.DB_POOL_NAME_ID), "db.pool.name has no slot"); + assertFalse(KnownTagCodec.hasSlot(KnownTags.RESOURCE_NAME_ID), "resource.name has no slot"); Map bySlot = new HashMap<>(); for (Field field : KnownTags.class.getFields()) { @@ -106,14 +107,14 @@ void aSlottedKnownTagIsStoredDenselyAndRoundTrips() { } @Test - void customAndUnslottedTagsStayInBuckets() { + void customTagsStayInBucketsAndUnslottedKnownTagsAreDense() { TagMap map = TagMap.create(); map.set(CUSTOM_A, "alpha"); - map.set(DB_POOL_NAME, "pool-1"); + map.set(UNSLOTTED_KNOWN, "pool-1"); // a known tag with no slot - assertEquals(0, knownCount(map)); + assertEquals(1, knownCount(map)); assertEquals("alpha", map.getObject(CUSTOM_A)); - assertEquals("pool-1", map.getObject(DB_POOL_NAME)); + assertEquals("pool-1", map.getObject(UNSLOTTED_KNOWN)); assertEquals(2, map.size()); map.checkIntegrity(); } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapDenseFuzzForkedTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapDenseFuzzForkedTest.java index 0f3762a3908..105db35b2dd 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapDenseFuzzForkedTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapDenseFuzzForkedTest.java @@ -143,11 +143,12 @@ private static String randomKey(Regime regime) { dense = r.nextBoolean(); } if (dense) { - return SLOTTED.get(r.nextInt(SLOTTED.size())); + // every known tag is dense, slotted or not + return r.nextInt(4) == 0 + ? UNSLOTTED.get(r.nextInt(UNSLOTTED.size())) + : SLOTTED.get(r.nextInt(SLOTTED.size())); } - return r.nextInt(4) == 0 - ? UNSLOTTED.get(r.nextInt(UNSLOTTED.size())) - : "custom-" + r.nextInt(CUSTOM_SPACE); + return "custom-" + r.nextInt(CUSTOM_SPACE); } private static String randomValue() { diff --git a/tag-conventions.yaml b/tag-conventions.yaml index 71b2c85172d..0e3d9083809 100644 --- a/tag-conventions.yaml +++ b/tag-conventions.yaml @@ -55,9 +55,8 @@ # interim, and goes away once resolution is direction-aware. A rename on a concrete type with no # span-kind still requires it. # Storage layout is NOT authored here. dd-trace-java's generator derives a dense-store slot for each -# required/conditional/recommended tag by graph coloring: tags resolved onto one concrete span type -# (or both trace-level) get distinct slots, and tags that never appear together may share one. The -# rest are stored by hash. +# tag by graph coloring: tags resolved onto one concrete span type (or both trace-level) get +# distinct slots, and tags that never appear together may share one. # --------------------------------------------------------------------------- # Trace-level tier: its own TagMap on the TraceSegment. Set once per trace, not per span. From e7a87212b6862df0ad07742f10a8cc23be2301d1 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sat, 10 Oct 2026 19:49:48 -0400 Subject: [PATCH 08/29] Give a shared tag name's undirected form its own id peer.port set by name before a span has a direction used to be stored as a custom tag, so every id write of a direction's tag had to remove it from the hash buckets. That pulled removeLocal into DDSpanContext.setTag(long, int) and pushed it past C2's InlineSmallCode limit. The registry now adds an undirected tag per shared Datadog name (PEER_PORT_UNDIRECTED_ID). It is serialed after every declared tag and colored into the same slot as its directional tags. Each directional id carries the undirected serial at bits [30-16], so undirectedIdOf folds for a constant id. In the dense store, a direction's write supersedes the undirected tag by relabeling it in place during the scan it already does. The hash path removes it by id. The span-level supersede calls are gone. keyOf(String) still resolves a shared name to 0. The span's by-name setters rely on that to take the directional path. Co-Authored-By: Claude Opus 5.5 --- .../tagRegistry/KnownTagsEmitter.kt | 19 +++-- .../buildlogic/tagRegistry/TagRegistry.kt | 84 ++++++++++++++++--- .../tagRegistry/TagRegistryGenerator.kt | 4 +- .../tagRegistry/TagRegistryGeneratorTest.kt | 54 +++++++++++- .../datadog/trace/core/DDSpanContext.java | 24 ++++-- .../trace/core/DDSpanDirectionalTagsTest.java | 51 +++++++++++ .../java/datadog/trace/api/KnownTagCodec.java | 32 ++++++- .../main/java/datadog/trace/api/TagMap.java | 76 +++++++++++------ .../java/datadog/trace/api/KnownTagsTest.java | 21 ++++- .../datadog/trace/api/TagMapSetByIdTest.java | 3 +- .../datadog/trace/api/TagMapTagHashTest.java | 4 +- 11 files changed, 307 insertions(+), 65 deletions(-) diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt index aa8ed7559fa..d9ff8e5999a 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt @@ -45,11 +45,11 @@ object KnownTagsEmitter { val serialOfConst = HashMap() val otelNameOfConst = HashMap() for (t in reg.tags) { - val base = sanitize(t.name) + val base = sanitize(t.name).let { if (t.undirected) "${it}_UNDIRECTED" else it } // Shared Datadog names get no NAME constant because they identify several tags. // Callers use a direction-specific ID (e.g. PEER_PORT_OUTBOUND_ID); nameOf emits the // shared Datadog name as a literal. - if (t.sharedNameDirection == null) nameOfConst[t.identity] = unique(withSuffix(base, "_NAME")) + if (t.sharedNameDirection == null && !t.undirected) nameOfConst[t.identity] = unique(withSuffix(base, "_NAME")) idOfConst[t.identity] = unique(withSuffix(base, "_ID")) serialOfConst[t.identity] = unique(withSuffix(base, "_SERIAL_NUM")) // Suffix the pre-suffix base (not nameC), same as the other three: suffixing an @@ -67,7 +67,7 @@ object KnownTagsEmitter { val order = reg.tags.map { it.identity } // stable emit order // keyOf(name) has no direction, so shared names are left out here and resolve to 0 (unknown); // they are added below, marked, for keyOf(name, direction). - val keyOfOrder = reg.tags.filter { it.sharedNameDirection == null }.map { it.identity } + val keyOfOrder = reg.tags.filter { it.sharedNameDirection == null && !it.undirected }.map { it.identity } // canonical name -> OpenTelemetry name, for the reverse (openTelemetryNameOf) switch. val otelName = reg.tags.mapNotNull { t -> t.otelName?.let { t.identity to it } }.toMap() // Names whose tag depends on the span's direction: a shared Datadog name (one tag per direction) @@ -76,6 +76,8 @@ object KnownTagsEmitter { val directional = sortedMapOf>() // Shared Datadog names, marked in the keyOf table so a lookup by that name alone can probe its tags. val sharedNames = sortedSetOf() + // A shared name's undirected tag: what the name denotes in a direction with no tag of its own. + val undirectedOf = reg.tags.filter { it.undirected }.associate { it.ddName to it.identity } for (t in reg.tags) { t.sharedNameDirection?.let { directional.getOrPut(t.ddName) { mutableMapOf() }[it] = t.identity @@ -105,7 +107,12 @@ object KnownTagsEmitter { for (t in reg.tags) { val direction = t.sharedNameDirection - if (direction == null) { + if (t.undirected) { + appendLine( + " /** {@code ${escape(t.ddName)}} set by that name on a span with no direction yet. Internal: " + + "instrumentation sets a direction's ID, which supersedes this one. */" + ) + } else if (direction == null) { appendLine(" public static final String ${nameC(t.identity)} = \"${escape(t.ddName)}\";") } else { appendLine( @@ -122,6 +129,7 @@ object KnownTagsEmitter { if (t.traceLevel) append(" + trace-level") if (t.intercepted) append(" + intercepted") if (t.sharedNameDirection != null) append(" + shared-name") + if (t.undirected) append(" + undirected") if (t.otelName != null) append(" -> ${escape(t.otelName)}") appendLine(" <${escape(t.required)}>") appendLine() @@ -276,7 +284,8 @@ object KnownTagsEmitter { appendLine(" return ${idC(tag)};") } appendLine(" default:") - appendLine(" return KnownTagCodec.NO_TAG_IN_DIRECTION_SENTINEL;") + val undirected = undirectedOf[name] + appendLine(" return ${if (undirected != null) idC(undirected) else "KnownTagCodec.NO_TAG_IN_DIRECTION_SENTINEL"};") appendLine(" }") } appendLine( diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt index d0d0b3eda02..3e8c753fa18 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt @@ -39,6 +39,11 @@ class TagRegistry private constructor(val tags: List) { * tag has one declaration, so its rename covers either every direction or exactly one. */ val otelDirection: TagConventions.Direction? = null, + /** + * Whether this is the undirected tag of a shared Datadog name: `peer.port` set by that name on a + * span with no direction (yet). Not declared; the registry adds one per shared name. + */ + val undirected: Boolean = false, ) { /** The identity's label, as reports and constant names show it. */ val name: String = identity.label @@ -57,6 +62,10 @@ class TagRegistry private constructor(val tags: List) { const val LEVEL_TRACE = 1L shl 2 // low-32 carve bit 2; mirrors KnownTagCodec.LEVEL_TRACE const val INTERCEPTED = 1L shl 1 // low-32 carve bit 1; mirrors KnownTagCodec.INTERCEPTED const val SHARED_NAME = 1L shl 0 // low-32 carve bit 0; mirrors KnownTagCodec.SHARED_NAME + const val UNDIRECTED = 1L shl 3 // low-32 carve bit 3; mirrors KnownTagCodec.UNDIRECTED + + /** A shared-name id's undirected serial window [30-16]; mirrors KnownTagCodec.UNDIRECTED_SHIFT. */ + const val UNDIRECTED_SHIFT = 16 const val SLOT_SHIFT = 32 // slot window [47-32]; mirrors KnownTagCodec.SLOT_SHIFT const val NO_SLOT = 0xFFFF // all-ones slot: stored by hash; mirrors KnownTagCodec.NO_SLOT @@ -71,14 +80,17 @@ class TagRegistry private constructor(val tags: List) { /** * Mirrors KnownTagCodec's layout -- must stay in sync: serial at [63-48], slot at [47-32], - * LEVEL_TRACE at bit 2, INTERCEPTED at bit 1, SHARED_NAME at bit 0, other low bits zero. Every id - * is made here, and the range check makes each one positive by construction. + * UNDIRECTED at bit 3, LEVEL_TRACE at bit 2, INTERCEPTED at bit 1, SHARED_NAME at bit 0; a + * shared-name id also carries its undirected tag's serial at [30-16]; other low bits zero. Every + * id is made here, and the range check makes each one positive by construction. */ fun encode( serial: Int, traceLevel: Boolean, intercepted: Boolean, sharedName: Boolean = false, + undirectedSerial: Int = 0, + undirected: Boolean = false, slot: Int, ): Long { require(slot == NO_SLOT || slot in 0 until MAX_SLOTS) { "slot $slot is outside [0, $MAX_SLOTS)" } @@ -89,7 +101,8 @@ class TagRegistry private constructor(val tags: List) { var id = (serial.toLong() shl 48) or (slot.toLong() shl SLOT_SHIFT) if (traceLevel) id = id or LEVEL_TRACE if (intercepted) id = id or INTERCEPTED - if (sharedName) id = id or SHARED_NAME + if (sharedName) id = id or SHARED_NAME or (undirectedSerial.toLong() shl UNDIRECTED_SHIFT) + if (undirected) id = id or UNDIRECTED return id } @@ -100,11 +113,21 @@ class TagRegistry private constructor(val tags: List) { val slots = colorSlots(conv) // Stable order (by name) so serials -- and therefore ids -- are a pure function of the input. - val tags = - conv.allDeclaredTags().sortedBy { it.name }.mapIndexed { i, t -> - val serial = FIRST_SERIAL + i - val isTraceLevel = t.identity in traceLevel - val isIntercepted = t.ddName in conv.interceptedNames + val declared = conv.allDeclaredTags().sortedBy { it.name } + // Each shared Datadog name gets an undirected tag, serialed after every declared tag (so adding + // one renumbers nothing declared) and slotted with its directional tags. + val sharedByName = declared.filter { it.identity.direction != null }.groupBy { it.ddName }.toSortedMap() + val undirectedSerials = HashMap() + for ((i, ddName) in sharedByName.keys.withIndex()) { + undirectedSerials[ddName] = FIRST_SERIAL + declared.size + i + } + val tags = ArrayList() + for ((i, t) in declared.withIndex()) { + val serial = FIRST_SERIAL + i + val isTraceLevel = t.identity in traceLevel + val isIntercepted = t.ddName in conv.interceptedNames + val slot = slots[t.identity] ?: NO_SLOT + tags.add( Tag( t.identity, t.type, @@ -112,12 +135,41 @@ class TagRegistry private constructor(val tags: List) { serial, isTraceLevel, isIntercepted, - slot = slots[t.identity] ?: NO_SLOT, - id = encode(serial, isTraceLevel, isIntercepted, t.identity.direction != null, slots[t.identity] ?: NO_SLOT), + slot = slot, + id = + encode( + serial, + isTraceLevel, + isIntercepted, + sharedName = t.identity.direction != null, + undirectedSerial = undirectedSerials[t.ddName] ?: 0, + slot = slot, + ), declaredOtelName = renames[t.identity]?.otelName, otelDirection = renames[t.identity]?.direction, ) - } + ) + } + for ((ddName, shared) in sharedByName) { + val serial = undirectedSerials.getValue(ddName) + val types = shared.map { it.type }.toSet() + require(types.size == 1) { "the tags of shared name '$ddName' disagree on type: $types" } + val isIntercepted = ddName in conv.interceptedNames + val slot = slots[shared.first().identity] ?: NO_SLOT + tags.add( + Tag( + TagConventions.TagIdentity(ddName), + types.single(), + "optional", + serial, + traceLevel = false, + intercepted = isIntercepted, + slot = slot, + id = encode(serial, false, isIntercepted, undirected = true, slot = slot), + undirected = true, + ) + ) + } validateOtelNames(tags) return TagRegistry(tags) @@ -129,11 +181,14 @@ class TagRegistry private constructor(val tags: List) { * then label), so the result is a pure function of the conventions. */ private fun colorSlots(conv: TagConventions): Map { + // A shared name's tags color as one node, so they -- and the undirected tag -- share a slot: + // writing a direction's tag then finds the undirected one under the same occupancy bit. + fun node(identity: TagConventions.TagIdentity) = TagConventions.TagIdentity(identity.ddName).takeIf { identity.direction != null } ?: identity val cliques = ArrayList>() for (type in conv.concreteTypes()) { - cliques.add(conv.resolve(type).map { it.identity }.toSet()) + cliques.add(conv.resolve(type).map { node(it.identity) }.toSet()) } - cliques.add(conv.traceLevelTags().map { it.identity }.toSet()) + cliques.add(conv.traceLevelTags().map { node(it.identity) }.toSet()) val neighbors = HashMap>() for (clique in cliques) { @@ -158,6 +213,9 @@ class TagRegistry private constructor(val tags: List) { } slots[tag] = slot } + for (t in conv.allDeclaredTags()) { + slots[node(t.identity)]?.let { slots[t.identity] = it } + } return slots } diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt index c92af8539c4..93fb625c189 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt @@ -84,7 +84,7 @@ object TagRegistryGenerator { if (t.slot == TagRegistry.NO_SLOT) "-" else t.slot.toString(), "0x%016X".format(Locale.ROOT, t.id), t.required, - t.name + if (t.undirected) "${t.name} (undirected)" else t.name ) ) } @@ -116,6 +116,8 @@ object TagRegistryGenerator { # SHARED DATADOG NAMES. One Datadog name for a tag per direction: emitting it needs no context, # but resolving the name to a tag needs the span's direction, so keyOf does not resolve it yet. + # Until a span has a direction (or in one with no tag of its own) the name denotes its + # undirected tag, which shares the directional tags' slot and is superseded by them. """.trimIndent() ) for ((ddName, shared) in reg.tags.filter { it.sharedNameDirection != null }.groupBy { it.ddName }.toSortedMap()) { diff --git a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt index e5cdaa68b72..185f2e94e4e 100644 --- a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt +++ b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt @@ -376,6 +376,56 @@ class TagRegistryGeneratorTest { .containsPattern("peer\\.port +-> peer\\.port@inbound, peer\\.port@outbound") } + @Test + fun `a Datadog name declared per direction gets an undirected tag in its directions' slot`() { + val conventions = tagConventions( + """ + span_types: + http.client: {span-kind: client, include: [outbound_peer]} + http.server: {span-kind: server, include: [inbound_peer], tags: [{dd-name: http.route}]} + mixins: + outbound_peer: {span-kind: client, tags: [{dd-name: peer.port, type: int}]} + inbound_peer: {span-kind: server, tags: [{dd-name: peer.port, type: int}]} + """ + ) + + val tags = TagRegistry.build(conventions).tags + val undirected = tags.single { it.undirected } + val byName = tags.associateBy { it.name } + val inbound = byName.getValue("peer.port@inbound") + val outbound = byName.getValue("peer.port@outbound") + + assertThat(undirected.ddName).isEqualTo("peer.port") + assertThat(undirected.serial).isEqualTo(tags.filter { !it.undirected }.maxOf { it.serial } + 1) + assertThat(listOf(inbound.slot, outbound.slot)).containsOnly(undirected.slot) + for (t in listOf(inbound, outbound)) { + assertThat((t.id ushr TagRegistry.UNDIRECTED_SHIFT) and 0x7FFF).isEqualTo(undirected.serial.toLong()) + } + assertThat(undirected.id and TagRegistry.UNDIRECTED).isNotZero() + assertThat(undirected.id and TagRegistry.SHARED_NAME).isZero() + } + + @Test + fun `a shared name with no tag in a direction resolves there to its undirected tag`() { + val yaml = directory.conventionsFile( + """ + span_types: + http.client: {span-kind: client, include: [outbound_peer]} + mixins: + outbound_peer: {span-kind: client, tags: [{dd-name: peer.port, type: int}]} + inbound_peer: {span-kind: server, tags: [{dd-name: peer.port, type: int}]} + """ + ) + val output = File(directory, "generated") + + TagRegistryGenerator.generate(yaml, output) + + val source = contents(output).getValue("java/datadog/trace/api/KnownTags.java") + assertThat(source).contains("PEER_PORT_UNDIRECTED_ID").doesNotContain("PEER_PORT_UNDIRECTED_NAME") + assertThat(source.substringAfter("case \"peer.port\":").substringAfter("default:").substringBefore(";")) + .contains("return PEER_PORT_UNDIRECTED_ID") + } + @Test fun `a tag declared per direction is emitted under its OpenTelemetry name without a direction`() { val yaml = directory.conventionsFile( @@ -489,7 +539,7 @@ class TagRegistryGeneratorTest { """ ) - assertThat(TagRegistry.build(conventions).tags.map { it.identity }) + assertThat(TagRegistry.build(conventions).tags.filterNot { it.undirected }.map { it.identity }) .containsExactlyInAnyOrder( TagConventions.TagIdentity("x", TagConventions.Direction.INBOUND), TagConventions.TagIdentity("x", TagConventions.Direction.OUTBOUND), @@ -511,7 +561,7 @@ class TagRegistryGeneratorTest { """ ) - assertThat(TagRegistry.build(conventions).tags.map { it.name }) + assertThat(TagRegistry.build(conventions).tags.filterNot { it.undirected }.map { it.name }) .containsExactlyInAnyOrder("peer.port@inbound", "peer.port@outbound", "user@domain") } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 0ac619261ef..4ca6b27f6f9 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -939,7 +939,8 @@ private int tagDirection() { */ private long directionalTagId(String tag) { long tagId = KnownTagCodec.directionalKeyOf(tag, tagDirection()); - return tagId > 0 ? tagId : 0L; + // an undirected tag is not the only one the name may find: TagMap checks each of them + return tagId > 0 && !KnownTagCodec.isUndirected(tagId) ? tagId : 0L; } /** @@ -949,14 +950,15 @@ private long directionalTagId(String tag) { private long directionalTagIdToStore(String tag) { int direction = tagDirection(); long tagId = KnownTagCodec.directionalKeyOf(tag, direction); - if (tagId > 0) { - return tagId; - } - if (tagId == KnownTagCodec.NO_TAG_IN_DIRECTION_SENTINEL - && direction == KnownTagCodec.DIRECTION_UNKNOWN) { + boolean undirected = tagId > 0 && KnownTagCodec.isUndirected(tagId); + // A shared name stored as its undirected tag, or a direction-scoped name stored as a custom + // tag, is re-keyed once the span has a direction. + if (direction == KnownTagCodec.DIRECTION_UNKNOWN + && (undirected || tagId == KnownTagCodec.NO_TAG_IN_DIRECTION_SENTINEL)) { unresolvedDirectionalTags = true; } - return 0L; + // TagMap picks an undirected name's key: a direction's tag it holds, else the undirected tag. + return tagId > 0 && !undirected ? tagId : 0L; } /** Re-keys tags stored under a direction-dependent name before the span had a direction. */ @@ -969,8 +971,12 @@ private void resolveDirectionalTags() { unresolvedDirectionalTags = false; List unresolved = new ArrayList<>(2); for (TagMap.EntryReader entry : unsafeTags) { - if (entry.tagId() == 0 && KnownTagCodec.directionalKeyOf(entry.tag(), direction) > 0) { - unresolved.add(entry.entry()); + long tagId = entry.tagId(); + if (tagId == 0 || KnownTagCodec.isUndirected(tagId)) { + long resolved = KnownTagCodec.directionalKeyOf(entry.tag(), direction); + if (resolved > 0 && !KnownTagCodec.isUndirected(resolved)) { + unresolved.add(entry.entry()); + } } } for (TagMap.Entry entry : unresolved) { diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagsTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagsTest.java index c61692bbebd..efff5b816d2 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagsTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanDirectionalTagsTest.java @@ -110,9 +110,60 @@ void anInternalSpanHasNoPeerPort() { assertNull(storedValue(KnownTags.PEER_PORT_OUTBOUND_ID)); assertNull(storedValue(KnownTags.PEER_PORT_INBOUND_ID)); + assertEquals(5432, storedValue(KnownTags.PEER_PORT_UNDIRECTED_ID)); assertEquals(5432, span.getTag(Tags.PEER_PORT)); } + @Test + void aSharedNameBeforeTheSpanHasADirectionIsTheUndirectedTag() { + span.setTag(Tags.PEER_PORT, 5432); + + assertEquals(5432, storedValue(KnownTags.PEER_PORT_UNDIRECTED_ID)); + assertEquals(5432, span.getTag(Tags.PEER_PORT)); + } + + @Test + void aDirectionsTagSetByIdSupersedesTheUndirectedTag() { + span.setTag(Tags.PEER_PORT, 8080); + + span.setTag(KnownTags.PEER_PORT_OUTBOUND_ID, 443); + + assertNull(storedValue(KnownTags.PEER_PORT_UNDIRECTED_ID)); + assertEquals(443, storedValue(KnownTags.PEER_PORT_OUTBOUND_ID)); + assertEquals(443, span.getTag(Tags.PEER_PORT)); + assertEquals(1, peerPortCount()); + } + + @Test + void aDirectionsTagSetByIdIsFoundByNameBeforeTheSpanHasADirection() { + span.setTag(KnownTags.PEER_PORT_OUTBOUND_ID, 443); + + assertEquals(443, span.getTag(Tags.PEER_PORT)); + } + + @Test + void aSharedNameBeforeTheSpanHasADirectionReplacesADirectionsTagSetById() { + span.setTag(KnownTags.PEER_PORT_OUTBOUND_ID, 443); + + span.setTag(Tags.PEER_PORT, 8443); + + assertEquals(8443, storedValue(KnownTags.PEER_PORT_OUTBOUND_ID)); + assertNull(storedValue(KnownTags.PEER_PORT_UNDIRECTED_ID)); + assertEquals(1, peerPortCount()); + } + + private int peerPortCount() { + int[] count = {0}; + span.getTags() + .forEach( + entry -> { + if (Tags.PEER_PORT.equals(entry.tag())) { + count[0]++; + } + }); + return count[0]; + } + private Object storedValue(long tagId) { TagMap.Entry entry = span.getTags().getEntry(tagId); return entry == null ? null : entry.objectValue(); diff --git a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java index 6f2cfe2d188..6fc026f7ee6 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -16,9 +16,10 @@ public final class KnownTagCodec { * unique per known tag and is the whole of the tag's identity — nameOf/openTelemetryNameOf key on * it, and the generator emits each id as a literal. Bits [47-32] hold the tag's dense-store slot * ({@link #slotOf}), assigned by graph coloring so tags on one span type never share a slot, or - * {@link #NO_SLOT} for a tag stored by hash. Of the low 32 flag bits, bit 2 is - * the trace/span LEVEL bit (set ⟹ trace-level), bit 1 is the INTERCEPTED bit, and bit 0 is the - * SHARED_NAME bit. Unknown (string-only) custom tags are NOT known ids — {@code keyOf} returns 0 for + * {@link #NO_SLOT} for a tag stored by hash. Of the low 32 flag bits, bit 3 is the UNDIRECTED + * bit, bit 2 is the trace/span LEVEL bit (set ⟹ trace-level), bit 1 is the INTERCEPTED bit, and + * bit 0 is the SHARED_NAME bit; a SHARED_NAME id also holds its undirected tag's serial at + * [30-16]. Unknown (string-only) custom tags are NOT known ids — {@code keyOf} returns 0 for * them. * *

The INTERCEPTED bit marks a tag TagInterceptor may route on the set-path, to a span field or @@ -73,6 +74,31 @@ public static boolean hasSharedName(long tagId) { return (tagId & SHARED_NAME) != 0L; } + /** + * UNDIRECTED bit (low-32 carve, bit 3): marks the undirected tag of a shared Datadog name -- what + * {@code peer.port} denotes when set by that name on a span with no direction yet, or in a + * direction with no tag of its own. It shares its directional tags' slot, and writing one of them + * supersedes it, so a map holds at most one tag per shared name. + */ + public static final long UNDIRECTED = 1L << 3; + + /** True if the tagId is the undirected tag of a shared Datadog name. */ + public static boolean isUndirected(long tagId) { + return (tagId & UNDIRECTED) != 0L; + } + + /** Bit position of a {@link #SHARED_NAME} id's undirected serial: bits [30-16]. */ + static final int UNDIRECTED_SHIFT = 16; + + /** + * The undirected tag of a {@link #SHARED_NAME} id's name: its serial is held in the id, and it + * shares the id's slot and interception. Arithmetic only, so a constant id folds it. + */ + public static long undirectedIdOf(long sharedNameId) { + long serial = (sharedNameId >>> UNDIRECTED_SHIFT) & 0x7FFFL; + return (serial << 48) | (sharedNameId & ((0xFFFFL << SLOT_SHIFT) | INTERCEPTED)) | UNDIRECTED; + } + /** Bit position of the dense-store slot: bits [47-32] of an id. */ public static final int SLOT_SHIFT = 32; 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 4fdfc13d1c1..1f681440fc7 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1507,8 +1507,11 @@ public Entry getEntry(String tag) { return entry; } } - // Set by that name alone, with no direction: stored as a custom tag. - return this.getEntry(tag, Entry.customHash(tag)); + // Set by that name alone, with no direction: stored as the undirected tag -- or, when built as + // an Entry by name, as a custom tag. + long undirectedId = KnownTagCodec.directionalKeyOf(tag, KnownTagCodec.DIRECTION_UNKNOWN); + Entry undirected = this.getEntry(tag, undirectedId); + return undirected != null ? undirected : this.getEntry(tag, Entry.customHash(tag)); } /** @@ -1631,15 +1634,38 @@ private int knownIndexOf(long denseId) { return -1; } + /** + * {@link #knownIndexOf} for a write: one direction of a shared name also matches the name's + * undirected tag, which it supersedes in place. Both share a slot, so one occupancy test covers + * them, and for a constant id the alias folds to a constant (or, for most tags, to the id + * itself). + */ + private int knownWriteIndexOf(long denseId) { + if ((this.knownOccupancy & slotBit(denseId)) == 0) { + return -1; // definitely absent + } + long alias = + KnownTagCodec.hasSharedName(denseId) ? KnownTagCodec.undirectedIdOf(denseId) : denseId; + long[] ids = this.knownIds; + for (int i = 0; i < this.knownCount; ++i) { + long id = ids[i]; + if (id == denseId || id == alias) return i; + } + return -1; + } + /** * Stores a dense value, replacing it in place when present or appending it, and returns the prior * value, or null when there was none. Dense values are never null. Builds no Entry, so a caller * that discards the result allocates nothing, whether or not the JIT inlines this. */ private Object storeKnownValue(long denseId, Object value) { - int i = this.knownIndexOf(denseId); + int i = this.knownWriteIndexOf(denseId); if (i >= 0) { - Object prior = this.knownValues[i]; + long[] ids = this.knownIds; + // a superseded undirected tag is relabeled, and was not this tag's prior value + Object prior = ids[i] == denseId ? this.knownValues[i] : null; + ids[i] = denseId; this.knownValues[i] = value; return prior; } @@ -1659,10 +1685,6 @@ private Object storeKnownValue(long denseId, Object value) { return null; } - /** - * Local dense write: clears any tombstone for the tag, then stores the value, returning the prior - * value or null. The {@code set(long, ...)} methods use this; it builds no Entry. - */ /** * {@code tagId}, when it names a known tag; the checked id-keyed setters reject anything else. */ @@ -1673,15 +1695,12 @@ private static long requireKnownId(long tagId) { return tagId; } + /** + * Local dense write: clears any tombstone for the tag, then stores the value, returning the prior + * value or null. The {@code set(long, ...)} methods use this; it builds no Entry. + */ private Object storeDenseLocal(long denseId, Object value) { this.checkWriteAccess(); - if (KnownTagCodec.hasSharedName(denseId)) { - // One direction of a tag declared per direction, written by id, supersedes a value written - // under the shared name alone (before a span knew its direction), so the map never holds - // both. Folds away for a constant id. - String sharedName = KnownTagCodec.nameOf(denseId); - this.removeLocal(sharedName, Entry.customHash(sharedName)); - } if (this.removedFromParent != null) { this.removedFromParent.remove(KnownTagCodec.nameOf(denseId)); } @@ -1767,8 +1786,8 @@ public void set(@Nullable TagMap.EntryReader newEntryReader) { /** * The id a write by name stores under: the tag's id, 0 for a custom tag, or -- for a Datadog name * shared by a tag per direction ({@code peer.port}) -- whichever of those tags this map already - * holds. When it holds neither, the write stays a custom tag under the name, which a span re-keys - * once it knows its direction. + * holds. When it holds none, the write is the name's undirected tag, which a span re-keys once it + * knows its direction. */ private long writeKeyOf(String tag) { long tagId = KnownTagCodec.keyOrSharedName(tag); @@ -1783,7 +1802,7 @@ private long writeKeyOf(String tag) { return sharingId; } } - return 0L; + return KnownTagCodec.directionalKeyOf(tag, KnownTagCodec.DIRECTION_UNKNOWN); } static Entry anyEntryFor(String tag, Object value) { @@ -1912,8 +1931,8 @@ private Internals(TagMap map) { /* * Sets a known tag by id without validating it, unlike TagMap.set(long, ...): the caller has - * already checked KnownTagCodec.isKnown(tagId). A slotted tag goes straight to the dense store, - * as in set(long, ...), so no Entry is built. + * already checked KnownTagCodec.isKnown(tagId). With the dense store on, the tag goes straight + * to the dense arrays, so no Entry is built. */ public void setKnown(long tagId, @Nonnull Object value) { if (DenseStore.ENABLED) { @@ -2074,10 +2093,9 @@ private Entry putEntry(@Nonnull Entry newEntry) { long newTagHash = newEntry.tagHash; if ((newTagHash >>> 32) != 0 && KnownTagCodec.hasSharedName(newTagHash)) { - // One direction of a tag declared per direction, written by id, supersedes a value written - // under the shared name alone (before a span knew its direction), so the map never holds - // both. - this.removeLocal(newEntry.tag, Entry.customHash(newEntry.tag)); + // One direction of a tag declared per direction supersedes the name's undirected tag (set + // before a span knew its direction), so the map never holds both. + this.removeLocal(newEntry.tag, KnownTagCodec.undirectedIdOf(newTagHash)); } // Re-setting a key clears any read-through tombstone for it (the new value overrides the @@ -2462,8 +2480,14 @@ public Entry getAndRemove(String tag) { } else if (tagId != KnownTagCodec.SHARED_DATADOG_NAME_SENTINEL) { return this.getAndRemove(KnownTagCodec.nameOf(tagId), tagId); } - // A shared name removes whichever of its tags the map holds, and any custom tag of that name. - Entry removed = this.getAndRemove(tag, Entry.customHash(tag)); + // A shared name removes whichever of its tags the map holds, including the undirected one, and + // any custom tag of that name. + long undirectedId = KnownTagCodec.directionalKeyOf(tag, KnownTagCodec.DIRECTION_UNKNOWN); + Entry removed = this.getAndRemove(tag, undirectedId); + Entry custom = this.getAndRemove(tag, Entry.customHash(tag)); + if (removed == null) { + removed = custom; + } for (int direction = KnownTagCodec.DIRECTION_INBOUND; direction <= KnownTagCodec.DIRECTION_NONE; direction++) { diff --git a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java index a039a2bb088..40d090a95ce 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagsTest.java @@ -166,7 +166,8 @@ void unknownNamesResolveToZero() { /** * {@code peer.port} means the server's port on outbound spans and the client's on inbound ones, * so it is one tag per direction. Both share the Datadog name, which therefore resolves to no tag - * until name resolution knows the span's direction. + * until name resolution knows the span's direction; with an unknown direction, it denotes the + * name's undirected tag. */ @Test void aNameDeclaredPerDirectionIsOneTagPerDirection() { @@ -176,14 +177,28 @@ void aNameDeclaredPerDirectionIsOneTagPerDirection() { assertTrue(KnownTagCodec.hasSharedName(KnownTags.PEER_PORT_INBOUND_ID)); assertTrue(KnownTagCodec.hasSharedName(KnownTags.PEER_PORT_OUTBOUND_ID)); assertEquals(0L, KnownTagCodec.keyOf(Tags.PEER_PORT)); + assertEquals(Tags.PEER_PORT, KnownTagCodec.nameOf(KnownTags.PEER_PORT_UNDIRECTED_ID)); + assertTrue(KnownTagCodec.isUndirected(KnownTags.PEER_PORT_UNDIRECTED_ID)); + assertFalse(KnownTagCodec.hasSharedName(KnownTags.PEER_PORT_UNDIRECTED_ID)); + } + + /** Each direction's id holds its undirected tag, so a constant id folds the supersede check. */ + @Test + void aDirectionsTagNamesItsUndirectedTag() { + for (long tagId : + new long[] {KnownTags.PEER_PORT_INBOUND_ID, KnownTags.PEER_PORT_OUTBOUND_ID}) { + assertEquals(KnownTags.PEER_PORT_UNDIRECTED_ID, KnownTagCodec.undirectedIdOf(tagId)); + assertEquals( + KnownTagCodec.slotOf(KnownTags.PEER_PORT_UNDIRECTED_ID), KnownTagCodec.slotOf(tagId)); + } } static Stream directionalNames() { return Stream.of( Arguments.of(Tags.PEER_PORT, DIRECTION_INBOUND, KnownTags.PEER_PORT_INBOUND_ID), Arguments.of(Tags.PEER_PORT, DIRECTION_OUTBOUND, KnownTags.PEER_PORT_OUTBOUND_ID), - Arguments.of(Tags.PEER_PORT, DIRECTION_NONE, 0L), - Arguments.of(Tags.PEER_PORT, DIRECTION_UNKNOWN, 0L), + Arguments.of(Tags.PEER_PORT, DIRECTION_NONE, KnownTags.PEER_PORT_UNDIRECTED_ID), + Arguments.of(Tags.PEER_PORT, DIRECTION_UNKNOWN, KnownTags.PEER_PORT_UNDIRECTED_ID), Arguments.of("server.address", DIRECTION_INBOUND, KnownTags.HTTP_HOSTNAME_ID), Arguments.of("server.address", DIRECTION_OUTBOUND, KnownTags.PEER_HOSTNAME_ID), Arguments.of("server.address", DIRECTION_UNKNOWN, 0L), diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java index 1afa19188cd..034a466641f 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java @@ -96,11 +96,12 @@ void aWriteBySharedNameReplacesTheHeldDirectionsTag() { @Test void aWriteByIdSupersedesAValueSetUnderTheSharedNameAlone() { TagMap map = TagMap.create(); - map.set("peer.port", 8080); // no direction known: stored under the name + map.set("peer.port", 8080); // no direction known: stored as the undirected tag map.set(KnownTags.PEER_PORT_OUTBOUND_ID, 443); assertEquals(1, map.size()); + assertNull(map.getEntry(KnownTags.PEER_PORT_UNDIRECTED_ID)); assertEquals(443, map.getEntry("peer.port").intValue()); assertEquals(443, map.getEntry(KnownTags.PEER_PORT_OUTBOUND_ID).intValue()); } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java index d5925380f62..d090627dc96 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapTagHashTest.java @@ -110,12 +110,12 @@ void aSharedNameFindsWhicheverOfItsTagsTheMapHolds() { } @Test - void aSharedNameSetWithoutADirectionIsACustomTag() { + void aSharedNameSetWithoutADirectionIsTheUndirectedTag() { TagMap map = TagMap.create(); map.set(Tags.PEER_PORT, 5432); assertEquals(5432, map.getEntry(Tags.PEER_PORT).intValue()); - assertEquals(0L, map.getEntry(Tags.PEER_PORT).tagId()); + assertEquals(KnownTags.PEER_PORT_UNDIRECTED_ID, map.getEntry(Tags.PEER_PORT).tagId()); } @Test From 7ec95f49568b869883dc367fa78be73ec6f7fbf8 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sun, 11 Oct 2026 08:49:56 -0400 Subject: [PATCH 09/29] Grow the dense tag arrays once, to the hard bound A map holds each known tag at most once, so a full dense store grows straight to KnownTagCodec.serialLimit() entries instead of doubling. Maps start on shared empty arrays, so one full-capacity test covers a map's first dense tag too. The first allocation, growth, tombstone clearing and the frozen-map exception are now separate methods. This change did not measurably shrink the compiled DDSpanContext.setTag in SetTagMixBenchmark. In both the reused-span and fresh-span profiles, the int overload's growth branch is already an uncommon trap. Co-Authored-By: Claude Opus 5.5 --- .../main/java/datadog/trace/api/TagMap.java | 67 +++++++++++++++---- 1 file changed, 53 insertions(+), 14 deletions(-) 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 1f681440fc7..602ec6af450 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1246,12 +1246,20 @@ private static final class DenseStore { /** Initial capacity of the dense arrays; the largest span type resolves about this many tags. */ private static final int KNOWN_INIT_CAP = 12; + /** + * Shared empty dense arrays a map starts on (and returns to on clear), so that one full-capacity + * test covers a map's first dense tag too. Never written: a full map spills to its own arrays. + */ + private static final long[] EMPTY_KNOWN_IDS = new long[0]; + + private static final Object[] EMPTY_KNOWN_VALUES = new Object[0]; + private Object[] buckets; private int size; private boolean frozen; - private long[] knownIds; - private Object[] knownValues; + private long[] knownIds = EMPTY_KNOWN_IDS; + private Object[] knownValues = EMPTY_KNOWN_VALUES; private int knownCount; private long knownOccupancy; private boolean knownTraceLevel; @@ -1669,17 +1677,13 @@ private Object storeKnownValue(long denseId, Object value) { this.knownValues[i] = value; return prior; } - if (this.knownIds == null) { - this.knownIds = new long[KNOWN_INIT_CAP]; - this.knownValues = new Object[KNOWN_INIT_CAP]; - } else if (this.knownCount == this.knownIds.length) { - int newCap = this.knownIds.length << 1; - this.knownIds = Arrays.copyOf(this.knownIds, newCap); - this.knownValues = Arrays.copyOf(this.knownValues, newCap); + int idx = this.knownCount; + if (idx == this.knownIds.length) { + this.spillKnown(); } - int idx = this.knownCount++; this.knownIds[idx] = denseId; this.knownValues[idx] = value; + this.knownCount = idx + 1; this.knownOccupancy |= slotBit(denseId); this.knownTraceLevel |= KnownTagCodec.isTraceLevel(denseId); return null; @@ -1695,6 +1699,36 @@ private static long requireKnownId(long tagId) { return tagId; } + /** + * Makes room for one more dense tag. A map's first dense tag allocates its own arrays -- a plain + * allocation, since on fresh spans this runs once per span and is compiled inline. A full map + * grows once, in {@link #growKnown}. + */ + private void spillKnown() { + if (this.knownIds.length == 0) { + this.knownIds = new long[KNOWN_INIT_CAP]; + this.knownValues = new Object[KNOWN_INIT_CAP]; + } else { + this.growKnown(); + } + } + + /** + * Grows full dense arrays once, straight to the hard bound: a map holds each known tag at most + * once, so {@link KnownTagCodec#serialLimit()} entries always suffice. Rare, so the copying stays + * out of the write. + */ + private void growKnown() { + int newCap = Math.max(KnownTagCodec.serialLimit(), this.knownIds.length << 1); + this.knownIds = Arrays.copyOf(this.knownIds, newCap); + this.knownValues = Arrays.copyOf(this.knownValues, newCap); + } + + /** Clears a read-through tombstone for a dense tag being set; out of line, as it is rare. */ + private void clearDenseTombstone(long denseId) { + this.removedFromParent.remove(KnownTagCodec.nameOf(denseId)); + } + /** * Local dense write: clears any tombstone for the tag, then stores the value, returning the prior * value or null. The {@code set(long, ...)} methods use this; it builds no Entry. @@ -1702,7 +1736,7 @@ private static long requireKnownId(long tagId) { private Object storeDenseLocal(long denseId, Object value) { this.checkWriteAccess(); if (this.removedFromParent != null) { - this.removedFromParent.remove(KnownTagCodec.nameOf(denseId)); + this.clearDenseTombstone(denseId); } return this.storeKnownValue(denseId, value); } @@ -2812,8 +2846,8 @@ public void clear() { Arrays.fill(this.buckets, null); } this.size = 0; - this.knownIds = null; - this.knownValues = null; + this.knownIds = EMPTY_KNOWN_IDS; + this.knownValues = EMPTY_KNOWN_VALUES; this.knownCount = 0; this.knownOccupancy = 0L; this.knownTraceLevel = false; @@ -2835,7 +2869,12 @@ public boolean isFrozen() { } public void checkWriteAccess() { - if (this.frozen) throw new IllegalStateException("TagMap frozen"); + if (this.frozen) throw frozenError(); + } + + /** Built out of line, so every write's access check stays a field test and a branch. */ + private static IllegalStateException frozenError() { + return new IllegalStateException("TagMap frozen"); } void checkIntegrity() { From cb135da824f8ac2471f7e0df6686aa7b53b96591 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sun, 11 Oct 2026 09:54:54 -0400 Subject: [PATCH 10/29] Split TagMap into hash-only and dense implementations Restore the factory design from before #11678 and #11967. TagMap becomes an abstract class with two final subclasses: HashTagMap (dense store off) and DenseTagMap (dense store on). TagMapFactory chooses one from the flag. EMPTY and every factory method go through the factory, so a JVM loads only one implementation and class hierarchy analysis devirtualizes TagMap calls. A forked test per mode checks that the other implementation never loads. This is rollout scaffolding, as LegacyTagMap was: the hash-only map stays one flag away until the dense store is proven, and is then folded away. The commit is structural only. The logic stays in TagMap; the next commit moves the dense state into DenseTagMap. The public no-arg constructor is removed; TagMap.create() is the intended way to make a map. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/api/DenseTagMap.java | 16 +++++ .../java/datadog/trace/api/HashTagMap.java | 15 +++++ .../main/java/datadog/trace/api/TagMap.java | 26 ++++---- .../java/datadog/trace/api/TagMapFactory.java | 59 +++++++++++++++++++ .../trace/api/TagMapDenseForkedTest.java | 10 ++++ .../datadog/trace/api/TagMapFuzzTest.java | 6 +- .../trace/api/TagMapHashOnlyForkedTest.java | 30 ++++++++++ .../trace/api/TagMapImplementations.java | 30 ++++++++++ .../java/datadog/trace/api/TagMapTest.java | 2 +- 9 files changed, 175 insertions(+), 19 deletions(-) create mode 100644 internal-api/src/main/java/datadog/trace/api/DenseTagMap.java create mode 100644 internal-api/src/main/java/datadog/trace/api/HashTagMap.java create mode 100644 internal-api/src/main/java/datadog/trace/api/TagMapFactory.java create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapHashOnlyForkedTest.java create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapImplementations.java diff --git a/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java b/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java new file mode 100644 index 00000000000..0b6aea89fae --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java @@ -0,0 +1,16 @@ +package datadog.trace.api; + +/** + * The dense/hash hybrid {@link TagMap}: known tags are stored densely, by id, with no per-tag + * {@link TagMap.Entry}; custom tags stay in the buckets. Used when the dense store is on; see + * {@link TagMapFactory}. + */ +final class DenseTagMap extends TagMap { + DenseTagMap(TagMap parent) { + super(parent); + } + + DenseTagMap(Object[] buckets, int size) { + super(buckets, size); + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/HashTagMap.java b/internal-api/src/main/java/datadog/trace/api/HashTagMap.java new file mode 100644 index 00000000000..55b67c8b5e8 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/HashTagMap.java @@ -0,0 +1,15 @@ +package datadog.trace.api; + +/** + * The hash-only {@link TagMap}: every tag, known or custom, is an {@link TagMap.Entry} in the + * buckets. Used when the dense store is off; see {@link TagMapFactory}. + */ +final class HashTagMap extends TagMap { + HashTagMap(TagMap parent) { + super(parent); + } + + HashTagMap(Object[] buckets, int size) { + super(buckets, size); + } +} 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 602ec6af450..992fb3c8cf9 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -48,12 +48,12 @@ *

  • adaptive collision * */ -public final class TagMap implements Map, Iterable { +public abstract class TagMap implements Map, Iterable { /** 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); + // would fail with ArrayIndexOutOfBoundsException, size 1 works). Built by the factory, so it is + // the one TagMap implementation this JVM loads; the constructor reads no TagMap statics. + public static final TagMap EMPTY = TagMapFactory.INSTANCE.empty(); // Sentinel for an ID not yet resolved by the reused EntryReadingHelper. Zero denotes an unknown // tag, so it cannot represent "not computed". @@ -76,11 +76,11 @@ public static final TagMap fromMapImmutable(@Nonnull Map map) { } public static final TagMap create() { - return new TagMap(); + return TagMapFactory.INSTANCE.create(null); } public static final TagMap create(int size) { - return new TagMap(); + return TagMapFactory.INSTANCE.create(null); } /** @@ -110,7 +110,7 @@ public static final TagMap createFromParent(TagMap parent) { parent = null; } } - return new TagMap(parent); + return TagMapFactory.INSTANCE.create(parent); } /** Creates a new TagMap.Ledger */ @@ -1236,7 +1236,7 @@ public EntryChange next() { */ /** Holder for the dense-store flag, read once from Config so the JIT folds it. */ - private static final class DenseStore { + static final class DenseStore { static final boolean ENABLED = Config.get().isTraceDenseTagsEnabled(); } @@ -1285,16 +1285,12 @@ private static final class DenseStore { */ private Set removedFromParent; - public TagMap() { - this((TagMap) null); - } - /** * Fresh mutable map that reads through to {@code parent} (may be null). The parent is set here at * construction and never re-pointed (only detached to null by {@link #clear()}), so read-through * optimizations can treat it as fixed. */ - private TagMap(TagMap parent) { + TagMap(TagMap parent) { // needs to be a power of 2 for bucket masking calculation to work as intended. With the dense // store on, start on the shared empty buckets; the first bucket write copies them. this.buckets = DenseStore.ENABLED ? EMPTY_BUCKETS : new Object[1 << 4]; @@ -1304,7 +1300,7 @@ private TagMap(TagMap parent) { } /** Used for inexpensive immutable */ - private TagMap(Object[] buckets, int size) { + TagMap(Object[] buckets, int size) { this.buckets = buckets; this.size = size; this.frozen = true; @@ -2616,7 +2612,7 @@ 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); + TagMap copy = TagMapFactory.INSTANCE.create(this.parent); copy.putAllIntoEmptyMap(this); if (this.removedFromParent != null) { copy.removedFromParent = new HashSet<>(this.removedFromParent); diff --git a/internal-api/src/main/java/datadog/trace/api/TagMapFactory.java b/internal-api/src/main/java/datadog/trace/api/TagMapFactory.java new file mode 100644 index 00000000000..b129acd71ad --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/TagMapFactory.java @@ -0,0 +1,59 @@ +package datadog.trace.api; + +/** + * Builds {@link TagMap}s of the one implementation this JVM uses: {@link DenseTagMap} when the + * dense store is on, {@link HashTagMap} otherwise. Rollout scaffolding, like the earlier + * legacy/optimized split: the hash-only map stays one flag away until the dense store is proven, + * and is then folded away. + * + *

    A class, not an interface, and its implementations are only reached through {@link #INSTANCE}: + * the factory not chosen is loaded (to check the field's type) but never linked, so its methods are + * never verified and its map class is never loaded. With one {@code TagMap} subclass loaded, class + * hierarchy analysis devirtualizes every call on a {@code TagMap} without a guard. + */ +abstract class TagMapFactory { + static final TagMapFactory INSTANCE = + TagMap.DenseStore.ENABLED ? DenseTagMapFactory.INSTANCE : HashTagMapFactory.INSTANCE; + + /** A fresh mutable map that reads through to {@code parent} (may be null). */ + abstract TagMap create(TagMap parent); + + /** The shared frozen empty map. */ + abstract TagMap empty(); +} + +final class HashTagMapFactory extends TagMapFactory { + static final HashTagMapFactory INSTANCE = new HashTagMapFactory(); + + private static final TagMap EMPTY = new HashTagMap(new Object[1], 0); + + private HashTagMapFactory() {} + + @Override + TagMap create(TagMap parent) { + return new HashTagMap(parent); + } + + @Override + TagMap empty() { + return EMPTY; + } +} + +final class DenseTagMapFactory extends TagMapFactory { + static final DenseTagMapFactory INSTANCE = new DenseTagMapFactory(); + + private static final TagMap EMPTY = new DenseTagMap(new Object[1], 0); + + private DenseTagMapFactory() {} + + @Override + TagMap create(TagMap parent) { + return new DenseTagMap(parent); + } + + @Override + TagMap empty() { + return EMPTY; + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java index 7a89e6b7af2..c1f43ceccc7 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java @@ -90,6 +90,16 @@ private static Object field(Object target, String name) { } } + @Test + void onlyTheDenseImplementationIsLoaded() { + TagMap map = TagMap.create(); + map.set(KnownTags.COMPONENT_ID, "okhttp"); + + assertSame(DenseTagMap.class, map.getClass()); + assertSame(DenseTagMap.class, TagMap.EMPTY.getClass()); + assertFalse(TagMapImplementations.isLoaded("datadog.trace.api.HashTagMap")); + } + @Test void aSlottedKnownTagIsStoredDenselyAndRoundTrips() { TagMap map = TagMap.create(); diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapFuzzTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapFuzzTest.java index 685dc1d9cdf..d97e389aa4b 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapFuzzTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapFuzzTest.java @@ -898,7 +898,7 @@ public static final TagMap makeTagMap(MapAction... actions) { } public static final TagMap makeTagMap(List actions) { - TagMap map = new TagMap(); + TagMap map = TagMap.create(); for (MapAction action : actions) { action.applyToTestMap(map); } @@ -909,7 +909,7 @@ public static final TagMap test(TestCase test) { List actions = test.actions(); Map hashMap = new HashMap<>(); - TagMap tagMap = new TagMap(); + TagMap tagMap = TagMap.create(); int actionIndex = 0; try { @@ -1100,7 +1100,7 @@ static final Map mapOf(String... keysAndValues) { } static final TagMap tagMapOf(String... keysAndValues) { - TagMap map = new TagMap(); + TagMap map = TagMap.create(); for (int i = 0; i < keysAndValues.length; i += 2) { String key = keysAndValues[i]; String value = keysAndValues[i + 1]; diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapHashOnlyForkedTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapHashOnlyForkedTest.java new file mode 100644 index 00000000000..50f8a051aba --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapHashOnlyForkedTest.java @@ -0,0 +1,30 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.junit.jupiter.api.Test; + +/** + * With the dense store off, TagMap is the hash-only implementation, and the dense one is never + * loaded: with one TagMap subclass loaded, every TagMap call devirtualizes without a guard. Runs in + * its own JVM, so no other test has loaded the dense implementation first. + */ +class TagMapHashOnlyForkedTest { + static { + System.setProperty("dd.trace.experimental.dense.tags.enabled", "false"); + } + + @Test + void onlyTheHashOnlyImplementationIsLoaded() { + TagMap map = TagMap.create(); + map.set(KnownTags.COMPONENT_ID, "okhttp"); + map.set("custom.tag", "value"); + TagMap copy = TagMap.createFromParent(map.freeze()); + + assertSame(HashTagMap.class, map.getClass()); + assertSame(HashTagMap.class, copy.getClass()); + assertSame(HashTagMap.class, TagMap.EMPTY.getClass()); + assertFalse(TagMapImplementations.isLoaded("datadog.trace.api.DenseTagMap")); + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapImplementations.java b/internal-api/src/test/java/datadog/trace/api/TagMapImplementations.java new file mode 100644 index 00000000000..3b3fc9c459e --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapImplementations.java @@ -0,0 +1,30 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.lang.reflect.Method; + +/** Whether a TagMap implementation class has been loaded, without loading it. */ +final class TagMapImplementations { + private TagMapImplementations() {} + + /** + * True if {@code className} has been loaded by TagMap's class loader. Skips the calling test when + * the JVM denies the reflective access this needs (JDK 17+ without {@code --add-opens}). + */ + static boolean isLoaded(String className) { + Method findLoadedClass; + try { + findLoadedClass = ClassLoader.class.getDeclaredMethod("findLoadedClass", String.class); + findLoadedClass.setAccessible(true); + } catch (RuntimeException | NoSuchMethodException e) { + assumeTrue(false, "cannot inspect loaded classes on this JVM: " + e); + return false; + } + try { + return findLoadedClass.invoke(TagMap.class.getClassLoader(), className) != null; + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapTest.java index 9a72b7bdd60..35d30ded83a 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapTest.java @@ -924,7 +924,7 @@ public void computeIfPresent() { @ParameterizedTest @ValueSource(ints = {0, 5, 25, 125}) public void _toInternalString(int size) { - TagMap tagMap = new TagMap(); + TagMap tagMap = TagMap.create(); fillMap(tagMap, size); String str = tagMap.toInternalString(); From 64d28784432a9869bc8b3e829963a85e7f1178a2 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sun, 11 Oct 2026 10:19:04 -0400 Subject: [PATCH 11/29] Move the dense tag store into DenseTagMap TagMap now holds only the hash-bucket store, read-through and views. DenseTagMap holds the dense fields and helpers. It overrides the routing methods (putEntry, getLocalEntry, removeLocal, the set(long, ...) overloads, forEach and the bulk copies) to handle known ids and calls super for custom tags. Shared walks over the parent chain reach dense state through per-level hooks (knownCount, countDenseVisibleIn, forEachDenseVisibleIn, readDenseIfVisible); in HashTagMap those return zero or do nothing. DenseStore.ENABLED is now read only by TagMapFactory. Internals.setKnown goes through setKnownTrusted, which skips the frozen check because the core's span maps are never frozen. Every other rule (tombstones, the shared-name supersede, routing) is unchanged. A new Internals.getKnownValue reads a known tag's value with the same semantics as getEntry(id).objectValue(), but builds no Entry for a local dense hit. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/api/DenseTagMap.java | 579 ++++++++++++++++- .../java/datadog/trace/api/HashTagMap.java | 2 +- .../main/java/datadog/trace/api/TagMap.java | 613 ++++-------------- .../trace/api/KnownTagCodecSlotTest.java | 4 +- .../trace/api/TagMapDenseForkedTest.java | 41 +- .../datadog/trace/api/TagMapSetByIdTest.java | 22 + 6 files changed, 774 insertions(+), 487 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java b/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java index 0b6aea89fae..ee860250efc 100644 --- a/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java @@ -1,16 +1,593 @@ package datadog.trace.api; +import datadog.trace.api.function.TriConsumer; +import java.util.Arrays; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import javax.annotation.Nonnull; + /** * The dense/hash hybrid {@link TagMap}: known tags are stored densely, by id, with no per-tag * {@link TagMap.Entry}; custom tags stay in the buckets. Used when the dense store is on; see * {@link TagMapFactory}. + * + *

    Each routing method handles a dense id here and defers everything else to {@link TagMap}'s + * hash-only behavior. Every map in a JVM is of the one implementation, so a parent is always a + * {@code DenseTagMap} too. */ final class DenseTagMap extends TagMap { + + /* + * Dense known-tag store (experimental; see TagMap.DenseStore.ENABLED, default off). + *

    + * With the flag on, a known tag that has a dense-store slot (KnownTagCodec.hasSlot -- the + * registry colors required/conditional/recommended tags into slots so tags on one span type never + * share one) is NOT stored as a bucket Entry. Its value lives in the insertion-ordered parallel + * arrays knownIds/knownValues instead, with no per-tag Entry object: that is the allocation win. + * Known tags without a slot (NO_SLOT) and custom tags stay in the buckets, exactly as before. + *

    + * Presence: knownOccupancy holds 1L << slot for every dense id ever added. A clear bit means the + * tag is definitely absent, so an append or a miss skips the scan. A set bit only means "maybe": + * the knownIds scan is authoritative, so two tags sharing a slot in one map (legal -- tags from + * span types that never co-occur can still both be set) still work. Bits are never cleared on + * remove; a stale bit costs a scan, never a wrong answer. knownTraceLevel records whether any + * trace-level id was added (trace and span tags reuse slot numbers), with the same superset rule. + *

    + * Disjointness: whether an id routes dense depends only on the id and the global flag, so in a + * given JVM a tag is always dense or always bucketed -- never both, in any map. Read-through + * therefore shadows within a region: an ancestor's dense value can only be shadowed by a nearer + * level's dense value of the same id (parentDenseVisible), an ancestor's bucket entry only by a + * nearer bucket entry (parentEntryVisible, unchanged). Tombstones (removedFromParent) are keyed by + * canonical name and apply to both regions. + *

    + * size counts bucket entries only; knownCount counts dense entries; the local total is + * size + knownCount. Buckets start on the shared EMPTY_BUCKETS and are copied on the first bucket write, so an all-dense map allocates no bucket array. + *

    + * Iteration (iterator(), forEach) emits a dense entry through a reused EntryReadingHelper + * flyweight: valid only until the next element, so call .entry() to retain one. Point reads + * (getEntry) materialize a fresh Entry. + */ + + /** Shared all-null buckets (length 16) that maps start on. */ + private static final Object[] EMPTY_BUCKETS = new Object[1 << 4]; + + /** Initial capacity of the dense arrays; the largest span type resolves about this many tags. */ + private static final int KNOWN_INIT_CAP = 12; + + /** + * Shared empty dense arrays a map starts on (and returns to on clear), so that one full-capacity + * test covers a map's first dense tag too. Never written: a full map spills to its own arrays. + */ + private static final long[] EMPTY_KNOWN_IDS = new long[0]; + + private static final Object[] EMPTY_KNOWN_VALUES = new Object[0]; + + private long[] knownIds = EMPTY_KNOWN_IDS; + private Object[] knownValues = EMPTY_KNOWN_VALUES; + private int knownCount; + private long knownOccupancy; + private boolean knownTraceLevel; + DenseTagMap(TagMap parent) { - super(parent); + // start on the shared empty buckets; the first bucket write copies them + super(parent, EMPTY_BUCKETS); } DenseTagMap(Object[] buckets, int size) { super(buckets, size); } + + @Override + int knownCount() { + return this.knownCount; + } + + @Override + int countDenseVisibleIn(TagMap ancestor) { + DenseTagMap dense = (DenseTagMap) ancestor; + long[] ancestorIds = dense.knownIds; + int count = 0; + for (int i = 0; i < dense.knownCount; ++i) { + if (this.parentDenseVisible(ancestorIds[i], dense)) count++; + } + return count; + } + + @Override + Entry getLocalEntry(String tag, long tagHash) { + long denseId = denseIdOf(tagHash); + if (denseId != 0) { + int i = this.knownIndexOf(denseId); + return i < 0 ? null : Entry.newAnyEntry(denseId, this.knownValues[i]); + } + return super.getLocalEntry(tag, tagHash); + } + + @Override + Object getKnownValue(long tagId) { + int i = this.knownIndexOf(tagId); + return i >= 0 ? this.knownValues[i] : super.getKnownValue(tagId); + } + + /** The buckets to write to, copying the shared empty buckets on the first write. */ + @Override + Object[] writableBuckets() { + Object[] b = this.buckets; + if (b == EMPTY_BUCKETS) { + b = new Object[1 << 4]; + this.buckets = b; + } + return b; + } + + @Override + Entry putEntry(@Nonnull Entry newEntry) { + long denseId = denseIdOf(newEntry.tagHash); + if (denseId != 0) { + return this.putDenseLocal(denseId, newEntry.objectValue()); + } + return super.putEntry(newEntry); + } + + @Override + public void set(long tagId, @Nonnull Object value) { + this.storeDenseLocal(requireKnownId(tagId), value); + } + + @Override + public void set(long tagId, @Nonnull CharSequence value) { + this.storeDenseLocal(requireKnownId(tagId), value); + } + + @Override + public void set(long tagId, boolean value) { + this.storeDenseLocal(requireKnownId(tagId), Boolean.valueOf(value)); + } + + @Override + public void set(long tagId, int value) { + this.storeDenseLocal(requireKnownId(tagId), Integer.valueOf(value)); + } + + @Override + public void set(long tagId, long value) { + this.storeDenseLocal(requireKnownId(tagId), Long.valueOf(value)); + } + + @Override + public void set(long tagId, float value) { + this.storeDenseLocal(requireKnownId(tagId), Float.valueOf(value)); + } + + @Override + public void set(long tagId, double value) { + this.storeDenseLocal(requireKnownId(tagId), Double.valueOf(value)); + } + + @Override + void setKnownTrusted(long tagId, @Nonnull Object value) { + this.storeDenseTrusted(tagId, value); + } + + @Override + void setKnownTrusted(long tagId, @Nonnull CharSequence value) { + this.storeDenseTrusted(tagId, value); + } + + @Override + void setKnownTrusted(long tagId, boolean value) { + this.storeDenseTrusted(tagId, Boolean.valueOf(value)); + } + + @Override + void setKnownTrusted(long tagId, int value) { + this.storeDenseTrusted(tagId, Integer.valueOf(value)); + } + + @Override + void setKnownTrusted(long tagId, long value) { + this.storeDenseTrusted(tagId, Long.valueOf(value)); + } + + @Override + void setKnownTrusted(long tagId, float value) { + this.storeDenseTrusted(tagId, Float.valueOf(value)); + } + + @Override + void setKnownTrusted(long tagId, double value) { + this.storeDenseTrusted(tagId, Double.valueOf(value)); + } + + @Override + void putAllMerge(TagMap that) { + DenseTagMap dense = (DenseTagMap) that; + // incoming dense values clobber existing ones, as incoming bucket entries do + for (int i = 0; i < dense.knownCount; ++i) { + this.storeKnownValue(dense.knownIds[i], dense.knownValues[i]); + } + super.putAllMerge(that); + } + + @Override + void putAllIntoEmptyMap(TagMap that) { + DenseTagMap dense = (DenseTagMap) that; + if (dense.knownCount > 0) { + // dense values are immutable objects, so the copy can share them + this.knownIds = Arrays.copyOf(dense.knownIds, dense.knownIds.length); + this.knownValues = Arrays.copyOf(dense.knownValues, dense.knownValues.length); + this.knownCount = dense.knownCount; + this.knownOccupancy = dense.knownOccupancy; + this.knownTraceLevel = dense.knownTraceLevel; + } + if (that.size == 0) { + return; // no bucket entries; stay on the shared empty buckets + } + super.putAllIntoEmptyMap(that); + } + + @Override + public void fillMap(Map map) { + super.fillMap(map); + if (this.parent == null) { // with a parent, super walked the visible union, dense included + for (int i = 0; i < this.knownCount; ++i) { + map.put(KnownTagCodec.nameOf(this.knownIds[i]), this.knownValues[i]); + } + } + } + + @Override + public void fillStringMap(Map stringMap) { + super.fillStringMap(stringMap); + if (this.parent == null) { // see fillMap + for (int i = 0; i < this.knownCount; ++i) { + stringMap.put( + KnownTagCodec.nameOf(this.knownIds[i]), + TagValueConversions.toString(this.knownValues[i])); + } + } + } + + @Override + Entry removeLocal(String tag, long tagHash) { + long denseId = denseIdOf(tagHash); + if (denseId != 0) { + return this.removeKnown(denseId); + } + return super.removeLocal(tag, tagHash); + } + + @Override + public void forEach(Consumer consumer) { + if (this.knownCount > 0) { + // dense values go through one reused flyweight -- no Entry per tag + EntryReadingHelper reader = new EntryReadingHelper(); + for (int i = 0; i < this.knownCount; ++i) { + reader.set(this.knownIds[i], this.knownValues[i]); + consumer.accept(reader); + } + } + super.forEach(consumer); + } + + @Override + public void forEach(T thisObj, BiConsumer consumer) { + if (this.knownCount > 0) { + EntryReadingHelper reader = new EntryReadingHelper(); + for (int i = 0; i < this.knownCount; ++i) { + reader.set(this.knownIds[i], this.knownValues[i]); + consumer.accept(thisObj, reader); + } + } + super.forEach(thisObj, consumer); + } + + @Override + public void forEach( + T thisObj, U otherObj, TriConsumer consumer) { + if (this.knownCount > 0) { + EntryReadingHelper reader = new EntryReadingHelper(); + for (int i = 0; i < this.knownCount; ++i) { + reader.set(this.knownIds[i], this.knownValues[i]); + consumer.accept(thisObj, otherObj, reader); + } + } + super.forEach(thisObj, otherObj, consumer); + } + + @Override + void forEachDenseVisibleIn(TagMap ancestor, Consumer consumer) { + DenseTagMap dense = (DenseTagMap) ancestor; + EntryReadingHelper reader = null; + for (int i = 0; i < dense.knownCount; ++i) { + long denseId = dense.knownIds[i]; + if (this.parentDenseVisible(denseId, dense)) { + if (reader == null) reader = new EntryReadingHelper(); + reader.set(denseId, dense.knownValues[i]); + consumer.accept(reader); + } + } + } + + @Override + void forEachDenseVisibleIn( + TagMap ancestor, T thisObj, BiConsumer consumer) { + DenseTagMap dense = (DenseTagMap) ancestor; + EntryReadingHelper reader = null; + for (int i = 0; i < dense.knownCount; ++i) { + long denseId = dense.knownIds[i]; + if (this.parentDenseVisible(denseId, dense)) { + if (reader == null) reader = new EntryReadingHelper(); + reader.set(denseId, dense.knownValues[i]); + consumer.accept(thisObj, reader); + } + } + } + + @Override + void forEachDenseVisibleIn( + TagMap ancestor, + T thisObj, + U otherObj, + TriConsumer consumer) { + DenseTagMap dense = (DenseTagMap) ancestor; + EntryReadingHelper reader = null; + for (int i = 0; i < dense.knownCount; ++i) { + long denseId = dense.knownIds[i]; + if (this.parentDenseVisible(denseId, dense)) { + if (reader == null) reader = new EntryReadingHelper(); + reader.set(denseId, dense.knownValues[i]); + consumer.accept(thisObj, otherObj, reader); + } + } + } + + @Override + boolean readDenseIfVisible(TagMap level, int index, EntryReadingHelper reader) { + DenseTagMap dense = (DenseTagMap) level; + long denseId = dense.knownIds[index]; + if (dense == this || this.parentDenseVisible(denseId, dense)) { + reader.set(denseId, dense.knownValues[index]); + return true; + } + return false; + } + + @Override + void clearLocal() { + this.buckets = EMPTY_BUCKETS; // never write to the shared empty buckets + this.size = 0; + this.knownIds = EMPTY_KNOWN_IDS; + this.knownValues = EMPTY_KNOWN_VALUES; + this.knownCount = 0; + this.knownOccupancy = 0L; + this.knownTraceLevel = false; + } + + @Override + void checkIntegrity() { + // dense store: every id slotted and unique, its slot bit set, the count within bounds + if (this.knownCount > 0) { + if (this.knownIds == null || this.knownCount > this.knownIds.length) { + throw new IllegalStateException("incorrect known count"); + } + for (int i = 0; i < this.knownCount; ++i) { + long denseId = this.knownIds[i]; + if (denseIdOf(denseId) == 0) { + throw new IllegalStateException("dense id without a slot"); + } + if ((this.knownOccupancy & slotBit(denseId)) == 0) { + throw new IllegalStateException("dense id missing from occupancy"); + } + for (int j = i + 1; j < this.knownCount; ++j) { + if (this.knownIds[j] == denseId) { + throw new IllegalStateException("duplicate dense id"); + } + } + } + } + // disjointness: no bucket entry may be one that routes dense + for (EntryReader entry : this) { + if (entry instanceof Entry && denseIdOf(((Entry) entry).tagHash) != 0) { + throw new IllegalStateException("dense-routed tag stored in a bucket: " + entry.tag()); + } + } + super.checkIntegrity(); + } + + @Override + String toInternalString() { + StringBuilder ledger = new StringBuilder(super.toInternalString()); + for (int i = 0; i < this.knownCount; ++i) { + ledger + .append("[dense ") + .append(i) + .append("] = {") + .append(KnownTagCodec.nameOf(this.knownIds[i])) + .append('=') + .append(this.knownValues[i]) + .append("}\n"); + } + return ledger.toString(); + } + + /** + * The id to store densely for {@code tagHash} (an Entry's tag hash, or a tag id), or 0 for a + * custom tag, which stays in the buckets. Every known tag is dense, slotted or not, so an + * id-keyed write never reaches the hash path. + */ + private static long denseIdOf(long tagHash) { + // a known tag's hash is its id (upper 32 bits nonzero); a custom tag's upper 32 bits are zero + return (tagHash >>> 32) != 0 ? tagHash : 0L; + } + + /** + * A dense id's occupancy bit. The bit only rules a tag out quickly; the scan decides, so ids may + * share one. No branch for a tag without a slot: a long shift uses only the low 6 bits of its + * count, so {@code NO_SLOT} (0xFFFF) lands on bit 63, which the generator reserves (real slots + * stay below 63) and every unslotted tag shares. + */ + static long slotBit(long denseId) { + return 1L << KnownTagCodec.slotOf(denseId); + } + + /** + * Index of {@code denseId} in the dense arrays, or -1; skips the scan when its slot bit is clear. + */ + private int knownIndexOf(long denseId) { + if ((this.knownOccupancy & slotBit(denseId)) == 0) { + return -1; // definitely absent + } + long[] ids = this.knownIds; + for (int i = 0; i < this.knownCount; ++i) { + if (ids[i] == denseId) return i; + } + return -1; + } + + /** + * {@link #knownIndexOf} for a write: one direction of a shared name also matches the name's + * undirected tag, which it supersedes in place. Both share a slot, so one occupancy test covers + * them, and for a constant id the alias folds to a constant (or, for most tags, to the id + * itself). + */ + private int knownWriteIndexOf(long denseId) { + if ((this.knownOccupancy & slotBit(denseId)) == 0) { + return -1; // definitely absent + } + long alias = + KnownTagCodec.hasSharedName(denseId) ? KnownTagCodec.undirectedIdOf(denseId) : denseId; + long[] ids = this.knownIds; + for (int i = 0; i < this.knownCount; ++i) { + long id = ids[i]; + if (id == denseId || id == alias) return i; + } + return -1; + } + + /** + * Stores a dense value, replacing it in place when present or appending it, and returns the prior + * value, or null when there was none. Dense values are never null. Builds no Entry, so a caller + * that discards the result allocates nothing, whether or not the JIT inlines this. + */ + private Object storeKnownValue(long denseId, Object value) { + int i = this.knownWriteIndexOf(denseId); + if (i >= 0) { + long[] ids = this.knownIds; + // a superseded undirected tag is relabeled, and was not this tag's prior value + Object prior = ids[i] == denseId ? this.knownValues[i] : null; + ids[i] = denseId; + this.knownValues[i] = value; + return prior; + } + int idx = this.knownCount; + if (idx == this.knownIds.length) { + this.spillKnown(); + } + this.knownIds[idx] = denseId; + this.knownValues[idx] = value; + this.knownCount = idx + 1; + this.knownOccupancy |= slotBit(denseId); + this.knownTraceLevel |= KnownTagCodec.isTraceLevel(denseId); + return null; + } + + /** + * {@code tagId}, when it names a known tag; the checked id-keyed setters reject anything else. + */ + private static long requireKnownId(long tagId) { + if (!KnownTagCodec.isKnown(tagId)) { + throw new IllegalArgumentException("not a known tag id: " + Long.toHexString(tagId)); + } + return tagId; + } + + /** + * Makes room for one more dense tag. A map's first dense tag allocates its own arrays -- a plain + * allocation, since on fresh spans this runs once per span and is compiled inline. A full map + * grows once, in {@link #growKnown}. + */ + private void spillKnown() { + if (this.knownIds.length == 0) { + this.knownIds = new long[KNOWN_INIT_CAP]; + this.knownValues = new Object[KNOWN_INIT_CAP]; + } else { + this.growKnown(); + } + } + + /** + * Grows full dense arrays once, straight to the hard bound: a map holds each known tag at most + * once, so {@link KnownTagCodec#serialLimit()} entries always suffice. Rare, so the copying stays + * out of the write. + */ + private void growKnown() { + int newCap = Math.max(KnownTagCodec.serialLimit(), this.knownIds.length << 1); + this.knownIds = Arrays.copyOf(this.knownIds, newCap); + this.knownValues = Arrays.copyOf(this.knownValues, newCap); + } + + /** Clears a read-through tombstone for a dense tag being set; out of line, as it is rare. */ + private void clearDenseTombstone(long denseId) { + this.removedFromParent.remove(KnownTagCodec.nameOf(denseId)); + } + + /** + * Local dense write: clears any tombstone for the tag, then stores the value, returning the prior + * value or null. The {@code set(long, ...)} methods use this; it builds no Entry. + */ + private Object storeDenseLocal(long denseId, Object value) { + this.checkWriteAccess(); + return this.storeDenseTrusted(denseId, value); + } + + /** {@link #storeDenseLocal} without the frozen check, for trusted writes. */ + private Object storeDenseTrusted(long denseId, Object value) { + if (this.removedFromParent != null) { + this.clearDenseTombstone(denseId); + } + return this.storeKnownValue(denseId, value); + } + + /** {@link #storeDenseLocal}, returning the prior value as an Entry, per the Map contract. */ + private Entry putDenseLocal(long denseId, Object value) { + Object prior = this.storeDenseLocal(denseId, value); + return prior == null ? null : Entry.newAnyEntry(denseId, prior); + } + + /** Removes a dense value (swap with the last), returning it as an Entry, or null. */ + private Entry removeKnown(long denseId) { + int i = this.knownIndexOf(denseId); + if (i < 0) return null; + Object prior = this.knownValues[i]; + int last = --this.knownCount; + this.knownIds[i] = this.knownIds[last]; + this.knownValues[i] = this.knownValues[last]; + this.knownIds[last] = 0L; + this.knownValues[last] = null; + // knownOccupancy is not cleared: another present id may share the slot bit. + return Entry.newAnyEntry(denseId, prior); + } + + /** + * Whether an ancestor's dense value for {@code denseId} (held at {@code fromAncestor}) is visible + * from this leaf: not shadowed by a nearer level's dense value of the same id, and not tombstoned + * by a nearer level. The dense counterpart of {@code parentEntryVisible}; by disjointness no + * bucket check is needed. A nearer level with no trace-level ids cannot shadow a trace-level one. + */ + private boolean parentDenseVisible(long denseId, TagMap fromAncestor) { + boolean traceLevel = KnownTagCodec.isTraceLevel(denseId); + String tag = null; // resolved lazily, only if a nearer level carries tombstones + for (DenseTagMap nearer = this; nearer != fromAncestor; nearer = (DenseTagMap) nearer.parent) { + if ((!traceLevel || nearer.knownTraceLevel) && nearer.knownIndexOf(denseId) >= 0) { + return false; // shadowed by a nearer dense value + } + if (nearer.removedFromParent != null) { + if (tag == null) tag = KnownTagCodec.nameOf(denseId); + if (nearer.removedFromParent.contains(tag)) return false; // tombstoned by a nearer level + } + } + return true; + } } diff --git a/internal-api/src/main/java/datadog/trace/api/HashTagMap.java b/internal-api/src/main/java/datadog/trace/api/HashTagMap.java index 55b67c8b5e8..ca98c57bbb8 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashTagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/HashTagMap.java @@ -6,7 +6,7 @@ */ final class HashTagMap extends TagMap { HashTagMap(TagMap parent) { - super(parent); + super(parent, new Object[1 << 4]); } HashTagMap(Object[] buckets, int size) { 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 992fb3c8cf9..c464b224e2d 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1203,67 +1203,15 @@ public EntryChange next() { * removed from the collision chain. */ - /* - * Dense known-tag store (experimental; see DenseStore.ENABLED, default off). - *

    - * With the flag on, a known tag that has a dense-store slot (KnownTagCodec.hasSlot -- the - * registry colors required/conditional/recommended tags into slots so tags on one span type never - * share one) is NOT stored as a bucket Entry. Its value lives in the insertion-ordered parallel - * arrays knownIds/knownValues instead, with no per-tag Entry object: that is the allocation win. - * Known tags without a slot (NO_SLOT) and custom tags stay in the buckets, exactly as before. - *

    - * Presence: knownOccupancy holds 1L << slot for every dense id ever added. A clear bit means the - * tag is definitely absent, so an append or a miss skips the scan. A set bit only means "maybe": - * the knownIds scan is authoritative, so two tags sharing a slot in one map (legal -- tags from - * span types that never co-occur can still both be set) still work. Bits are never cleared on - * remove; a stale bit costs a scan, never a wrong answer. knownTraceLevel records whether any - * trace-level id was added (trace and span tags reuse slot numbers), with the same superset rule. - *

    - * Disjointness: whether an id routes dense depends only on the id and the global flag, so in a - * given JVM a tag is always dense or always bucketed -- never both, in any map. Read-through - * therefore shadows within a region: an ancestor's dense value can only be shadowed by a nearer - * level's dense value of the same id (parentDenseVisible), an ancestor's bucket entry only by a - * nearer bucket entry (parentEntryVisible, unchanged). Tombstones (removedFromParent) are keyed by - * canonical name and apply to both regions. - *

    - * size counts bucket entries only; knownCount counts dense entries; the local total is - * size + knownCount. Buckets start on the shared EMPTY_BUCKETS (when the flag is on) and are - * copied on the first bucket write, so an all-dense map allocates no bucket array. - *

    - * Iteration (iterator(), forEach) emits a dense entry through a reused EntryReadingHelper - * flyweight: valid only until the next element, so call .entry() to retain one. Point reads - * (getEntry) materialize a fresh Entry. - */ - /** Holder for the dense-store flag, read once from Config so the JIT folds it. */ static final class DenseStore { static final boolean ENABLED = Config.get().isTraceDenseTagsEnabled(); } - /** Shared all-null buckets (length 16) that maps start on when the dense store is enabled. */ - private static final Object[] EMPTY_BUCKETS = new Object[1 << 4]; - - /** Initial capacity of the dense arrays; the largest span type resolves about this many tags. */ - private static final int KNOWN_INIT_CAP = 12; - - /** - * Shared empty dense arrays a map starts on (and returns to on clear), so that one full-capacity - * test covers a map's first dense tag too. Never written: a full map spills to its own arrays. - */ - private static final long[] EMPTY_KNOWN_IDS = new long[0]; - - private static final Object[] EMPTY_KNOWN_VALUES = new Object[0]; - - private Object[] buckets; - private int size; + Object[] buckets; + int size; private boolean frozen; - private long[] knownIds = EMPTY_KNOWN_IDS; - private Object[] knownValues = EMPTY_KNOWN_VALUES; - private int knownCount; - private long knownOccupancy; - private boolean knownTraceLevel; - /** * 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 @@ -1283,17 +1231,17 @@ static final class DenseStore { * and rare, so it costs a lazy allocation on removal rather than complicating the hot bucket * code. */ - private Set removedFromParent; + Set removedFromParent; /** * Fresh mutable map that reads through to {@code parent} (may be null). The parent is set here at * construction and never re-pointed (only detached to null by {@link #clear()}), so read-through - * optimizations can treat it as fixed. + * optimizations can treat it as fixed. {@code buckets} is the starting bucket array (a power of + * two in length); a subclass may pass a shared empty one that {@link #writableBuckets} copies. */ - TagMap(TagMap parent) { - // needs to be a power of 2 for bucket masking calculation to work as intended. With the dense - // store on, start on the shared empty buckets; the first bucket write copies them. - this.buckets = DenseStore.ENABLED ? EMPTY_BUCKETS : new Object[1 << 4]; + TagMap(TagMap parent, Object[] buckets) { + // needs to be a power of 2 for bucket masking calculation to work as intended + this.buckets = buckets; this.size = 0; this.frozen = false; this.parent = parent; @@ -1310,7 +1258,7 @@ static final class DenseStore { @Override public int size() { // Exact (Map contract). Under read-through resolves the union; prefer estimateSize() for hints. - int local = this.size + this.knownCount; // buckets + dense + int local = this.size + this.knownCount(); // buckets + dense TagMap parent = this.parent; return parent == null ? local : local + this.visibleParentCount(); } @@ -1322,10 +1270,7 @@ public int size() { private int visibleParentCount() { int count = 0; for (TagMap ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { - long[] ancestorIds = ancestor.knownIds; - for (int i = 0; i < ancestor.knownCount; ++i) { - if (this.parentDenseVisible(ancestorIds[i], ancestor)) count++; - } + count += this.countDenseVisibleIn(ancestor); Object[] parentBuckets = ancestor.buckets; for (int i = 0; i < parentBuckets.length; ++i) { Object parentBucket = parentBuckets[i]; @@ -1349,7 +1294,7 @@ private int visibleParentCount() { @Override public boolean isEmpty() { // Exact (Map contract). Under read-through resolves the parent; prefer isDefinitelyEmpty(). - if (this.size != 0 || this.knownCount != 0) { + if (this.size != 0 || this.knownCount() != 0) { return false; } TagMap parent = this.parent; @@ -1370,7 +1315,7 @@ public boolean 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 || level.knownCount != 0) { + if (level.size != 0 || level.knownCount() != 0) { return false; } } @@ -1382,11 +1327,24 @@ public int estimateSize() { // shadowing/removals. int total = 0; for (TagMap level = this; level != null; level = level.parent) { - total += level.size + level.knownCount; + total += level.size + level.knownCount(); } return total; } + /** The number of dense values held locally; 0 for a map without a dense store. */ + int knownCount() { + return 0; + } + + /** + * The number of {@code ancestor}'s dense values visible from this leaf; 0 for a map without a + * dense store. + */ + int countDenseVisibleIn(TagMap ancestor) { + return 0; + } + @Deprecated @Override public Object get(Object tag) { @@ -1547,14 +1505,7 @@ private Entry getEntry(String canonicalTag, long tagHash) { } /** Looks up an entry in this map's own storage only — no read-through to the parent. */ - private Entry getLocalEntry(String tag, long tagHash) { - if (DenseStore.ENABLED) { - long denseId = denseIdOf(tagHash); - if (denseId != 0) { - int i = this.knownIndexOf(denseId); - return i < 0 ? null : Entry.newAnyEntry(denseId, this.knownValues[i]); - } - } + Entry getLocalEntry(String tag, long tagHash) { Object[] thisBuckets = this.buckets; int hash = Entry.bucketHash(tagHash); return findInBucket(thisBuckets[hash & (thisBuckets.length - 1)], hash, tag); @@ -1602,190 +1553,9 @@ private boolean parentEntryVisible(Entry parentEntry, TagMap fromAncestor) { return true; } - // ---- dense known-tag store (see the class-level note above the fields) ---- - - /** - * The id to store densely for {@code tagHash} (an Entry's tag hash, or a tag id), or 0 for a - * custom tag, which stays in the buckets. Every known tag is dense, slotted or not, so an - * id-keyed write never reaches the hash path. - */ - private static long denseIdOf(long tagHash) { - // a known tag's hash is its id (upper 32 bits nonzero); a custom tag's upper 32 bits are zero - return (tagHash >>> 32) != 0 ? tagHash : 0L; - } - - /** - * A dense id's occupancy bit. The bit only rules a tag out quickly; the scan decides, so ids may - * share one. No branch for a tag without a slot: a long shift uses only the low 6 bits of its - * count, so {@code NO_SLOT} (0xFFFF) lands on bit 63, which the generator reserves (real slots - * stay below 63) and every unslotted tag shares. - */ - static long slotBit(long denseId) { - return 1L << KnownTagCodec.slotOf(denseId); - } - - /** - * Index of {@code denseId} in the dense arrays, or -1; skips the scan when its slot bit is clear. - */ - private int knownIndexOf(long denseId) { - if ((this.knownOccupancy & slotBit(denseId)) == 0) { - return -1; // definitely absent - } - long[] ids = this.knownIds; - for (int i = 0; i < this.knownCount; ++i) { - if (ids[i] == denseId) return i; - } - return -1; - } - - /** - * {@link #knownIndexOf} for a write: one direction of a shared name also matches the name's - * undirected tag, which it supersedes in place. Both share a slot, so one occupancy test covers - * them, and for a constant id the alias folds to a constant (or, for most tags, to the id - * itself). - */ - private int knownWriteIndexOf(long denseId) { - if ((this.knownOccupancy & slotBit(denseId)) == 0) { - return -1; // definitely absent - } - long alias = - KnownTagCodec.hasSharedName(denseId) ? KnownTagCodec.undirectedIdOf(denseId) : denseId; - long[] ids = this.knownIds; - for (int i = 0; i < this.knownCount; ++i) { - long id = ids[i]; - if (id == denseId || id == alias) return i; - } - return -1; - } - - /** - * Stores a dense value, replacing it in place when present or appending it, and returns the prior - * value, or null when there was none. Dense values are never null. Builds no Entry, so a caller - * that discards the result allocates nothing, whether or not the JIT inlines this. - */ - private Object storeKnownValue(long denseId, Object value) { - int i = this.knownWriteIndexOf(denseId); - if (i >= 0) { - long[] ids = this.knownIds; - // a superseded undirected tag is relabeled, and was not this tag's prior value - Object prior = ids[i] == denseId ? this.knownValues[i] : null; - ids[i] = denseId; - this.knownValues[i] = value; - return prior; - } - int idx = this.knownCount; - if (idx == this.knownIds.length) { - this.spillKnown(); - } - this.knownIds[idx] = denseId; - this.knownValues[idx] = value; - this.knownCount = idx + 1; - this.knownOccupancy |= slotBit(denseId); - this.knownTraceLevel |= KnownTagCodec.isTraceLevel(denseId); - return null; - } - - /** - * {@code tagId}, when it names a known tag; the checked id-keyed setters reject anything else. - */ - private static long requireKnownId(long tagId) { - if (!KnownTagCodec.isKnown(tagId)) { - throw new IllegalArgumentException("not a known tag id: " + Long.toHexString(tagId)); - } - return tagId; - } - - /** - * Makes room for one more dense tag. A map's first dense tag allocates its own arrays -- a plain - * allocation, since on fresh spans this runs once per span and is compiled inline. A full map - * grows once, in {@link #growKnown}. - */ - private void spillKnown() { - if (this.knownIds.length == 0) { - this.knownIds = new long[KNOWN_INIT_CAP]; - this.knownValues = new Object[KNOWN_INIT_CAP]; - } else { - this.growKnown(); - } - } - - /** - * Grows full dense arrays once, straight to the hard bound: a map holds each known tag at most - * once, so {@link KnownTagCodec#serialLimit()} entries always suffice. Rare, so the copying stays - * out of the write. - */ - private void growKnown() { - int newCap = Math.max(KnownTagCodec.serialLimit(), this.knownIds.length << 1); - this.knownIds = Arrays.copyOf(this.knownIds, newCap); - this.knownValues = Arrays.copyOf(this.knownValues, newCap); - } - - /** Clears a read-through tombstone for a dense tag being set; out of line, as it is rare. */ - private void clearDenseTombstone(long denseId) { - this.removedFromParent.remove(KnownTagCodec.nameOf(denseId)); - } - - /** - * Local dense write: clears any tombstone for the tag, then stores the value, returning the prior - * value or null. The {@code set(long, ...)} methods use this; it builds no Entry. - */ - private Object storeDenseLocal(long denseId, Object value) { - this.checkWriteAccess(); - if (this.removedFromParent != null) { - this.clearDenseTombstone(denseId); - } - return this.storeKnownValue(denseId, value); - } - - /** {@link #storeDenseLocal}, returning the prior value as an Entry, per the Map contract. */ - private Entry putDenseLocal(long denseId, Object value) { - Object prior = this.storeDenseLocal(denseId, value); - return prior == null ? null : Entry.newAnyEntry(denseId, prior); - } - - /** Removes a dense value (swap with the last), returning it as an Entry, or null. */ - private Entry removeKnown(long denseId) { - int i = this.knownIndexOf(denseId); - if (i < 0) return null; - Object prior = this.knownValues[i]; - int last = --this.knownCount; - this.knownIds[i] = this.knownIds[last]; - this.knownValues[i] = this.knownValues[last]; - this.knownIds[last] = 0L; - this.knownValues[last] = null; - // knownOccupancy is not cleared: another present id may share the slot bit. - return Entry.newAnyEntry(denseId, prior); - } - - /** - * Whether an ancestor's dense value for {@code denseId} (held at {@code fromAncestor}) is visible - * from this leaf: not shadowed by a nearer level's dense value of the same id, and not tombstoned - * by a nearer level. The dense counterpart of {@link #parentEntryVisible}; by disjointness no - * bucket check is needed. A nearer level with no trace-level ids cannot shadow a trace-level one. - */ - private boolean parentDenseVisible(long denseId, TagMap fromAncestor) { - boolean traceLevel = KnownTagCodec.isTraceLevel(denseId); - String tag = null; // resolved lazily, only if a nearer level carries tombstones - for (TagMap nearer = this; nearer != fromAncestor; nearer = nearer.parent) { - if ((!traceLevel || nearer.knownTraceLevel) && nearer.knownIndexOf(denseId) >= 0) { - return false; // shadowed by a nearer dense value - } - if (nearer.removedFromParent != null) { - if (tag == null) tag = KnownTagCodec.nameOf(denseId); - if (nearer.removedFromParent.contains(tag)) return false; // tombstoned by a nearer level - } - } - return true; - } - - /** The buckets to write to, copying the shared empty buckets on the first write. */ - private Object[] writableBuckets() { - Object[] b = this.buckets; - if (DenseStore.ENABLED && b == EMPTY_BUCKETS) { - b = new Object[1 << 4]; - this.buckets = b; - } - return b; + /** The buckets to write to; a subclass starting on shared buckets copies them here. */ + Object[] writableBuckets() { + return this.buckets; } @Deprecated @@ -1961,121 +1731,112 @@ private Internals(TagMap map) { /* * Sets a known tag by id without validating it, unlike TagMap.set(long, ...): the caller has - * already checked KnownTagCodec.isKnown(tagId). With the dense store on, the tag goes straight - * to the dense arrays, so no Entry is built. + * already checked KnownTagCodec.isKnown(tagId), and the span's map is never frozen, so the write + * skips the frozen check. With the dense store on, the tag goes straight to the dense arrays, so + * no Entry is built. */ public void setKnown(long tagId, @Nonnull Object value) { - if (DenseStore.ENABLED) { - this.map.storeDenseLocal(tagId, value); - return; - } - this.map.putEntry(new Entry(tagId, null, Entry.ANY, 0L, value)); + this.map.setKnownTrusted(tagId, value); } public void setKnown(long tagId, @Nonnull CharSequence value) { - if (DenseStore.ENABLED) { - this.map.storeDenseLocal(tagId, value); - return; - } - this.map.putEntry(new Entry(tagId, null, EntryReader.OBJECT, 0L, value)); + this.map.setKnownTrusted(tagId, value); } public void setKnown(long tagId, boolean value) { - if (DenseStore.ENABLED) { - this.map.storeDenseLocal(tagId, Boolean.valueOf(value)); - return; - } - this.map.putEntry( - new Entry( - tagId, null, EntryReader.BOOLEAN, Entry.boolean2Prim(value), Boolean.valueOf(value))); + this.map.setKnownTrusted(tagId, value); } public void setKnown(long tagId, int value) { - if (DenseStore.ENABLED) { - this.map.storeDenseLocal(tagId, Integer.valueOf(value)); - return; - } - this.map.putEntry(new Entry(tagId, null, EntryReader.INT, Entry.int2Prim(value), null)); + this.map.setKnownTrusted(tagId, value); } public void setKnown(long tagId, long value) { - if (DenseStore.ENABLED) { - this.map.storeDenseLocal(tagId, Long.valueOf(value)); - return; - } - this.map.putEntry(new Entry(tagId, null, EntryReader.LONG, Entry.long2Prim(value), null)); + this.map.setKnownTrusted(tagId, value); } public void setKnown(long tagId, float value) { - if (DenseStore.ENABLED) { - this.map.storeDenseLocal(tagId, Float.valueOf(value)); - return; - } - this.map.putEntry(new Entry(tagId, null, EntryReader.FLOAT, Entry.float2Prim(value), null)); + this.map.setKnownTrusted(tagId, value); } public void setKnown(long tagId, double value) { - if (DenseStore.ENABLED) { - this.map.storeDenseLocal(tagId, Double.valueOf(value)); - return; - } - this.map.putEntry(new Entry(tagId, null, EntryReader.DOUBLE, Entry.double2Prim(value), null)); + this.map.setKnownTrusted(tagId, value); + } + + /** + * The value of a known tag, boxed if necessary, or null: {@code getEntry(tagId)}'s value, + * including read-through, without building an Entry for a local dense value. + */ + public Object getKnownValue(long tagId) { + return this.map.getKnownValue(tagId); } } + /* + * Trusted, unchecked known-tag writes behind Internals.setKnown: no id validation and no frozen + * check. A subclass with a dense store overrides them to write the dense arrays. + */ + void setKnownTrusted(long tagId, @Nonnull Object value) { + this.putEntryLocal(new Entry(tagId, null, Entry.ANY, 0L, value)); + } + + void setKnownTrusted(long tagId, @Nonnull CharSequence value) { + this.putEntryLocal(new Entry(tagId, null, EntryReader.OBJECT, 0L, value)); + } + + void setKnownTrusted(long tagId, boolean value) { + this.putEntryLocal( + new Entry( + tagId, null, EntryReader.BOOLEAN, Entry.boolean2Prim(value), Boolean.valueOf(value))); + } + + void setKnownTrusted(long tagId, int value) { + this.putEntryLocal(new Entry(tagId, null, EntryReader.INT, Entry.int2Prim(value), null)); + } + + void setKnownTrusted(long tagId, long value) { + this.putEntryLocal(new Entry(tagId, null, EntryReader.LONG, Entry.long2Prim(value), null)); + } + + void setKnownTrusted(long tagId, float value) { + this.putEntryLocal(new Entry(tagId, null, EntryReader.FLOAT, Entry.float2Prim(value), null)); + } + + void setKnownTrusted(long tagId, double value) { + this.putEntryLocal(new Entry(tagId, null, EntryReader.DOUBLE, Entry.double2Prim(value), null)); + } + + /** {@code getEntry(tagId)}'s value, or null; see {@link Internals#getKnownValue}. */ + Object getKnownValue(long tagId) { + Entry entry = this.getEntry(tagId); + return entry == null ? null : entry.objectValue(); + } + public void set(long tagId, @Nonnull Object value) { - if (DenseStore.ENABLED) { - this.storeDenseLocal(requireKnownId(tagId), value); - return; - } this.putEntry(Entry.newAnyEntry(tagId, value)); } public void set(long tagId, @Nonnull CharSequence value) { - if (DenseStore.ENABLED) { - this.storeDenseLocal(requireKnownId(tagId), value); - return; - } this.putEntry(Entry.newObjectEntry(tagId, value)); } public void set(long tagId, boolean value) { - if (DenseStore.ENABLED) { - this.storeDenseLocal(requireKnownId(tagId), Boolean.valueOf(value)); - return; - } this.putEntry(Entry.newBooleanEntry(tagId, value)); } public void set(long tagId, int value) { - if (DenseStore.ENABLED) { - this.storeDenseLocal(requireKnownId(tagId), Integer.valueOf(value)); - return; - } this.putEntry(Entry.newIntEntry(tagId, value)); } public void set(long tagId, long value) { - if (DenseStore.ENABLED) { - this.storeDenseLocal(requireKnownId(tagId), Long.valueOf(value)); - return; - } this.putEntry(Entry.newLongEntry(tagId, value)); } public void set(long tagId, float value) { - if (DenseStore.ENABLED) { - this.storeDenseLocal(requireKnownId(tagId), Float.valueOf(value)); - return; - } this.putEntry(Entry.newFloatEntry(tagId, value)); } public void set(long tagId, double value) { - if (DenseStore.ENABLED) { - this.storeDenseLocal(requireKnownId(tagId), Double.valueOf(value)); - return; - } this.putEntry(Entry.newDoubleEntry(tagId, value)); } @@ -2112,15 +1873,13 @@ public Entry getAndSet(@Nullable Entry newEntry) { * for a prior-value lookup they discard; {@link #getAndSet(Entry)} layers the parent fallback on * top. */ - private Entry putEntry(@Nonnull Entry newEntry) { - if (DenseStore.ENABLED) { - long denseId = denseIdOf(newEntry.tagHash); - if (denseId != 0) { - return this.putDenseLocal(denseId, newEntry.objectValue()); - } - } + Entry putEntry(@Nonnull Entry newEntry) { this.checkWriteAccess(); + return this.putEntryLocal(newEntry); + } + /** {@link #putEntry} without the frozen check, for trusted writes. */ + private Entry putEntryLocal(@Nonnull Entry newEntry) { long newTagHash = newEntry.tagHash; if ((newTagHash >>> 32) != 0 && KnownTagCodec.hasSharedName(newTagHash)) { // One direction of a tag declared per direction supersedes the name's undirected tag (set @@ -2260,18 +2019,14 @@ private void putAllOptimizedMap(TagMap that) { return; } // empty must consider both regions: a map holding only dense values has size == 0 - if (this.size == 0 && this.knownCount == 0) { + if (this.size == 0 && this.knownCount() == 0) { this.putAllIntoEmptyMap(that); } else { this.putAllMerge(that); } } - private void putAllMerge(TagMap that) { - // incoming dense values clobber existing ones, as incoming bucket entries do below - for (int i = 0; i < that.knownCount; ++i) { - this.storeKnownValue(that.knownIds[i], that.knownValues[i]); - } + void putAllMerge(TagMap that) { if (that.size == 0) { return; // no bucket entries to merge; leaves shared empty buckets shared } @@ -2392,19 +2147,7 @@ private void putAllMerge(TagMap that) { /* * Specially optimized version of putAll for the common case of destination map being empty */ - private void putAllIntoEmptyMap(TagMap that) { - if (that.knownCount > 0) { - // dense values are immutable objects, so the copy can share them - this.knownIds = Arrays.copyOf(that.knownIds, that.knownIds.length); - this.knownValues = Arrays.copyOf(that.knownValues, that.knownValues.length); - this.knownCount = that.knownCount; - this.knownOccupancy = that.knownOccupancy; - this.knownTraceLevel = that.knownTraceLevel; - } - if (DenseStore.ENABLED && that.size == 0) { - return; // no bucket entries; stay on the shared empty buckets - } - + void putAllIntoEmptyMap(TagMap that) { Object[] thisBuckets = this.writableBuckets(); Object[] thatBuckets = that.buckets; @@ -2452,9 +2195,6 @@ public void fillMap(Map map) { thisGroup.fillMapFromChain(map); } } - for (int i = 0; i < this.knownCount; ++i) { - map.put(KnownTagCodec.nameOf(this.knownIds[i]), this.knownValues[i]); - } } public void fillStringMap(Map stringMap) { @@ -2480,11 +2220,6 @@ public void fillStringMap(Map stringMap) { thisGroup.fillStringMapFromChain(stringMap); } } - for (int i = 0; i < this.knownCount; ++i) { - stringMap.put( - KnownTagCodec.nameOf(this.knownIds[i]), - TagValueConversions.toString(this.knownValues[i])); - } } @Override @@ -2565,13 +2300,7 @@ private Entry getAndRemove(String canonicalTag, long tagHash) { } /** Removes an entry from this map's own storage only — no parent/tombstone handling. */ - private Entry removeLocal(String tag, long tagHash) { - if (DenseStore.ENABLED) { - long denseId = denseIdOf(tagHash); - if (denseId != 0) { - return this.removeKnown(denseId); - } - } + Entry removeLocal(String tag, long tagHash) { Object[] thisBuckets = this.buckets; int hash = Entry.bucketHash(tagHash); @@ -2639,14 +2368,6 @@ public Stream stream() { @Override public void forEach(Consumer consumer) { - if (this.knownCount > 0) { - // dense values go through one reused flyweight -- no Entry per tag - EntryReadingHelper reader = new EntryReadingHelper(); - for (int i = 0; i < this.knownCount; ++i) { - reader.set(this.knownIds[i], this.knownValues[i]); - consumer.accept(reader); - } - } Object[] thisBuckets = this.buckets; for (int i = 0; i < thisBuckets.length; ++i) { @@ -2673,16 +2394,8 @@ public void forEach(Consumer 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. - EntryReadingHelper reader = null; for (TagMap ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { - for (int i = 0; i < ancestor.knownCount; ++i) { - long denseId = ancestor.knownIds[i]; - if (this.parentDenseVisible(denseId, ancestor)) { - if (reader == null) reader = new EntryReadingHelper(); - reader.set(denseId, ancestor.knownValues[i]); - consumer.accept(reader); - } - } + this.forEachDenseVisibleIn(ancestor, consumer); Object[] parentBuckets = ancestor.buckets; for (int i = 0; i < parentBuckets.length; ++i) { Object parentBucket = parentBuckets[i]; @@ -2705,13 +2418,6 @@ private void forEachParent(Consumer consumer) { } public void forEach(T thisObj, BiConsumer consumer) { - if (this.knownCount > 0) { - EntryReadingHelper reader = new EntryReadingHelper(); - for (int i = 0; i < this.knownCount; ++i) { - reader.set(this.knownIds[i], this.knownValues[i]); - consumer.accept(thisObj, reader); - } - } Object[] thisBuckets = this.buckets; for (int i = 0; i < thisBuckets.length; ++i) { @@ -2735,16 +2441,8 @@ public void forEach(T thisObj, BiConsumer con } private void forEachParent(T thisObj, BiConsumer consumer) { - EntryReadingHelper reader = null; for (TagMap ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { - for (int i = 0; i < ancestor.knownCount; ++i) { - long denseId = ancestor.knownIds[i]; - if (this.parentDenseVisible(denseId, ancestor)) { - if (reader == null) reader = new EntryReadingHelper(); - reader.set(denseId, ancestor.knownValues[i]); - consumer.accept(thisObj, reader); - } - } + this.forEachDenseVisibleIn(ancestor, thisObj, consumer); Object[] parentBuckets = ancestor.buckets; for (int i = 0; i < parentBuckets.length; ++i) { Object parentBucket = parentBuckets[i]; @@ -2769,13 +2467,6 @@ private void forEachParent(T thisObj, BiConsumer void forEach( T thisObj, U otherObj, TriConsumer consumer) { - if (this.knownCount > 0) { - EntryReadingHelper reader = new EntryReadingHelper(); - for (int i = 0; i < this.knownCount; ++i) { - reader.set(this.knownIds[i], this.knownValues[i]); - consumer.accept(thisObj, otherObj, reader); - } - } Object[] thisBuckets = this.buckets; for (int i = 0; i < thisBuckets.length; ++i) { @@ -2800,16 +2491,8 @@ public void forEach( private void forEachParent( T thisObj, U otherObj, TriConsumer consumer) { - EntryReadingHelper reader = null; for (TagMap ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { - for (int i = 0; i < ancestor.knownCount; ++i) { - long denseId = ancestor.knownIds[i]; - if (this.parentDenseVisible(denseId, ancestor)) { - if (reader == null) reader = new EntryReadingHelper(); - reader.set(denseId, ancestor.knownValues[i]); - consumer.accept(thisObj, otherObj, reader); - } - } + this.forEachDenseVisibleIn(ancestor, thisObj, otherObj, consumer); Object[] parentBuckets = ancestor.buckets; for (int i = 0; i < parentBuckets.length; ++i) { Object parentBucket = parentBuckets[i]; @@ -2833,20 +2516,34 @@ private void forEachParent( } } + /* + * Emit ancestor's dense values visible from this leaf, through a reused flyweight; no-ops for a + * map without a dense store. + */ + void forEachDenseVisibleIn(TagMap ancestor, Consumer consumer) {} + + void forEachDenseVisibleIn( + TagMap ancestor, T thisObj, BiConsumer consumer) {} + + void forEachDenseVisibleIn( + TagMap ancestor, + T thisObj, + U otherObj, + TriConsumer consumer) {} + + /** + * Loads {@code level}'s dense value at {@code index} into {@code reader} if it is visible from + * this leaf ({@code level} is this map or an ancestor), returning whether it was. Only called for + * an index below {@code level.knownCount()}, so never for a map without a dense store. + */ + boolean readDenseIfVisible(TagMap level, int index, EntryReadingHelper reader) { + return false; + } + public void clear() { this.checkWriteAccess(); - if (DenseStore.ENABLED) { - this.buckets = EMPTY_BUCKETS; // never write to the shared empty buckets - } else { - Arrays.fill(this.buckets, null); - } - this.size = 0; - this.knownIds = EMPTY_KNOWN_IDS; - this.knownValues = EMPTY_KNOWN_VALUES; - this.knownCount = 0; - this.knownOccupancy = 0L; - this.knownTraceLevel = false; + this.clearLocal(); // 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 // empty, parent-less map. Detach is one-way -- the parent is never re-pointed. @@ -2854,6 +2551,12 @@ public void clear() { this.removedFromParent = null; } + /** Empties this map's own storage (not the parent link or tombstones), for {@link #clear()}. */ + void clearLocal() { + Arrays.fill(this.buckets, null); + this.size = 0; + } + public TagMap freeze() { this.frozen = true; @@ -2911,35 +2614,6 @@ void checkIntegrity() { } } - // dense store: every id slotted and unique, its slot bit set, the count within bounds - if (this.knownCount > 0) { - if (this.knownIds == null || this.knownCount > this.knownIds.length) { - throw new IllegalStateException("incorrect known count"); - } - for (int i = 0; i < this.knownCount; ++i) { - long denseId = this.knownIds[i]; - if (denseIdOf(denseId) == 0) { - throw new IllegalStateException("dense id without a slot"); - } - if ((this.knownOccupancy & slotBit(denseId)) == 0) { - throw new IllegalStateException("dense id missing from occupancy"); - } - for (int j = i + 1; j < this.knownCount; ++j) { - if (this.knownIds[j] == denseId) { - throw new IllegalStateException("duplicate dense id"); - } - } - } - } - // disjointness: with the dense store on, no bucket entry may be one that routes dense - if (DenseStore.ENABLED) { - for (EntryReader entry : this) { - if (entry instanceof Entry && denseIdOf(((Entry) entry).tagHash) != 0) { - throw new IllegalStateException("dense-routed tag stored in a bucket: " + entry.tag()); - } - } - } - if (this.size != this.computeSize()) { throw new IllegalStateException("incorrect size"); } @@ -3059,16 +2733,6 @@ String toInternalString() { } ledger.append('\n'); } - for (int i = 0; i < this.knownCount; ++i) { - ledger - .append("[dense ") - .append(i) - .append("] = {") - .append(KnownTagCodec.nameOf(this.knownIds[i])) - .append('=') - .append(this.knownValues[i]) - .append("}\n"); - } return ledger.toString(); } @@ -3133,15 +2797,12 @@ final EntryReader nextEntryOrNull() { private final EntryReader advance() { while (true) { // the current level's dense values first, then its buckets - if (this.knownIndex < this.level.knownCount) { - int i = this.knownIndex++; - long denseId = this.level.knownIds[i]; - if (this.level == this.map || this.map.parentDenseVisible(denseId, this.level)) { - EntryReadingHelper reader = this.denseReader; - if (reader == null) { - reader = this.denseReader = new EntryReadingHelper(); - } - reader.set(denseId, this.level.knownValues[i]); + if (this.knownIndex < this.level.knownCount()) { + EntryReadingHelper reader = this.denseReader; + if (reader == null) { + reader = this.denseReader = new EntryReadingHelper(); + } + if (this.map.readDenseIfVisible(this.level, this.knownIndex++, reader)) { return reader; } continue; // ancestor dense value shadowed/tombstoned -> skip diff --git a/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java b/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java index 030f29b2b1c..d965bad2486 100644 --- a/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java @@ -69,8 +69,8 @@ void theSlotDoesNotChangeTheSerialOrFlags() { @Test void anUnslottedTagSharesTheReservedTopBit() { - // TagMap.slotBit relies on the long shift masking NO_SLOT (0xFFFF) down to 63 - assertEquals(1L << 63, TagMap.slotBit(KnownTags.RESOURCE_NAME_ID)); + // DenseTagMap.slotBit relies on the long shift masking NO_SLOT (0xFFFF) down to 63 + assertEquals(1L << 63, DenseTagMap.slotBit(KnownTags.RESOURCE_NAME_ID)); assertTrue(KnownTagCodec.slotCount() < 63, "real slots must stay below the shared bit 63"); } } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java index c1f43ceccc7..711d76b49b4 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java @@ -72,7 +72,7 @@ static void checkSetup() throws IllegalAccessException { private static int knownCount(TagMap map) { try { - Field f = TagMap.class.getDeclaredField("knownCount"); + Field f = DenseTagMap.class.getDeclaredField("knownCount"); f.setAccessible(true); return f.getInt(map); } catch (ReflectiveOperationException e) { @@ -80,14 +80,20 @@ private static int knownCount(TagMap map) { } } + /** A field of DenseTagMap, or of TagMap for the shared bucket state. */ private static Object field(Object target, String name) { - try { - Field f = TagMap.class.getDeclaredField(name); - f.setAccessible(true); - return f.get(target); - } catch (ReflectiveOperationException e) { - throw new AssertionError(e); + for (Class c = DenseTagMap.class; c != null; c = c.getSuperclass()) { + try { + Field f = c.getDeclaredField(name); + f.setAccessible(true); + return f.get(target); + } catch (NoSuchFieldException e) { + // look in the superclass + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } } + throw new AssertionError("no field " + name); } @Test @@ -495,4 +501,25 @@ void aMultiLevelChainResolvesNearestFirst() { assertEquals("middle", union.get(COMPONENT)); assertFalse(union.containsKey(BASE_SERVICE)); } + + @Test + void theCoresKnownValueReadMatchesGetEntry() { + TagMap child = TagMap.createFromParent(frozenParent()); + child.set(HTTP_ROUTE, "/users/{id}"); // dense + child.set(COMPONENT, "child"); // dense, shadows the parent's + child.remove(BASE_SERVICE); // tombstones the parent's + + // local hit, no Entry built + assertEquals("/users/{id}", child.internals().getKnownValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals("child", child.internals().getKnownValue(KnownTags.COMPONENT_ID)); + // read-through parent hit, then a tombstoned one and a plain miss + TagMap reader = TagMap.createFromParent(frozenParent()); + assertEquals("web", reader.internals().getKnownValue(KnownTags.COMPONENT_ID)); + assertEquals("billing", reader.internals().getKnownValue(KnownTags.DD_BASE_SERVICE_ID)); + assertNull(child.internals().getKnownValue(KnownTags.DD_BASE_SERVICE_ID)); + assertNull(child.internals().getKnownValue(KnownTags.DB_TYPE_ID)); + // a known tag without a slot, held in the dense store all the same + child.set(UNSLOTTED_KNOWN, "GET /users"); + assertEquals("GET /users", child.internals().getKnownValue(KnownTags.RESOURCE_NAME_ID)); + } } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java index 034a466641f..027106c6293 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java @@ -123,4 +123,26 @@ void theCoresTrustedSetsStoreWhatTheCheckedSetsStore() { assertEquals(new HashMap<>(checked), new HashMap<>(trusted)); assertEquals(KnownTags.HTTP_ROUTE_ID, trusted.getEntry(KnownTags.HTTP_ROUTE_NAME).tagId()); } + + @Test + void theCoresKnownValueReadMatchesGetEntry() { + TagMap parent = TagMap.create(); + parent.set(KnownTags.HTTP_ROUTE_ID, "/parent"); + parent.set(KnownTags.HTTP_METHOD_ID, "GET"); + parent.set(KnownTags.PEER_HOSTNAME_ID, "db.internal"); + TagMap child = TagMap.createFromParent(parent.freeze()); + child.set(KnownTags.HTTP_STATUS_CODE_ID, 200); + child.set(KnownTags.HTTP_ROUTE_ID, "/child"); + child.remove(KnownTags.PEER_HOSTNAME_NAME); + + // local hit, boxed + assertEquals(200, child.internals().getKnownValue(KnownTags.HTTP_STATUS_CODE_ID)); + // local shadows the parent + assertEquals("/child", child.internals().getKnownValue(KnownTags.HTTP_ROUTE_ID)); + // read-through parent hit + assertEquals("GET", child.internals().getKnownValue(KnownTags.HTTP_METHOD_ID)); + // tombstoned parent value and plain miss + assertNull(child.internals().getKnownValue(KnownTags.PEER_HOSTNAME_ID)); + assertNull(child.internals().getKnownValue(KnownTags.DB_INSTANCE_ID)); + } } From 5e600f7306ceb0b77459db72dd69940f22ff3d95 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sun, 11 Oct 2026 10:46:28 -0400 Subject: [PATCH 12/29] Box a primitive id-keyed tag once, for both interception and storage DDSpanContext's primitive setTag(long, ...) overloads boxed the value twice in their compiled code: once to hand it to the interceptor, through setBox, and again inside Internals.setKnown when storing it. They now box once, and pass the same box to the interceptor and to new boxed Internals.setKnown overloads. HashTagMap builds the tag's primitive-typed Entry around that box, so an intercepted tag that is stored keeps its primitive type rather than becoming ANY. DenseTagMap stores the box as is. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 48 +++++++---------- .../java/datadog/trace/api/DenseTagMap.java | 25 +++++++++ .../main/java/datadog/trace/api/TagMap.java | 52 +++++++++++++++++++ 3 files changed, 97 insertions(+), 28 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 4ca6b27f6f9..418a2b7b461 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -1250,11 +1250,11 @@ public void setTag(final long tagId, final boolean value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId)) { - this.setBox(tagId, value); - } else { + // Boxed once: the interceptor and the store share the box. + final Boolean box = value; + if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, box)) { synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, value); + unsafeTags.internals().setKnown(tagId, box); } } } @@ -1263,11 +1263,11 @@ public void setTag(final long tagId, final int value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId)) { - this.setBox(tagId, value); - } else { + // Boxed once: the interceptor and the store share the box. + final Integer box = value; + if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, box)) { synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, value); + unsafeTags.internals().setKnown(tagId, box); } } } @@ -1276,11 +1276,11 @@ public void setTag(final long tagId, final long value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId)) { - this.setBox(tagId, value); - } else { + // Boxed once: the interceptor and the store share the box. + final Long box = value; + if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, box)) { synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, value); + unsafeTags.internals().setKnown(tagId, box); } } } @@ -1289,11 +1289,11 @@ public void setTag(final long tagId, final float value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId)) { - this.setBox(tagId, value); - } else { + // Boxed once: the interceptor and the store share the box. + final Float box = value; + if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, box)) { synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, value); + unsafeTags.internals().setKnown(tagId, box); } } } @@ -1302,11 +1302,11 @@ public void setTag(final long tagId, final double value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (tagInterceptor.needsIntercept(tagId)) { - this.setBox(tagId, value); - } else { + // Boxed once: the interceptor and the store share the box. + final Double box = value; + if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, box)) { synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, value); + unsafeTags.internals().setKnown(tagId, box); } } } @@ -1341,14 +1341,6 @@ public void setTag(TagMap.EntryReader entry) { * The TagMap isn't optimized and will need to box the primitive regardless of * tag interception */ - private void setBox(long tagId, Object box) { - if (!tagInterceptor.interceptTag(this, tagId, box)) { - synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, box); - } - } - } - public void setTag(final String tag, final boolean value) { if (null == tag) { return; diff --git a/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java b/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java index ee860250efc..f5ce523c7a5 100644 --- a/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java @@ -200,6 +200,31 @@ void setKnownTrusted(long tagId, double value) { this.storeDenseTrusted(tagId, Double.valueOf(value)); } + @Override + void setKnownTrusted(long tagId, @Nonnull Boolean value) { + this.storeDenseTrusted(tagId, value); + } + + @Override + void setKnownTrusted(long tagId, @Nonnull Integer value) { + this.storeDenseTrusted(tagId, value); + } + + @Override + void setKnownTrusted(long tagId, @Nonnull Long value) { + this.storeDenseTrusted(tagId, value); + } + + @Override + void setKnownTrusted(long tagId, @Nonnull Float value) { + this.storeDenseTrusted(tagId, value); + } + + @Override + void setKnownTrusted(long tagId, @Nonnull Double value) { + this.storeDenseTrusted(tagId, value); + } + @Override void putAllMerge(TagMap that) { DenseTagMap dense = (DenseTagMap) that; 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 c464b224e2d..91ddd5be013 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1763,6 +1763,31 @@ public void setKnown(long tagId, double value) { this.map.setKnownTrusted(tagId, value); } + /* + * Boxed variants, for a caller that has already boxed the value (to hand it to the + * interceptor): the box is stored as is, so the value is boxed once. The tag keeps its + * primitive type. + */ + public void setKnown(long tagId, @Nonnull Boolean value) { + this.map.setKnownTrusted(tagId, value); + } + + public void setKnown(long tagId, @Nonnull Integer value) { + this.map.setKnownTrusted(tagId, value); + } + + public void setKnown(long tagId, @Nonnull Long value) { + this.map.setKnownTrusted(tagId, value); + } + + public void setKnown(long tagId, @Nonnull Float value) { + this.map.setKnownTrusted(tagId, value); + } + + public void setKnown(long tagId, @Nonnull Double value) { + this.map.setKnownTrusted(tagId, value); + } + /** * The value of a known tag, boxed if necessary, or null: {@code getEntry(tagId)}'s value, * including read-through, without building an Entry for a local dense value. @@ -1806,6 +1831,33 @@ void setKnownTrusted(long tagId, double value) { this.putEntryLocal(new Entry(tagId, null, EntryReader.DOUBLE, Entry.double2Prim(value), null)); } + /** The boxed {@code setKnownTrusted}s: a typed Entry that keeps the caller's box. */ + void setKnownTrusted(long tagId, @Nonnull Boolean value) { + this.putEntryLocal( + new Entry( + tagId, null, EntryReader.BOOLEAN, Entry.boolean2Prim(value.booleanValue()), value)); + } + + void setKnownTrusted(long tagId, @Nonnull Integer value) { + this.putEntryLocal( + new Entry(tagId, null, EntryReader.INT, Entry.int2Prim(value.intValue()), value)); + } + + void setKnownTrusted(long tagId, @Nonnull Long value) { + this.putEntryLocal( + new Entry(tagId, null, EntryReader.LONG, Entry.long2Prim(value.longValue()), value)); + } + + void setKnownTrusted(long tagId, @Nonnull Float value) { + this.putEntryLocal( + new Entry(tagId, null, EntryReader.FLOAT, Entry.float2Prim(value.floatValue()), value)); + } + + void setKnownTrusted(long tagId, @Nonnull Double value) { + this.putEntryLocal( + new Entry(tagId, null, EntryReader.DOUBLE, Entry.double2Prim(value.doubleValue()), value)); + } + /** {@code getEntry(tagId)}'s value, or null; see {@link Internals#getKnownValue}. */ Object getKnownValue(long tagId) { Entry entry = this.getEntry(tagId); From e5e5ae375eefbb13896cd33001158999bbe2bd1a Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sun, 11 Oct 2026 11:23:46 -0400 Subject: [PATCH 13/29] Let C2 inline the constructors of TagMap's nested classes Entry's full constructor and the Internals and Ledger constructors were private, so javac (targeting Java 8, without nestmates) gave each one a synthetic access constructor taking the never-loaded class TagMap$1. C2 refuses to inline a call whose signature names an unloaded class ("unloaded signature classes"). So the Internals view the core creates for each id write could not be scalar-replaced, and every Entry built through that constructor went through a real call. Making the constructors package-private removes the accessors and TagMap$1. Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/datadog/trace/api/TagMap.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 91ddd5be013..fcbe704eebb 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -471,7 +471,7 @@ private Entry(String tag, byte type, long prim, Object obj) { assert KnownTagCodec.keyOf(tag) == 0 : "'" + tag + "' is a known tag; build it from its id"; } - private Entry(long tagId, byte type, long prim, Object obj) { + Entry(long tagId, byte type, long prim, Object obj) { // Resolve the name once: it both validates the id and names the entry. super(requireKnownName(tagId)); this.tagHash = tagId; @@ -482,7 +482,7 @@ private Entry(long tagId, byte type, long prim, Object obj) { } /** {@code tagId} is a known id, or 0 for the custom tag {@code customTag}. */ - private Entry(long tagId, String customTag, byte type, long prim, Object obj) { + Entry(long tagId, String customTag, byte type, long prim, Object obj) { super(tagId != 0 ? KnownTagCodec.nameOf(tagId) : customTag); this.tagHash = tagId != 0 ? tagId : customHash(customTag); @@ -996,11 +996,11 @@ public static final class Ledger implements Iterable { int nextPos = 0; boolean containsRemovals = false; - private Ledger() { + Ledger() { this(8); } - private Ledger(int size) { + Ledger(int size) { this.entryChanges = new EntryChange[size]; } @@ -1725,7 +1725,7 @@ public Internals internals() { public static final class Internals { private final TagMap map; - private Internals(TagMap map) { + Internals(TagMap map) { this.map = map; } From 56b4c5a5b51635ed205849af8d659b350c7f4a7a Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sun, 11 Oct 2026 11:43:16 -0400 Subject: [PATCH 14/29] Remove TagMap's remaining synthetic accessors and guard against them Entry's *2Prim helpers and requireKnownName, and TagMap.parentEntryVisible, were private but used across nested classes, so javac generated access$ methods for them. Each one adds a level of inlining depth, and the *2Prim helpers are on HashTagMap's typed id-write path. They are now package-private. TagMapSyntheticAccessorTest fails if any class in the TagMap family gains a synthetic accessor again. It is the prototype for the @NoSyntheticAccess contract (APMLP-1976). Co-Authored-By: Claude Opus 5.5 --- .../main/java/datadog/trace/api/TagMap.java | 14 ++--- .../api/TagMapSyntheticAccessorTest.java | 56 +++++++++++++++++++ 2 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapSyntheticAccessorTest.java 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 fcbe704eebb..1397d1a4280 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -491,7 +491,7 @@ private Entry(String tag, byte type, long prim, Object obj) { this.rawObj = obj; } - private static String requireKnownName(long tagId) { + static String requireKnownName(long tagId) { String name = tagId == 0 ? null : KnownTagCodec.nameOf(tagId); if (name == null) { throw new IllegalArgumentException("not a known tag id: " + Long.toHexString(tagId)); @@ -941,7 +941,7 @@ public boolean equals(Object obj) { return this.tag.equals(that.tag) && this.objectValue().equals(that.objectValue()); } - private static long boolean2Prim(boolean value) { + static long boolean2Prim(boolean value) { return value ? 1L : 0L; } @@ -949,7 +949,7 @@ private static boolean prim2Boolean(long prim) { return (prim != 0L); } - private static long int2Prim(int value) { + static long int2Prim(int value) { return value; } @@ -957,7 +957,7 @@ private static int prim2Int(long prim) { return (int) prim; } - private static long long2Prim(long value) { + static long long2Prim(long value) { return value; } @@ -965,7 +965,7 @@ private static long prim2Long(long prim) { return prim; } - private static long float2Prim(float value) { + static long float2Prim(float value) { return Float.floatToIntBits(value); } @@ -973,7 +973,7 @@ private static float prim2Float(long prim) { return Float.intBitsToFloat((int) prim); } - private static long double2Prim(double value) { + static long double2Prim(double value) { return Double.doubleToRawLongBits(value); } @@ -1535,7 +1535,7 @@ private static Entry findInBucket(Object bucket, int hash, String tag) { * 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). */ - private boolean parentEntryVisible(Entry parentEntry, TagMap fromAncestor) { + boolean parentEntryVisible(Entry parentEntry, TagMap fromAncestor) { int hash = parentEntry.hash(); String tag = parentEntry.tag; for (TagMap nearer = this; nearer != fromAncestor; nearer = nearer.parent) { diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapSyntheticAccessorTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapSyntheticAccessorTest.java new file mode 100644 index 00000000000..a44585d1e87 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapSyntheticAccessorTest.java @@ -0,0 +1,56 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * TagMap and its implementations have no synthetic accessors. Targeting Java 8, javac reaches a + * private member of a nested or outer class through a generated {@code access$NNN} method, or a + * constructor taking a dummy {@code $1} class. C2 refuses to inline a call whose signature names a + * never-loaded class, and every accessor adds a level of inlining depth, so on TagMap's hot paths a + * member shared between nested classes must be package-private instead. Becomes a + * {@code @NoSyntheticAccess} contract check (APMLP-1976). + */ +class TagMapSyntheticAccessorTest { + @Test + void theTagMapFamilyHasNoSyntheticAccessors() { + List> classes = new ArrayList<>(); + for (Class root : + Arrays.asList( + TagMap.class, + HashTagMap.class, + DenseTagMap.class, + TagMapFactory.class, + HashTagMapFactory.class, + DenseTagMapFactory.class)) { + collect(root, classes); + } + List accessors = new ArrayList<>(); + for (Class c : classes) { + for (Method method : c.getDeclaredMethods()) { + if (method.isSynthetic() && method.getName().startsWith("access$")) { + accessors.add(method.toString()); + } + } + for (Constructor constructor : c.getDeclaredConstructors()) { + if (constructor.isSynthetic()) { + accessors.add(constructor.toString()); + } + } + } + assertEquals(new ArrayList(), accessors); + } + + private static void collect(Class c, List> into) { + into.add(c); + for (Class nested : c.getDeclaredClasses()) { + collect(nested, into); + } + } +} From 5165117683671099557836244c684dcfe1189345 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sun, 11 Oct 2026 16:28:32 -0400 Subject: [PATCH 15/29] Size a copied dense store to what its source holds putAllIntoEmptyMap copied the source's dense arrays at their full capacity. A source that had grown to the bound would then hand every copy, including each span's starting tags, a full-bound array. The copy now allocates the source's count, with the initial capacity as a floor. Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/datadog/trace/api/DenseTagMap.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java b/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java index f5ce523c7a5..7347a46c227 100644 --- a/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java @@ -239,9 +239,12 @@ void putAllMerge(TagMap that) { void putAllIntoEmptyMap(TagMap that) { DenseTagMap dense = (DenseTagMap) that; if (dense.knownCount > 0) { - // dense values are immutable objects, so the copy can share them - this.knownIds = Arrays.copyOf(dense.knownIds, dense.knownIds.length); - this.knownValues = Arrays.copyOf(dense.knownValues, dense.knownValues.length); + // dense values are immutable objects, so the copy can share them. Sized to what the source + // holds, not its capacity: a source that grew to the bound would otherwise hand every copy + // a full-bound array. + int cap = Math.max(dense.knownCount, KNOWN_INIT_CAP); + this.knownIds = Arrays.copyOf(dense.knownIds, cap); + this.knownValues = Arrays.copyOf(dense.knownValues, cap); this.knownCount = dense.knownCount; this.knownOccupancy = dense.knownOccupancy; this.knownTraceLevel = dense.knownTraceLevel; From 8951fbc2d231892c089942145cfa68587e5215af Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Sun, 11 Oct 2026 16:47:27 -0400 Subject: [PATCH 16/29] Size each span's dense tag store per operation Port the per-operation sizing hint from #12056 onto the split TagMap and the current FlatHashtable.D1 API. SizingHintTable keeps two fixed 512-operation lanes, one for entry spans and one for child spans, each with a capped overflow hint for once it is full. CoreTracer resolves the hint when it builds a span context. DenseTagMap presizes its arrays from it, and DDSpanContext records the span's known-tag count when the span is serialized. Each hint is a racy best-effort max: a stale or lost update only mis-sizes an array. This changes two things from #12056. A fresh hint is seeded at 4, not 1: the hint only grows, so a large seed would lock small spans into oversized arrays. And dense growth now doubles, bounded by serialLimit(), instead of jumping straight to the bound, so outgrowing a small seed costs one doubling. With the dense store off, hintFor folds to null, its tables are never loaded, and span creation does no lookup. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/core/CoreTracer.java | 15 +- .../datadog/trace/core/DDSpanContext.java | 99 +++++++- .../java/datadog/trace/api/DenseTagMap.java | 52 +++- .../java/datadog/trace/api/SizingHint.java | 45 ++++ .../datadog/trace/api/SizingHintTable.java | 100 ++++++++ .../main/java/datadog/trace/api/TagMap.java | 40 ++- .../java/datadog/trace/api/TagMapFactory.java | 16 ++ .../trace/api/SizingHintTableForkedTest.java | 236 ++++++++++++++++++ .../SizingHintTableHashOnlyForkedTest.java | 43 ++++ 9 files changed, 634 insertions(+), 12 deletions(-) create mode 100644 internal-api/src/main/java/datadog/trace/api/SizingHint.java create mode 100644 internal-api/src/main/java/datadog/trace/api/SizingHintTable.java create mode 100644 internal-api/src/test/java/datadog/trace/api/SizingHintTableForkedTest.java create mode 100644 internal-api/src/test/java/datadog/trace/api/SizingHintTableHashOnlyForkedTest.java diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index 8f52b49052c..a0cea3cc11b 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -42,6 +42,8 @@ import datadog.trace.api.IdGenerationStrategy; import datadog.trace.api.InstrumenterConfig; import datadog.trace.api.Pair; +import datadog.trace.api.SizingHint; +import datadog.trace.api.SizingHintTable; import datadog.trace.api.TagMap; import datadog.trace.api.TraceConfig; import datadog.trace.api.civisibility.config.BazelMode; @@ -2328,6 +2330,16 @@ protected static final DDSpanContext buildSpanContext( requestContextDataIast = builderRequestContextDataIast; } + // Per-operation dense-store sizing: an entry (local-root) span carries the trace-metadata / + // enriching tags a child doesn't, so pick the lane by whether we have a local parent. A + // resolved hint sizes the span's TagMap and self-tunes at serialization; null (dense store + // off, or no operation name) falls back to the generic default capacity. hintFor tests the + // dense-store flag first, so with it off -- the default -- this constant-folds to null and + // the default span-creation path does no lookup (do no harm). + final SizingHint sizingHint = + SizingHintTable.hintFor( + operationName, !(resolvedParentSpanContext instanceof DDSpanContext)); + // some attributes are inherited from the parent context = new DDSpanContext( @@ -2356,7 +2368,8 @@ protected static final DDSpanContext buildSpanContext( tracer.profilingContextIntegration, tracer.injectBaggageAsTags, tracer.injectLinksAsTags, - mergedTracerTagsNeedsIntercept ? null : mergedTracerTags); + mergedTracerTagsNeedsIntercept ? null : mergedTracerTags, + sizingHint); // By setting the tags on the context we apply decorators to any tags that have been set via // the builder. The `mergedTracerTags` are always applied first (the precedence floor: diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 418a2b7b461..89489c57205 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -13,6 +13,7 @@ import datadog.trace.api.KnownTagCodec; import datadog.trace.api.KnownTags; import datadog.trace.api.ProcessTags; +import datadog.trace.api.SizingHint; import datadog.trace.api.TagMap; import datadog.trace.api.cache.DDCache; import datadog.trace.api.cache.DDCaches; @@ -52,6 +53,7 @@ import java.util.concurrent.ThreadLocalRandom; import java.util.function.Function; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -165,6 +167,15 @@ public class DDSpanContext */ private final TagMap unsafeTags; + /** + * Per-operation sizing hint (from SizingHintTable, keyed by operation name) this span's dense + * store was sized from, if any. Held so the span can feed its final dense-store size back at + * serialization (recordDenseSize) -- the self-tuning loop that lets the reused hint converge to + * the operation's real known-tag high-water mark. Null when unsized, which is always the case + * with the dense store off. + */ + @Nullable private final SizingHint sizingHint; + /** The service name is required, otherwise the span are dropped by the agent */ private volatile String serviceName; @@ -403,6 +414,64 @@ public DDSpanContext( final boolean injectBaggageAsTags, final boolean injectLinksAsTags, final TagMap readThroughParent) { + this( + traceId, + spanId, + parentId, + parentServiceName, + serviceNameSource, + serviceName, + operationName, + resourceName, + samplingPriority, + origin, + baggageItems, + w3cBaggage, + errorFlag, + spanType, + tagsSize, + traceCollector, + requestContextDataAppSec, + requestContextDataIast, + CiVisibilityContextData, + pathwayContext, + disableSamplingMechanismValidation, + propagationTags, + profilingContextIntegration, + injectBaggageAsTags, + injectLinksAsTags, + readThroughParent, + null); + } + + public DDSpanContext( + final DDTraceId traceId, + final long spanId, + final long parentId, + final CharSequence parentServiceName, + final CharSequence serviceNameSource, + final String serviceName, + final CharSequence operationName, + final CharSequence resourceName, + final int samplingPriority, + final CharSequence origin, + final Map baggageItems, + final Baggage w3cBaggage, + final boolean errorFlag, + final CharSequence spanType, + final int tagsSize, + final TraceCollector traceCollector, + final Object requestContextDataAppSec, + final Object requestContextDataIast, + final Object CiVisibilityContextData, + final PathwayContext pathwayContext, + final boolean disableSamplingMechanismValidation, + final PropagationTags propagationTags, + final ProfilingContextIntegration profilingContextIntegration, + final boolean injectBaggageAsTags, + final boolean injectLinksAsTags, + final TagMap readThroughParent, + @Nullable final SizingHint sizingHint) { assert traceCollector != null; this.traceCollector = traceCollector; @@ -433,8 +502,9 @@ public DDSpanContext( final int capacity = Math.max((tagsSize <= 0 ? 3 : (tagsSize + 1)) * 4 / 3, 8); this.unsafeTags = readThroughParent != null - ? TagMap.createFromParent(readThroughParent) - : TagMap.create(capacity); + ? TagMap.createFromParent(readThroughParent, sizingHint) + : sizingHint != null ? TagMap.create(sizingHint) : TagMap.create(capacity); + this.sizingHint = sizingHint; // must set this before setting the service and resource names below this.profilingContextIntegration = profilingContextIntegration; @@ -1655,6 +1725,27 @@ void processTagsAndBaggageWithStructuredLinks( firstInChunk ? getPropagationTags() : null); } + /** + * Feeds this span's final dense-store size back into the sizing hint it was created from (if + * any), so a reused hint self-tunes to the operation's observed known-tag high-water mark and + * later spans of the operation are sized correctly. + * + *

    Called from {@link #processTagsAndBaggage} at serialization — the one terminal point that + * (a) runs for every serialized span regardless of finish mode (plain {@code finish()} AND {@code + * phasedFinish()}+{@code publish()}, which the async gRPC/Netty/WebFlux paths use and which never + * enter {@code finishAndAddToTrace}), and (b) runs AFTER the lazy tag post-processors have + * appended their serialization-time tags ({@code _dd.integration}, host, ...), so the recorded + * count is the span's true final footprint rather than an underestimate. The caller already holds + * {@code synchronized (unsafeTags)}, so the {@code knownCount} read is consistent; the hint write + * itself is best-effort racy by design (see {@link TagMap#recordSize}). With the dense store off + * the hint is always null, so this is a single null check. + */ + void recordDenseSize() { + if (sizingHint != null) { + unsafeTags.recordSize(sizingHint); + } + } + void processTagsAndBaggage( final MetadataConsumer consumer, int longRunningVersion, @@ -1679,6 +1770,10 @@ void processTagsAndBaggage( } } + // Self-tune the per-operation sizing hint now that every tag (incl. the just-appended + // serialization-time tags) is present -- the terminal point shared by all finish modes. + recordDenseSize(); + // Baggage Map baggageItemsWithPropagationTags; if (injectBaggageAsTags) { diff --git a/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java b/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java index 7347a46c227..f82a290be05 100644 --- a/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java @@ -52,7 +52,10 @@ final class DenseTagMap extends TagMap { /** Shared all-null buckets (length 16) that maps start on. */ private static final Object[] EMPTY_BUCKETS = new Object[1 << 4]; - /** Initial capacity of the dense arrays; the largest span type resolves about this many tags. */ + /** + * Initial capacity of the dense arrays when no {@link SizingHint} presized them; the largest span + * type resolves about this many tags. + */ private static final int KNOWN_INIT_CAP = 12; /** @@ -74,6 +77,22 @@ final class DenseTagMap extends TagMap { super(parent, EMPTY_BUCKETS); } + /** + * Like {@link #DenseTagMap(TagMap)}, with the dense arrays presized from {@code hint} (see {@link + * SizingHintTable}). A {@code null} hint, or one not yet seeded (a racy reader can see 0), keeps + * the shared empty arrays, so the first dense tag allocates at {@link #KNOWN_INIT_CAP}. + */ + DenseTagMap(TagMap parent, SizingHint hint) { + super(parent, EMPTY_BUCKETS); + if (hint != null) { + int cap = hint.size; // read the racy field once + if (cap > 0) { + this.knownIds = new long[cap]; + this.knownValues = new Object[cap]; + } + } + } + DenseTagMap(Object[] buckets, int size) { super(buckets, size); } @@ -83,6 +102,18 @@ int knownCount() { return this.knownCount; } + @Override + public void recordSize(SizingHint hint) { + // Capped hints (the heterogeneous shared overflow) are fixed -- don't let unlike sharers grow + // them. Best-effort racy max (see TagMap#recordSize); benign plain-int write. + if (hint != null && !hint.capped) { + int count = this.knownCount; + if (count > hint.size) { + hint.size = count; + } + } + } + @Override int countDenseVisibleIn(TagMap ancestor) { DenseTagMap dense = (DenseTagMap) ancestor; @@ -532,9 +563,10 @@ private static long requireKnownId(long tagId) { } /** - * Makes room for one more dense tag. A map's first dense tag allocates its own arrays -- a plain - * allocation, since on fresh spans this runs once per span and is compiled inline. A full map - * grows once, in {@link #growKnown}. + * Makes room for one more dense tag. A map's first dense tag on the shared empty arrays allocates + * its own at {@link #KNOWN_INIT_CAP} -- a plain allocation, since on fresh unhinted spans this + * runs once per span and is compiled inline. A map presized from a {@link SizingHint} skips that + * step. A full map grows in {@link #growKnown}. */ private void spillKnown() { if (this.knownIds.length == 0) { @@ -546,12 +578,16 @@ private void spillKnown() { } /** - * Grows full dense arrays once, straight to the hard bound: a map holds each known tag at most - * once, so {@link KnownTagCodec#serialLimit()} entries always suffice. Rare, so the copying stays - * out of the write. + * Grows full dense arrays by doubling, never below one more slot than they hold, and bounded by + * {@link KnownTagCodec#serialLimit()}: a map holds each known tag at most once, so that many + * entries always suffice. Doubling rather than jumping straight to the bound keeps a span that + * outgrows a small {@link SizingHint} seed from paying for a full-bound array; the hint then + * tunes up so later spans of the operation are presized. Rare, so the copying stays out of the + * write. */ private void growKnown() { - int newCap = Math.max(KnownTagCodec.serialLimit(), this.knownIds.length << 1); + int length = this.knownIds.length; + int newCap = Math.max(Math.min(length << 1, KnownTagCodec.serialLimit()), length + 1); this.knownIds = Arrays.copyOf(this.knownIds, newCap); this.knownValues = Arrays.copyOf(this.knownValues, newCap); } diff --git a/internal-api/src/main/java/datadog/trace/api/SizingHint.java b/internal-api/src/main/java/datadog/trace/api/SizingHint.java new file mode 100644 index 00000000000..734650f6d47 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/SizingHint.java @@ -0,0 +1,45 @@ +package datadog.trace.api; + +import datadog.trace.util.FlatHashtable; + +/** + * Opaque per-operation dense-store sizing hint, and a self-contained {@link FlatHashtable.D1} + * entry: it carries everything the probe compares (the operation name as the {@link + * FlatHashtable.D1.Entry} key, plus the cached hash from {@link FlatHashtable.Entry#hash}) and the + * tuned payload ({@link #size}). Holding key, hash, and value in ONE object behind ONE array slot + * is deliberate — entry publication is a single reference store, so a reader sees {@code null} or a + * complete entry (never a torn one), and the {@code final} identity fields are visible even under + * racy publication (JMM final-field guarantee). That sidesteps the memory-ordering / visibility + * problems parallel key/hash/value arrays would create, no volatile or atomics. + * + *

    Opaque to everything outside {@code datadog.trace.api}: no public members. {@link TagMap} + * reads {@link #size} to size a fresh dense store and writes it back (best-effort max, see {@link + * TagMap#recordSize}) at a terminal point; {@link SizingHintTable} mints them. Callers only ever + * hold the reference. + */ +public final class SizingHint extends FlatHashtable.D1.Entry { + // Payload: the tuned dense-store size. Plain racy int, updated best-effort max (see + // TagMap#recordSize -- not strictly monotonic under concurrent finishes). A stale/lost/lowered + // read only mis-sizes an array (over/under-provision), never corrupts tag data, so no + // synchronization. Not safely published by a racing create either (FlatHashtable class doc): a + // reader may see the default 0, which TagMap treats as "no presize" -- benign. + int size; + + // When true, size is fixed and recordSize won't grow it. For the shared overflow hint (a + // HETEROGENEOUS catch-all for over-budget operations): self-tuning it toward the observed max + // would converge to the max across unlike sharers and over-provision the lean ones. + final boolean capped; + + /** + * A fresh, uncapped hint for {@code operationName}, seeded at {@link SizingHintTable#SEED_SIZE}. + */ + SizingHint(String operationName) { + this(operationName, SizingHintTable.SEED_SIZE, false); + } + + SizingHint(String operationName, int seedSize, boolean capped) { + super(operationName); + this.size = seedSize; + this.capped = capped; + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/SizingHintTable.java b/internal-api/src/main/java/datadog/trace/api/SizingHintTable.java new file mode 100644 index 00000000000..6b0d530f145 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/SizingHintTable.java @@ -0,0 +1,100 @@ +package datadog.trace.api; + +import datadog.trace.util.FlatHashtable; +import javax.annotation.Nullable; + +/** + * Process-wide, self-tuning registry of per-operation {@link SizingHint}s, keyed by operation name. + * Pure static: the tracer resolves a hint here at span build and hands it to the span, which sizes + * its dense {@link TagMap} from it and records the actual size back at serialization — the hint + * converges to the operation's real known-tag high-water mark, so later spans of that operation + * size correctly. + * + *

    Only meaningful with the dense store on ({@link TagMap.DenseStore#ENABLED}); with it off, + * {@link #hintFor} returns {@code null} before touching anything, so the default span path does no + * lookup and the lane tables are never even allocated (they live in a holder class). + * + *

    Two lanes, because a span's dense size depends systematically on whether it is an entry + * (local-root) span or not: entry spans carry the trace-metadata / enriching tags and children + * don't, so the same operation name has two different steady-state sizes. {@link #hintFor} picks + * the lane. + * + *

    Bounded + racy by design. Each lane is a fixed-capacity {@link FlatHashtable.D1} (never + * resized) so memory is bounded even under unbounded/dynamic operation names; once a lane's + * cardinality budget is spent, further operations share a capped overflow hint. Insertion and the + * best-effort-max size update are lock-free and deliberately racy (the table is used from many + * threads without synchronization): a lost size-counter update lets a lane slightly overshoot its + * budget (still bounded by the backing array, whose full-table insert just fails), and two threads + * minting the same operation at once may leave a shadowed, unreachable duplicate. Either only + * mis-sizes an array (over/under-provision) for a span or two, never corrupts tag data (see {@link + * FlatHashtable} and {@link SizingHint} for the rationale). + * + *

    Keyed by the operation name's {@code String} form. In practice every operation name we see is + * a {@code String} or a {@link datadog.trace.bootstrap.instrumentation.api.UTF8BytesString}, whose + * {@code toString()} just returns its backing field — so the {@code toString()} here is O(1) and + * allocation-free on the hot path. A {@code null} operation name gets no hint (the span falls back + * to the generic default capacity); a missed hint is benign, never wrong. + */ +public final class SizingHintTable { + private SizingHintTable() {} + + // Seed for a fresh per-operation hint: a small floor that self-tunes up via best-effort-max + // recordSize. Kept small because the hint only grows -- a large seed would lock small spans into + // oversized arrays forever; a span that outgrows it pays one doubling. + static final int SEED_SIZE = 4; + + // Fixed size for the shared over-budget hint: today's untuned default (DenseTagMap's initial + // capacity). Capped (never grown by recordSize) because it's a heterogeneous catch-all -- growing + // it would over-provision lean sharers to the max of an unlike cohort. + static final int OVERFLOW_SEED = 12; + + // Max distinct operation names per lane that get their own hint before collapsing to the capped + // overflow hint. + static final int CARDINALITY_LIMIT = 512; + + /** + * The lanes, in a holder so they are only allocated once the dense store is on and a span asks + * for a hint. + */ + static final class Lanes { + private Lanes() {} + + // Entry (local-root, enriched) lane and non-entry (child) lane, keyed by operation name. + static final FlatHashtable.D1 ENTRY = + FlatHashtable.D1.createFixed(SizingHint.class, CARDINALITY_LIMIT); + static final FlatHashtable.D1 CHILD = + FlatHashtable.D1.createFixed(SizingHint.class, CARDINALITY_LIMIT); + + // Shared capped hint each lane returns once its budget is exhausted. + static final SizingHint ENTRY_OVERFLOW = new SizingHint(null, OVERFLOW_SEED, /* capped */ true); + static final SizingHint CHILD_OVERFLOW = new SizingHint(null, OVERFLOW_SEED, /* capped */ true); + } + + /** + * The sizing hint for {@code operationName} in the given lane: the existing one, a freshly-minted + * (seeded) one if the lane has budget, or the shared capped overflow hint if it's full. Returns + * {@code null} when the dense store is off (constant-folded, so the default span path does no + * lookup) and for a {@code null} operation name — the span then uses the generic default capacity + * (operation-less spans aren't reliably similar, so they get no hint). Hits are a single probe; + * the create path is warmup-rare. + */ + @Nullable + public static SizingHint hintFor(@Nullable CharSequence operationName, boolean entrySpan) { + if (!TagMap.DenseStore.ENABLED) { + return null; + } + if (operationName == null) { + return null; + } + // O(1) for String / UTF8BytesString (see class doc) + final String key = operationName.toString(); + final FlatHashtable.D1 lane = entrySpan ? Lanes.ENTRY : Lanes.CHILD; + // get() then getOrCreate() would probe twice on a miss; getOrCreate() already returns a hit + // directly, and returns null only when the fixed lane is full and the key is absent. + final SizingHint hint = lane.getOrCreate(key, SizingHint::new); + if (hint != null) { + return hint; + } + return entrySpan ? Lanes.ENTRY_OVERFLOW : Lanes.CHILD_OVERFLOW; + } +} 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 1397d1a4280..d83e7a7c26c 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -83,6 +83,15 @@ public static final TagMap create(int size) { return TagMapFactory.INSTANCE.create(null); } + /** + * Creates a mutable TagMap whose dense store is sized per-operation from {@code hint} (see {@link + * SizingHintTable}). A {@code null} hint behaves exactly like {@link #create()}; the hash-only + * implementation ignores the hint. + */ + public static final TagMap create(@Nullable SizingHint hint) { + return TagMapFactory.INSTANCE.create(null, hint); + } + /** * Creates a fresh, mutable TagMap that reads through to {@code parent} on local misses. The * parent must be frozen and is fixed for the life of the returned map (no re-parenting), so @@ -98,6 +107,19 @@ public static final TagMap create(int size) { * local miss/removal for no benefit. */ public static final TagMap createFromParent(TagMap parent) { + return createFromParent(parent, null); + } + + /** + * Read-through variant of {@link #create(SizingHint)}: reads through to {@code parent} on local + * misses AND sizes the LOCAL dense store from {@code hint}. The two are orthogonal — the parent + * supplies inherited reads, while locally-set known tags live in this map's own dense array, + * whose initial capacity the hint tunes. So the common child / shared-parent path (a non-null + * read-through parent) still gets per-operation sizing; without this the local store would fall + * back to the fixed default. A {@code null} hint behaves exactly like {@link + * #createFromParent(TagMap)}. + */ + public static final TagMap createFromParent(TagMap parent, @Nullable SizingHint hint) { if (parent != null) { if (!parent.frozen) { throw new IllegalStateException("read-through parent must be frozen"); @@ -110,7 +132,7 @@ public static final TagMap createFromParent(TagMap parent) { parent = null; } } - return TagMapFactory.INSTANCE.create(parent); + return TagMapFactory.INSTANCE.create(parent, hint); } /** Creates a new TagMap.Ledger */ @@ -1337,6 +1359,22 @@ int knownCount() { return 0; } + /** + * Feeds this map's final dense-entry count back to {@code hint} (call at a terminal point, e.g. + * serialization), so future maps of the same operation are sized correctly. A no-op for a map + * without a dense store, and for a {@code null} hint. + * + *

    Best-effort max, not a strict monotonic max: the read-compare-write is unsynchronized, so + * two spans of the same operation finishing concurrently can both read the same old size and the + * later write can lower it (a 10-tag span then a 5-tag span settles at 5). And the size is a + * plain field, so a fresh update isn't promptly visible to span-creation threads. Both are + * deliberately tolerated: a stale or lowered hint only mis-sizes a later span's dense array (one + * extra grow-copy), never corrupts tag data — so it stays lock- and atomic-free, and the next + * heavy span of the operation nudges it back up. A CAS/atomic max would buy exactness at the cost + * of contention on this terminal path, which isn't worth it for a sizing hint. + */ + public void recordSize(@Nullable SizingHint hint) {} + /** * The number of {@code ancestor}'s dense values visible from this leaf; 0 for a map without a * dense store. diff --git a/internal-api/src/main/java/datadog/trace/api/TagMapFactory.java b/internal-api/src/main/java/datadog/trace/api/TagMapFactory.java index b129acd71ad..2bbbd21858e 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMapFactory.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMapFactory.java @@ -18,6 +18,12 @@ abstract class TagMapFactory { /** A fresh mutable map that reads through to {@code parent} (may be null). */ abstract TagMap create(TagMap parent); + /** + * A fresh mutable map that reads through to {@code parent} (may be null), with its dense store + * presized from {@code hint} (may be null; ignored by the hash-only map). + */ + abstract TagMap create(TagMap parent, SizingHint hint); + /** The shared frozen empty map. */ abstract TagMap empty(); } @@ -34,6 +40,11 @@ TagMap create(TagMap parent) { return new HashTagMap(parent); } + @Override + TagMap create(TagMap parent, SizingHint hint) { + return new HashTagMap(parent); // no dense store to size + } + @Override TagMap empty() { return EMPTY; @@ -52,6 +63,11 @@ TagMap create(TagMap parent) { return new DenseTagMap(parent); } + @Override + TagMap create(TagMap parent, SizingHint hint) { + return new DenseTagMap(parent, hint); + } + @Override TagMap empty() { return EMPTY; diff --git a/internal-api/src/test/java/datadog/trace/api/SizingHintTableForkedTest.java b/internal-api/src/test/java/datadog/trace/api/SizingHintTableForkedTest.java new file mode 100644 index 00000000000..541f4bca8c0 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/SizingHintTableForkedTest.java @@ -0,0 +1,236 @@ +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.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Registry semantics for {@link SizingHintTable}, and how {@link DenseTagMap} sizes from and tunes + * a {@link SizingHint}. Hints are only minted with the dense store on, and the flag is read once + * into a static final, so this runs in its own JVM ({@code ForkedTest}) with the flag set first. + * + *

    The table is process-wide static state; each test uses distinct operation names so tests don't + * interfere. The overflow test fills the child lane, so no other test here uses that lane expecting + * a fresh hint. + */ +class SizingHintTableForkedTest { + static { + System.setProperty("dd.trace.experimental.dense.tags.enabled", "true"); + } + + /** Distinct known tag ids that each add one dense entry, in a stable order. */ + static final List KNOWN_IDS = new ArrayList<>(); + + @BeforeAll + static void checkSetup() throws IllegalAccessException { + assertTrue(TagMap.DenseStore.ENABLED, "dense store must be on in this JVM"); + Set ids = new LinkedHashSet<>(); + for (Field field : KnownTags.class.getFields()) { + if (field.getType() != long.class) continue; + long id = field.getLong(null); + if (!KnownTagCodec.isKnown(id)) continue; + String name = KnownTagCodec.nameOf(id); + if (name == null || KnownTagCodec.keyOf(name) != id) continue; // skip shared names + ids.add(id); + } + // keep only ids that really add a dense entry (no relabeling of another id) + TagMap probe = TagMap.create(); + for (long id : ids) { + int before = knownCount(probe); + probe.set(id, "v"); + if (knownCount(probe) == before + 1) { + KNOWN_IDS.add(id); + } + } + assertTrue(KNOWN_IDS.size() > 24, "need enough known tags to exercise growth"); + } + + private static Object field(TagMap map, String name) { + try { + Field f = DenseTagMap.class.getDeclaredField(name); + f.setAccessible(true); + return f.get(map); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } + + private static int knownCount(TagMap map) { + return (Integer) field(map, "knownCount"); + } + + private static int knownCapacity(TagMap map) { + long[] ids = (long[]) field(map, "knownIds"); + Object[] values = (Object[]) field(map, "knownValues"); + assertEquals(ids.length, values.length, "dense arrays stay parallel"); + return ids.length; + } + + /** Adds the first {@code n} known tags. */ + private static void fill(TagMap map, int n) { + for (int i = 0; i < n; ++i) { + map.set(KNOWN_IDS.get(i), "v" + i); + } + assertEquals(n, knownCount(map)); + } + + @Test + void nullOperationNameGetsNoHint() { + assertNull(SizingHintTable.hintFor(null, true)); + assertNull(SizingHintTable.hintFor(null, false)); + } + + @Test + void nonStringCharSequenceIsKeyedByContent() { + // Operation names are commonly UTF8BytesString, not String; a content-equal name must resolve + // to the same hint as its String form (keyed by toString(), which is O(1) for these types). + SizingHint viaString = SizingHintTable.hintFor("registry.utf8", true); + CharSequence utf8 = UTF8BytesString.create("registry.utf8"); + assertNotNull(viaString); + assertSame(viaString, SizingHintTable.hintFor(utf8, true)); + } + + @Test + void freshHintIsSeededAndUncapped() { + SizingHint hint = SizingHintTable.hintFor("registry.fresh", true); + assertNotNull(hint); + assertEquals(SizingHintTable.SEED_SIZE, hint.size); + assertFalse(hint.capped); + assertEquals("registry.fresh", hint.key()); + } + + @Test + void sameOperationAndLaneReturnsSameInstance() { + SizingHint a = SizingHintTable.hintFor("registry.stable", true); + SizingHint b = SizingHintTable.hintFor("registry.stable", true); + assertSame(a, b); + } + + @Test + void entryAndChildLanesAreIndependent() { + SizingHint entry = SizingHintTable.hintFor("registry.twolane", true); + SizingHint child = SizingHintTable.hintFor("registry.twolane", false); + assertNotNull(entry); + assertNotNull(child); + assertNotSame(entry, child, "each lane holds its own hint for the same operation name"); + // ...and each lane is internally stable. + assertSame(entry, SizingHintTable.hintFor("registry.twolane", true)); + assertSame(child, SizingHintTable.hintFor("registry.twolane", false)); + } + + @Test + void beyondCardinalityBudgetSharesACappedOverflowHint() { + // A hint minted before the lane fills keeps resolving to itself afterwards. + SizingHint early = SizingHintTable.hintFor("registry.flood.early", false); + assertFalse(early.capped); + + // Fill the child lane past its budget; a capped shared hint must appear. + SizingHint firstOverflow = null; + for (int i = 0; i <= SizingHintTable.CARDINALITY_LIMIT && firstOverflow == null; i++) { + SizingHint hint = SizingHintTable.hintFor("registry.flood." + i, false); + assertNotNull(hint); + if (hint.capped) { + firstOverflow = hint; + } + } + assertNotNull(firstOverflow, "lane collapses to a capped overflow hint once full"); + assertEquals(SizingHintTable.OVERFLOW_SEED, firstOverflow.size); + + // Every further over-budget name shares that same capped instance... + SizingHint another = SizingHintTable.hintFor("registry.flood.after", false); + assertTrue(another.capped); + assertSame(firstOverflow, another); + // ...existing hints still resolve (the cap blocks only creation)... + assertSame(early, SizingHintTable.hintFor("registry.flood.early", false)); + // ...and the entry lane has its own budget. + assertFalse(SizingHintTable.hintFor("registry.flood.after", true).capped); + + // A capped hint is never tuned: a heavy span sharing it does not grow it. + TagMap heavy = TagMap.create(another); + fill(heavy, SizingHintTable.OVERFLOW_SEED + 4); + heavy.recordSize(another); + assertEquals(SizingHintTable.OVERFLOW_SEED, another.size); + } + + @Test + void sizingHintPresizesTheDenseStore() { + SizingHint hint = SizingHintTable.hintFor("registry.presize", true); + TagMap map = TagMap.create(hint); + assertSame(DenseTagMap.class, map.getClass()); + assertEquals(hint.size, knownCapacity(map), "presized at the hint's size, before any write"); + + TagMap child = TagMap.createFromParent(TagMap.create().freeze(), hint); + assertEquals(hint.size, knownCapacity(child)); + + // Without a hint, a map starts on the shared empty arrays. + assertEquals(0, knownCapacity(TagMap.create((SizingHint) null))); + assertEquals(0, knownCapacity(TagMap.create())); + } + + @Test + void sizingHintFeedsAndTunesTheDenseStore() { + SizingHint hint = SizingHintTable.hintFor("registry.tuning", true); + assertEquals(SizingHintTable.SEED_SIZE, hint.size); + + int observed = SizingHintTable.SEED_SIZE + 3; + TagMap map = TagMap.create(hint); + fill(map, observed); + map.recordSize(hint); + assertEquals(observed, hint.size, "hint self-tunes up to the observed known-tag high-water"); + + // The next map of the operation is presized to fit, so it never grows. + TagMap next = TagMap.create(hint); + assertEquals(observed, knownCapacity(next)); + fill(next, observed); + assertEquals(observed, knownCapacity(next)); + + // A smaller later observation does not shrink the hint. + TagMap smaller = TagMap.create(hint); + fill(smaller, 1); + smaller.recordSize(hint); + assertEquals(observed, hint.size, "recordSize never shrinks the hint"); + + // A null hint is a no-op. + smaller.recordSize(null); + } + + @Test + void fullDenseArraysDoubleUpToTheSerialLimit() { + // Presized small: 4 -> 8 -> 16 + SizingHint hint = new SizingHint("registry.growth"); + assertEquals(4, hint.size); + TagMap map = TagMap.create(hint); + fill(map, 4); + assertEquals(4, knownCapacity(map)); + fill(map, 5); + assertEquals(8, knownCapacity(map)); + fill(map, 9); + assertEquals(16, knownCapacity(map)); + + // Unhinted: first allocation at the default 12, then 24 + TagMap unhinted = TagMap.create(); + fill(unhinted, 1); + assertEquals(12, knownCapacity(unhinted)); + fill(unhinted, 13); + assertEquals(24, knownCapacity(unhinted)); + + // Growth never passes the serial limit, yet always makes room. + TagMap all = TagMap.create(new SizingHint("registry.growth.all")); + fill(all, KNOWN_IDS.size()); + assertTrue(knownCapacity(all) <= KnownTagCodec.serialLimit()); + assertTrue(knownCapacity(all) >= KNOWN_IDS.size()); + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/SizingHintTableHashOnlyForkedTest.java b/internal-api/src/test/java/datadog/trace/api/SizingHintTableHashOnlyForkedTest.java new file mode 100644 index 00000000000..b1134c413a6 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/SizingHintTableHashOnlyForkedTest.java @@ -0,0 +1,43 @@ +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.junit.jupiter.api.Test; + +/** + * With the dense store off, {@link SizingHintTable} hands out no hints, never allocates its lanes, + * and the hash-only map ignores a hint. Runs in its own JVM so the flag is read as off. + */ +class SizingHintTableHashOnlyForkedTest { + static { + System.setProperty("dd.trace.experimental.dense.tags.enabled", "false"); + } + + @Test + void noHintWhenTheDenseStoreIsOff() { + assertFalse(TagMap.DenseStore.ENABLED); + assertNull(SizingHintTable.hintFor("registry.off", true)); + assertNull(SizingHintTable.hintFor("registry.off", false)); + assertNull(SizingHintTable.hintFor(null, true)); + assertFalse(TagMapImplementations.isLoaded("datadog.trace.api.SizingHintTable$Lanes")); + } + + @Test + void theHashOnlyMapIgnoresAHint() { + SizingHint hint = new SizingHint("registry.off.direct"); + TagMap map = TagMap.create(hint); + map.set(KnownTags.COMPONENT_ID, "okhttp"); + map.set(KnownTags.DD_BASE_SERVICE_ID, "svc"); + map.recordSize(hint); + + assertSame(HashTagMap.class, map.getClass()); + assertSame( + HashTagMap.class, TagMap.createFromParent(map.freeze(), hint).getClass(), "read-through"); + assertEquals( + SizingHintTable.SEED_SIZE, hint.size, "recordSize is a no-op without a dense store"); + assertFalse(TagMapImplementations.isLoaded("datadog.trace.api.DenseTagMap")); + } +} From d345274e8b551f39aae14e47352b7802897dcae1 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 12 Oct 2026 10:54:46 -0400 Subject: [PATCH 17/29] Tune a span's sizing hint when it finishes, not only at serialization Spans that are never serialized, such as those dropped by sampling, never fed their known-tag count back. Their operation's hint stayed at the seed, and each such span grew its dense arrays from 4 slots upward. The span now records its size at finish too. Recording at serialization still captures the tags added then. Co-Authored-By: Claude Opus 5.5 --- .../main/java/datadog/trace/core/DDSpan.java | 2 ++ .../datadog/trace/core/DDSpanContext.java | 20 +++++++------------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java index 3427d085306..095a47eba02 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java @@ -165,6 +165,8 @@ private void finishAndAddToTrace(final long durationNano) { wrapper.onSpanFinished(); } this.metrics.onSpanFinished(); + // every finished span tunes its operation's sizing hint, serialized or not + context.recordDenseSize(); TraceCollector.PublishState publishState = context.getTraceCollector().onPublish(this); log.debug("Finished span ({}): {}", publishState, this); } else { diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 89489c57205..c0c9575fe13 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -1726,19 +1726,13 @@ void processTagsAndBaggageWithStructuredLinks( } /** - * Feeds this span's final dense-store size back into the sizing hint it was created from (if - * any), so a reused hint self-tunes to the operation's observed known-tag high-water mark and - * later spans of the operation are sized correctly. - * - *

    Called from {@link #processTagsAndBaggage} at serialization — the one terminal point that - * (a) runs for every serialized span regardless of finish mode (plain {@code finish()} AND {@code - * phasedFinish()}+{@code publish()}, which the async gRPC/Netty/WebFlux paths use and which never - * enter {@code finishAndAddToTrace}), and (b) runs AFTER the lazy tag post-processors have - * appended their serialization-time tags ({@code _dd.integration}, host, ...), so the recorded - * count is the span's true final footprint rather than an underestimate. The caller already holds - * {@code synchronized (unsafeTags)}, so the {@code knownCount} read is consistent; the hint write - * itself is best-effort racy by design (see {@link TagMap#recordSize}). With the dense store off - * the hint is always null, so this is a single null check. + * Feeds this span's known-tag count back into its operation's {@link SizingHint}, so later spans + * of the operation are presized. Called when the span finishes, so every span tunes its hint even + * if it is never serialized (dropped by sampling, say), and again at serialization, after the + * serialization-time tags are added, so the hint also covers the span's final footprint. At + * finish the {@code knownCount} read is unlocked and may be stale; the hint is a best-effort racy + * max anyway (see {@link TagMap#recordSize}). With the dense store off the hint is always null, + * so this is a single null check. */ void recordDenseSize() { if (sizingHint != null) { From abda6629c76820d066446dfa4f119e09fedcc112 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 12 Oct 2026 12:43:28 -0400 Subject: [PATCH 18/29] Copy a source's dense tags into a presized map's own arrays putAllIntoEmptyMap always allocated new dense arrays. A span map presized from its SizingHint therefore threw its hinted arrays away when its starting tags were copied in (CoreTracer -> setAllTags -> putAll), and allocated 12-slot arrays instead: two allocations, the wrong size. The copy now goes into the map's own arrays when they fit, and allocates only when they don't. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/api/DenseTagMap.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java b/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java index f82a290be05..4296046ff66 100644 --- a/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java @@ -270,12 +270,18 @@ void putAllMerge(TagMap that) { void putAllIntoEmptyMap(TagMap that) { DenseTagMap dense = (DenseTagMap) that; if (dense.knownCount > 0) { - // dense values are immutable objects, so the copy can share them. Sized to what the source - // holds, not its capacity: a source that grew to the bound would otherwise hand every copy - // a full-bound array. - int cap = Math.max(dense.knownCount, KNOWN_INIT_CAP); - this.knownIds = Arrays.copyOf(dense.knownIds, cap); - this.knownValues = Arrays.copyOf(dense.knownValues, cap); + // dense values are immutable objects, so the copy can share them. Into this map's own arrays + // when they already fit -- a map presized from a SizingHint -- else sized to what the source + // holds, not its capacity, so a source that grew to the bound doesn't hand every copy a + // full-bound array. + int count = dense.knownCount; + if (this.knownIds.length < count) { + int cap = Math.max(count, KNOWN_INIT_CAP); + this.knownIds = new long[cap]; + this.knownValues = new Object[cap]; + } + System.arraycopy(dense.knownIds, 0, this.knownIds, 0, count); + System.arraycopy(dense.knownValues, 0, this.knownValues, 0, count); this.knownCount = dense.knownCount; this.knownOccupancy = dense.knownOccupancy; this.knownTraceLevel = dense.knownTraceLevel; From e935d16ece23b610dbd95de9a12cd9c1f3a5db96 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 12 Oct 2026 17:23:59 -0400 Subject: [PATCH 19/29] Add a by-id counterpart to SpanCreationBenchmark SpanCreationByIdBenchmark repeats SpanCreationBenchmark's tagged span shapes (web server, JDBC client, and their SpanPrototype variants) but sets each tag by its KnownTags id instead of its name. Same values, threads and forks, so each arm compares directly with its namesake. peer.port uses its directional id. On Zulu 17, with 3 forks and debug logging off, setting tags by id matched setting them by name within the error bars, in both throughput and allocation, with the dense store on and off. Co-Authored-By: Claude Opus 5.5 --- .../trace/core/SpanCreationByIdBenchmark.java | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 dd-trace-core/src/jmh/java/datadog/trace/core/SpanCreationByIdBenchmark.java diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/SpanCreationByIdBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/SpanCreationByIdBenchmark.java new file mode 100644 index 00000000000..5cef4d4e957 --- /dev/null +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/SpanCreationByIdBenchmark.java @@ -0,0 +1,172 @@ +package datadog.trace.core; + +import static java.util.concurrent.TimeUnit.MICROSECONDS; + +import datadog.trace.api.KnownTags; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** + * {@link SpanCreationBenchmark}'s tagged span shapes, setting each tag by its {@link KnownTags} id + * instead of its name: same tags, values, threads and forks, so each arm here compares directly + * with the same-named arm there. {@code peer.port} uses its directional id ({@code + * PEER_PORT_INBOUND_ID} on the server span, {@code PEER_PORT_OUTBOUND_ID} on the client span), + * which a by-name write resolves through the span's kind. The bare spans set no tags, and the + * builder path has no id-keyed {@code withTag}, so those arms are not repeated. + * + *

    Read {@code gc.alloc.rate.norm} (B/op) first, as for {@link SpanCreationBenchmark}; throughput + * is directional. Run with {@code -prof gc}, and append {@code -DTEST_LOG_LEVEL=warn} to any {@code + * -jvmArgsAppend}, which replaces the {@code @Fork} arguments below. + */ +@State(Scope.Benchmark) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@BenchmarkMode(Mode.Throughput) +@Threads(8) +@OutputTimeUnit(MICROSECONDS) +@Fork(value = 3, jvmArgsAppend = "-DTEST_LOG_LEVEL=warn") +public class SpanCreationByIdBenchmark { + private static final String INSTRUMENTATION_NAME = "bench"; + private static final String SERVER_OPERATION_NAME = "servlet.request"; + private static final String JDBC_OPERATION_NAME = "database.query"; + + // Same values as SpanCreationBenchmark; int values stay inside Integer's cache. + private static final String COMPONENT_VALUE = "tomcat-server"; + private static final String HTTP_METHOD_VALUE = "GET"; + private static final String HTTP_ROUTE_VALUE = "/owners/{ownerId}"; + private static final String HTTP_URL_VALUE = "http://localhost:8080/owners/42"; + private static final int HTTP_STATUS_VALUE = 100; + private static final int PEER_PORT_VALUE = 80; + + private static final String DB_COMPONENT_VALUE = "java-jdbc-statement"; + private static final String DB_TYPE_VALUE = "postgresql"; + private static final String DB_INSTANCE_VALUE = "petclinic"; + private static final String DB_USER_VALUE = "app"; + private static final String DB_OPERATION_VALUE = "SELECT"; + private static final String DB_STATEMENT_VALUE = "SELECT * FROM owners WHERE id = ?"; + private static final String DB_PEER_HOSTNAME_VALUE = "db.internal"; + private static final int DB_PEER_PORT_VALUE = 90; + + CoreTracer tracer; + SpanPrototype webProto; + SpanPrototype jdbcProto; + + @Setup + public void setup(Blackhole blackhole) { + this.tracer = CoreTracer.builder().writer(new DropWriter(blackhole)).build(); + this.webProto = + SpanPrototype.builder() + .initInstrumentationName(INSTRUMENTATION_NAME) + .initOperationName(SERVER_OPERATION_NAME) + .initComponentOnly(COMPONENT_VALUE) + .initKind(Tags.SPAN_KIND_SERVER) + .build(); + this.jdbcProto = + SpanPrototype.builder() + .initInstrumentationName(INSTRUMENTATION_NAME) + .initOperationName(JDBC_OPERATION_NAME) + .initComponentOnly(DB_COMPONENT_VALUE) + .initKind(Tags.SPAN_KIND_CLIENT) + .initTag(Tags.DB_TYPE, DB_TYPE_VALUE) + .build(); + } + + @TearDown + public void tearDown() { + this.tracer.close(); + } + + /** {@link SpanCreationBenchmark#webServerSpan}, by id. */ + @Benchmark + public void webServerSpan() { + AgentSpan span = tracer.buildSpan(INSTRUMENTATION_NAME, SERVER_OPERATION_NAME).start(); + span.setTag(KnownTags.COMPONENT_ID, COMPONENT_VALUE); + span.setTag(KnownTags.SPAN_KIND_ID, Tags.SPAN_KIND_SERVER); + span.setTag(KnownTags.HTTP_METHOD_ID, HTTP_METHOD_VALUE); + span.setTag(KnownTags.HTTP_ROUTE_ID, HTTP_ROUTE_VALUE); + span.setTag(KnownTags.HTTP_URL_ID, HTTP_URL_VALUE); + span.setTag(KnownTags.HTTP_STATUS_CODE_ID, HTTP_STATUS_VALUE); + span.setTag(KnownTags.PEER_PORT_INBOUND_ID, PEER_PORT_VALUE); + span.finish(); + } + + /** {@link SpanCreationBenchmark#jdbcClientSpan}, by id. */ + @Benchmark + public void jdbcClientSpan() { + AgentSpan span = tracer.buildSpan(INSTRUMENTATION_NAME, JDBC_OPERATION_NAME).start(); + span.setTag(KnownTags.COMPONENT_ID, DB_COMPONENT_VALUE); + span.setTag(KnownTags.SPAN_KIND_ID, Tags.SPAN_KIND_CLIENT); + span.setTag(KnownTags.DB_TYPE_ID, DB_TYPE_VALUE); + span.setTag(KnownTags.DB_INSTANCE_ID, DB_INSTANCE_VALUE); + span.setTag(KnownTags.DB_USER_ID, DB_USER_VALUE); + span.setTag(KnownTags.DB_OPERATION_ID, DB_OPERATION_VALUE); + span.setTag(KnownTags.DB_STATEMENT_ID, DB_STATEMENT_VALUE); + span.setTag(KnownTags.PEER_HOSTNAME_ID, DB_PEER_HOSTNAME_VALUE); + span.setTag(KnownTags.PEER_PORT_OUTBOUND_ID, DB_PEER_PORT_VALUE); + span.finish(); + } + + /** {@link SpanCreationBenchmark#webServerSpanViaPrototype}, by id. */ + @Benchmark + public void webServerSpanViaPrototype() { + AgentSpan span = tracer.buildSpan(webProto, null).start(); + span.setTag(KnownTags.HTTP_METHOD_ID, HTTP_METHOD_VALUE); + span.setTag(KnownTags.HTTP_ROUTE_ID, HTTP_ROUTE_VALUE); + span.setTag(KnownTags.HTTP_URL_ID, HTTP_URL_VALUE); + span.setTag(KnownTags.HTTP_STATUS_CODE_ID, HTTP_STATUS_VALUE); + span.setTag(KnownTags.PEER_PORT_INBOUND_ID, PEER_PORT_VALUE); + span.finish(); + } + + /** {@link SpanCreationBenchmark#jdbcClientSpanViaPrototype}, by id. */ + @Benchmark + public void jdbcClientSpanViaPrototype() { + AgentSpan span = tracer.buildSpan(jdbcProto, null).start(); + span.setTag(KnownTags.DB_INSTANCE_ID, DB_INSTANCE_VALUE); + span.setTag(KnownTags.DB_USER_ID, DB_USER_VALUE); + span.setTag(KnownTags.DB_OPERATION_ID, DB_OPERATION_VALUE); + span.setTag(KnownTags.DB_STATEMENT_ID, DB_STATEMENT_VALUE); + span.setTag(KnownTags.PEER_HOSTNAME_ID, DB_PEER_HOSTNAME_VALUE); + span.setTag(KnownTags.PEER_PORT_OUTBOUND_ID, DB_PEER_PORT_VALUE); + span.finish(); + } + + /** {@link SpanCreationBenchmark#webServerSpanViaPrototypeStartSpan}, by id. */ + @Benchmark + public void webServerSpanViaPrototypeStartSpan() { + AgentSpan span = tracer.startSpan(webProto, null); + span.setTag(KnownTags.HTTP_METHOD_ID, HTTP_METHOD_VALUE); + span.setTag(KnownTags.HTTP_ROUTE_ID, HTTP_ROUTE_VALUE); + span.setTag(KnownTags.HTTP_URL_ID, HTTP_URL_VALUE); + span.setTag(KnownTags.HTTP_STATUS_CODE_ID, HTTP_STATUS_VALUE); + span.setTag(KnownTags.PEER_PORT_INBOUND_ID, PEER_PORT_VALUE); + span.finish(); + } + + /** {@link SpanCreationBenchmark#jdbcClientSpanViaPrototypeStartSpan}, by id. */ + @Benchmark + public void jdbcClientSpanViaPrototypeStartSpan() { + AgentSpan span = tracer.startSpan(jdbcProto, null); + span.setTag(KnownTags.DB_INSTANCE_ID, DB_INSTANCE_VALUE); + span.setTag(KnownTags.DB_USER_ID, DB_USER_VALUE); + span.setTag(KnownTags.DB_OPERATION_ID, DB_OPERATION_VALUE); + span.setTag(KnownTags.DB_STATEMENT_ID, DB_STATEMENT_VALUE); + span.setTag(KnownTags.PEER_HOSTNAME_ID, DB_PEER_HOSTNAME_VALUE); + span.setTag(KnownTags.PEER_PORT_OUTBOUND_ID, DB_PEER_PORT_VALUE); + span.finish(); + } +} From 7e56c8823f752026d0adccfe0965bc9658dab766 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 13 Oct 2026 14:55:19 -0400 Subject: [PATCH 20/29] Box a primitive tag only when the store keeps the box Box-once (5e600f7306) made every DDSpanContext.setTag(long, ) box first and pass the box down. With the dense store off, HashTagMap then kept that box in the tag's Entry, although the serializers read the primitive. That was a retained allocation the hash path never had, for any value outside the box cache (most status codes and ports, nearly all longs, every float and double). The box escapes into interceptTag, which is not inlined, so C2 cannot remove it. TagMap.Internals.STORES_BOXES, a static final that is true only for the dense store, chooses between the two shapes, and the check folds. The dense store keeps box-once. The hash store goes back to boxing only on the interception path and storing the primitive otherwise. SetTagBenchmark gains a knownIntTag_byId_uncached arm, since the existing arms only use cached values and could not see the allocation. Also fixes stale documentation found by the techdebt review: - an orphaned comment describing the removed setBox; - comments that still said known tags without a slot are stored by hash (every known tag is dense now); - comments that said sizing hints tune only at serialization (they also tune when a span finishes). SizingHintTable.OVERFLOW_SEED now reads DenseTagMap.KNOWN_INIT_CAP instead of repeating its value. Co-Authored-By: Claude Opus 5.5 --- .../buildlogic/tagRegistry/TagRegistry.kt | 4 +- .../datadog/trace/core/SetTagBenchmark.java | 9 ++ .../java/datadog/trace/core/CoreTracer.java | 2 +- .../datadog/trace/core/DDSpanContext.java | 130 +++++++++++++----- .../java/datadog/trace/api/DenseTagMap.java | 14 +- .../java/datadog/trace/api/KnownTagCodec.java | 7 +- .../datadog/trace/api/SizingHintTable.java | 11 +- .../main/java/datadog/trace/api/TagMap.java | 8 ++ 8 files changed, 132 insertions(+), 53 deletions(-) diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt index 3e8c753fa18..4575c297c0c 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt @@ -29,7 +29,7 @@ class TagRegistry private constructor(val tags: List) { val serial: Int, val traceLevel: Boolean, val intercepted: Boolean, - /** The dense-store slot, or [NO_SLOT] when the tag is stored by hash. */ + /** The dense-store slot, or [NO_SLOT] for a tag on no modeled span type. */ val slot: Int, val id: Long, /** The tag's OpenTelemetry name, in [otelDirection] or every direction; null when not renamed. */ @@ -68,7 +68,7 @@ class TagRegistry private constructor(val tags: List) { const val UNDIRECTED_SHIFT = 16 const val SLOT_SHIFT = 32 // slot window [47-32]; mirrors KnownTagCodec.SLOT_SHIFT - const val NO_SLOT = 0xFFFF // all-ones slot: stored by hash; mirrors KnownTagCodec.NO_SLOT + const val NO_SLOT = 0xFFFF // all-ones slot: no modeled span type; mirrors KnownTagCodec.NO_SLOT const val MAX_SLOTS = 63 // one occupancy long in the dense store; bit 63 is shared by unslotted tags diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java index a9d1c25d13e..02a805cb88c 100644 --- a/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/SetTagBenchmark.java @@ -100,6 +100,15 @@ public Object knownIntTag_byId() { return span.setTag(KnownTags.HTTP_RESEND_COUNT_ID, 2); } + /** + * {@link #knownIntTag_byId} with a value outside {@code Integer}'s cache, so a box, if the path + * makes one, is a real allocation -- as for most status codes and ports. + */ + @Benchmark + public Object knownIntTag_byId_uncached() { + return span.setTag(KnownTags.HTTP_RESEND_COUNT_ID, 5432); + } + @Benchmark public Object knownIntTag_byName() { return span.setTag(KnownTags.HTTP_RESEND_COUNT_NAME, 2); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index a0cea3cc11b..e927aa64ddc 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -2332,7 +2332,7 @@ protected static final DDSpanContext buildSpanContext( // Per-operation dense-store sizing: an entry (local-root) span carries the trace-metadata / // enriching tags a child doesn't, so pick the lane by whether we have a local parent. A - // resolved hint sizes the span's TagMap and self-tunes at serialization; null (dense store + // resolved hint sizes the span's TagMap and self-tunes as spans finish; null (dense store // off, or no operation name) falls back to the generic default capacity. hintFor tests the // dense-store flag first, so with it off -- the default -- this constant-folds to null and // the default span-creation path does no lookup (do no harm). diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index c0c9575fe13..684b5005d79 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -169,10 +169,10 @@ public class DDSpanContext /** * Per-operation sizing hint (from SizingHintTable, keyed by operation name) this span's dense - * store was sized from, if any. Held so the span can feed its final dense-store size back at - * serialization (recordDenseSize) -- the self-tuning loop that lets the reused hint converge to - * the operation's real known-tag high-water mark. Null when unsized, which is always the case - * with the dense store off. + * store was sized from, if any. Held so the span can feed its dense-store size back when it + * finishes and when it is serialized (recordDenseSize) -- the self-tuning loop that lets the + * reused hint converge to the operation's real known-tag high-water mark. Null when unsized, + * which is always the case with the dense store off. */ @Nullable private final SizingHint sizingHint; @@ -1320,11 +1320,25 @@ public void setTag(final long tagId, final boolean value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - // Boxed once: the interceptor and the store share the box. - final Boolean box = value; - if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, box)) { + if (TagMap.Internals.STORES_BOXES) { + // The dense store keeps the box: box once, shared by the interceptor and the store. + final Boolean box = value; + if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, box)) { + synchronized (unsafeTags) { + unsafeTags.internals().setKnown(tagId, box); + } + } + } else if (tagInterceptor.needsIntercept(tagId)) { + final Boolean box = value; + if (!tagInterceptor.interceptTag(this, tagId, box)) { + synchronized (unsafeTags) { + unsafeTags.internals().setKnown(tagId, box); + } + } + } else { + // The hash store keeps the primitive: no box. synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, box); + unsafeTags.internals().setKnown(tagId, value); } } } @@ -1333,11 +1347,25 @@ public void setTag(final long tagId, final int value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - // Boxed once: the interceptor and the store share the box. - final Integer box = value; - if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, box)) { + if (TagMap.Internals.STORES_BOXES) { + // The dense store keeps the box: box once, shared by the interceptor and the store. + final Integer box = value; + if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, box)) { + synchronized (unsafeTags) { + unsafeTags.internals().setKnown(tagId, box); + } + } + } else if (tagInterceptor.needsIntercept(tagId)) { + final Integer box = value; + if (!tagInterceptor.interceptTag(this, tagId, box)) { + synchronized (unsafeTags) { + unsafeTags.internals().setKnown(tagId, box); + } + } + } else { + // The hash store keeps the primitive: no box. synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, box); + unsafeTags.internals().setKnown(tagId, value); } } } @@ -1346,11 +1374,25 @@ public void setTag(final long tagId, final long value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - // Boxed once: the interceptor and the store share the box. - final Long box = value; - if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, box)) { + if (TagMap.Internals.STORES_BOXES) { + // The dense store keeps the box: box once, shared by the interceptor and the store. + final Long box = value; + if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, box)) { + synchronized (unsafeTags) { + unsafeTags.internals().setKnown(tagId, box); + } + } + } else if (tagInterceptor.needsIntercept(tagId)) { + final Long box = value; + if (!tagInterceptor.interceptTag(this, tagId, box)) { + synchronized (unsafeTags) { + unsafeTags.internals().setKnown(tagId, box); + } + } + } else { + // The hash store keeps the primitive: no box. synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, box); + unsafeTags.internals().setKnown(tagId, value); } } } @@ -1359,11 +1401,25 @@ public void setTag(final long tagId, final float value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - // Boxed once: the interceptor and the store share the box. - final Float box = value; - if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, box)) { + if (TagMap.Internals.STORES_BOXES) { + // The dense store keeps the box: box once, shared by the interceptor and the store. + final Float box = value; + if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, box)) { + synchronized (unsafeTags) { + unsafeTags.internals().setKnown(tagId, box); + } + } + } else if (tagInterceptor.needsIntercept(tagId)) { + final Float box = value; + if (!tagInterceptor.interceptTag(this, tagId, box)) { + synchronized (unsafeTags) { + unsafeTags.internals().setKnown(tagId, box); + } + } + } else { + // The hash store keeps the primitive: no box. synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, box); + unsafeTags.internals().setKnown(tagId, value); } } } @@ -1372,11 +1428,25 @@ public void setTag(final long tagId, final double value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - // Boxed once: the interceptor and the store share the box. - final Double box = value; - if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, box)) { + if (TagMap.Internals.STORES_BOXES) { + // The dense store keeps the box: box once, shared by the interceptor and the store. + final Double box = value; + if (!tagInterceptor.needsIntercept(tagId) || !tagInterceptor.interceptTag(this, tagId, box)) { + synchronized (unsafeTags) { + unsafeTags.internals().setKnown(tagId, box); + } + } + } else if (tagInterceptor.needsIntercept(tagId)) { + final Double box = value; + if (!tagInterceptor.interceptTag(this, tagId, box)) { + synchronized (unsafeTags) { + unsafeTags.internals().setKnown(tagId, box); + } + } + } else { + // The hash store keeps the primitive: no box. synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, box); + unsafeTags.internals().setKnown(tagId, value); } } } @@ -1399,18 +1469,6 @@ public void setTag(TagMap.EntryReader entry) { } } - /* - * Used when the interceptor's precheck says the tag may be intercepted, so boxing is unavoidable - * - * Either because the tagInterceptor needs to be fully checked (which requires boxing) - * In that case, a box has already been created so it makes sense to pass the box - * onto TagMap, since optimized TagMap will cache the box - * - * -- OR -- - * - * The TagMap isn't optimized and will need to box the primitive regardless of - * tag interception - */ public void setTag(final String tag, final boolean value) { if (null == tag) { return; diff --git a/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java b/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java index 4296046ff66..672f6d2dc22 100644 --- a/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java @@ -21,11 +21,11 @@ final class DenseTagMap extends TagMap { /* * Dense known-tag store (experimental; see TagMap.DenseStore.ENABLED, default off). *

    - * With the flag on, a known tag that has a dense-store slot (KnownTagCodec.hasSlot -- the - * registry colors required/conditional/recommended tags into slots so tags on one span type never - * share one) is NOT stored as a bucket Entry. Its value lives in the insertion-ordered parallel - * arrays knownIds/knownValues instead, with no per-tag Entry object: that is the allocation win. - * Known tags without a slot (NO_SLOT) and custom tags stay in the buckets, exactly as before. + * With the flag on, every known tag is NOT stored as a bucket Entry. Its value lives in the + * insertion-ordered parallel arrays knownIds/knownValues instead, with no per-tag Entry object: + * that is the allocation win. The registry colors the tags of each span type into distinct slots + * (KnownTagCodec.slotOf); a known tag on no modeled span type has NO_SLOT, which shares occupancy + * bit 63. Custom tags stay in the buckets, exactly as before. *

    * Presence: knownOccupancy holds 1L << slot for every dense id ever added. A clear bit means the * tag is definitely absent, so an append or a miss skips the scan. A set bit only means "maybe": @@ -56,7 +56,7 @@ final class DenseTagMap extends TagMap { * Initial capacity of the dense arrays when no {@link SizingHint} presized them; the largest span * type resolves about this many tags. */ - private static final int KNOWN_INIT_CAP = 12; + static final int KNOWN_INIT_CAP = 12; /** * Shared empty dense arrays a map starts on (and returns to on clear), so that one full-capacity @@ -440,7 +440,7 @@ void checkIntegrity() { for (int i = 0; i < this.knownCount; ++i) { long denseId = this.knownIds[i]; if (denseIdOf(denseId) == 0) { - throw new IllegalStateException("dense id without a slot"); + throw new IllegalStateException("dense id that is not a known tag"); } if ((this.knownOccupancy & slotBit(denseId)) == 0) { throw new IllegalStateException("dense id missing from occupancy"); diff --git a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java index 6fc026f7ee6..c8c3a221426 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -16,7 +16,7 @@ public final class KnownTagCodec { * unique per known tag and is the whole of the tag's identity — nameOf/openTelemetryNameOf key on * it, and the generator emits each id as a literal. Bits [47-32] hold the tag's dense-store slot * ({@link #slotOf}), assigned by graph coloring so tags on one span type never share a slot, or - * {@link #NO_SLOT} for a tag stored by hash. Of the low 32 flag bits, bit 3 is the UNDIRECTED + * {@link #NO_SLOT} for a tag on no modeled span type. Of the low 32 flag bits, bit 3 is the UNDIRECTED * bit, bit 2 is the trace/span LEVEL bit (set ⟹ trace-level), bit 1 is the INTERCEPTED bit, and * bit 0 is the SHARED_NAME bit; a SHARED_NAME id also holds its undirected tag's serial at * [30-16]. Unknown (string-only) custom tags are NOT known ids — {@code keyOf} returns 0 for @@ -102,7 +102,10 @@ public static long undirectedIdOf(long sharedNameId) { /** Bit position of the dense-store slot: bits [47-32] of an id. */ public static final int SLOT_SHIFT = 32; - /** The slot of a tag stored by hash rather than in a dense-store slot. */ + /** + * The slot of a known tag on no modeled span type. The dense store still stores it densely; its + * occupancy bit is bit 63, shared by every such tag. + */ public static final int NO_SLOT = 0xFFFF; /** diff --git a/internal-api/src/main/java/datadog/trace/api/SizingHintTable.java b/internal-api/src/main/java/datadog/trace/api/SizingHintTable.java index 6b0d530f145..b8793624a83 100644 --- a/internal-api/src/main/java/datadog/trace/api/SizingHintTable.java +++ b/internal-api/src/main/java/datadog/trace/api/SizingHintTable.java @@ -6,7 +6,7 @@ /** * Process-wide, self-tuning registry of per-operation {@link SizingHint}s, keyed by operation name. * Pure static: the tracer resolves a hint here at span build and hands it to the span, which sizes - * its dense {@link TagMap} from it and records the actual size back at serialization — the hint + * its dense {@link TagMap} from it and records the actual size back when it finishes — the hint * converges to the operation's real known-tag high-water mark, so later spans of that operation * size correctly. * @@ -43,10 +43,11 @@ private SizingHintTable() {} // oversized arrays forever; a span that outgrows it pays one doubling. static final int SEED_SIZE = 4; - // Fixed size for the shared over-budget hint: today's untuned default (DenseTagMap's initial - // capacity). Capped (never grown by recordSize) because it's a heterogeneous catch-all -- growing - // it would over-provision lean sharers to the max of an unlike cohort. - static final int OVERFLOW_SEED = 12; + // Fixed size for the shared over-budget hint: the untuned default, DenseTagMap's initial capacity + // (a compile-time constant, so reading it loads no class). Capped (never grown by recordSize) + // because it's a heterogeneous catch-all -- growing it would over-provision lean sharers to the + // max of an unlike cohort. + static final int OVERFLOW_SEED = DenseTagMap.KNOWN_INIT_CAP; // Max distinct operation names per lane that get their own hint before collapsing to the capped // overflow hint. 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 d83e7a7c26c..9192931cedd 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1761,6 +1761,14 @@ public Internals internals() { */ @NoEscape public static final class Internals { + /** + * Whether the store keeps a known tag's value as an object: true for the dense store. A caller + * holding a primitive then boxes it once and passes the box down (to the interceptor and the + * store alike); otherwise the hash store keeps the primitive, and the caller should not box. A + * static final, so the check folds. + */ + public static final boolean STORES_BOXES = DenseStore.ENABLED; + private final TagMap map; Internals(TagMap map) { From 1c88d5ef54f52b5849719cf656c1fd2cfdb67514 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 14 Oct 2026 17:34:05 -0400 Subject: [PATCH 21/29] Re-apply split-by-tags language by its id resolveSplitByTagsPrecedence checked language through the custom-tag overload, which only matches names that resolve to no known tag. language is known, so the check was always false and a prototype's language was never re-applied over another split-by-tags candidate the prototype set. Check and read it by LANGUAGE_ID, as for component. Found by Codex, via a review comment from AlexeyKuznetsov-DD. Co-Authored-By: Claude Opus 5.5 --- .../core/taginterceptor/TagInterceptor.java | 7 +++-- .../taginterceptor/TagInterceptorTest.java | 31 +++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java index 87f9438ffb6..b487d4665b7 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java @@ -2,6 +2,7 @@ import static datadog.trace.api.DDTags.ANALYTICS_SAMPLE_RATE; import static datadog.trace.api.KnownTags.COMPONENT_ID; +import static datadog.trace.api.KnownTags.LANGUAGE_ID; import static datadog.trace.api.KnownTags.SERVLET_CONTEXT_ID; import static datadog.trace.api.sampling.PrioritySampling.USER_DROP; import static datadog.trace.bootstrap.instrumentation.api.ServiceNameSources.SPLIT_BY_SERVLET_CONTEXT; @@ -340,10 +341,10 @@ public void resolveSplitByTagsPrecedence(DDSpanContext span, TagMap prototypeTag return; } } - if (isSplitServiceTag(DDTags.LANGUAGE_TAG_KEY)) { - Object language = prototypeTags.getObject(DDTags.LANGUAGE_TAG_KEY); + if (isSplitServiceTag(LANGUAGE_ID)) { + TagMap.Entry language = prototypeTags.getEntry(LANGUAGE_ID); if (language != null) { - splitService(span, language); + splitService(span, language.objectValue()); } } } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java index 533f35a6759..6cb01e76f08 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java @@ -30,6 +30,7 @@ import datadog.trace.api.KnownTagCodec; import datadog.trace.api.KnownTags; import datadog.trace.api.ProductTraceSource; +import datadog.trace.api.TagMap; import datadog.trace.api.env.CapturedEnvironment; import datadog.trace.api.remoteconfig.ServiceNameCollector; import datadog.trace.api.remoteconfig.ServiceNameCollectorTestBridge; @@ -375,6 +376,36 @@ void splitByTagsDecoratorComponentOverwritesCandidateSetAtConstruction() { assertEquals("my-component", span.getServiceName()); } + /** + * Without a {@code component}, a prototype's {@code language} is re-applied after the prototype's + * tags are intercepted, so it wins over another split-by-tags candidate the prototype sets. + * Called directly: the prototype's map is intercepted in hash order, so a full application can't + * pin the order that needs this. + */ + @Test + void splitByTagsLanguageIsReappliedWithoutAComponent() { + TagInterceptor interceptor = + new TagInterceptor( + true, + "my-service", + new HashSet<>(Arrays.asList("tenant", DDTags.LANGUAGE_TAG_KEY)), + new RuleFlags(), + false); + CoreTracer tracer = + tracerBuilder().writer(new LoggingWriter()).tagInterceptor(interceptor).build(); + AgentSpan span = tracer.buildSpan("datadog", "some span").start(); + span.setTag("tenant", "acme"); // as if the prototype's tenant was intercepted last + assertEquals("acme", span.getServiceName()); + + TagMap prototypeTags = TagMap.create(); + prototypeTags.set(DDTags.LANGUAGE_TAG_KEY, DDTags.LANGUAGE_TAG_VALUE); + prototypeTags.set("tenant", "acme"); + interceptor.resolveSplitByTagsPrecedence((DDSpanContext) span.spanContext(), prototypeTags); + span.finish(); + + assertEquals(DDTags.LANGUAGE_TAG_VALUE, span.getServiceName()); + } + /** Separate tag writes stay last-writer-wins, whichever split-by-tags candidate they set. */ @Test void splitByTagsSeparateWritesAreLastWriterWins() { From 5ba0a09509ffb468a2ecb52015cc7f9e77710d16 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 14 Oct 2026 17:34:07 -0400 Subject: [PATCH 22/29] Say what a tag id's intercepted bit does and doesn't decide Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/datadog/trace/api/KnownTagCodec.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java index 80d63829564..605f7140d4c 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -7,8 +7,9 @@ * the ID layout and namespace naming policy. * *

    Datadog and OpenTelemetry names can resolve to the same ID. {@link #canonicalTagName(String)} - * provides the Datadog key used by {@link TagMap}; the namespace readers choose output names. The - * ID does not determine whether a tag is intercepted or where it is stored. + * provides the Datadog key used by {@link TagMap}; the namespace readers choose output names. An ID + * marks a tag that may be intercepted ({@link #INTERCEPTED}), but {@code TagInterceptor} decides + * whether to consume the value. The ID says nothing about where the tag is stored. */ public final class KnownTagCodec { /* From 0faed2d8ef9129fd8078b68ffc90ac2f5230a6d3 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 14 Oct 2026 21:31:36 -0400 Subject: [PATCH 23/29] Color known tags into co-occurrence slots in their ids The tag-registry generator now assigns each known tag a slot in id bits [47-32] by graph coloring: each concrete span type's resolved tags, and the trace-level tags, form a clique and get distinct slots. So 1L << slotOf(id) is an exact presence bit for a span's tags, and one long holds them all. Tags on no modeled span type get NO_SLOT, which a long shift maps onto bit 63; real slots are capped below it. KnownTagCodec gains SLOT_SHIFT, NO_SLOT, slotOf, hasSlot and slotCount. Nothing in the tracer reads slots yet: this is split out of the dense store (#12856) so the store, the span builder's tag ledger and a map-level routing summary can each build on it. Co-Authored-By: Claude Opus 5.5 --- .../tagRegistry/KnownTagsEmitter.kt | 4 + .../buildlogic/tagRegistry/TagRegistry.kt | 74 +++++++++++++++--- .../tagRegistry/TagRegistryGenerator.kt | 5 +- .../tagRegistry/TagRegistryGeneratorTest.kt | 44 ++++++++++- .../java/datadog/trace/api/KnownTagCodec.java | 40 ++++++++-- .../trace/api/KnownTagCodecSlotTest.java | 77 +++++++++++++++++++ 6 files changed, 221 insertions(+), 23 deletions(-) create mode 100644 internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt index a68d39d6701..63715868802 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt @@ -83,6 +83,7 @@ object KnownTagsEmitter { appendLine(" public static final String ${otelNameC(t.ddName)} = \"${escape(t.otelName)}\";") } append("// makeTagId(serial=${t.serial})") + append(if (t.slot == TagRegistry.NO_SLOT) " + no slot" else " + slot=${t.slot}") if (t.traceLevel) append(" + trace-level") if (t.intercepted) append(" + intercepted") if (t.otelName != null) append(" -> ${escape(t.otelName)}") @@ -100,6 +101,9 @@ object KnownTagsEmitter { // Every known tag's serial is below SERIAL_LIMIT; serial 0 is no tag. val serialLimit = (reg.tags.maxOfOrNull { it.serial } ?: 0) + 1 appendLine(" public static final int SERIAL_LIMIT = $serialLimit;") + // Slots in use: every slotted tag's slot is below SLOT_COUNT. + val slotCount = (reg.tags.filter { it.slot != TagRegistry.NO_SLOT }.maxOfOrNull { it.slot } ?: -1) + 1 + appendLine(" public static final int SLOT_COUNT = $slotCount;") // OpenTelemetry name -> canonical tag name. Validation ensures aliases are distinct from all // canonical names. Sort by OTel name to keep output deterministic. diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt index 248471c228a..cf1eb3e572b 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistry.kt @@ -2,12 +2,20 @@ package datadog.buildlogic.tagRegistry /** * Assigns tag ids from a parsed [TagConventions]. The id encoding mirrors KnownTagCodec: [63-48 - * serial][47-32 reserved][31-0 flags]. + * serial][47-32 slot][31-0 flags]. * - *

    An id is a globally unique serial plus two classification bits: trace-level and intercepted. It - * carries no storage-layout coordinate -- bits [47-32] are held vacant for the co-occurrence slot - * that the dense tag store assigns by graph coloring, which lands with the dense store itself. - * Nothing here needs to know how (or whether) a tag is stored. + *

    An id is a globally unique serial, a slot, and two classification bits: trace-level and + * intercepted. + * + *

    The slot comes from GRAPH COLORING the tag co-occurrence graph. Each concrete span type's + * resolved tags appear together on one span, so they form a clique and need distinct slots; the + * trace-level tier is its own clique (it lives in a separate TagMap). Tags that never share a clique + * may share a slot, so the slot count stays bounded by the largest clique, small enough for one + * occupancy `long`. Every tag resolved onto a span type, or trace-level, is slotted whatever its + * requirement level; a tag on no modeled span type (the tracer overlay's routing-only keys, tags of + * unmodeled span types) gets [NO_SLOT]. Nothing in the tracer reads slots yet: they are for + * per-span structures that want an exact, compact presence word, such as a dense tag store or a + * span builder's tag ledger. * *

    The intercepted bit marks a tag the tracer may route on the set-path (to a span field or a * sampling directive instead of tag storage), as listed by the tracer overlay. A setter called with @@ -26,6 +34,8 @@ class TagRegistry private constructor(val tags: List) { val serial: Int, val traceLevel: Boolean, val intercepted: Boolean, + /** The tag's slot, or [NO_SLOT] for a tag on no modeled span type. */ + val slot: Int, val id: Long, /** The tag's OpenTelemetry name in every direction; null when not renamed, or renamed per direction. */ val otelName: String? = null, @@ -42,13 +52,17 @@ class TagRegistry private constructor(val tags: List) { const val INTERCEPTED = 1L shl 1 // low-32 carve bit 1; mirrors KnownTagCodec.INTERCEPTED const val LEVEL_TRACE = 1L shl 2 // low-32 carve bit 2; mirrors KnownTagCodec.LEVEL_TRACE + const val SLOT_SHIFT = 32 // slot window [47-32]; mirrors KnownTagCodec.SLOT_SHIFT + const val NO_SLOT = 0xFFFF // all-ones slot: no modeled span type; mirrors KnownTagCodec.NO_SLOT + const val MAX_SLOTS = 63 // fits one occupancy long; bit 63 is left for unslotted tags to share + /** - * Mirrors KnownTagCodec.makeTagId(serial) + intercepted() + traceLevel() -- must stay in sync. - * INTERCEPTED at bit 1, LEVEL_TRACE at bit 2, other low bits and the reserved [47-32] window - * zero. + * Mirrors KnownTagCodec's layout -- must stay in sync: serial at [63-48], slot at [47-32], + * INTERCEPTED at bit 1, LEVEL_TRACE at bit 2, other low bits zero. */ - fun encode(serial: Int, traceLevel: Boolean, intercepted: Boolean): Long { - var id = serial.toLong() shl 48 + fun encode(serial: Int, traceLevel: Boolean, intercepted: Boolean, slot: Int = NO_SLOT): Long { + require(slot == NO_SLOT || slot in 0 until MAX_SLOTS) { "slot $slot is outside [0, $MAX_SLOTS)" } + var id = (serial.toLong() shl 48) or (slot.toLong() shl SLOT_SHIFT) if (traceLevel) id = id or LEVEL_TRACE if (intercepted) id = id or INTERCEPTED return id @@ -57,6 +71,7 @@ class TagRegistry private constructor(val tags: List) { fun build(conv: TagConventions): TagRegistry { val traceLevel = conv.traceLevelTags().map { it.ddName }.toSet() val renames = conv.otelMappings().groupBy { it.tag.ddName } + val slots = colorSlots(conv) // One tag per Datadog name, in a stable order (by name) so serials -- and therefore ids -- are // a pure function of the input. A name's declarations per direction merge into its one tag. @@ -70,6 +85,7 @@ class TagRegistry private constructor(val tags: List) { val isTraceLevel = first.ddName in traceLevel val isIntercepted = first.ddName in conv.interceptedNames val (otelName, scoped) = otelNamesOf(first.ddName, renames[first.ddName].orEmpty()) + val slot = slots[first.ddName] ?: NO_SLOT Tag( first.ddName, first.type, @@ -77,7 +93,8 @@ class TagRegistry private constructor(val tags: List) { serial, isTraceLevel, isIntercepted, - id = encode(serial, isTraceLevel, isIntercepted), + slot, + id = encode(serial, isTraceLevel, isIntercepted, slot), otelName = otelName, scopedOtelNames = scoped, ) @@ -87,6 +104,41 @@ class TagRegistry private constructor(val tags: List) { return TagRegistry(tags) } + /** + * Colors the co-occurrence graph of slotted tags: tags resolved onto one concrete span type, or + * both trace-level, must get distinct slots. Greedy, most-constrained first (by clique count, + * then name), so the result is a pure function of the conventions. A name declared per direction + * is one tag, so it is one vertex. + */ + private fun colorSlots(conv: TagConventions): Map { + val cliques = ArrayList>() + for (type in conv.concreteTypes()) { + cliques.add(conv.resolve(type).map { it.ddName }.toSet()) + } + cliques.add(conv.traceLevelTags().map { it.ddName }.toSet()) + + val neighbors = HashMap>() + for (clique in cliques) { + for (a in clique) { + val adjacent = neighbors.getOrPut(a) { HashSet() } + for (b in clique) if (a != b) adjacent.add(b) + } + } + val cliqueCount = neighbors.keys.associateWith { tag -> cliques.count { tag in it } } + val order = neighbors.keys.sortedWith(compareByDescending { cliqueCount.getValue(it) }.thenBy { it }) + val slots = HashMap() + for (tag in order) { + val taken = neighbors.getValue(tag).mapNotNull { slots[it] }.toSet() + var slot = 0 + while (slot in taken) slot++ + require(slot < MAX_SLOTS) { + "coloring needs more than $MAX_SLOTS slots at '$tag'; one occupancy long holds $MAX_SLOTS" + } + slots[tag] = slot + } + return slots + } + /** * A tag's renames, as one name for every direction or one name per direction. The rename of a * declaration made per direction applies in that declaration's direction. diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt index eaae7ccc1a6..3baf2214638 100644 --- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt +++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGenerator.kt @@ -70,16 +70,17 @@ object TagRegistryGenerator { """ # Tag id assignment. tags=${reg.tags.size} - # TAGS serial lvl int id required name + # TAGS serial lvl int slot id required name """.trimIndent() ) for (t in reg.tags) { appendLine( - " %6d %s %s %-18s %-12s %s".format( + " %6d %s %s %4s %-18s %-12s %s".format( Locale.ROOT, t.serial, if (t.traceLevel) "T" else "-", if (t.intercepted) "I" else "-", + if (t.slot == TagRegistry.NO_SLOT) "-" else t.slot.toString(), "0x%016X".format(Locale.ROOT, t.id), t.required, t.name diff --git a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt index a88f7509144..6db58e6f27b 100644 --- a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt +++ b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorTest.kt @@ -535,9 +535,9 @@ class TagRegistryGeneratorTest { val knownTags = contents(output).getValue("java/datadog/trace/api/KnownTags.java") assertThat(knownTags) .contains( - "COMPONENT_ID = 0x0001000000000000L", - "RESOURCE_NAME_ID = 0x0002000000000002L", - "SERVICE_ID = 0x0003000000000002L", + "COMPONENT_ID = 0x0001FFFF00000000L", + "RESOURCE_NAME_ID = 0x0002FFFF00000002L", + "SERVICE_ID = 0x0003FFFF00000002L", "public static final int SERVICE_SERIAL_NUM = 3;", "public static final int SERIAL_LIMIT = 4;" ) @@ -561,6 +561,44 @@ class TagRegistryGeneratorTest { .withMessageContaining(message) } + @Test + fun `tags on one span type get distinct slots and tags that never co-occur share one`() { + val yaml = directory.conventionsFile( + """ + trace_level: + tags: [{dd-name: env, required: required}] + span_types: + base: + abstract: true + tags: [{dd-name: service, required: required}] + http.server: + extends: base + span-kind: server + tags: [{dd-name: http.route, required: required}] + db.client: + extends: base + span-kind: client + tags: [{dd-name: db.type, required: required}, {dd-name: db.user}] + """ + ) + val output = File(directory, "generated") + + TagRegistryGenerator.generate(yaml, output) + + // service is on both span types, so it takes slot 0; db.type and db.user share db.client, so + // they take distinct slots, and http.route -- never on a span type with either -- reuses one of + // them; env is trace-level (its own TagMap) and reuses slot 0. Every level is slotted. + assertThat(contents(output).getValue("java/datadog/trace/api/KnownTags.java")) + .contains( + "DB_TYPE_ID = 0x0001000100000000L", + "DB_USER_ID = 0x0002000200000000L", + "ENV_ID = 0x0003000000000004L", + "HTTP_ROUTE_ID = 0x0004000100000000L", + "SERVICE_ID = 0x0005000000000000L", + "public static final int SLOT_COUNT = 3;" + ) + } + private fun tagConventions(@Language("yaml") yamlText: String) = TagConventions.parse( ObjectMapper(YAMLFactory()).readValue( directory.conventionsFile(yamlText), diff --git a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java index 605f7140d4c..6e09e4d1aad 100644 --- a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -13,11 +13,11 @@ */ public final class KnownTagCodec { /* - * tagId bit layout: [63-48 serialNum (16 bits)] [47-32 reserved, zero] [31-0 flags]. serialNum is + * tagId bit layout: [63-48 serialNum (16 bits)] [47-32 slot] [31-0 flags]. serialNum is * globally unique per known tag and is the whole of the tag's identity — nameOf/ * openTelemetryNameOf switch on it, and the generator emits each id as a literal. Bits [47-32] - * are RESERVED and always zero here: they are the window the dense tag store uses for its - * co-occurrence slot coordinate, which arrives with that store. Of the low 32 flag bits, bit 2 is + * hold the tag's slot ({@link #slotOf}), assigned by graph coloring so tags on one span type never + * share a slot, or {@link #NO_SLOT} for a tag on no modeled span type. Of the low 32 flag bits, bit 2 is * the trace/span LEVEL bit (set ⟹ trace-level), and bit 1 is the INTERCEPTED bit. Unknown * (string-only) custom tags are NOT known ids — {@code keyOf} returns 0 for them. * @@ -65,16 +65,42 @@ public static boolean isIntercepted(long tagId) { return (tagId & INTERCEPTED) != 0L; } + /** Bit position of the slot: bits [47-32] of an id. */ + public static final int SLOT_SHIFT = 32; + + /** The slot of a known tag on no modeled span type. */ + public static final int NO_SLOT = 0xFFFF; + + /** + * The tag's slot, or {@link #NO_SLOT}. The generator colors tags that appear together on a span + * type with distinct slots, so a span's slotted tags never collide: {@code 1L << slotOf(id)} is + * an exact presence bit for them. Every tag without a slot shares bit 63, which no real slot + * uses. + */ + public static int slotOf(long tagId) { + return (int) (tagId >>> SLOT_SHIFT) & 0xFFFF; + } + + /** True if the tag has a slot, below {@link #slotCount()}. */ + public static boolean hasSlot(long tagId) { + return slotOf(tagId) != NO_SLOT; + } + + /** Slots in use: every slotted tag's slot is below this. */ + public static int slotCount() { + return KnownTags.SLOT_COUNT; + } + /** Returns the tagId with the {@link #LEVEL_TRACE} flag set. */ public static long traceLevel(long tagId) { return tagId | LEVEL_TRACE; } /** - * Builds a tagId from its {@code serialNum} (globally unique per known tag). The reserved [47-32] - * window and the low 32 bits are zero, so the id is fully determined by the serial — the - * generator emits it as a literal. Inverse of {@link #serialNum}. Intended for the code generator - * and tests. + * Builds an id carrying only {@code serialNum}: slot 0 and no flags, so it equals a tag's real id + * only when that tag has neither. Enough for serial-keyed lookups such as {@link #nameOf}; + * compare against a {@code KnownTags} constant, never against this. Inverse of {@link + * #serialNum}. Intended for tests and serial-keyed lookups. */ public static long makeTagId(int serialNum) { return (long) serialNum << 48; diff --git a/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java b/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java new file mode 100644 index 00000000000..4dd09044be8 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/KnownTagCodecSlotTest.java @@ -0,0 +1,77 @@ +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.assertTrue; + +import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; + +/** {@link KnownTagCodec}'s slot: bits [47-32] of an id, assigned by graph coloring. */ +class KnownTagCodecSlotTest { + @Test + void everySlotIsBelowTheSlotCount() throws IllegalAccessException { + for (Field field : KnownTags.class.getFields()) { + if (field.getType() != long.class) { + continue; + } + long tagId = field.getLong(null); + if (KnownTagCodec.hasSlot(tagId)) { + assertTrue(KnownTagCodec.slotOf(tagId) < KnownTagCodec.slotCount(), field.getName()); + } else { + assertEquals(KnownTagCodec.NO_SLOT, KnownTagCodec.slotOf(tagId), field.getName()); + } + } + } + + @Test + void tagsOnAnHttpServerSpanHaveDistinctSlots() { + long[] httpServerTags = { + KnownTags.SERVICE_ID, + KnownTags.COMPONENT_ID, + KnownTags.SPAN_KIND_ID, + KnownTags.HTTP_METHOD_ID, + KnownTags.HTTP_URL_ID, + KnownTags.HTTP_STATUS_CODE_ID, + KnownTags.HTTP_ROUTE_ID, + KnownTags.HTTP_HOSTNAME_ID, + }; + Set slots = new HashSet<>(); + for (long tagId : httpServerTags) { + assertTrue(KnownTagCodec.hasSlot(tagId), Long.toHexString(tagId)); + assertTrue(slots.add(KnownTagCodec.slotOf(tagId)), Long.toHexString(tagId)); + } + } + + @Test + void anOptionalTagOnASpanTypeHasASlot() { + assertTrue(KnownTagCodec.hasSlot(KnownTags.PEER_PORT_ID)); + assertTrue(KnownTagCodec.hasSlot(KnownTags.DB_POOL_NAME_ID)); + } + + @Test + void aTagOnNoSpanTypeHasNoSlot() { + // the tracer overlay's routing-only keys appear on no modeled span type + assertFalse(KnownTagCodec.hasSlot(KnownTags.RESOURCE_NAME_ID)); + } + + @Test + void theSlotDoesNotChangeTheSerialOrFlags() { + long tagId = KnownTags.HTTP_METHOD_ID; + + assertTrue(KnownTagCodec.hasSlot(tagId)); + assertEquals(KnownTags.HTTP_METHOD_NAME, KnownTagCodec.nameOf(tagId)); + assertTrue(KnownTagCodec.isIntercepted(tagId)); + assertFalse(KnownTagCodec.isTraceLevel(tagId)); + } + + @Test + void anUnslottedTagSharesTheReservedTopBit() { + // An occupancy bit of 1L << slotOf(id) needs no branch for NO_SLOT: a long shift uses only the + // low 6 bits of its count, so 0xFFFF lands on bit 63, which no real slot uses + assertEquals(1L << 63, 1L << KnownTagCodec.slotOf(KnownTags.RESOURCE_NAME_ID)); + assertTrue(KnownTagCodec.slotCount() < 63, "real slots must stay below the shared bit 63"); + } +} From 0cc3794441212549a7127f6ec367ceb9d48c210b Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 14 Oct 2026 22:02:38 -0400 Subject: [PATCH 24/29] Drop a local benchmark committed by mistake ForEachContextBenchmark is a local scratch benchmark that the previous merge picked up; it isn't part of this PR. Co-Authored-By: Claude Opus 5.5 --- .../trace/api/ForEachContextBenchmark.java | 102 ------------------ 1 file changed, 102 deletions(-) delete mode 100644 internal-api/src/jmh/java/datadog/trace/api/ForEachContextBenchmark.java diff --git a/internal-api/src/jmh/java/datadog/trace/api/ForEachContextBenchmark.java b/internal-api/src/jmh/java/datadog/trace/api/ForEachContextBenchmark.java deleted file mode 100644 index 2360f6cc7a1..00000000000 --- a/internal-api/src/jmh/java/datadog/trace/api/ForEachContextBenchmark.java +++ /dev/null @@ -1,102 +0,0 @@ -package datadog.trace.api; - -import java.util.concurrent.TimeUnit; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Fork; -import org.openjdk.jmh.annotations.Level; -import org.openjdk.jmh.annotations.Measurement; -import org.openjdk.jmh.annotations.Mode; -import org.openjdk.jmh.annotations.OutputTimeUnit; -import org.openjdk.jmh.annotations.Param; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Warmup; - -/** - * Counts a map's number and non-number tags -- the serializers' size-counting pass -- three ways: - * {@code forEach} with a fresh context object, the iterator with local counters, and {@code - * forEach} with a context object reused from this state (no allocation however the JIT does). - * - *

    The question is whether {@code forEach}'s reader and a per-call context object are scalar - * replaced: if {@code forEachFreshContext} allocates nothing, {@code forEach} with a context object - * replaces the iterator for internal iteration. Run with {@code -prof gc}, and with {@code - * -XX:-DoEscapeAnalysis} to see what escape analysis removes. The map is built once, so only the - * iteration is measured; {@code scenario} is the tag mix, as in {@link DenseStoreAllocBenchmark}. - */ -@State(Scope.Benchmark) -@BenchmarkMode(Mode.AverageTime) -@OutputTimeUnit(TimeUnit.NANOSECONDS) -@Fork(2) -@Warmup(iterations = 5, time = 1) -@Measurement(iterations = 5, time = 1) -public class ForEachContextBenchmark { - @Param({"custom", "mixed", "allSlotted"}) - String scenario; - - private TagMap map; - private final Counts reused = new Counts(); - - /** A per-call result holder, read after the iteration. */ - static final class Counts { - int numbers; - int others; - } - - @Setup(Level.Trial) - public void setup() { - int tagCount = 12; - int slotted = - "allSlotted".equals(scenario) - ? tagCount - : "mixed".equals(scenario) ? (tagCount * 7) / 10 : 0; - this.map = TagMap.create(); - for (int i = 0; i < tagCount; i++) { - Object value = (i % 3 == 0) ? (Object) Integer.valueOf(i) : "value-" + i; - if (i < slotted) { - this.map.set(DenseStoreAllocBenchmark.SLOTTED_IDS[i], value); - } else { - this.map.set("custom.tag." + i, value); - } - } - } - - private static void count(Counts counts, TagMap.EntryReader entry) { - if (entry.isNumber()) { - counts.numbers++; - } else { - counts.others++; - } - } - - @Benchmark - public long forEachFreshContext() { - Counts counts = new Counts(); - this.map.forEach(counts, ForEachContextBenchmark::count); - return ((long) counts.numbers << 32) | counts.others; - } - - @Benchmark - public long iterator() { - int numbers = 0; - int others = 0; - for (TagMap.EntryReader entry : this.map) { - if (entry.isNumber()) { - numbers++; - } else { - others++; - } - } - return ((long) numbers << 32) | others; - } - - @Benchmark - public long forEachReusedContext() { - Counts counts = this.reused; - counts.numbers = 0; - counts.others = 0; - this.map.forEach(counts, ForEachContextBenchmark::count); - return ((long) counts.numbers << 32) | counts.others; - } -} From 2c3326ee2a44558b0663275ef7356f6702bfe3aa Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 14 Oct 2026 23:32:56 -0400 Subject: [PATCH 25/29] Make SizingHintTable package-private behind SizingHint.forOperation CoreTracer was its only caller outside datadog.trace.api. It now looks a hint up through SizingHint.forOperation, on the type it already holds. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/core/CoreTracer.java | 8 ++++---- .../java/datadog/trace/api/SizingHint.java | 19 +++++++++++++++---- .../datadog/trace/api/SizingHintTable.java | 12 ++++++------ 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index e927aa64ddc..4acca3f17b3 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -43,7 +43,6 @@ import datadog.trace.api.InstrumenterConfig; import datadog.trace.api.Pair; import datadog.trace.api.SizingHint; -import datadog.trace.api.SizingHintTable; import datadog.trace.api.TagMap; import datadog.trace.api.TraceConfig; import datadog.trace.api.civisibility.config.BazelMode; @@ -2333,11 +2332,12 @@ protected static final DDSpanContext buildSpanContext( // Per-operation dense-store sizing: an entry (local-root) span carries the trace-metadata / // enriching tags a child doesn't, so pick the lane by whether we have a local parent. A // resolved hint sizes the span's TagMap and self-tunes as spans finish; null (dense store - // off, or no operation name) falls back to the generic default capacity. hintFor tests the - // dense-store flag first, so with it off -- the default -- this constant-folds to null and + // off, or no operation name) falls back to the generic default capacity. forOperation tests + // the dense-store flag first, so with it off -- the default -- this constant-folds to null + // and // the default span-creation path does no lookup (do no harm). final SizingHint sizingHint = - SizingHintTable.hintFor( + SizingHint.forOperation( operationName, !(resolvedParentSpanContext instanceof DDSpanContext)); // some attributes are inherited from the parent diff --git a/internal-api/src/main/java/datadog/trace/api/SizingHint.java b/internal-api/src/main/java/datadog/trace/api/SizingHint.java index 734650f6d47..5189eaaa4c5 100644 --- a/internal-api/src/main/java/datadog/trace/api/SizingHint.java +++ b/internal-api/src/main/java/datadog/trace/api/SizingHint.java @@ -1,6 +1,7 @@ package datadog.trace.api; import datadog.trace.util.FlatHashtable; +import javax.annotation.Nullable; /** * Opaque per-operation dense-store sizing hint, and a self-contained {@link FlatHashtable.D1} @@ -12,10 +13,10 @@ * racy publication (JMM final-field guarantee). That sidesteps the memory-ordering / visibility * problems parallel key/hash/value arrays would create, no volatile or atomics. * - *

    Opaque to everything outside {@code datadog.trace.api}: no public members. {@link TagMap} - * reads {@link #size} to size a fresh dense store and writes it back (best-effort max, see {@link - * TagMap#recordSize}) at a terminal point; {@link SizingHintTable} mints them. Callers only ever - * hold the reference. + *

    Opaque to everything outside {@code datadog.trace.api}: its only public member is the lookup, + * {@link #forOperation}. {@link TagMap} reads {@link #size} to size a fresh dense store and writes + * it back (best-effort max, see {@link TagMap#recordSize}) at a terminal point; {@link + * SizingHintTable} mints them. Callers only ever hold the reference. */ public final class SizingHint extends FlatHashtable.D1.Entry { // Payload: the tuned dense-store size. Plain racy int, updated best-effort max (see @@ -33,6 +34,16 @@ public final class SizingHint extends FlatHashtable.D1.Entry { /** * A fresh, uncapped hint for {@code operationName}, seeded at {@link SizingHintTable#SEED_SIZE}. */ + /** + * The hint for spans of {@code operationName}, in the entry-span or child-span lane; null when + * the dense store is off (constant-folded) or there is no operation name. See {@link + * SizingHintTable#hintFor}. + */ + @Nullable + public static SizingHint forOperation(@Nullable CharSequence operationName, boolean entrySpan) { + return SizingHintTable.hintFor(operationName, entrySpan); + } + SizingHint(String operationName) { this(operationName, SizingHintTable.SEED_SIZE, false); } diff --git a/internal-api/src/main/java/datadog/trace/api/SizingHintTable.java b/internal-api/src/main/java/datadog/trace/api/SizingHintTable.java index b8793624a83..84bb2199a90 100644 --- a/internal-api/src/main/java/datadog/trace/api/SizingHintTable.java +++ b/internal-api/src/main/java/datadog/trace/api/SizingHintTable.java @@ -5,10 +5,10 @@ /** * Process-wide, self-tuning registry of per-operation {@link SizingHint}s, keyed by operation name. - * Pure static: the tracer resolves a hint here at span build and hands it to the span, which sizes - * its dense {@link TagMap} from it and records the actual size back when it finishes — the hint - * converges to the operation's real known-tag high-water mark, so later spans of that operation - * size correctly. + * Pure static and package-private: the tracer resolves a hint at span build through {@link + * SizingHint#forOperation} and hands it to the span, which sizes its dense {@link TagMap} from it + * and records the actual size back when it finishes — the hint converges to the operation's real + * known-tag high-water mark, so later spans of that operation size correctly. * *

    Only meaningful with the dense store on ({@link TagMap.DenseStore#ENABLED}); with it off, * {@link #hintFor} returns {@code null} before touching anything, so the default span path does no @@ -35,7 +35,7 @@ * allocation-free on the hot path. A {@code null} operation name gets no hint (the span falls back * to the generic default capacity); a missed hint is benign, never wrong. */ -public final class SizingHintTable { +final class SizingHintTable { private SizingHintTable() {} // Seed for a fresh per-operation hint: a small floor that self-tunes up via best-effort-max @@ -80,7 +80,7 @@ private Lanes() {} * the create path is warmup-rare. */ @Nullable - public static SizingHint hintFor(@Nullable CharSequence operationName, boolean entrySpan) { + static SizingHint hintFor(@Nullable CharSequence operationName, boolean entrySpan) { if (!TagMap.DenseStore.ENABLED) { return null; } From 768339e2e331cc6db40da0e836e9a550fd4c942d Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 14 Oct 2026 23:32:59 -0400 Subject: [PATCH 26/29] Share one boxed path across the primitive id setters Each primitive setTag(long, ...) repeated the same three branches. They now box only when the box is needed -- the store keeps boxes (STORES_BOXES, folded) or the tag may be intercepted -- and pass it to one shared setKnownBox; otherwise they store the primitive. That keeps a single boxing site per setter and the hash store's unboxed plain path. setTag(long, int) drops from 195 to 76 bytecode bytes. SetTagBenchmark's int and peer.port arms are unchanged in time and allocation with the dense store off and on, and C2 still inlines the setter at its call sites (no too-big refusals in either version). The typed boxed Internals.setKnown and setKnownTrusted overloads, used only by the old setters, go: an intercepted-then-stored primitive on the hash store is now an ANY entry, which resolves to the same type on read. Co-Authored-By: Claude Opus 5.5 --- .../datadog/trace/core/DDSpanContext.java | 110 ++++-------------- .../java/datadog/trace/api/DenseTagMap.java | 25 ---- .../main/java/datadog/trace/api/TagMap.java | 52 --------- 3 files changed, 25 insertions(+), 162 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 662299ac2f5..5daf6ba2c2d 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -1339,24 +1339,9 @@ public void setTag(final long tagId, final boolean value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (TagMap.Internals.STORES_BOXES) { - // The dense store keeps the box: box once, shared by the interceptor and the store. - final Boolean box = value; - if (!tagInterceptor.needsIntercept(tagId, routedTagFlags) - || !tagInterceptor.interceptTag(this, tagId, box)) { - synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, box); - } - } - } else if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { - final Boolean box = value; - if (!tagInterceptor.interceptTag(this, tagId, box)) { - synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, box); - } - } + if (TagMap.Internals.STORES_BOXES || tagInterceptor.needsIntercept(tagId, routedTagFlags)) { + setKnownBox(tagId, value); // the one boxing site } else { - // The hash store keeps the primitive: no box. synchronized (unsafeTags) { unsafeTags.internals().setKnown(tagId, value); } @@ -1367,24 +1352,9 @@ public void setTag(final long tagId, final int value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (TagMap.Internals.STORES_BOXES) { - // The dense store keeps the box: box once, shared by the interceptor and the store. - final Integer box = value; - if (!tagInterceptor.needsIntercept(tagId, routedTagFlags) - || !tagInterceptor.interceptTag(this, tagId, box)) { - synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, box); - } - } - } else if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { - final Integer box = value; - if (!tagInterceptor.interceptTag(this, tagId, box)) { - synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, box); - } - } + if (TagMap.Internals.STORES_BOXES || tagInterceptor.needsIntercept(tagId, routedTagFlags)) { + setKnownBox(tagId, value); // the one boxing site } else { - // The hash store keeps the primitive: no box. synchronized (unsafeTags) { unsafeTags.internals().setKnown(tagId, value); } @@ -1395,24 +1365,9 @@ public void setTag(final long tagId, final long value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (TagMap.Internals.STORES_BOXES) { - // The dense store keeps the box: box once, shared by the interceptor and the store. - final Long box = value; - if (!tagInterceptor.needsIntercept(tagId, routedTagFlags) - || !tagInterceptor.interceptTag(this, tagId, box)) { - synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, box); - } - } - } else if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { - final Long box = value; - if (!tagInterceptor.interceptTag(this, tagId, box)) { - synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, box); - } - } + if (TagMap.Internals.STORES_BOXES || tagInterceptor.needsIntercept(tagId, routedTagFlags)) { + setKnownBox(tagId, value); // the one boxing site } else { - // The hash store keeps the primitive: no box. synchronized (unsafeTags) { unsafeTags.internals().setKnown(tagId, value); } @@ -1423,24 +1378,9 @@ public void setTag(final long tagId, final float value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (TagMap.Internals.STORES_BOXES) { - // The dense store keeps the box: box once, shared by the interceptor and the store. - final Float box = value; - if (!tagInterceptor.needsIntercept(tagId, routedTagFlags) - || !tagInterceptor.interceptTag(this, tagId, box)) { - synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, box); - } - } - } else if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { - final Float box = value; - if (!tagInterceptor.interceptTag(this, tagId, box)) { - synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, box); - } - } + if (TagMap.Internals.STORES_BOXES || tagInterceptor.needsIntercept(tagId, routedTagFlags)) { + setKnownBox(tagId, value); // the one boxing site } else { - // The hash store keeps the primitive: no box. synchronized (unsafeTags) { unsafeTags.internals().setKnown(tagId, value); } @@ -1451,30 +1391,30 @@ public void setTag(final long tagId, final double value) { if (!KnownTagCodec.isKnown(tagId)) { return; } - if (TagMap.Internals.STORES_BOXES) { - // The dense store keeps the box: box once, shared by the interceptor and the store. - final Double box = value; - if (!tagInterceptor.needsIntercept(tagId, routedTagFlags) - || !tagInterceptor.interceptTag(this, tagId, box)) { - synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, box); - } - } - } else if (tagInterceptor.needsIntercept(tagId, routedTagFlags)) { - final Double box = value; - if (!tagInterceptor.interceptTag(this, tagId, box)) { - synchronized (unsafeTags) { - unsafeTags.internals().setKnown(tagId, box); - } - } + if (TagMap.Internals.STORES_BOXES || tagInterceptor.needsIntercept(tagId, routedTagFlags)) { + setKnownBox(tagId, value); // the one boxing site } else { - // The hash store keeps the primitive: no box. synchronized (unsafeTags) { unsafeTags.internals().setKnown(tagId, value); } } } + /** + * The boxed path of the primitive id setters, which box at most once: always for the dense store, + * which keeps the box ({@code STORES_BOXES} is a static final, so the test folds), and for a tag + * that may be intercepted, since the interceptor takes an Object. The hash store's plain path + * stores the primitive unboxed. One shared path keeps each setter's compiled code small. + */ + private void setKnownBox(long tagId, Object box) { + if (!tagInterceptor.needsIntercept(tagId, routedTagFlags) + || !tagInterceptor.interceptTag(this, tagId, box)) { + synchronized (unsafeTags) { + unsafeTags.internals().setKnown(tagId, box); + } + } + } + public void setTag(TagMap.EntryReader entry) { if (entry == null) { return; diff --git a/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java b/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java index 7235087638a..122e8ba3539 100644 --- a/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java @@ -231,31 +231,6 @@ void setKnownTrusted(long tagId, double value) { this.storeDenseTrusted(tagId, Double.valueOf(value)); } - @Override - void setKnownTrusted(long tagId, @Nonnull Boolean value) { - this.storeDenseTrusted(tagId, value); - } - - @Override - void setKnownTrusted(long tagId, @Nonnull Integer value) { - this.storeDenseTrusted(tagId, value); - } - - @Override - void setKnownTrusted(long tagId, @Nonnull Long value) { - this.storeDenseTrusted(tagId, value); - } - - @Override - void setKnownTrusted(long tagId, @Nonnull Float value) { - this.storeDenseTrusted(tagId, value); - } - - @Override - void setKnownTrusted(long tagId, @Nonnull Double value) { - this.storeDenseTrusted(tagId, value); - } - @Override void putAllMerge(TagMap that) { DenseTagMap dense = (DenseTagMap) that; 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 50d9be0a497..e2639693a4d 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -1820,31 +1820,6 @@ public void setKnown(long tagId, double value) { this.map.setKnownTrusted(tagId, value); } - /* - * Boxed variants, for a caller that has already boxed the value (to hand it to the - * interceptor): the box is stored as is, so the value is boxed once. The tag keeps its - * primitive type. - */ - public void setKnown(long tagId, @Nonnull Boolean value) { - this.map.setKnownTrusted(tagId, value); - } - - public void setKnown(long tagId, @Nonnull Integer value) { - this.map.setKnownTrusted(tagId, value); - } - - public void setKnown(long tagId, @Nonnull Long value) { - this.map.setKnownTrusted(tagId, value); - } - - public void setKnown(long tagId, @Nonnull Float value) { - this.map.setKnownTrusted(tagId, value); - } - - public void setKnown(long tagId, @Nonnull Double value) { - this.map.setKnownTrusted(tagId, value); - } - /** * The value of a known tag, boxed if necessary, or null: {@code getEntry(tagId)}'s value, * including read-through, without building an Entry for a local dense value. @@ -1888,33 +1863,6 @@ void setKnownTrusted(long tagId, double value) { this.putEntryLocal(new Entry(tagId, null, EntryReader.DOUBLE, Entry.double2Prim(value), null)); } - /** The boxed {@code setKnownTrusted}s: a typed Entry that keeps the caller's box. */ - void setKnownTrusted(long tagId, @Nonnull Boolean value) { - this.putEntryLocal( - new Entry( - tagId, null, EntryReader.BOOLEAN, Entry.boolean2Prim(value.booleanValue()), value)); - } - - void setKnownTrusted(long tagId, @Nonnull Integer value) { - this.putEntryLocal( - new Entry(tagId, null, EntryReader.INT, Entry.int2Prim(value.intValue()), value)); - } - - void setKnownTrusted(long tagId, @Nonnull Long value) { - this.putEntryLocal( - new Entry(tagId, null, EntryReader.LONG, Entry.long2Prim(value.longValue()), value)); - } - - void setKnownTrusted(long tagId, @Nonnull Float value) { - this.putEntryLocal( - new Entry(tagId, null, EntryReader.FLOAT, Entry.float2Prim(value.floatValue()), value)); - } - - void setKnownTrusted(long tagId, @Nonnull Double value) { - this.putEntryLocal( - new Entry(tagId, null, EntryReader.DOUBLE, Entry.double2Prim(value.doubleValue()), value)); - } - /** {@code getEntry(tagId)}'s value, or null; see {@link Internals#getKnownValue}. */ Object getKnownValue(long tagId) { Entry entry = this.getEntry(tagId); From a056c1afbfd09d31e7e8812d13f5a1a764fea289 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 15 Oct 2026 00:05:27 -0400 Subject: [PATCH 27/29] Make TagMap's factory initialization independent of order TagMapFactory computed INSTANCE in its own static initializer from a subclass's INSTANCE. Initializing a factory subclass first initializes TagMapFactory, which then read that subclass's INSTANCE while it was still null; TagMap's initializer later failed on it, poisoning TagMap for the JVM's life. And a factory that built its empty map in its own initializer started TagMap's initializer mid-way, so TagMap.EMPTY could read a still-null cached empty map. - The JVM's factory moves to a holder, TagMapFactory.Default.INSTANCE; TagMapFactory has no static state. - TagMap.EMPTY is built fresh by newEmpty(). The JVM's factory returns TagMap.EMPTY from empty(); only the other store (tests) caches its own. Nothing in production initializes a factory before TagMap today; the parameterized tests did. TagMapFactoryInitOrderForkedTest initializes both factories first, in a fresh JVM: it fails before this change. Co-Authored-By: Claude Opus 5.5 --- .../main/java/datadog/trace/api/TagMap.java | 12 ++-- .../java/datadog/trace/api/TagMapFactory.java | 55 +++++++++++++++---- .../api/TagMapFactoryInitOrderForkedTest.java | 31 +++++++++++ 3 files changed, 81 insertions(+), 17 deletions(-) create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapFactoryInitOrderForkedTest.java 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 e2639693a4d..0feaa693b1b 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -53,7 +53,7 @@ public abstract class TagMap implements Map, Iterable map) { } public static final TagMap create() { - return TagMapFactory.INSTANCE.create(null); + return TagMapFactory.Default.INSTANCE.create(null); } public static final TagMap create(int size) { - return TagMapFactory.INSTANCE.create(null); + return TagMapFactory.Default.INSTANCE.create(null); } /** @@ -89,7 +89,7 @@ public static final TagMap create(int size) { * implementation ignores the hint. */ public static final TagMap create(@Nullable SizingHint hint) { - return TagMapFactory.INSTANCE.create(null, hint); + return TagMapFactory.Default.INSTANCE.create(null, hint); } /** @@ -132,7 +132,7 @@ public static final TagMap createFromParent(TagMap parent, @Nullable SizingHint parent = null; } } - return TagMapFactory.INSTANCE.create(parent, hint); + return TagMapFactory.Default.INSTANCE.create(parent, hint); } /** Creates a new TagMap.Ledger */ @@ -2372,7 +2372,7 @@ 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 = TagMapFactory.INSTANCE.create(this.parent); + TagMap copy = TagMapFactory.Default.INSTANCE.create(this.parent); copy.putAllIntoEmptyMap(this); if (this.removedFromParent != null) { copy.removedFromParent = new HashSet<>(this.removedFromParent); diff --git a/internal-api/src/main/java/datadog/trace/api/TagMapFactory.java b/internal-api/src/main/java/datadog/trace/api/TagMapFactory.java index 2bbbd21858e..5d333731972 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMapFactory.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMapFactory.java @@ -6,14 +6,25 @@ * legacy/optimized split: the hash-only map stays one flag away until the dense store is proven, * and is then folded away. * - *

    A class, not an interface, and its implementations are only reached through {@link #INSTANCE}: - * the factory not chosen is loaded (to check the field's type) but never linked, so its methods are - * never verified and its map class is never loaded. With one {@code TagMap} subclass loaded, class - * hierarchy analysis devirtualizes every call on a {@code TagMap} without a guard. + *

    A class, not an interface, and its implementations are only reached through {@link + * Default#INSTANCE}: the factory not chosen is loaded (to check the field's type) but never linked, + * so its methods are never verified and its map class is never loaded. With one {@code TagMap} + * subclass loaded, class hierarchy analysis devirtualizes every call on a {@code TagMap} without a + * guard. + * + *

    Class initialization must work whichever of TagMap and the factories initializes first: an + * error in a static initializer poisons that class for the JVM's life, and a read of a class still + * initializing on the same thread silently sees null. So this class has no static state + * (initializing a subclass initializes it first, before the subclass's own {@code INSTANCE} is + * set), and {@link TagMap#EMPTY} is built fresh by {@link #newEmpty} rather than read from a + * factory's cache. */ abstract class TagMapFactory { - static final TagMapFactory INSTANCE = - TagMap.DenseStore.ENABLED ? DenseTagMapFactory.INSTANCE : HashTagMapFactory.INSTANCE; + /** The JVM's store, chosen once by the dense-store flag. */ + static final class Default { + static final TagMapFactory INSTANCE = + TagMap.DenseStore.ENABLED ? DenseTagMapFactory.INSTANCE : HashTagMapFactory.INSTANCE; + } /** A fresh mutable map that reads through to {@code parent} (may be null). */ abstract TagMap create(TagMap parent); @@ -24,14 +35,23 @@ abstract class TagMapFactory { */ abstract TagMap create(TagMap parent, SizingHint hint); - /** The shared frozen empty map. */ + /** A new frozen empty map of this store; {@link TagMap#EMPTY} is built with it. */ + abstract TagMap newEmpty(); + + /** + * The shared frozen empty map: {@link TagMap#EMPTY} for the JVM's store, or one of this store's + * own for the other (only tests build maps of the store the JVM didn't choose). + */ abstract TagMap empty(); } final class HashTagMapFactory extends TagMapFactory { static final HashTagMapFactory INSTANCE = new HashTagMapFactory(); - private static final TagMap EMPTY = new HashTagMap(new Object[1], 0); + /** This store's own empty map, when it isn't the JVM's store; see {@link #empty}. */ + static final class OwnEmpty { + static final TagMap EMPTY = INSTANCE.newEmpty(); + } private HashTagMapFactory() {} @@ -45,16 +65,24 @@ TagMap create(TagMap parent, SizingHint hint) { return new HashTagMap(parent); // no dense store to size } + @Override + TagMap newEmpty() { + return new HashTagMap(new Object[1], 0); + } + @Override TagMap empty() { - return EMPTY; + return this == Default.INSTANCE ? TagMap.EMPTY : OwnEmpty.EMPTY; } } final class DenseTagMapFactory extends TagMapFactory { static final DenseTagMapFactory INSTANCE = new DenseTagMapFactory(); - private static final TagMap EMPTY = new DenseTagMap(new Object[1], 0); + /** This store's own empty map, when it isn't the JVM's store; see {@link #empty}. */ + static final class OwnEmpty { + static final TagMap EMPTY = INSTANCE.newEmpty(); + } private DenseTagMapFactory() {} @@ -68,8 +96,13 @@ TagMap create(TagMap parent, SizingHint hint) { return new DenseTagMap(parent, hint); } + @Override + TagMap newEmpty() { + return new DenseTagMap(new Object[1], 0); + } + @Override TagMap empty() { - return EMPTY; + return this == Default.INSTANCE ? TagMap.EMPTY : OwnEmpty.EMPTY; } } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapFactoryInitOrderForkedTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapFactoryInitOrderForkedTest.java new file mode 100644 index 00000000000..e5a81718821 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapFactoryInitOrderForkedTest.java @@ -0,0 +1,31 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * The factories can initialize before {@link TagMap} (forked, so this JVM hasn't initialized + * either). A factory that built its empty map in its own initializer started TagMap's initializer + * there, which read the still-null {@link TagMapFactory#INSTANCE} and poisoned TagMap for the JVM. + */ +class TagMapFactoryInitOrderForkedTest { + @Test + void theFactoriesCanInitializeBeforeTagMap() { + TagMapFactory hash = HashTagMapFactory.INSTANCE; + TagMapFactory dense = DenseTagMapFactory.INSTANCE; + + assertTrue(hash.empty().isEmpty()); + assertTrue(dense.empty().isEmpty()); + assertNotNull(TagMap.EMPTY); + assertSame(TagMap.EMPTY, TagMapFactory.Default.INSTANCE.empty()); + assertNotSame(hash.empty(), dense.empty()); + TagMap map = TagMap.create(); + map.set("a", "b"); + assertEquals("b", map.getString("a")); + } +} From 6015cbbe2c0963bf500b3cf5c7d541c5b341f6bf Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 15 Oct 2026 00:08:19 -0400 Subject: [PATCH 28/29] Run the shared TagMap suites against both stores The dense-store flag picks one TagMap implementation per JVM, so TagMapTest, TagMapReadThroughTest, TagMapFuzzTest and TagMapSetByIdTest only ever exercised HashTagMap. A TagMapType test enum (HASH, DENSE) builds maps through each store's own factory, as TagMapType once did for the optimized and legacy maps, and those suites now run against both (TagMapTest 112 -> 215 tests, ReadThrough 51 -> 102). - copy() now creates through the map's own factory (TagMap.factory()), so a copy keeps its implementation instead of taking the JVM's default store. - createFromParent's read-through checks take an explicit factory (package-private), which TagMapType uses. - 22 of TagMapDenseForkedTest's 24 tests didn't need the flag; they move to DenseTagMapTest in the normal test JVM. The forked test keeps the two that only the flag decides: that HashTagMap never loads, and that a ledger builds a dense map. - TagMapFuzzTest's PutAllTagMap builds its source through the test map's own store, so the two never mix stores. No test fails for one store and not the other. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/api/DenseTagMap.java | 5 + .../java/datadog/trace/api/HashTagMap.java | 5 + .../main/java/datadog/trace/api/TagMap.java | 17 +- .../datadog/trace/api/DenseTagMapTest.java | 501 ++++++++++++++++++ .../trace/api/TagMapDenseForkedTest.java | 477 +---------------- .../trace/api/TagMapDenseFuzzForkedTest.java | 6 +- .../datadog/trace/api/TagMapFuzzTest.java | 79 +-- .../trace/api/TagMapReadThroughTest.java | 410 +++++++------- .../datadog/trace/api/TagMapScenario.java | 8 + .../datadog/trace/api/TagMapSetByIdTest.java | 69 +-- .../java/datadog/trace/api/TagMapTest.java | 300 ++++++----- .../java/datadog/trace/api/TagMapType.java | 32 ++ 12 files changed, 1057 insertions(+), 852 deletions(-) create mode 100644 internal-api/src/test/java/datadog/trace/api/DenseTagMapTest.java create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapType.java diff --git a/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java b/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java index 122e8ba3539..0698e139bd4 100644 --- a/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/DenseTagMap.java @@ -72,6 +72,11 @@ final class DenseTagMap extends TagMap { private long knownOccupancy; private boolean knownTraceLevel; + @Override + TagMapFactory factory() { + return DenseTagMapFactory.INSTANCE; + } + DenseTagMap(TagMap parent) { // start on the shared empty buckets; the first bucket write copies them super(parent, EMPTY_BUCKETS); diff --git a/internal-api/src/main/java/datadog/trace/api/HashTagMap.java b/internal-api/src/main/java/datadog/trace/api/HashTagMap.java index ca98c57bbb8..aef9ad8b459 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashTagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/HashTagMap.java @@ -12,4 +12,9 @@ final class HashTagMap extends TagMap { HashTagMap(Object[] buckets, int size) { super(buckets, size); } + + @Override + TagMapFactory factory() { + return HashTagMapFactory.INSTANCE; + } } 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 0feaa693b1b..52628cf93d4 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -120,6 +120,13 @@ public static final TagMap createFromParent(TagMap parent) { * #createFromParent(TagMap)}. */ public static final TagMap createFromParent(TagMap parent, @Nullable SizingHint hint) { + return createFromParent(TagMapFactory.Default.INSTANCE, parent, hint); + } + + /** + * {@link #createFromParent(TagMap, SizingHint)} through {@code factory}; tests pick the store. + */ + static TagMap createFromParent(TagMapFactory factory, TagMap parent, @Nullable SizingHint hint) { if (parent != null) { if (!parent.frozen) { throw new IllegalStateException("read-through parent must be frozen"); @@ -132,9 +139,15 @@ public static final TagMap createFromParent(TagMap parent, @Nullable SizingHint parent = null; } } - return TagMapFactory.Default.INSTANCE.create(parent, hint); + return factory.create(parent, hint); } + /** + * The factory for this map's own implementation, so that instance methods that make a new map + * ({@link #copy}) keep its type rather than taking the JVM's default store. + */ + abstract TagMapFactory factory(); + /** Creates a new TagMap.Ledger */ public static final Ledger ledger() { return new Ledger(); @@ -2372,7 +2385,7 @@ 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 = TagMapFactory.Default.INSTANCE.create(this.parent); + TagMap copy = this.factory().create(this.parent); copy.putAllIntoEmptyMap(this); if (this.removedFromParent != null) { copy.removedFromParent = new HashSet<>(this.removedFromParent); diff --git a/internal-api/src/test/java/datadog/trace/api/DenseTagMapTest.java b/internal-api/src/test/java/datadog/trace/api/DenseTagMapTest.java new file mode 100644 index 00000000000..4eba5e9ae5d --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/DenseTagMapTest.java @@ -0,0 +1,501 @@ +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.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 java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * TagMap's dense store, built through {@link TagMapType#DENSE} so it runs in the normal test JVM + * whatever the dense-store flag says. The shared TagMap suites also run against it; this covers + * what is specific to the dense store. {@link TagMapDenseForkedTest} covers what needs the flag on. + * + *

    A known tag with a dense-store slot is stored densely; a known tag without a slot and a custom + * tag stay in the buckets. Behavior must match the bucket-only store. + */ +class DenseTagMapTest { + // slotted, span-level + static final String COMPONENT = KnownTags.COMPONENT_NAME; + static final String DB_TYPE = KnownTags.DB_TYPE_NAME; + static final String HTTP_ROUTE = KnownTags.HTTP_ROUTE_NAME; + // slotted, trace-level + static final String BASE_SERVICE = KnownTags.DD_BASE_SERVICE_NAME; + // known, no slot + static final String DB_POOL_NAME = KnownTags.DB_POOL_NAME; + static final String UNSLOTTED_KNOWN = KnownTags.RESOURCE_NAME; // on no span type: no slot + // custom + static final String CUSTOM_A = "custom.tag.a"; + static final String CUSTOM_B = "custom.tag.b"; + + /** Two slotted span-level tags that the registry gave the same slot. */ + static long sharedSlotA; + + static long sharedSlotB; + + @BeforeAll + static void checkSetup() throws IllegalAccessException { + assertTrue(KnownTagCodec.hasSlot(KnownTags.COMPONENT_ID)); + assertTrue(KnownTagCodec.hasSlot(KnownTags.DD_BASE_SERVICE_ID)); + assertFalse(KnownTagCodec.hasSlot(KnownTags.RESOURCE_NAME_ID), "resource.name has no slot"); + + Map bySlot = new HashMap<>(); + for (Field field : KnownTags.class.getFields()) { + if (field.getType() != long.class) continue; + long id = field.getLong(null); + if (!KnownTagCodec.hasSlot(id) || KnownTagCodec.isTraceLevel(id)) continue; + if (KnownTagCodec.keyOf(KnownTagCodec.nameOf(id)) != id) continue; // skip shared names + Long other = bySlot.putIfAbsent(KnownTagCodec.slotOf(id), id); + if (other != null) { + sharedSlotA = other; + sharedSlotB = id; + return; + } + } + throw new AssertionError("no two slotted tags share a slot"); + } + + private static int knownCount(TagMap map) { + try { + Field f = DenseTagMap.class.getDeclaredField("knownCount"); + f.setAccessible(true); + return f.getInt(map); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } + + /** A field of DenseTagMap, or of TagMap for the shared bucket state. */ + private static Object field(Object target, String name) { + for (Class c = DenseTagMap.class; c != null; c = c.getSuperclass()) { + try { + Field f = c.getDeclaredField(name); + f.setAccessible(true); + return f.get(target); + } catch (NoSuchFieldException e) { + // look in the superclass + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } + throw new AssertionError("no field " + name); + } + + @Test + void aSlottedKnownTagIsStoredDenselyAndRoundTrips() { + TagMap map = TagMapType.DENSE.create(); + map.set(COMPONENT, "spring-web"); + map.set(BASE_SERVICE, "billing"); + + assertEquals(2, knownCount(map)); + assertEquals("spring-web", map.getObject(COMPONENT)); + assertEquals("billing", map.getString(BASE_SERVICE)); + assertEquals("spring-web", map.getEntry(COMPONENT).objectValue()); + assertEquals(KnownTags.COMPONENT_ID, map.getEntry(COMPONENT).tagId()); + assertTrue(map.containsKey(COMPONENT)); + assertEquals(2, map.size()); + map.checkIntegrity(); + } + + @Test + void customTagsStayInBucketsAndUnslottedKnownTagsAreDense() { + TagMap map = TagMapType.DENSE.create(); + map.set(CUSTOM_A, "alpha"); + map.set(UNSLOTTED_KNOWN, "pool-1"); // a known tag with no slot + + assertEquals(1, knownCount(map)); + assertEquals("alpha", map.getObject(CUSTOM_A)); + assertEquals("pool-1", map.getObject(UNSLOTTED_KNOWN)); + assertEquals(2, map.size()); + map.checkIntegrity(); + } + + @Test + void typedValuesRoundTrip() { + TagMap map = TagMapType.DENSE.create(); + map.set(KnownTags.HTTP_STATUS_CODE_NAME, 404); + map.set(KnownTags.HTTP_RESEND_COUNT_NAME, 3L); + map.set(KnownTags.DD_PROFILING_ENABLED_NAME, true); + map.set(KnownTags.HTTP_ROUTE_ID, (CharSequence) "/users/{id}"); + + assertEquals(404, map.getInt(KnownTags.HTTP_STATUS_CODE_NAME)); + assertEquals(3L, map.getLong(KnownTags.HTTP_RESEND_COUNT_NAME)); + assertTrue(map.getBoolean(KnownTags.DD_PROFILING_ENABLED_NAME)); + assertEquals("/users/{id}", map.getString(HTTP_ROUTE)); + assertEquals(4, knownCount(map)); + map.checkIntegrity(); + } + + @Test + void idKeyedSetGetAndRemove() { + TagMap map = TagMapType.DENSE.create(); + map.set(KnownTags.DB_TYPE_ID, "postgresql"); + + assertEquals("postgresql", map.getEntry(KnownTags.DB_TYPE_ID).objectValue()); + assertEquals("postgresql", map.getObject(DB_TYPE)); + assertEquals("postgresql", map.getAndRemove(KnownTags.DB_TYPE_ID).objectValue()); + assertNull(map.getEntry(KnownTags.DB_TYPE_ID)); + assertEquals(0, map.size()); + map.checkIntegrity(); + } + + @Test + void anOpenTelemetryNameLandsOnTheSameDenseValue() { + TagMap map = TagMapType.DENSE.create(); + map.set(KnownTags.HTTP_METHOD_OTEL_NAME, "GET"); + + assertEquals(1, knownCount(map)); + assertEquals("GET", map.getObject(KnownTags.HTTP_METHOD_NAME)); + assertEquals("GET", map.getObject(KnownTags.HTTP_METHOD_OTEL_NAME)); + map.set(KnownTags.HTTP_METHOD_NAME, "POST"); + assertEquals(1, map.size()); + assertEquals("POST", map.getObject(KnownTags.HTTP_METHOD_OTEL_NAME)); + } + + @Test + void twoTagsSharingASlotInOneMapBothWork() { + assertEquals(KnownTagCodec.slotOf(sharedSlotA), KnownTagCodec.slotOf(sharedSlotB)); + TagMap map = TagMapType.DENSE.create(); + map.set(sharedSlotA, "a"); + map.set(sharedSlotB, "b"); + + assertEquals(2, knownCount(map)); + assertEquals("a", map.getEntry(sharedSlotA).objectValue()); + assertEquals("b", map.getEntry(sharedSlotB).objectValue()); + + map.set(sharedSlotA, "a2"); + assertEquals(2, map.size()); + assertEquals("a2", map.getEntry(sharedSlotA).objectValue()); + + assertEquals("a2", map.getAndRemove(sharedSlotA).objectValue()); + assertNull(map.getEntry(sharedSlotA)); + assertEquals("b", map.getEntry(sharedSlotB).objectValue()); + map.checkIntegrity(); + } + + @Test + void overwriteReplacesInPlaceAndGetAndSetReturnsThePrior() { + TagMap map = TagMapType.DENSE.create(); + map.set(COMPONENT, "first"); + TagMap.Entry prior = map.getAndSet(COMPONENT, "second"); + + assertEquals("first", prior.objectValue()); + assertEquals("second", map.getObject(COMPONENT)); + assertEquals(1, map.size()); + map.checkIntegrity(); + } + + @Test + void removeClearsADenseValue() { + TagMap map = TagMapType.DENSE.create(); + map.set(COMPONENT, "web"); + map.set(CUSTOM_A, "alpha"); + + assertEquals("web", map.remove((Object) COMPONENT)); + assertNull(map.getObject(COMPONENT)); + assertFalse(map.remove(COMPONENT)); + assertEquals(1, map.size()); + map.checkIntegrity(); + } + + @Test + void forEachIteratorAndEntrySetEmitBothRegions() { + TagMap map = TagMapType.DENSE.create(); + map.set(COMPONENT, "web"); + map.set(BASE_SERVICE, "billing"); + map.set(CUSTOM_A, "alpha"); + map.set(DB_POOL_NAME, "pool-1"); + + Map viaForEach = new HashMap<>(); + map.forEach(reader -> viaForEach.put(reader.tag(), reader.objectValue())); + Map viaBiForEach = new HashMap<>(); + map.forEach(viaBiForEach, (m, reader) -> m.put(reader.tag(), reader.objectValue())); + Map viaIterator = new HashMap<>(); + for (TagMap.EntryReader reader : map) { + viaIterator.put(reader.tag(), reader.objectValue()); + } + Map viaEntrySet = new HashMap<>(map); + Map viaFill = new HashMap<>(); + map.fillMap(viaFill); + + assertEquals(4, viaForEach.size()); + assertEquals("web", viaForEach.get(COMPONENT)); + assertEquals(viaForEach, viaBiForEach); + assertEquals(viaForEach, viaIterator); + assertEquals(viaForEach, viaEntrySet); + assertEquals(viaForEach, viaFill); + assertEquals(4, map.keySet().size()); + assertTrue(map.values().contains("billing")); + } + + @Test + void entrySetEntriesAndEntryCopiesAreRetainSafe() { + TagMap map = TagMapType.DENSE.create(); + map.set(COMPONENT, "web"); + map.set(DB_TYPE, "h2"); + + List> entries = new ArrayList<>(map.entrySet()); + List copies = new ArrayList<>(); + for (TagMap.EntryReader reader : map) { + copies.add(reader.entry()); + } + + Map fromEntries = new HashMap<>(); + for (Map.Entry e : entries) fromEntries.put(e.getKey(), e.getValue()); + Map fromCopies = new HashMap<>(); + for (TagMap.Entry e : copies) fromCopies.put(e.tag(), e.objectValue()); + + assertEquals(2, fromEntries.size()); + assertEquals("web", fromEntries.get(COMPONENT)); + assertEquals("h2", fromEntries.get(DB_TYPE)); + assertEquals(fromEntries, fromCopies); + } + + @Test + void anAllDenseMapAllocatesNoBuckets() { + TagMap map = TagMapType.DENSE.create(); + map.set(COMPONENT, "web"); + map.set(DB_TYPE, "h2"); + Object emptyBuckets = field(null, "EMPTY_BUCKETS"); + + assertSame(emptyBuckets, field(map, "buckets")); + TagMap copy = map.copy(); + assertSame(emptyBuckets, field(copy, "buckets")); + + map.set(CUSTOM_A, "alpha"); + assertNotEquals(emptyBuckets, field(map, "buckets")); + for (Object bucket : (Object[]) emptyBuckets) { + assertNull(bucket, "the shared empty buckets were written"); + } + map.clear(); + assertSame(emptyBuckets, field(map, "buckets")); + } + + @Test + void copyIsIndependent() { + TagMap map = TagMapType.DENSE.create(); + map.set(COMPONENT, "web"); + map.set(CUSTOM_A, "alpha"); + + TagMap copy = map.copy(); + copy.set(COMPONENT, "grpc"); + copy.remove(CUSTOM_A); + + assertEquals("web", map.getObject(COMPONENT)); + assertEquals("alpha", map.getObject(CUSTOM_A)); + assertEquals("grpc", copy.getObject(COMPONENT)); + assertEquals(1, copy.size()); + map.checkIntegrity(); + copy.checkIntegrity(); + } + + @Test + void clearEmptiesTheDenseStore() { + TagMap map = TagMapType.DENSE.create(); + map.set(COMPONENT, "web"); + map.set(CUSTOM_A, "alpha"); + map.clear(); + + assertEquals(0, map.size()); + assertTrue(map.isEmpty()); + assertTrue(map.isDefinitelyEmpty()); + assertNull(map.getObject(COMPONENT)); + map.set(COMPONENT, "again"); + assertEquals("again", map.getObject(COMPONENT)); + map.checkIntegrity(); + } + + @Test + void putAllMergesTheDenseStore() { + TagMap src = TagMapType.DENSE.create(); + src.set(BASE_SERVICE, "billing"); + src.set(DB_TYPE, "h2"); + src.set(CUSTOM_A, "alpha"); + + TagMap dst = TagMapType.DENSE.create(); + dst.set(COMPONENT, "web"); + dst.set(BASE_SERVICE, "old"); + dst.putAll(src); + + assertEquals("billing", dst.getObject(BASE_SERVICE)); + assertEquals("h2", dst.getObject(DB_TYPE)); + assertEquals("web", dst.getObject(COMPONENT)); + assertEquals("alpha", dst.getObject(CUSTOM_A)); + assertEquals(4, dst.size()); + dst.checkIntegrity(); + + TagMap empty = TagMapType.DENSE.create(); + empty.putAll(src); + assertEquals(3, empty.size()); + assertEquals("h2", empty.getObject(DB_TYPE)); + empty.checkIntegrity(); + } + + @Test + void aFrozenDenseMapRejectsWrites() { + TagMap map = TagMapType.DENSE.create(); + map.set(COMPONENT, "web"); + TagMap frozen = map.immutableCopy(); + + assertThrows(IllegalStateException.class, () -> frozen.set(COMPONENT, "x")); + assertThrows(IllegalStateException.class, () -> frozen.set(KnownTags.DB_TYPE_ID, "x")); + assertThrows(IllegalStateException.class, () -> frozen.remove(COMPONENT)); + assertEquals("web", frozen.getObject(COMPONENT)); + } + + // ---- read-through: dense values in a frozen parent ---- + + private static TagMap frozenParent() { + TagMap parent = TagMapType.DENSE.create(); + parent.set(BASE_SERVICE, "billing"); // dense, trace-level + parent.set(COMPONENT, "web"); // dense + parent.set(CUSTOM_A, "alpha"); // bucket + return parent.freeze(); + } + + @Test + void aChildReadsThroughToItsParentsDenseValues() { + TagMap child = TagMapType.DENSE.createFromParent(frozenParent()); + child.set(DB_TYPE, "h2"); + child.set(CUSTOM_B, "beta"); + + assertEquals("billing", child.getObject(BASE_SERVICE)); + assertEquals("web", child.getObject(COMPONENT)); + assertEquals("alpha", child.getObject(CUSTOM_A)); + assertEquals("h2", child.getObject(DB_TYPE)); + assertEquals(5, child.size()); + assertEquals(5, child.estimateSize()); + + Map viaForEach = new HashMap<>(); + child.forEach(reader -> viaForEach.put(reader.tag(), reader.objectValue())); + Map viaIterator = new HashMap<>(); + for (TagMap.EntryReader reader : child) viaIterator.put(reader.tag(), reader.objectValue()); + Map viaFill = new HashMap<>(); + child.fillMap(viaFill); + + assertEquals(5, viaForEach.size()); + assertEquals(viaForEach, viaIterator); + assertEquals(viaForEach, viaFill); + child.checkIntegrity(); + } + + @Test + void aChildsDenseValueShadowsItsParents() { + TagMap child = TagMapType.DENSE.createFromParent(frozenParent()); + child.set(BASE_SERVICE, "shipping"); + child.set(COMPONENT, "grpc"); + + assertEquals("shipping", child.getObject(BASE_SERVICE)); + assertEquals("grpc", child.getObject(COMPONENT)); + assertEquals(3, child.size()); + + Map union = new HashMap<>(); + child.forEach(reader -> union.put(reader.tag(), reader.objectValue())); + assertEquals(3, union.size()); + assertEquals("shipping", union.get(BASE_SERVICE)); + assertEquals("grpc", union.get(COMPONENT)); + + List iterated = new ArrayList<>(); + for (TagMap.EntryReader reader : child) iterated.add(reader.tag()); + assertEquals(3, iterated.size()); + } + + @Test + void removingAParentsDenseValueTombstonesIt() { + TagMap child = TagMapType.DENSE.createFromParent(frozenParent()); + + TagMap.Entry removed = child.getAndRemove(BASE_SERVICE); + assertNotNull(removed); + assertEquals("billing", removed.objectValue()); + assertNull(child.getObject(BASE_SERVICE)); + assertEquals("web", child.getObject(COMPONENT)); + assertEquals(2, child.size()); + + Map union = new HashMap<>(); + child.forEach(reader -> union.put(reader.tag(), reader.objectValue())); + assertEquals(2, union.size()); + assertFalse(union.containsKey(BASE_SERVICE)); + + child.set(BASE_SERVICE, "restored"); // re-setting clears the tombstone + assertEquals("restored", child.getObject(BASE_SERVICE)); + assertEquals(3, child.size()); + child.checkIntegrity(); + } + + @Test + void getAndSetReturnsAParentsDenseValueAsThePrior() { + TagMap child = TagMapType.DENSE.createFromParent(frozenParent()); + + TagMap.Entry prior = child.getAndSet(COMPONENT, "grpc"); + assertNotNull(prior); + assertEquals("web", prior.objectValue()); + assertEquals("grpc", child.getObject(COMPONENT)); + } + + @Test + void putAllFromAReadThroughSourceCopiesTheUnion() { + TagMap child = TagMapType.DENSE.createFromParent(frozenParent()); + child.set(DB_TYPE, "h2"); + + TagMap dst = TagMapType.DENSE.create(); + dst.putAll(child); + + assertEquals(4, dst.size()); + assertEquals("billing", dst.getObject(BASE_SERVICE)); + assertEquals("h2", dst.getObject(DB_TYPE)); + assertEquals("alpha", dst.getObject(CUSTOM_A)); + dst.checkIntegrity(); + } + + @Test + void aMultiLevelChainResolvesNearestFirst() { + TagMap middle = TagMapType.DENSE.createFromParent(frozenParent()); + middle.set(COMPONENT, "middle"); + middle.remove(BASE_SERVICE); + TagMap leaf = TagMapType.DENSE.createFromParent(middle.freeze()); + leaf.set(DB_TYPE, "leaf"); + + assertEquals("middle", leaf.getObject(COMPONENT)); + assertNull(leaf.getObject(BASE_SERVICE)); + assertEquals("leaf", leaf.getObject(DB_TYPE)); + assertEquals(3, leaf.size()); + + Map union = new HashMap<>(); + leaf.forEach(reader -> union.put(reader.tag(), reader.objectValue())); + assertEquals(3, union.size()); + assertEquals("middle", union.get(COMPONENT)); + assertFalse(union.containsKey(BASE_SERVICE)); + } + + @Test + void theCoresKnownValueReadMatchesGetEntry() { + TagMap child = TagMapType.DENSE.createFromParent(frozenParent()); + child.set(HTTP_ROUTE, "/users/{id}"); // dense + child.set(COMPONENT, "child"); // dense, shadows the parent's + child.remove(BASE_SERVICE); // tombstones the parent's + + // local hit, no Entry built + assertEquals("/users/{id}", child.internals().getKnownValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals("child", child.internals().getKnownValue(KnownTags.COMPONENT_ID)); + // read-through parent hit, then a tombstoned one and a plain miss + TagMap reader = TagMapType.DENSE.createFromParent(frozenParent()); + assertEquals("web", reader.internals().getKnownValue(KnownTags.COMPONENT_ID)); + assertEquals("billing", reader.internals().getKnownValue(KnownTags.DD_BASE_SERVICE_ID)); + assertNull(child.internals().getKnownValue(KnownTags.DD_BASE_SERVICE_ID)); + assertNull(child.internals().getKnownValue(KnownTags.DB_TYPE_ID)); + // a known tag without a slot, held in the dense store all the same + child.set(UNSLOTTED_KNOWN, "GET /users"); + assertEquals("GET /users", child.internals().getKnownValue(KnownTags.RESOURCE_NAME_ID)); + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java index 711d76b49b4..df27c220972 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapDenseForkedTest.java @@ -2,27 +2,17 @@ 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.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 java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; /** - * TagMap's dense store, enabled. The flag is read once into a static final, so this runs in its own - * JVM ({@code ForkedTest}) with the flag set before TagMap first checks it. - * - *

    A known tag with a dense-store slot is stored densely; a known tag without a slot and a custom - * tag stay in the buckets. Behavior must match the bucket-only store. + * TagMap with the dense-store flag on: what only the flag decides. The flag is read once into a + * static final, so this runs in its own JVM ({@code ForkedTest}) with the flag set before TagMap + * first checks it. The dense store's behavior is covered by {@link DenseTagMapTest} and the shared + * suites, which build through {@link TagMapType#DENSE}. */ class TagMapDenseForkedTest { static { @@ -31,44 +21,8 @@ class TagMapDenseForkedTest { // slotted, span-level static final String COMPONENT = KnownTags.COMPONENT_NAME; - static final String DB_TYPE = KnownTags.DB_TYPE_NAME; - static final String HTTP_ROUTE = KnownTags.HTTP_ROUTE_NAME; - // slotted, trace-level - static final String BASE_SERVICE = KnownTags.DD_BASE_SERVICE_NAME; - // known, no slot - static final String DB_POOL_NAME = KnownTags.DB_POOL_NAME; - static final String UNSLOTTED_KNOWN = KnownTags.RESOURCE_NAME; // on no span type: no slot // custom static final String CUSTOM_A = "custom.tag.a"; - static final String CUSTOM_B = "custom.tag.b"; - - /** Two slotted span-level tags that the registry gave the same slot. */ - static long sharedSlotA; - - static long sharedSlotB; - - @BeforeAll - static void checkSetup() throws IllegalAccessException { - assertTrue(Config.get().isTraceDenseTagsEnabled(), "dense store must be on in this JVM"); - assertTrue(KnownTagCodec.hasSlot(KnownTags.COMPONENT_ID)); - assertTrue(KnownTagCodec.hasSlot(KnownTags.DD_BASE_SERVICE_ID)); - assertFalse(KnownTagCodec.hasSlot(KnownTags.RESOURCE_NAME_ID), "resource.name has no slot"); - - Map bySlot = new HashMap<>(); - for (Field field : KnownTags.class.getFields()) { - if (field.getType() != long.class) continue; - long id = field.getLong(null); - if (!KnownTagCodec.hasSlot(id) || KnownTagCodec.isTraceLevel(id)) continue; - if (KnownTagCodec.keyOf(KnownTagCodec.nameOf(id)) != id) continue; // skip shared names - Long other = bySlot.putIfAbsent(KnownTagCodec.slotOf(id), id); - if (other != null) { - sharedSlotA = other; - sharedSlotB = id; - return; - } - } - throw new AssertionError("no two slotted tags share a slot"); - } private static int knownCount(TagMap map) { try { @@ -80,22 +34,6 @@ private static int knownCount(TagMap map) { } } - /** A field of DenseTagMap, or of TagMap for the shared bucket state. */ - private static Object field(Object target, String name) { - for (Class c = DenseTagMap.class; c != null; c = c.getSuperclass()) { - try { - Field f = c.getDeclaredField(name); - f.setAccessible(true); - return f.get(target); - } catch (NoSuchFieldException e) { - // look in the superclass - } catch (ReflectiveOperationException e) { - throw new AssertionError(e); - } - } - throw new AssertionError("no field " + name); - } - @Test void onlyTheDenseImplementationIsLoaded() { TagMap map = TagMap.create(); @@ -106,256 +44,6 @@ void onlyTheDenseImplementationIsLoaded() { assertFalse(TagMapImplementations.isLoaded("datadog.trace.api.HashTagMap")); } - @Test - void aSlottedKnownTagIsStoredDenselyAndRoundTrips() { - TagMap map = TagMap.create(); - map.set(COMPONENT, "spring-web"); - map.set(BASE_SERVICE, "billing"); - - assertEquals(2, knownCount(map)); - assertEquals("spring-web", map.getObject(COMPONENT)); - assertEquals("billing", map.getString(BASE_SERVICE)); - assertEquals("spring-web", map.getEntry(COMPONENT).objectValue()); - assertEquals(KnownTags.COMPONENT_ID, map.getEntry(COMPONENT).tagId()); - assertTrue(map.containsKey(COMPONENT)); - assertEquals(2, map.size()); - map.checkIntegrity(); - } - - @Test - void customTagsStayInBucketsAndUnslottedKnownTagsAreDense() { - TagMap map = TagMap.create(); - map.set(CUSTOM_A, "alpha"); - map.set(UNSLOTTED_KNOWN, "pool-1"); // a known tag with no slot - - assertEquals(1, knownCount(map)); - assertEquals("alpha", map.getObject(CUSTOM_A)); - assertEquals("pool-1", map.getObject(UNSLOTTED_KNOWN)); - assertEquals(2, map.size()); - map.checkIntegrity(); - } - - @Test - void typedValuesRoundTrip() { - TagMap map = TagMap.create(); - map.set(KnownTags.HTTP_STATUS_CODE_NAME, 404); - map.set(KnownTags.HTTP_RESEND_COUNT_NAME, 3L); - map.set(KnownTags.DD_PROFILING_ENABLED_NAME, true); - map.set(KnownTags.HTTP_ROUTE_ID, (CharSequence) "/users/{id}"); - - assertEquals(404, map.getInt(KnownTags.HTTP_STATUS_CODE_NAME)); - assertEquals(3L, map.getLong(KnownTags.HTTP_RESEND_COUNT_NAME)); - assertTrue(map.getBoolean(KnownTags.DD_PROFILING_ENABLED_NAME)); - assertEquals("/users/{id}", map.getString(HTTP_ROUTE)); - assertEquals(4, knownCount(map)); - map.checkIntegrity(); - } - - @Test - void idKeyedSetGetAndRemove() { - TagMap map = TagMap.create(); - map.set(KnownTags.DB_TYPE_ID, "postgresql"); - - assertEquals("postgresql", map.getEntry(KnownTags.DB_TYPE_ID).objectValue()); - assertEquals("postgresql", map.getObject(DB_TYPE)); - assertEquals("postgresql", map.getAndRemove(KnownTags.DB_TYPE_ID).objectValue()); - assertNull(map.getEntry(KnownTags.DB_TYPE_ID)); - assertEquals(0, map.size()); - map.checkIntegrity(); - } - - @Test - void anOpenTelemetryNameLandsOnTheSameDenseValue() { - TagMap map = TagMap.create(); - map.set(KnownTags.HTTP_METHOD_OTEL_NAME, "GET"); - - assertEquals(1, knownCount(map)); - assertEquals("GET", map.getObject(KnownTags.HTTP_METHOD_NAME)); - assertEquals("GET", map.getObject(KnownTags.HTTP_METHOD_OTEL_NAME)); - map.set(KnownTags.HTTP_METHOD_NAME, "POST"); - assertEquals(1, map.size()); - assertEquals("POST", map.getObject(KnownTags.HTTP_METHOD_OTEL_NAME)); - } - - @Test - void twoTagsSharingASlotInOneMapBothWork() { - assertEquals(KnownTagCodec.slotOf(sharedSlotA), KnownTagCodec.slotOf(sharedSlotB)); - TagMap map = TagMap.create(); - map.set(sharedSlotA, "a"); - map.set(sharedSlotB, "b"); - - assertEquals(2, knownCount(map)); - assertEquals("a", map.getEntry(sharedSlotA).objectValue()); - assertEquals("b", map.getEntry(sharedSlotB).objectValue()); - - map.set(sharedSlotA, "a2"); - assertEquals(2, map.size()); - assertEquals("a2", map.getEntry(sharedSlotA).objectValue()); - - assertEquals("a2", map.getAndRemove(sharedSlotA).objectValue()); - assertNull(map.getEntry(sharedSlotA)); - assertEquals("b", map.getEntry(sharedSlotB).objectValue()); - map.checkIntegrity(); - } - - @Test - void overwriteReplacesInPlaceAndGetAndSetReturnsThePrior() { - TagMap map = TagMap.create(); - map.set(COMPONENT, "first"); - TagMap.Entry prior = map.getAndSet(COMPONENT, "second"); - - assertEquals("first", prior.objectValue()); - assertEquals("second", map.getObject(COMPONENT)); - assertEquals(1, map.size()); - map.checkIntegrity(); - } - - @Test - void removeClearsADenseValue() { - TagMap map = TagMap.create(); - map.set(COMPONENT, "web"); - map.set(CUSTOM_A, "alpha"); - - assertEquals("web", map.remove((Object) COMPONENT)); - assertNull(map.getObject(COMPONENT)); - assertFalse(map.remove(COMPONENT)); - assertEquals(1, map.size()); - map.checkIntegrity(); - } - - @Test - void forEachIteratorAndEntrySetEmitBothRegions() { - TagMap map = TagMap.create(); - map.set(COMPONENT, "web"); - map.set(BASE_SERVICE, "billing"); - map.set(CUSTOM_A, "alpha"); - map.set(DB_POOL_NAME, "pool-1"); - - Map viaForEach = new HashMap<>(); - map.forEach(reader -> viaForEach.put(reader.tag(), reader.objectValue())); - Map viaBiForEach = new HashMap<>(); - map.forEach(viaBiForEach, (m, reader) -> m.put(reader.tag(), reader.objectValue())); - Map viaIterator = new HashMap<>(); - for (TagMap.EntryReader reader : map) { - viaIterator.put(reader.tag(), reader.objectValue()); - } - Map viaEntrySet = new HashMap<>(map); - Map viaFill = new HashMap<>(); - map.fillMap(viaFill); - - assertEquals(4, viaForEach.size()); - assertEquals("web", viaForEach.get(COMPONENT)); - assertEquals(viaForEach, viaBiForEach); - assertEquals(viaForEach, viaIterator); - assertEquals(viaForEach, viaEntrySet); - assertEquals(viaForEach, viaFill); - assertEquals(4, map.keySet().size()); - assertTrue(map.values().contains("billing")); - } - - @Test - void entrySetEntriesAndEntryCopiesAreRetainSafe() { - TagMap map = TagMap.create(); - map.set(COMPONENT, "web"); - map.set(DB_TYPE, "h2"); - - List> entries = new ArrayList<>(map.entrySet()); - List copies = new ArrayList<>(); - for (TagMap.EntryReader reader : map) { - copies.add(reader.entry()); - } - - Map fromEntries = new HashMap<>(); - for (Map.Entry e : entries) fromEntries.put(e.getKey(), e.getValue()); - Map fromCopies = new HashMap<>(); - for (TagMap.Entry e : copies) fromCopies.put(e.tag(), e.objectValue()); - - assertEquals(2, fromEntries.size()); - assertEquals("web", fromEntries.get(COMPONENT)); - assertEquals("h2", fromEntries.get(DB_TYPE)); - assertEquals(fromEntries, fromCopies); - } - - @Test - void anAllDenseMapAllocatesNoBuckets() { - TagMap map = TagMap.create(); - map.set(COMPONENT, "web"); - map.set(DB_TYPE, "h2"); - Object emptyBuckets = field(null, "EMPTY_BUCKETS"); - - assertSame(emptyBuckets, field(map, "buckets")); - TagMap copy = map.copy(); - assertSame(emptyBuckets, field(copy, "buckets")); - - map.set(CUSTOM_A, "alpha"); - assertNotEquals(emptyBuckets, field(map, "buckets")); - for (Object bucket : (Object[]) emptyBuckets) { - assertNull(bucket, "the shared empty buckets were written"); - } - map.clear(); - assertSame(emptyBuckets, field(map, "buckets")); - } - - @Test - void copyIsIndependent() { - TagMap map = TagMap.create(); - map.set(COMPONENT, "web"); - map.set(CUSTOM_A, "alpha"); - - TagMap copy = map.copy(); - copy.set(COMPONENT, "grpc"); - copy.remove(CUSTOM_A); - - assertEquals("web", map.getObject(COMPONENT)); - assertEquals("alpha", map.getObject(CUSTOM_A)); - assertEquals("grpc", copy.getObject(COMPONENT)); - assertEquals(1, copy.size()); - map.checkIntegrity(); - copy.checkIntegrity(); - } - - @Test - void clearEmptiesTheDenseStore() { - TagMap map = TagMap.create(); - map.set(COMPONENT, "web"); - map.set(CUSTOM_A, "alpha"); - map.clear(); - - assertEquals(0, map.size()); - assertTrue(map.isEmpty()); - assertTrue(map.isDefinitelyEmpty()); - assertNull(map.getObject(COMPONENT)); - map.set(COMPONENT, "again"); - assertEquals("again", map.getObject(COMPONENT)); - map.checkIntegrity(); - } - - @Test - void putAllMergesTheDenseStore() { - TagMap src = TagMap.create(); - src.set(BASE_SERVICE, "billing"); - src.set(DB_TYPE, "h2"); - src.set(CUSTOM_A, "alpha"); - - TagMap dst = TagMap.create(); - dst.set(COMPONENT, "web"); - dst.set(BASE_SERVICE, "old"); - dst.putAll(src); - - assertEquals("billing", dst.getObject(BASE_SERVICE)); - assertEquals("h2", dst.getObject(DB_TYPE)); - assertEquals("web", dst.getObject(COMPONENT)); - assertEquals("alpha", dst.getObject(CUSTOM_A)); - assertEquals(4, dst.size()); - dst.checkIntegrity(); - - TagMap empty = TagMap.create(); - empty.putAll(src); - assertEquals(3, empty.size()); - assertEquals("h2", empty.getObject(DB_TYPE)); - empty.checkIntegrity(); - } - @Test void aLedgerBuildsADenseMap() { TagMap map = TagMap.ledger().set(COMPONENT, "web").set(CUSTOM_A, "alpha").buildImmutable(); @@ -365,161 +53,4 @@ void aLedgerBuildsADenseMap() { assertEquals("alpha", map.getObject(CUSTOM_A)); assertThrows(IllegalStateException.class, () -> map.set(COMPONENT, "x")); } - - @Test - void aFrozenDenseMapRejectsWrites() { - TagMap map = TagMap.create(); - map.set(COMPONENT, "web"); - TagMap frozen = map.immutableCopy(); - - assertThrows(IllegalStateException.class, () -> frozen.set(COMPONENT, "x")); - assertThrows(IllegalStateException.class, () -> frozen.set(KnownTags.DB_TYPE_ID, "x")); - assertThrows(IllegalStateException.class, () -> frozen.remove(COMPONENT)); - assertEquals("web", frozen.getObject(COMPONENT)); - } - - // ---- read-through: dense values in a frozen parent ---- - - private static TagMap frozenParent() { - TagMap parent = TagMap.create(); - parent.set(BASE_SERVICE, "billing"); // dense, trace-level - parent.set(COMPONENT, "web"); // dense - parent.set(CUSTOM_A, "alpha"); // bucket - return parent.freeze(); - } - - @Test - void aChildReadsThroughToItsParentsDenseValues() { - TagMap child = TagMap.createFromParent(frozenParent()); - child.set(DB_TYPE, "h2"); - child.set(CUSTOM_B, "beta"); - - assertEquals("billing", child.getObject(BASE_SERVICE)); - assertEquals("web", child.getObject(COMPONENT)); - assertEquals("alpha", child.getObject(CUSTOM_A)); - assertEquals("h2", child.getObject(DB_TYPE)); - assertEquals(5, child.size()); - assertEquals(5, child.estimateSize()); - - Map viaForEach = new HashMap<>(); - child.forEach(reader -> viaForEach.put(reader.tag(), reader.objectValue())); - Map viaIterator = new HashMap<>(); - for (TagMap.EntryReader reader : child) viaIterator.put(reader.tag(), reader.objectValue()); - Map viaFill = new HashMap<>(); - child.fillMap(viaFill); - - assertEquals(5, viaForEach.size()); - assertEquals(viaForEach, viaIterator); - assertEquals(viaForEach, viaFill); - child.checkIntegrity(); - } - - @Test - void aChildsDenseValueShadowsItsParents() { - TagMap child = TagMap.createFromParent(frozenParent()); - child.set(BASE_SERVICE, "shipping"); - child.set(COMPONENT, "grpc"); - - assertEquals("shipping", child.getObject(BASE_SERVICE)); - assertEquals("grpc", child.getObject(COMPONENT)); - assertEquals(3, child.size()); - - Map union = new HashMap<>(); - child.forEach(reader -> union.put(reader.tag(), reader.objectValue())); - assertEquals(3, union.size()); - assertEquals("shipping", union.get(BASE_SERVICE)); - assertEquals("grpc", union.get(COMPONENT)); - - List iterated = new ArrayList<>(); - for (TagMap.EntryReader reader : child) iterated.add(reader.tag()); - assertEquals(3, iterated.size()); - } - - @Test - void removingAParentsDenseValueTombstonesIt() { - TagMap child = TagMap.createFromParent(frozenParent()); - - TagMap.Entry removed = child.getAndRemove(BASE_SERVICE); - assertNotNull(removed); - assertEquals("billing", removed.objectValue()); - assertNull(child.getObject(BASE_SERVICE)); - assertEquals("web", child.getObject(COMPONENT)); - assertEquals(2, child.size()); - - Map union = new HashMap<>(); - child.forEach(reader -> union.put(reader.tag(), reader.objectValue())); - assertEquals(2, union.size()); - assertFalse(union.containsKey(BASE_SERVICE)); - - child.set(BASE_SERVICE, "restored"); // re-setting clears the tombstone - assertEquals("restored", child.getObject(BASE_SERVICE)); - assertEquals(3, child.size()); - child.checkIntegrity(); - } - - @Test - void getAndSetReturnsAParentsDenseValueAsThePrior() { - TagMap child = TagMap.createFromParent(frozenParent()); - - TagMap.Entry prior = child.getAndSet(COMPONENT, "grpc"); - assertNotNull(prior); - assertEquals("web", prior.objectValue()); - assertEquals("grpc", child.getObject(COMPONENT)); - } - - @Test - void putAllFromAReadThroughSourceCopiesTheUnion() { - TagMap child = TagMap.createFromParent(frozenParent()); - child.set(DB_TYPE, "h2"); - - TagMap dst = TagMap.create(); - dst.putAll(child); - - assertEquals(4, dst.size()); - assertEquals("billing", dst.getObject(BASE_SERVICE)); - assertEquals("h2", dst.getObject(DB_TYPE)); - assertEquals("alpha", dst.getObject(CUSTOM_A)); - dst.checkIntegrity(); - } - - @Test - void aMultiLevelChainResolvesNearestFirst() { - TagMap middle = TagMap.createFromParent(frozenParent()); - middle.set(COMPONENT, "middle"); - middle.remove(BASE_SERVICE); - TagMap leaf = TagMap.createFromParent(middle.freeze()); - leaf.set(DB_TYPE, "leaf"); - - assertEquals("middle", leaf.getObject(COMPONENT)); - assertNull(leaf.getObject(BASE_SERVICE)); - assertEquals("leaf", leaf.getObject(DB_TYPE)); - assertEquals(3, leaf.size()); - - Map union = new HashMap<>(); - leaf.forEach(reader -> union.put(reader.tag(), reader.objectValue())); - assertEquals(3, union.size()); - assertEquals("middle", union.get(COMPONENT)); - assertFalse(union.containsKey(BASE_SERVICE)); - } - - @Test - void theCoresKnownValueReadMatchesGetEntry() { - TagMap child = TagMap.createFromParent(frozenParent()); - child.set(HTTP_ROUTE, "/users/{id}"); // dense - child.set(COMPONENT, "child"); // dense, shadows the parent's - child.remove(BASE_SERVICE); // tombstones the parent's - - // local hit, no Entry built - assertEquals("/users/{id}", child.internals().getKnownValue(KnownTags.HTTP_ROUTE_ID)); - assertEquals("child", child.internals().getKnownValue(KnownTags.COMPONENT_ID)); - // read-through parent hit, then a tombstoned one and a plain miss - TagMap reader = TagMap.createFromParent(frozenParent()); - assertEquals("web", reader.internals().getKnownValue(KnownTags.COMPONENT_ID)); - assertEquals("billing", reader.internals().getKnownValue(KnownTags.DD_BASE_SERVICE_ID)); - assertNull(child.internals().getKnownValue(KnownTags.DD_BASE_SERVICE_ID)); - assertNull(child.internals().getKnownValue(KnownTags.DB_TYPE_ID)); - // a known tag without a slot, held in the dense store all the same - child.set(UNSLOTTED_KNOWN, "GET /users"); - assertEquals("GET /users", child.internals().getKnownValue(KnownTags.RESOURCE_NAME_ID)); - } } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapDenseFuzzForkedTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapDenseFuzzForkedTest.java index 105db35b2dd..cfcb5f9ff39 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapDenseFuzzForkedTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapDenseFuzzForkedTest.java @@ -81,11 +81,11 @@ void mixedFuzz() { private static void runRegime(Regime regime) { for (int i = 0; i < SINGLE_MAP_CASES; ++i) { - TagMapFuzzTest.test(generateTest(regime)); + TagMapFuzzTest.test(TagMapType.DENSE, generateTest(regime)); } for (int i = 0; i < MERGE_CASES; ++i) { - TagMap mapA = TagMapFuzzTest.test(generateTest(regime)); - TagMap mapB = TagMapFuzzTest.test(generateTest(regime)); + TagMap mapA = TagMapFuzzTest.test(TagMapType.DENSE, generateTest(regime)); + TagMap mapB = TagMapFuzzTest.test(TagMapType.DENSE, generateTest(regime)); HashMap hashA = new HashMap<>(mapA); HashMap hashB = new HashMap<>(mapB); diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapFuzzTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapFuzzTest.java index d97e389aa4b..3289c34ddc2 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapFuzzTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapFuzzTest.java @@ -13,25 +13,28 @@ import java.util.Map; import java.util.concurrent.ThreadLocalRandom; import java.util.function.Supplier; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; public final class TagMapFuzzTest { static final int NUM_KEYS = 128; static final int MAX_NUM_ACTIONS = 32; static final int MIN_NUM_ACTIONS = 8; - @Test - void test() { - test(generateTest()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void test(TagMapType type) { + test(type, generateTest()); } - @Test - void testMerge() { + @ParameterizedTest + @EnumSource(TagMapType.class) + void testMerge(TagMapType type) { TestCase mapACase = generateTest(); TestCase mapBCase = generateTest(); - TagMap tagMapA = test(mapACase); - TagMap tagMapB = test(mapBCase); + TagMap tagMapA = test(type, mapACase); + TagMap tagMapB = test(type, mapBCase); HashMap hashMapA = new HashMap<>(tagMapA); HashMap hashMapB = new HashMap<>(tagMapB); @@ -42,10 +45,12 @@ void testMerge() { assertMapEquals(hashMapA, tagMapA); } - @Test - void priorFailingCase0() { + @ParameterizedTest + @EnumSource(TagMapType.class) + void priorFailingCase0(TagMapType type) { TagMap map = makeTagMap( + type, remove("key-4"), put("key-71", "values-443049055"), put("key-2", "values-1227065898"), @@ -302,9 +307,11 @@ void priorFailingCase0() { failingAction.verifyTestMap(map); } - @Test - void priorFailingCase1() { - TagMap map = makeTagMap(put("key-68", "values--37178328"), put("key-93", "values--2093086281")); + @ParameterizedTest + @EnumSource(TagMapType.class) + void priorFailingCase1(TagMapType type) { + TagMap map = + makeTagMap(type, put("key-68", "values--37178328"), put("key-93", "values--2093086281")); MapAction failingAction = putAllTagMap( @@ -522,8 +529,9 @@ void priorFailingCase1() { failingAction.verifyTestMap(map); } - @Test - void priorFailingCase2() { + @ParameterizedTest + @EnumSource(TagMapType.class) + void priorFailingCase2(TagMapType type) { TestCase testCase = new TestCase( remove("key-34"), @@ -858,7 +866,7 @@ void priorFailingCase2() { put("key-41", "values--904162962")); Map expected = makeMap(testCase); - TagMap actual = makeTagMap(testCase); + TagMap actual = makeTagMap(type, testCase); MapAction failingAction = remove("key-127"); failingAction.applyToExpectedMap(expected); @@ -869,8 +877,8 @@ void priorFailingCase2() { assertMapEquals(expected, actual); } - public static final TagMap test(MapAction... actions) { - return test(new TestCase(Arrays.asList(actions))); + public static final TagMap test(TagMapType type, MapAction... actions) { + return test(type, new TestCase(Arrays.asList(actions))); } public static final Map makeMap(TestCase testCase) { @@ -889,27 +897,27 @@ public static final Map makeMap(List actions) { return map; } - public static final TagMap makeTagMap(TestCase testCase) { - return makeTagMap(testCase.actions); + public static final TagMap makeTagMap(TagMapType type, TestCase testCase) { + return makeTagMap(type, testCase.actions); } - public static final TagMap makeTagMap(MapAction... actions) { - return makeTagMap(Arrays.asList(actions)); + public static final TagMap makeTagMap(TagMapType type, MapAction... actions) { + return makeTagMap(type, Arrays.asList(actions)); } - public static final TagMap makeTagMap(List actions) { - TagMap map = TagMap.create(); + public static final TagMap makeTagMap(TagMapType type, List actions) { + TagMap map = type.create(); for (MapAction action : actions) { action.applyToTestMap(map); } return map; } - public static final TagMap test(TestCase test) { + public static final TagMap test(TagMapType type, TestCase test) { List actions = test.actions(); Map hashMap = new HashMap<>(); - TagMap tagMap = TagMap.create(); + TagMap tagMap = type.create(); int actionIndex = 0; try { @@ -1099,8 +1107,8 @@ static final Map mapOf(String... keysAndValues) { return map; } - static final TagMap tagMapOf(String... keysAndValues) { - TagMap map = TagMap.create(); + static final TagMap tagMapOf(TagMapType type, String... keysAndValues) { + TagMap map = type.create(); for (int i = 0; i < keysAndValues.length; i += 2) { String key = keysAndValues[i]; String value = keysAndValues[i + 1]; @@ -1112,6 +1120,10 @@ static final TagMap tagMapOf(String... keysAndValues) { return map; } + static final TagMapType typeOf(TagMap map) { + return map instanceof DenseTagMap ? TagMapType.DENSE : TagMapType.HASH; + } + static final TagMap.Ledger ledgerOf(String... keysAndValues) { TagMap.Ledger ledger = TagMap.ledger(); for (int i = 0; i < keysAndValues.length; i += 2) { @@ -1359,27 +1371,26 @@ public String toString() { static final class PutAllTagMap extends BasicAction { final String[] keysAndValues; - final TagMap tagMap; PutAllTagMap(String... keysAndValues) { this.keysAndValues = keysAndValues; - this.tagMap = tagMapOf(keysAndValues); } @Override protected void _applyToTestMap(TagMap testMap) { - testMap.putAll(this.tagMap); + // the source map is built through the test map's own store, so the two never mix stores + testMap.putAll(tagMapOf(typeOf(testMap), this.keysAndValues)); } @Override protected void _applyToExpectedMap(Map expectedMap) { - expectedMap.putAll(this.tagMap); + expectedMap.putAll(mapOf(this.keysAndValues)); } @Override public void verifyTestMap(TagMap expectedMap) { - for (TagMap.EntryReader entry : this.tagMap) { - assertEquals(entry.objectValue(), expectedMap.get(entry.tag()), "key=" + entry.tag()); + for (Map.Entry entry : mapOf(this.keysAndValues).entrySet()) { + assertEquals(entry.getValue(), expectedMap.get(entry.getKey()), "key=" + entry.getKey()); } } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapReadThroughTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapReadThroughTest.java index 5001994e2b0..b1d6331d4e2 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapReadThroughTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapReadThroughTest.java @@ -11,7 +11,8 @@ import java.util.Iterator; import java.util.Map; import java.util.Set; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; /** * Read-through support, slice 1 (read path): a child {@link TagMap} with a frozen parent reads @@ -20,17 +21,18 @@ */ class TagMapReadThroughTest { - private static TagMap frozenParent() { - TagMap parent = TagMap.create(); + private static TagMap frozenParent(TagMapType type) { + TagMap parent = type.create(); parent.set("a", "parent-a"); parent.set("b", "parent-b"); parent.freeze(); return parent; } - @Test - void readsThroughToParentOnMiss() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void readsThroughToParentOnMiss(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.set("c", "child-c"); assertEquals("parent-a", child.getString("a")); // miss locally -> read through @@ -41,18 +43,20 @@ void readsThroughToParentOnMiss() { assertFalse(child.containsKey("missing")); } - @Test - void localEntryShadowsParent() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void localEntryShadowsParent(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.set("b", "child-b"); // same key as parent assertEquals("child-b", child.getString("b")); // local wins assertEquals("parent-a", child.getString("a")); // parent still visible } - @Test - void estimateSizeIsUpperBound() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void estimateSizeIsUpperBound(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.set("b", "child-b"); // shadows parent "b" child.set("c", "child-c"); @@ -61,31 +65,34 @@ void estimateSizeIsUpperBound() { assertTrue(child.estimateSize() >= 3, "estimateSize must be an upper bound on the true size"); } - @Test - void emptinessSemantics() { - TagMap emptyOverEmpty = TagMap.createFromParent(TagMap.create().freeze()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void emptinessSemantics(TagMapType type) { + TagMap emptyOverEmpty = type.createFromParent(type.create().freeze()); assertTrue(emptyOverEmpty.isEmpty()); assertTrue(emptyOverEmpty.isDefinitelyEmpty()); - TagMap emptyOverNonEmpty = TagMap.createFromParent(frozenParent()); + TagMap emptyOverNonEmpty = type.createFromParent(frozenParent(type)); assertFalse(emptyOverNonEmpty.isEmpty(), "a non-empty parent makes the map non-empty"); assertFalse(emptyOverNonEmpty.isDefinitelyEmpty()); - assertTrue((TagMap.create()).isDefinitelyEmpty()); + assertTrue((type.create()).isDefinitelyEmpty()); } - @Test - void parentMustBeFrozen() { - TagMap mutableParent = TagMap.create(); - assertThrows(IllegalStateException.class, () -> TagMap.createFromParent(mutableParent)); + @ParameterizedTest + @EnumSource(TagMapType.class) + void parentMustBeFrozen(TagMapType type) { + TagMap mutableParent = type.create(); + assertThrows(IllegalStateException.class, () -> type.createFromParent(mutableParent)); } - @Test - void emptyParentIsDroppedNotAttached() { - TagMap emptyFrozen = TagMap.create(); + @ParameterizedTest + @EnumSource(TagMapType.class) + void emptyParentIsDroppedNotAttached(TagMapType type) { + TagMap emptyFrozen = type.create(); emptyFrozen.freeze(); - TagMap overEmpty = TagMap.createFromParent(emptyFrozen); + TagMap overEmpty = type.createFromParent(emptyFrozen); // an empty frozen parent contributes nothing and never will -> dropped, no read-through cost assertNull(overEmpty.parent, "empty parent should be dropped"); assertTrue(overEmpty.isDefinitelyEmpty()); @@ -93,17 +100,18 @@ void emptyParentIsDroppedNotAttached() { assertEquals("x-val", overEmpty.getString("x")); // a non-empty parent is still attached - TagMap overNonEmpty = TagMap.createFromParent(frozenParent()); + TagMap overNonEmpty = type.createFromParent(frozenParent(type)); assertNotNull(overNonEmpty.parent, "non-empty parent must be attached"); assertEquals("parent-a", overNonEmpty.getString("a")); } // --- slice 2: removal / tombstones --- - @Test - void removingParentKeyHidesItFromChildButNotFromParent() { - TagMap parent = frozenParent(); - TagMap child = TagMap.createFromParent(parent); + @ParameterizedTest + @EnumSource(TagMapType.class) + void removingParentKeyHidesItFromChildButNotFromParent(TagMapType type) { + TagMap parent = frozenParent(type); + TagMap child = type.createFromParent(parent); assertEquals("parent-a", child.getString("a")); // visible before removal child.remove("a"); @@ -114,18 +122,20 @@ void removingParentKeyHidesItFromChildButNotFromParent() { assertEquals("parent-a", parent.getString("a")); // frozen parent untouched } - @Test - void removeReturnsPriorVisibleValueViaParent() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void removeReturnsPriorVisibleValueViaParent(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); // Map.remove contract: the key was present (via read-through), so removal reports it. assertTrue(child.remove("a"), "removing a parent-exposed key should report it was present"); assertNull(child.getString("a")); } - @Test - void reSettingARemovedKeyRestoresVisibility() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void reSettingARemovedKeyRestoresVisibility(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.remove("a"); assertNull(child.getString("a")); @@ -134,16 +144,17 @@ void reSettingARemovedKeyRestoresVisibility() { assertEquals("child-a", child.getString("a")); } - @Test - void removingAKeyThatIsBothLocalAndParentHidesBoth() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void removingAKeyThatIsBothLocalAndParentHidesBoth(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.set("b", "child-b"); // shadows parent "b" assertEquals("child-b", child.getString("b")); child.remove("b"); assertNull(child.getString("b"), "removal must hide both the local entry and the parent's"); - assertEquals("parent-b", frozenParent().getString("b")); // parent still has it + assertEquals("parent-b", frozenParent(type).getString("b")); // parent still has it } // --- slice 3a: bulk forEach union + exact size/isEmpty --- @@ -154,9 +165,10 @@ private static Map collect(TagMap map) { return out; } - @Test - void forEachEmitsDedupedUnionLocalWins() { - TagMap child = TagMap.createFromParent(frozenParent()); // parent {a, b} + @ParameterizedTest + @EnumSource(TagMapType.class) + void forEachEmitsDedupedUnionLocalWins(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); // parent {a, b} child.set("b", "child-b"); // shadows parent "b" child.set("c", "child-c"); @@ -167,9 +179,10 @@ void forEachEmitsDedupedUnionLocalWins() { assertEquals("child-c", u.get("c")); } - @Test - void forEachSkipsTombstonedParentKeys() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void forEachSkipsTombstonedParentKeys(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.set("c", "child-c"); child.remove("a"); // tombstone parent's "a" @@ -180,9 +193,10 @@ void forEachSkipsTombstonedParentKeys() { assertEquals("child-c", u.get("c")); } - @Test - void biConsumerForEachAlsoEmitsUnion() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void biConsumerForEachAlsoEmitsUnion(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.set("c", "child-c"); Map out = new HashMap<>(); @@ -192,9 +206,10 @@ void biConsumerForEachAlsoEmitsUnion() { assertEquals("child-c", out.get("c")); } - @Test - void sizeIsExactUnion() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void sizeIsExactUnion(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.set("b", "child-b"); // shadows child.set("c", "child-c"); assertEquals(3, child.size()); // {a, b, c} — b deduped, not 4 @@ -203,9 +218,10 @@ void sizeIsExactUnion() { assertEquals(2, child.size()); // {b, c} } - @Test - void isEmptyExactWhenAllParentKeysTombstonedAndNoLocal() { - TagMap child = TagMap.createFromParent(frozenParent()); // parent {a, b} + @ParameterizedTest + @EnumSource(TagMapType.class) + void isEmptyExactWhenAllParentKeysTombstonedAndNoLocal(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); // parent {a, b} assertFalse(child.isEmpty()); child.remove("a"); @@ -216,9 +232,10 @@ void isEmptyExactWhenAllParentKeysTombstonedAndNoLocal() { // --- slice 3b: pull-based iterators / collection views --- - @Test - void iteratorEmitsDedupedUnion() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void iteratorEmitsDedupedUnion(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.set("b", "child-b"); // shadows parent "b" child.set("c", "child-c"); @@ -234,9 +251,10 @@ void iteratorEmitsDedupedUnion() { assertEquals("child-c", u.get("c")); } - @Test - void keySetReflectsUnionAndTombstones() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void keySetReflectsUnionAndTombstones(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.set("c", "child-c"); Set keys = child.keySet(); @@ -249,9 +267,10 @@ void keySetReflectsUnionAndTombstones() { assertFalse(child.keySet().contains("a")); } - @Test - void valuesAndEntrySetReflectUnion() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void valuesAndEntrySetReflectUnion(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.set("b", "child-b"); // shadows parent "b" assertEquals(2, child.entrySet().size()); // {a, b} — b deduped @@ -262,13 +281,14 @@ void valuesAndEntrySetReflectUnion() { // --- slice 3c: putAll from a read-through source copies the visible union, not just locals --- - @Test - void putAllFromReadThroughSourceCopiesFullVisibleUnion() { - TagMap source = TagMap.createFromParent(frozenParent()); // parent {a, b} + @ParameterizedTest + @EnumSource(TagMapType.class) + void putAllFromReadThroughSourceCopiesFullVisibleUnion(TagMapType type) { + TagMap source = type.createFromParent(frozenParent(type)); // parent {a, b} source.set("b", "child-b"); // shadows parent b source.set("c", "child-c"); - TagMap dest = TagMap.create(); // empty -> putAllIntoEmptyMap path + TagMap dest = type.create(); // empty -> putAllIntoEmptyMap path dest.putAll(source); // the parent-visible "a" must land too, not just source's local entries @@ -278,11 +298,12 @@ void putAllFromReadThroughSourceCopiesFullVisibleUnion() { assertEquals("child-c", dest.getString("c")); } - @Test - void putAllMergeFromReadThroughSourceCopiesVisibleUnion() { - TagMap source = TagMap.createFromParent(frozenParent()); // {a, b} + @ParameterizedTest + @EnumSource(TagMapType.class) + void putAllMergeFromReadThroughSourceCopiesVisibleUnion(TagMapType type) { + TagMap source = type.createFromParent(frozenParent(type)); // {a, b} - TagMap dest = TagMap.create(); + TagMap dest = type.create(); dest.set("z", "dest-z"); // dest non-empty -> putAllMerge path dest.putAll(source); @@ -292,12 +313,13 @@ void putAllMergeFromReadThroughSourceCopiesVisibleUnion() { assertEquals("dest-z", dest.getString("z")); } - @Test - void putAllFromReadThroughSourceHonorsTombstones() { - TagMap source = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void putAllFromReadThroughSourceHonorsTombstones(TagMapType type) { + TagMap source = type.createFromParent(frozenParent(type)); source.remove("a"); // tombstone parent's "a" - TagMap dest = TagMap.create(); + TagMap dest = type.create(); dest.putAll(source); assertEquals(1, dest.size()); @@ -307,9 +329,10 @@ void putAllFromReadThroughSourceHonorsTombstones() { // --- slice 4: behavior-identical to a copy-down / flat map --- - @Test - void copyIsObservationallyIdentical() { - TagMap child = TagMap.createFromParent(frozenParent()); // {a, b} + @ParameterizedTest + @EnumSource(TagMapType.class) + void copyIsObservationallyIdentical(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); // {a, b} child.set("b", "child-b"); // shadows parent "b" child.set("c", "child-c"); @@ -321,9 +344,10 @@ void copyIsObservationallyIdentical() { assertEquals(collect(child), collect(copy)); // same union } - @Test - void copyIsIndependentlyMutable() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void copyIsIndependentlyMutable(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.set("c", "child-c"); TagMap copy = child.copy(); @@ -336,9 +360,10 @@ void copyIsIndependentlyMutable() { assertNull(copy.getString("a")); } - @Test - void copyPreservesTombstones() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void copyPreservesTombstones(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.remove("a"); // tombstone "a" TagMap copy = child.copy(); @@ -347,13 +372,14 @@ void copyPreservesTombstones() { } /** The contract that lets the consumer flip mergedTracerTags to a parent. */ - @Test - void readThroughMatchesAnEquivalentFlatMap() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void readThroughMatchesAnEquivalentFlatMap(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.set("b", "child-b"); child.set("c", "child-c"); - TagMap flat = TagMap.create(); + TagMap flat = type.create(); flat.set("a", "parent-a"); flat.set("b", "child-b"); flat.set("c", "child-c"); @@ -366,9 +392,10 @@ void readThroughMatchesAnEquivalentFlatMap() { } } - @Test - void immutableCopyOfReadThroughIsFrozenAndStillReadsThrough() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void immutableCopyOfReadThroughIsFrozenAndStillReadsThrough(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.set("c", "child-c"); TagMap frozen = child.immutableCopy(); @@ -384,27 +411,28 @@ void immutableCopyOfReadThroughIsFrozenAndStillReadsThrough() { * Builds a 3-level chain leaf -> mid -> grandparent (both ancestors frozen) and returns the * leaf. Visible union, nearest-level-wins: {a=gp-a, b=mid-b, c=leaf-c, d=mid-d, e=leaf-e}. */ - private static TagMap threeLevelLeaf() { - TagMap grandparent = TagMap.create(); + private static TagMap threeLevelLeaf(TagMapType type) { + TagMap grandparent = type.create(); grandparent.set("a", "gp-a"); grandparent.set("b", "gp-b"); grandparent.set("c", "gp-c"); grandparent.freeze(); - TagMap mid = TagMap.createFromParent(grandparent); + TagMap mid = type.createFromParent(grandparent); mid.set("b", "mid-b"); // shadows grandparent b mid.set("d", "mid-d"); mid.freeze(); - TagMap leaf = TagMap.createFromParent(mid); + TagMap leaf = type.createFromParent(mid); leaf.set("c", "leaf-c"); // shadows grandparent c (mid doesn't define c) leaf.set("e", "leaf-e"); return leaf; } - @Test - void getWalksTheWholeChainNearestWins() { - TagMap leaf = threeLevelLeaf(); + @ParameterizedTest + @EnumSource(TagMapType.class) + void getWalksTheWholeChainNearestWins(TagMapType type) { + TagMap leaf = threeLevelLeaf(type); assertEquals("gp-a", leaf.getString("a")); // only in grandparent (two levels up) assertEquals("mid-b", leaf.getString("b")); // mid shadows grandparent assertEquals("leaf-c", leaf.getString("c")); // leaf shadows grandparent @@ -413,14 +441,16 @@ void getWalksTheWholeChainNearestWins() { assertNull(leaf.getString("missing")); } - @Test - void sizeIsExactUnionAcrossChain() { - assertEquals(5, threeLevelLeaf().size()); // {a, b, c, d, e}, shadowed duplicates deduped + @ParameterizedTest + @EnumSource(TagMapType.class) + void sizeIsExactUnionAcrossChain(TagMapType type) { + assertEquals(5, threeLevelLeaf(type).size()); // {a, b, c, d, e}, shadowed duplicates deduped } - @Test - void forEachEmitsDedupedUnionAcrossChain() { - Map u = collect(threeLevelLeaf()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void forEachEmitsDedupedUnionAcrossChain(TagMapType type) { + Map u = collect(threeLevelLeaf(type)); assertEquals(5, u.size()); assertEquals("gp-a", u.get("a")); assertEquals("mid-b", u.get("b")); // nearest ancestor wins over grandparent @@ -429,10 +459,11 @@ void forEachEmitsDedupedUnionAcrossChain() { assertEquals("leaf-e", u.get("e")); } - @Test - void iteratorEmitsDedupedUnionAcrossChain() { + @ParameterizedTest + @EnumSource(TagMapType.class) + void iteratorEmitsDedupedUnionAcrossChain(TagMapType type) { Map u = new HashMap<>(); - Iterator it = threeLevelLeaf().iterator(); + Iterator it = threeLevelLeaf(type).iterator(); while (it.hasNext()) { TagMap.EntryReader e = it.next(); u.put(e.tag(), e.objectValue()); @@ -443,18 +474,20 @@ void iteratorEmitsDedupedUnionAcrossChain() { assertEquals("leaf-c", u.get("c")); } - @Test - void keySetReflectsChainUnion() { - Set keys = threeLevelLeaf().keySet(); + @ParameterizedTest + @EnumSource(TagMapType.class) + void keySetReflectsChainUnion(TagMapType type) { + Set keys = threeLevelLeaf(type).keySet(); assertEquals(5, keys.size()); for (String k : new String[] {"a", "b", "c", "d", "e"}) { assertTrue(keys.contains(k), "missing key " + k); } } - @Test - void leafTombstoneHidesGrandparentOnlyKey() { - TagMap leaf = threeLevelLeaf(); + @ParameterizedTest + @EnumSource(TagMapType.class) + void leafTombstoneHidesGrandparentOnlyKey(TagMapType type) { + TagMap leaf = threeLevelLeaf(type); leaf.remove("a"); // "a" lives only in the grandparent, two levels up assertNull(leaf.getString("a")); assertFalse(leaf.containsKey("a")); @@ -462,21 +495,22 @@ void leafTombstoneHidesGrandparentOnlyKey() { assertEquals(4, leaf.size()); // {b, c, d, e} } - @Test - void intermediateAncestorTombstoneIsHonoredByBulkViews() { + @ParameterizedTest + @EnumSource(TagMapType.class) + void intermediateAncestorTombstoneIsHonoredByBulkViews(TagMapType type) { // mid removes an inherited grandparent key, THEN is frozen and reused as a parent. A non-leaf // level can therefore carry its own tombstones. Point lookups recurse through mid's tombstone // (correct); the bulk views must agree and not re-emit the key. - TagMap grandparent = TagMap.create(); + TagMap grandparent = type.create(); grandparent.set("a", "gp-a"); grandparent.set("b", "gp-b"); grandparent.freeze(); - TagMap mid = TagMap.createFromParent(grandparent); + TagMap mid = type.createFromParent(grandparent); mid.remove("a"); // tombstone an inherited key before freezing mid.freeze(); - TagMap leaf = TagMap.createFromParent(mid); + TagMap leaf = type.createFromParent(mid); // point lookups (recurse -> already correct) assertNull(leaf.getString("a")); @@ -502,22 +536,23 @@ void intermediateAncestorTombstoneIsHonoredByBulkViews() { assertFalse(viaIterator.containsKey("a")); } - @Test - void observationallyEmptyIntermediateParentIsDroppedNotAttached() { + @ParameterizedTest + @EnumSource(TagMapType.class) + void observationallyEmptyIntermediateParentIsDroppedNotAttached(TagMapType type) { // A frozen mid level with no local entries whose every inherited key is tombstoned is // observationally empty even though a local (grandparent) level still holds entries. // createFromParent must drop it (exact isEmpty), so the leaf stays consistent: isEmpty() must // agree with size()/lookup/iteration. - TagMap grandparent = TagMap.create(); + TagMap grandparent = type.create(); grandparent.set("a", "gp-a"); grandparent.freeze(); - TagMap mid = TagMap.createFromParent(grandparent); + TagMap mid = type.createFromParent(grandparent); mid.remove("a"); // tombstone the only inherited key -> mid is observationally empty mid.freeze(); assertTrue(mid.isEmpty()); - TagMap leaf = TagMap.createFromParent(mid); + TagMap leaf = type.createFromParent(mid); assertEquals(0, leaf.size()); assertTrue(leaf.isEmpty(), "isEmpty() must agree with size()==0"); assertNull(leaf.getString("a")); @@ -525,11 +560,12 @@ void observationallyEmptyIntermediateParentIsDroppedNotAttached() { assertFalse(leaf.iterator().hasNext()); } - @Test - void chainReadThroughMatchesEquivalentFlatMap() { - TagMap leaf = threeLevelLeaf(); + @ParameterizedTest + @EnumSource(TagMapType.class) + void chainReadThroughMatchesEquivalentFlatMap(TagMapType type) { + TagMap leaf = threeLevelLeaf(type); - TagMap flat = TagMap.create(); + TagMap flat = type.create(); flat.set("a", "gp-a"); flat.set("b", "mid-b"); flat.set("c", "leaf-c"); @@ -546,47 +582,53 @@ void chainReadThroughMatchesEquivalentFlatMap() { // --- slice 6: put/getAndSet report the prior visible value, including inherited --- - @Test - void putReturnsInheritedParentValueAsPrior() { - TagMap child = TagMap.createFromParent(frozenParent()); // parent {a, b} + @ParameterizedTest + @EnumSource(TagMapType.class) + void putReturnsInheritedParentValueAsPrior(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); // parent {a, b} Object prior = child.put("a", "child-a"); // "a" exists only in the parent assertEquals("parent-a", prior, "put must report the inherited value as the previous mapping"); assertEquals("child-a", child.getString("a")); // new value stored locally } - @Test - void putReturnsNullForAKeyInNeitherLocalNorParent() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void putReturnsNullForAKeyInNeitherLocalNorParent(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); assertNull(child.put("brand-new", "v")); } - @Test - void putReturnsLocalPriorWhenShadowingParent() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void putReturnsLocalPriorWhenShadowingParent(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.set("a", "local-a"); // local now shadows the parent assertEquals("local-a", child.put("a", "local-a2"), "the local prior wins over the parent's"); } - @Test - void putAfterRemoveReportsNoPriorNotTheParentValue() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void putAfterRemoveReportsNoPriorNotTheParentValue(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.remove("a"); // tombstone the parent's "a": no longer visible assertNull(child.put("a", "child-a"), "a tombstoned key had no visible prior value"); assertEquals("child-a", child.getString("a")); } - @Test - void getAndSetReturnsInheritedEntryAsPrior() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void getAndSetReturnsInheritedEntryAsPrior(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); TagMap.Entry prior = child.getAndSet("b", "child-b"); assertEquals("parent-b", prior.objectValue()); } - @Test - void getAndSetByIdReturnsTheInheritedEntryOfThatId() { - TagMap parent = TagMap.create(); + @ParameterizedTest + @EnumSource(TagMapType.class) + void getAndSetByIdReturnsTheInheritedEntryOfThatId(TagMapType type) { + TagMap parent = type.create(); parent.set(KnownTags.HTTP_ROUTE_ID, "/parent/{id}"); - TagMap child = TagMap.createFromParent(parent.freeze()); + TagMap child = type.createFromParent(parent.freeze()); TagMap.Entry prior = child.getAndSet(TagMap.Entry.create(KnownTags.HTTP_ROUTE_ID, (Object) "/child/{id}")); @@ -594,9 +636,10 @@ void getAndSetByIdReturnsTheInheritedEntryOfThatId() { assertEquals("/parent/{id}", prior.objectValue()); } - @Test - void setDoesNotReportPriorButStillClearsTombstone() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void setDoesNotReportPriorButStillClearsTombstone(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.remove("b"); // tombstone child.set("b", "child-b"); // void set: no prior lookup, but must clear the tombstone assertEquals("child-b", child.getString("b")); @@ -604,9 +647,10 @@ void setDoesNotReportPriorButStillClearsTombstone() { // --- slice 7: clear() removes inherited mappings too (detaches the parent) --- - @Test - void clearRemovesInheritedMappingsAndDetachesParent() { - TagMap child = TagMap.createFromParent(frozenParent()); // {a, b} + @ParameterizedTest + @EnumSource(TagMapType.class) + void clearRemovesInheritedMappingsAndDetachesParent(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); // {a, b} child.set("c", "child-c"); child.clear(); @@ -618,17 +662,19 @@ void clearRemovesInheritedMappingsAndDetachesParent() { assertFalse(child.containsKey("a")); } - @Test - void clearDoesNotAffectTheFrozenParent() { - TagMap parent = frozenParent(); - TagMap child = TagMap.createFromParent(parent); + @ParameterizedTest + @EnumSource(TagMapType.class) + void clearDoesNotAffectTheFrozenParent(TagMapType type) { + TagMap parent = frozenParent(type); + TagMap child = type.createFromParent(parent); child.clear(); assertEquals("parent-a", parent.getString("a"), "the shared frozen parent is untouched"); } - @Test - void putAfterClearBehavesAsAPlainMap() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void putAfterClearBehavesAsAPlainMap(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.clear(); child.set("x", "x-val"); assertEquals(1, child.size()); @@ -650,9 +696,10 @@ private static Map fillStringMapCollect(TagMap map) { return out; } - @Test - void fillMapMaterializesDedupedUnionLocalWins() { - TagMap child = TagMap.createFromParent(frozenParent()); // parent {a, b} + @ParameterizedTest + @EnumSource(TagMapType.class) + void fillMapMaterializesDedupedUnionLocalWins(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); // parent {a, b} child.set("b", "child-b"); // shadows parent "b" child.set("c", "child-c"); @@ -663,9 +710,10 @@ void fillMapMaterializesDedupedUnionLocalWins() { assertEquals("child-c", out.get("c")); } - @Test - void fillMapHonorsTombstonedParentKeys() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void fillMapHonorsTombstonedParentKeys(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.set("c", "child-c"); child.remove("a"); // tombstone parent's "a" @@ -676,9 +724,10 @@ void fillMapHonorsTombstonedParentKeys() { assertEquals("child-c", out.get("c")); } - @Test - void fillStringMapMaterializesDedupedUnionLocalWins() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void fillStringMapMaterializesDedupedUnionLocalWins(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.set("b", "child-b"); // shadows parent "b" child.set("c", "child-c"); @@ -689,9 +738,10 @@ void fillStringMapMaterializesDedupedUnionLocalWins() { assertEquals("child-c", out.get("c")); } - @Test - void fillStringMapHonorsTombstonedParentKeys() { - TagMap child = TagMap.createFromParent(frozenParent()); + @ParameterizedTest + @EnumSource(TagMapType.class) + void fillStringMapHonorsTombstonedParentKeys(TagMapType type) { + TagMap child = type.createFromParent(frozenParent(type)); child.remove("a"); // tombstone parent's "a" Map out = fillStringMapCollect(child); @@ -700,11 +750,12 @@ void fillStringMapHonorsTombstonedParentKeys() { assertEquals("parent-b", out.get("b")); } - @Test - void fillMapAcrossChainMatchesEquivalentFlatMap() { - TagMap leaf = threeLevelLeaf(); + @ParameterizedTest + @EnumSource(TagMapType.class) + void fillMapAcrossChainMatchesEquivalentFlatMap(TagMapType type) { + TagMap leaf = threeLevelLeaf(type); - TagMap flat = TagMap.create(); + TagMap flat = type.create(); flat.set("a", "gp-a"); flat.set("b", "mid-b"); flat.set("c", "leaf-c"); @@ -715,10 +766,11 @@ void fillMapAcrossChainMatchesEquivalentFlatMap() { assertEquals(fillStringMapCollect(flat), fillStringMapCollect(leaf)); } - @Test - void fillMapWithoutParentFillsOnlyLocalEntries() { + @ParameterizedTest + @EnumSource(TagMapType.class) + void fillMapWithoutParentFillsOnlyLocalEntries(TagMapType type) { // regression: the no-parent fast path is unchanged - TagMap flat = TagMap.create(); + TagMap flat = type.create(); flat.set("x", "x-val"); flat.set("y", "y-val"); diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapScenario.java b/internal-api/src/test/java/datadog/trace/api/TagMapScenario.java index 1c07b31c563..9fa6267386a 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapScenario.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapScenario.java @@ -24,4 +24,12 @@ public final TagMap create() { } return map; } + + public final TagMap create(TagMapType type) { + TagMap map = type.create(); + for (int i = 0; i < this.size; ++i) { + map.put("filler-key-" + i, "filler-value-" + i); + } + return map; + } } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java index af24d1a0b30..fd73172d616 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java @@ -5,13 +5,15 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.HashMap; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; /** {@link TagMap#set(long, Object)} and its overloads: setting a known tag by its id. */ class TagMapSetByIdTest { - @Test - void storesUnderTheCanonicalName() { - TagMap map = TagMap.create(); + @ParameterizedTest + @EnumSource(TagMapType.class) + void storesUnderTheCanonicalName(TagMapType type) { + TagMap map = type.create(); map.set(KnownTags.PEER_HOSTNAME_ID, "db.internal"); @@ -19,10 +21,11 @@ void storesUnderTheCanonicalName() { assertEquals(KnownTags.PEER_HOSTNAME_ID, map.getEntry(KnownTags.PEER_HOSTNAME_NAME).tagId()); } - @Test - void anOpenTelemetryRenameIsStoredUnderTheDatadogName() { - TagMap byId = TagMap.create(); - TagMap byOtelName = TagMap.create(); + @ParameterizedTest + @EnumSource(TagMapType.class) + void anOpenTelemetryRenameIsStoredUnderTheDatadogName(TagMapType type) { + TagMap byId = type.create(); + TagMap byOtelName = type.create(); byId.set(KnownTags.HTTP_METHOD_ID, "GET"); byOtelName.set(KnownTags.HTTP_METHOD_OTEL_NAME, "GET"); @@ -30,9 +33,10 @@ void anOpenTelemetryRenameIsStoredUnderTheDatadogName() { assertEquals(new HashMap<>(byOtelName), new HashMap<>(byId)); } - @Test - void matchesTheNameKeyedSettersForEveryValueType() { - TagMap byId = TagMap.create(); + @ParameterizedTest + @EnumSource(TagMapType.class) + void matchesTheNameKeyedSettersForEveryValueType(TagMapType type) { + TagMap byId = type.create(); byId.set(KnownTags.HTTP_STATUS_CODE_ID, 5432); byId.set(KnownTags.HTTP_RESEND_COUNT_ID, 2L); byId.set(KnownTags.PEER_HOSTNAME_ID, (CharSequence) "db.internal"); @@ -41,7 +45,7 @@ void matchesTheNameKeyedSettersForEveryValueType() { byId.set(KnownTags.DB_USER_ID, 1.5f); byId.set(KnownTags.DB_POOL_NAME_ID, 2.5d); - TagMap byName = TagMap.create(); + TagMap byName = type.create(); byName.set(KnownTags.HTTP_STATUS_CODE_NAME, 5432); byName.set(KnownTags.HTTP_RESEND_COUNT_NAME, 2L); byName.set(KnownTags.PEER_HOSTNAME_NAME, (CharSequence) "db.internal"); @@ -54,9 +58,10 @@ void matchesTheNameKeyedSettersForEveryValueType() { assertEquals(5432, byId.getEntry(KnownTags.HTTP_STATUS_CODE_NAME).intValue()); } - @Test - void getsAndRemovesById() { - TagMap map = TagMap.create(); + @ParameterizedTest + @EnumSource(TagMapType.class) + void getsAndRemovesById(TagMapType type) { + TagMap map = type.create(); map.set(KnownTags.HTTP_METHOD_NAME, "GET"); assertEquals("GET", map.getEntry(KnownTags.HTTP_METHOD_ID).stringValue()); @@ -65,23 +70,25 @@ void getsAndRemovesById() { assertNull(map.getAndRemove(KnownTags.HTTP_METHOD_ID)); } - @Test - void anUnknownIdIsRejected() { - TagMap map = TagMap.create(); + @ParameterizedTest + @EnumSource(TagMapType.class) + void anUnknownIdIsRejected(TagMapType type) { + TagMap map = type.create(); assertThrows(IllegalArgumentException.class, () -> map.set(0L, "value")); assertThrows(IllegalArgumentException.class, () -> map.set(KnownTagCodec.makeTagId(9999), 1)); } - @Test - void theCoresTrustedSetsStoreWhatTheCheckedSetsStore() { - TagMap checked = TagMap.create(); + @ParameterizedTest + @EnumSource(TagMapType.class) + void theCoresTrustedSetsStoreWhatTheCheckedSetsStore(TagMapType type) { + TagMap checked = type.create(); checked.set(KnownTags.HTTP_ROUTE_ID, "/users/{id}"); checked.set(KnownTags.HTTP_STATUS_CODE_ID, 200); checked.set(KnownTags.HTTP_RESEND_COUNT_ID, 2L); checked.set(KnownTags.DD_PROFILING_ENABLED_ID, true); - TagMap trusted = TagMap.create(); + TagMap trusted = type.create(); trusted.internals().setKnown(KnownTags.HTTP_ROUTE_ID, "/users/{id}"); trusted.internals().setKnown(KnownTags.HTTP_STATUS_CODE_ID, 200); trusted.internals().setKnown(KnownTags.HTTP_RESEND_COUNT_ID, 2L); @@ -91,13 +98,14 @@ void theCoresTrustedSetsStoreWhatTheCheckedSetsStore() { assertEquals(KnownTags.HTTP_ROUTE_ID, trusted.getEntry(KnownTags.HTTP_ROUTE_NAME).tagId()); } - @Test - void theCoresKnownValueReadMatchesGetEntry() { - TagMap parent = TagMap.create(); + @ParameterizedTest + @EnumSource(TagMapType.class) + void theCoresKnownValueReadMatchesGetEntry(TagMapType type) { + TagMap parent = type.create(); parent.set(KnownTags.HTTP_ROUTE_ID, "/parent"); parent.set(KnownTags.HTTP_METHOD_ID, "GET"); parent.set(KnownTags.PEER_HOSTNAME_ID, "db.internal"); - TagMap child = TagMap.createFromParent(parent.freeze()); + TagMap child = type.createFromParent(parent.freeze()); child.set(KnownTags.HTTP_STATUS_CODE_ID, 200); child.set(KnownTags.HTTP_ROUTE_ID, "/child"); child.remove(KnownTags.PEER_HOSTNAME_NAME); @@ -113,15 +121,16 @@ void theCoresKnownValueReadMatchesGetEntry() { assertNull(child.internals().getKnownValue(KnownTags.DB_INSTANCE_ID)); } - @Test - void theCoresTrustedSetsCoverTheRemainingValueShapes() { + @ParameterizedTest + @EnumSource(TagMapType.class) + void theCoresTrustedSetsCoverTheRemainingValueShapes(TagMapType type) { Object component = new StringBuilder("jdbc"); // not a String: exercises the Object overload - TagMap checked = TagMap.create(); + TagMap checked = type.create(); checked.set(KnownTags.COMPONENT_ID, component); checked.set(KnownTags.DB_POOL_NAME_ID, 1.5f); checked.set(KnownTags.DB_INSTANCE_ID, 2.25d); - TagMap trusted = TagMap.create(); + TagMap trusted = type.create(); trusted.internals().setKnown(KnownTags.COMPONENT_ID, component); trusted.internals().setKnown(KnownTags.DB_POOL_NAME_ID, 1.5f); trusted.internals().setKnown(KnownTags.DB_INSTANCE_ID, 2.25d); diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapTest.java index 35d30ded83a..688410efb2b 100644 --- a/internal-api/src/test/java/datadog/trace/api/TagMapTest.java +++ b/internal-api/src/test/java/datadog/trace/api/TagMapTest.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -16,10 +17,13 @@ import java.util.Set; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.params.provider.MethodSource; public class TagMapTest { // size is chosen to make sure to stress all types of collisions in the Map @@ -81,9 +85,9 @@ public void fromMapImmutable_nonEmptyMap() { } @ParameterizedTest - @EnumSource(TagMapScenario.class) - public void map_put(TagMapScenario scenario) { - TagMap map = scenario.create(); + @MethodSource("typesAndScenarios") + public void map_put(TagMapType type, TagMapScenario scenario) { + TagMap map = scenario.create(type); Object prev = map.put("foo", "bar"); assertNull(prev); @@ -97,15 +101,15 @@ public void map_put(TagMapScenario scenario) { } @ParameterizedTest - @EnumSource(TagMapScenario.class) - public void booleanEntry(TagMapScenario scenario) { + @MethodSource("typesAndScenarios") + public void booleanEntry(TagMapType type, TagMapScenario scenario) { String BOOL = "bool"; String UNSET = "unset"; boolean first = false; boolean second = true; - TagMap map = scenario.create(); + TagMap map = scenario.create(type); map.set(BOOL, first); TagMap.Entry firstEntry = map.getEntry(BOOL); @@ -219,9 +223,10 @@ public void booleanToNumericCoercion_false() { checkIntegrity(map); } - @Test - public void emptyToPrimitiveCoercion() { - TagMap map = TagMap.EMPTY; + @ParameterizedTest + @EnumSource(TagMapType.class) + public void emptyToPrimitiveCoercion(TagMapType type) { + TagMap map = type.empty(); // DQH - assert helpers also check getOrDefault, so they don't work here assertEquals(false, map.getBoolean("dne")); @@ -234,15 +239,15 @@ public void emptyToPrimitiveCoercion() { } @ParameterizedTest - @EnumSource(TagMapScenario.class) - public void intEntry(TagMapScenario scenario) { + @MethodSource("typesAndScenarios") + public void intEntry(TagMapType type, TagMapScenario scenario) { String INT = "int"; String UNSET = "unset"; int first = 3142; int second = 2718; - TagMap map = scenario.create(); + TagMap map = scenario.create(type); map.set(INT, first); TagMap.Entry firstEntry = map.getEntry("int"); @@ -264,15 +269,15 @@ public void intEntry(TagMapScenario scenario) { } @ParameterizedTest - @EnumSource(TagMapScenario.class) - public void longEntry(TagMapScenario scenario) { + @MethodSource("typesAndScenarios") + public void longEntry(TagMapType type, TagMapScenario scenario) { String LONG = "long"; String UNSET = "unset"; long first = 3142L; long second = 2718L; - TagMap map = scenario.create(); + TagMap map = scenario.create(type); map.set(LONG, first); TagMap.Entry firstEntry = map.getEntry("long"); @@ -295,15 +300,15 @@ public void longEntry(TagMapScenario scenario) { } @ParameterizedTest - @EnumSource(TagMapScenario.class) - public void floatEntry(TagMapScenario scenario) { + @MethodSource("typesAndScenarios") + public void floatEntry(TagMapType type, TagMapScenario scenario) { String FLOAT = "float"; String UNSET = "unset"; float first = 3.14F; float second = 2.718F; - TagMap map = scenario.create(); + TagMap map = scenario.create(type); map.set(FLOAT, first); TagMap.Entry firstEntry = map.getEntry(FLOAT); @@ -326,15 +331,15 @@ public void floatEntry(TagMapScenario scenario) { } @ParameterizedTest - @EnumSource(TagMapScenario.class) - public void doubleEntry(TagMapScenario scenario) { + @MethodSource("typesAndScenarios") + public void doubleEntry(TagMapType type, TagMapScenario scenario) { String DOUBLE = "double"; String UNSET = "unset"; double first = Math.PI; double second = Math.E; - TagMap map = scenario.create(); + TagMap map = scenario.create(type); map.set(DOUBLE, Math.PI); TagMap.Entry firstEntry = map.getEntry(DOUBLE); @@ -356,9 +361,10 @@ public void doubleEntry(TagMapScenario scenario) { checkIntegrity(map); } - @Test - public void empty() { - TagMap empty = TagMap.EMPTY; + @ParameterizedTest + @EnumSource(TagMapType.class) + public void empty(TagMapType type) { + TagMap empty = type.empty(); assertFrozen(empty); assertNull(empty.getEntry("foo")); @@ -368,12 +374,13 @@ public void empty() { checkIntegrity(empty); } - @Test - public void putAll_empty() { - // TagMap.EMPTY breaks the rules and uses a different size bucket array + @ParameterizedTest + @EnumSource(TagMapType.class) + public void putAll_empty(TagMapType type) { + // type.empty() breaks the rules and uses a different size bucket array // This test is just to verify that the common use of putAll still works with EMPTY - TagMap newMap = TagMap.create(); - newMap.putAll(TagMap.EMPTY); + TagMap newMap = type.create(); + newMap.putAll(type.empty()); assertSize(0, newMap); assertEmpty(newMap); @@ -382,9 +389,9 @@ public void putAll_empty() { } @ParameterizedTest - @EnumSource(TagMapScenario.class) - public void clear(TagMapScenario scenario) { - TagMap map = scenario.create(); + @MethodSource("typesAndScenarios") + public void clear(TagMapType type, TagMapScenario scenario) { + TagMap map = scenario.create(type); assertSize(scenario.size(), map); assertEmptiness(scenario.size() == 0, map); @@ -397,9 +404,9 @@ public void clear(TagMapScenario scenario) { } @ParameterizedTest - @EnumSource(TagMapScenario.class) - public void map_put_replacement(TagMapScenario scenario) { - TagMap map = scenario.create(); + @MethodSource("typesAndScenarios") + public void map_put_replacement(TagMapType type, TagMapScenario scenario) { + TagMap map = scenario.create(type); Object prev1 = map.put("foo", "bar"); assertNull(prev1); @@ -417,9 +424,9 @@ public void map_put_replacement(TagMapScenario scenario) { } @ParameterizedTest - @EnumSource(TagMapScenario.class) - public void map_remove_Object(TagMapScenario scenario) { - TagMap map = scenario.create(); + @MethodSource("typesAndScenarios") + public void map_remove_Object(TagMapType type, TagMapScenario scenario) { + TagMap map = scenario.create(type); Object prev = map.remove((Object) "foo"); assertNull(prev); @@ -438,9 +445,9 @@ public void map_remove_Object(TagMapScenario scenario) { } @ParameterizedTest - @EnumSource(TagMapScenario.class) - public void map_remove_String(TagMapScenario scenario) { - TagMap map = scenario.create(); + @MethodSource("typesAndScenarios") + public void map_remove_String(TagMapType type, TagMapScenario scenario) { + TagMap map = scenario.create(type); boolean hadPrev1 = map.remove("foo"); assertFalse(hadPrev1); @@ -459,9 +466,9 @@ public void map_remove_String(TagMapScenario scenario) { } @ParameterizedTest - @EnumSource(TagMapScenario.class) - public void map_getAndRemove(TagMapScenario scenario) { - TagMap map = scenario.create(); + @MethodSource("typesAndScenarios") + public void map_getAndRemove(TagMapType type, TagMapScenario scenario) { + TagMap map = scenario.create(type); TagMap.Entry prevEntry1 = map.getAndRemove("foo"); assertNull(prevEntry1); @@ -480,9 +487,9 @@ public void map_getAndRemove(TagMapScenario scenario) { } @ParameterizedTest - @EnumSource(TagMapScenario.class) - public void freeze(TagMapScenario scenario) { - TagMap map = scenario.create(); + @MethodSource("typesAndScenarios") + public void freeze(TagMapType type, TagMapScenario scenario) { + TagMap map = scenario.create(type); map.put("foo", "bar"); assertEntry("foo", "bar", map); @@ -499,9 +506,10 @@ public void freeze(TagMapScenario scenario) { checkIntegrity(map); } - @Test - public void emptyMap() { - TagMap map = TagMap.EMPTY; + @ParameterizedTest + @EnumSource(TagMapType.class) + public void emptyMap(TagMapType type) { + TagMap map = type.empty(); assertSize(0, map); assertEmpty(map); @@ -512,9 +520,9 @@ public void emptyMap() { } @ParameterizedTest - @EnumSource(TagMapScenario.class) - public void putMany(TagMapScenario scenario) { - TagMap map = scenario.create(); + @MethodSource("typesAndScenarios") + public void putMany(TagMapType type, TagMapScenario scenario) { + TagMap map = scenario.create(type); int size = scenario.size(); fillMap(map, size); @@ -530,10 +538,10 @@ public void putMany(TagMapScenario scenario) { } @ParameterizedTest - @EnumSource(TagMapScenario.class) - public void copyMany(TagMapScenario scenario) { + @MethodSource("typesAndScenarios") + public void copyMany(TagMapType type, TagMapScenario scenario) { int size = scenario.size(); - TagMap orig = scenario.create(); + TagMap orig = scenario.create(type); fillMap(orig, size); assertSize(scenario.size() + size, orig); @@ -550,10 +558,11 @@ public void copyMany(TagMapScenario scenario) { checkIntegrity(copy); } - @Test - public void immutableCopy() { + @ParameterizedTest + @EnumSource(TagMapType.class) + public void immutableCopy(TagMapType type) { int size = randomSize(); - TagMap orig = createTagMap(size); + TagMap orig = createTagMap(type, size); TagMap immutableCopy = orig.immutableCopy(); orig.clear(); // doing this to make sure that copied isn't modified @@ -568,10 +577,11 @@ public void immutableCopy() { checkIntegrity(immutableCopy); } - @Test - public void replaceALot() { + @ParameterizedTest + @EnumSource(TagMapType.class) + public void replaceALot(TagMapType type) { int size = randomSize(); - TagMap map = createTagMap(size); + TagMap map = createTagMap(type, size); for (int i = 0; i < size; ++i) { int index = ThreadLocalRandom.current().nextInt(size); @@ -583,12 +593,13 @@ public void replaceALot() { checkIntegrity(map); } - @Test - public void shareEntry() { - TagMap orig = TagMap.create(); + @ParameterizedTest + @EnumSource(TagMapType.class) + public void shareEntry(TagMapType type) { + TagMap orig = type.create(); orig.set("foo", "bar"); - TagMap dest = TagMap.create(); + TagMap dest = type.create(); dest.set(orig.getEntry("foo")); assertEquals(orig.getEntry("foo"), dest.getEntry("foo")); @@ -598,13 +609,14 @@ public void shareEntry() { checkIntegrity(dest); } - @Test - public void putAll_clobberAll() { + @ParameterizedTest + @EnumSource(TagMapType.class) + public void putAll_clobberAll(TagMapType type) { int size = randomSize(); - TagMap orig = createTagMap(size); + TagMap orig = createTagMap(type, size); assertSize(size, orig); - TagMap dest = TagMap.create(); + TagMap dest = type.create(); for (int i = size - 1; i >= 0; --i) { dest.set(key(i), altValue(i)); } @@ -622,13 +634,13 @@ public void putAll_clobberAll() { } @ParameterizedTest - @EnumSource(TagMapScenario.class) - public void putAll_cloberAll(TagMapScenario scenario) { + @MethodSource("typesAndScenarios") + public void putAll_cloberAll(TagMapType type, TagMapScenario scenario) { int size = scenario.size(); - TagMap orig = scenario.create(); + TagMap orig = scenario.create(type); assertSize(size, orig); - TagMap dest = scenario.create(); + TagMap dest = scenario.create(type); assertSize(size, dest); dest.putAll(orig); @@ -640,13 +652,14 @@ public void putAll_cloberAll(TagMapScenario scenario) { checkIntegrity(dest); } - @Test - public void putAll_clobberAndExtras() { + @ParameterizedTest + @EnumSource(TagMapType.class) + public void putAll_clobberAndExtras(TagMapType type) { int size = randomSize(); - TagMap orig = createTagMap(size); + TagMap orig = createTagMap(type, size); assertSize(size, orig); - TagMap dest = TagMap.create(); + TagMap dest = type.create(); for (int i = size / 2 - 1; i >= 0; --i) { dest.set(key(i), altValue(i)); } @@ -664,10 +677,11 @@ public void putAll_clobberAndExtras() { checkIntegrity(dest); } - @Test - public void removeMany() { + @ParameterizedTest + @EnumSource(TagMapType.class) + public void removeMany(TagMapType type) { int size = randomSize(); - TagMap map = createTagMap(size); + TagMap map = createTagMap(type, size); for (int i = 0; i < size; ++i) { assertEntry(key(i), value(i), map); @@ -689,10 +703,11 @@ public void removeMany() { checkIntegrity(map); } - @Test - public void fillMap() { + @ParameterizedTest + @EnumSource(TagMapType.class) + public void fillMap(TagMapType type) { int size = randomSize(); - TagMap map = TagMap.create(); + TagMap map = type.create(); for (int i = 0; i < size; ++i) { map.set(key(i), i); } @@ -708,10 +723,11 @@ public void fillMap() { checkIntegrity(map); } - @Test - public void fillStringMap() { + @ParameterizedTest + @EnumSource(TagMapType.class) + public void fillStringMap(TagMapType type) { int size = randomSize(); - TagMap map = TagMap.create(); + TagMap map = type.create(); for (int i = 0; i < size; ++i) { map.set(key(i), i); } @@ -727,10 +743,11 @@ public void fillStringMap() { checkIntegrity(map); } - @Test - public void iterator() { + @ParameterizedTest + @EnumSource(TagMapType.class) + public void iterator(TagMapType type) { int size = randomSize(); - TagMap map = createTagMap(size); + TagMap map = createTagMap(type, size); Set keys = new HashSet<>(); for (TagMap.EntryReader entry : map) { @@ -749,10 +766,11 @@ public void iterator() { checkIntegrity(map); } - @Test - public void forEachConsumer() { + @ParameterizedTest + @EnumSource(TagMapType.class) + public void forEachConsumer(TagMapType type) { int size = randomSize(); - TagMap map = createTagMap(size); + TagMap map = createTagMap(type, size); Set keys = new HashSet<>(size); map.forEach((entry) -> keys.add(entry.tag())); @@ -768,10 +786,11 @@ public void forEachConsumer() { checkIntegrity(map); } - @Test - public void forEachBiConsumer() { + @ParameterizedTest + @EnumSource(TagMapType.class) + public void forEachBiConsumer(TagMapType type) { int size = randomSize(); - TagMap map = createTagMap(size); + TagMap map = createTagMap(type, size); Set keys = new HashSet<>(size); map.forEach(keys, (k, entry) -> k.add(entry.tag())); @@ -787,10 +806,11 @@ public void forEachBiConsumer() { checkIntegrity(map); } - @Test - public void forEachTriConsumer() { + @ParameterizedTest + @EnumSource(TagMapType.class) + public void forEachTriConsumer(TagMapType type) { int size = randomSize(); - TagMap map = createTagMap(size); + TagMap map = createTagMap(type, size); Set keys = new HashSet<>(size); map.forEach(keys, "hi", (k, msg, entry) -> keys.add(entry.tag())); @@ -806,10 +826,11 @@ public void forEachTriConsumer() { checkIntegrity(map); } - @Test - public void entrySet() { + @ParameterizedTest + @EnumSource(TagMapType.class) + public void entrySet(TagMapType type) { int size = randomSize(); - TagMap map = createTagMap(size); + TagMap map = createTagMap(type, size); Set> actualEntries = map.entrySet(); assertEquals(size, actualEntries.size()); @@ -824,10 +845,11 @@ public void entrySet() { checkIntegrity(map); } - @Test - public void keySet() { + @ParameterizedTest + @EnumSource(TagMapType.class) + public void keySet(TagMapType type) { int size = randomSize(); - TagMap map = createTagMap(size); + TagMap map = createTagMap(type, size); Set actualKeys = map.keySet(); assertEquals(size, actualKeys.size()); @@ -842,10 +864,11 @@ public void keySet() { checkIntegrity(map); } - @Test - public void values() { + @ParameterizedTest + @EnumSource(TagMapType.class) + public void values(TagMapType type) { int size = randomSize(); - TagMap map = createTagMap(size); + TagMap map = createTagMap(type, size); Collection actualValues = map.values(); assertEquals(size, actualValues.size()); @@ -860,10 +883,11 @@ public void values() { checkIntegrity(map); } - @Test - public void _toString() { + @ParameterizedTest + @EnumSource(TagMapType.class) + public void _toString(TagMapType type) { int size = 4; - TagMap map = createTagMap(size); + TagMap map = createTagMap(type, size); String str = map.toString(); @@ -872,10 +896,11 @@ public void _toString() { } } - @Test - public void stream() { + @ParameterizedTest + @EnumSource(TagMapType.class) + public void stream(TagMapType type) { int size = randomSize(); - TagMap map = createTagMap(size); + TagMap map = createTagMap(type, size); Set keys = map.stream().map(TagMap.EntryReader::tag).collect(Collectors.toSet()); @@ -885,9 +910,10 @@ public void stream() { } } - @Test - public void compute() { - TagMap map = TagMap.create(); + @ParameterizedTest + @EnumSource(TagMapType.class) + public void compute(TagMapType type) { + TagMap map = type.create(); map.set("key", "original"); map.compute("key", (k, v) -> "updated"); @@ -897,9 +923,10 @@ public void compute() { assertEquals("created", map.get("new-key")); } - @Test - public void computeIfAbsent() { - TagMap map = TagMap.create(); + @ParameterizedTest + @EnumSource(TagMapType.class) + public void computeIfAbsent(TagMapType type) { + TagMap map = type.create(); map.set("key", "existing"); map.computeIfAbsent("key", k -> "ignored"); @@ -909,9 +936,10 @@ public void computeIfAbsent() { assertEquals("added", map.get("new-key")); } - @Test - public void computeIfPresent() { - TagMap map = TagMap.create(); + @ParameterizedTest + @EnumSource(TagMapType.class) + public void computeIfPresent(TagMapType type) { + TagMap map = type.create(); map.set("key", "original"); map.computeIfPresent("key", (k, v) -> "updated"); @@ -922,9 +950,9 @@ public void computeIfPresent() { } @ParameterizedTest - @ValueSource(ints = {0, 5, 25, 125}) - public void _toInternalString(int size) { - TagMap tagMap = TagMap.create(); + @MethodSource("typesAndSizes") + public void _toInternalString(TagMapType type, int size) { + TagMap tagMap = type.create(); fillMap(tagMap, size); String str = tagMap.toInternalString(); @@ -934,16 +962,26 @@ public void _toInternalString(int size) { } } + static Stream typesAndScenarios() { + return Arrays.stream(TagMapType.values()) + .flatMap(type -> Arrays.stream(TagMapScenario.values()).map(s -> Arguments.of(type, s))); + } + + static Stream typesAndSizes() { + return Arrays.stream(TagMapType.values()) + .flatMap(type -> IntStream.of(0, 5, 25, 125).mapToObj(size -> Arguments.of(type, size))); + } + static int randomSize() { return ThreadLocalRandom.current().nextInt(16, MANY_SIZE); } - static TagMap createTagMap() { - return createTagMap(randomSize()); + static TagMap createTagMap(TagMapType type) { + return createTagMap(type, randomSize()); } - static TagMap createTagMap(int size) { - TagMap map = TagMap.create(); + static TagMap createTagMap(TagMapType type, int size) { + TagMap map = type.create(); fillMap(map, size); return map; } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapType.java b/internal-api/src/test/java/datadog/trace/api/TagMapType.java new file mode 100644 index 00000000000..bda3a2cf96a --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapType.java @@ -0,0 +1,32 @@ +package datadog.trace.api; + +/** + * The two {@link TagMap} stores, for tests that run against both in one JVM. In production the + * dense-store flag picks one store per JVM; here each value builds maps through its own factory, so + * a test must create, parent and copy through the type it was given, never through {@link TagMap}'s + * static factories, which use the JVM's default store. + */ +public enum TagMapType { + HASH(HashTagMapFactory.INSTANCE), + DENSE(DenseTagMapFactory.INSTANCE); + + final TagMapFactory factory; + + TagMapType(TagMapFactory factory) { + this.factory = factory; + } + + public final TagMap create() { + return factory.create(null); + } + + /** {@link TagMap#createFromParent(TagMap)}, through this store. */ + public final TagMap createFromParent(TagMap parent) { + return TagMap.createFromParent(factory, parent, null); + } + + /** {@link TagMap#EMPTY} for this store. */ + public final TagMap empty() { + return factory.empty(); + } +} From 06419b4a505094e69a92a1d95dc25b1cc395debb Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 15 Oct 2026 08:38:32 -0400 Subject: [PATCH 29/29] Call the default TagMap factory directly when creating from a parent 6015cbbe2c routed createFromParent(TagMap, SizingHint) through the new factory-taking overload, so on the span-creation path the factory was a parameter instead of a static final constant receiver, behind an extra call level. With the dense store off, SpanCreationBenchmark allocated 24 more bytes per span (jdbcClientSpan 1,472 -> 1,496 B, webServerSpan 1,840 -> 1,864 B). The production path calls TagMapFactory.Default.INSTANCE.create(...) directly again; both overloads share the read-through checks in readThroughParent. Both arms are back to 1,472 and 1,840 B (webServerSpan in 6 of 6 forks; one earlier 2-fork run still had one fork at 1,864). Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/datadog/trace/api/TagMap.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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 52628cf93d4..79388d0707f 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -120,13 +120,21 @@ public static final TagMap createFromParent(TagMap parent) { * #createFromParent(TagMap)}. */ public static final TagMap createFromParent(TagMap parent, @Nullable SizingHint hint) { - return createFromParent(TagMapFactory.Default.INSTANCE, parent, hint); + // Calls the JVM's factory directly, a static final constant receiver, rather than through the + // factory-taking overload below: a factory parameter costs the span-creation path an inlining + // level and the constant receiver. + return TagMapFactory.Default.INSTANCE.create(readThroughParent(parent), hint); } /** * {@link #createFromParent(TagMap, SizingHint)} through {@code factory}; tests pick the store. */ static TagMap createFromParent(TagMapFactory factory, TagMap parent, @Nullable SizingHint hint) { + return factory.create(readThroughParent(parent), hint); + } + + /** {@code parent} checked for read-through: it must be frozen, and an empty one is dropped. */ + private static TagMap readThroughParent(TagMap parent) { if (parent != null) { if (!parent.frozen) { throw new IllegalStateException("read-through parent must be frozen"); @@ -136,10 +144,10 @@ static TagMap createFromParent(TagMapFactory factory, TagMap parent, @Nullable S // 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 null; } } - return factory.create(parent, hint); + return parent; } /**