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 74737d0bee4..c9738a15360 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 @@ -37,6 +37,7 @@ import datadog.trace.api.EndpointTracker; import datadog.trace.api.IdGenerationStrategy; import datadog.trace.api.InstrumenterConfig; +import datadog.trace.api.KnownTags; import datadog.trace.api.Pair; import datadog.trace.api.TagMap; import datadog.trace.api.TraceConfig; @@ -653,6 +654,14 @@ private CoreTracer( // preload this enum to avoid triggering classloading on the hot path TraceCollector.PublishState.values(); + // Dense known-tag store (experimental, OFF by default): registering the KnownTagCodec resolver + // flips the dense store live so known tags store without a per-tag Entry. Gated by a system + // property for A/B benchmarking; when off, keyOf stays a no-op and tag storage is byte-identical + // to today. Promote to a Config flag if this becomes a permanent rollout. + if (Boolean.getBoolean("dd.trace.dense.tags.enabled")) { + KnownTags.init(); + } + if (reportInTracerFlare) { TracerFlare.addReporter(this); } 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..6c11f8bc212 --- /dev/null +++ b/internal-api/src/jmh/java/datadog/trace/api/DenseStoreAllocBenchmark.java @@ -0,0 +1,148 @@ +package datadog.trace.api; + +import datadog.trace.bootstrap.instrumentation.api.Tags; +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; + +/** + * Deterministic allocation A/B for the dense known-tag store, using the REAL {@link KnownTags} + * resolver (a {@code StringIndex} probe + a constant-returning {@code switch} — allocation-free, + * exactly like production). An earlier synthetic prefix resolver allocated in {@code keyOf} + * (substring) and {@code nameOf} (concat), contaminating the dense arm; this measures the store, + * not the resolver. + * + *
Models how a real span's tags route: {@code today} = all custom (what ships now — every tag + * buckets, since nothing is registered as known), {@code dense} = the same tag count with a + * realistic fraction routed to the dense store (real known tag names) and the rest custom. Run with + * {@code -prof gc}; the {@code gc.alloc.rate.norm} (B/op) delta at the same {@code tagCount} is + * what enabling the dense store does to a real span's per-build allocation. + * + *
Results — buildMap, JDK 17 (Zulu 17.0.7, Apple Silicon), {@code -prof gc -f 1 -wi 2 -i 3}, + * 2026-07-08. Allocation is deterministic (±0.001 B/op); throughput on this run is NOT + * trustworthy (single fork, short) — read B/op only. + * + *
{@code
+ * scenario tagCount=7 tagCount=12
+ * today 408 B/op 704 B/op
+ * dense 376 B/op 416 B/op
+ * allKnown 176 B/op 400 B/op
+ * }
+ *
+ * Gate met: {@code dense < today} at both counts (the over-provision artifact is gone). The + * Entry-less win scales with the known-tag fraction — ~8% at 7 tags (~70% known), ~41% at 12; + * {@code allKnown} (the codegen endgame / read-through parent shape) reaches ~57% at 7. + * + *
Serialize paths (same run, B/op). {@code buildAndSerialize} (alloc-free {@code forEach} + * flyweight) adds a flat +16 B/op over {@code buildMap} in every scenario (7: 392, 12: 432 dense). + * {@code buildAndSerializeViaIterator} — the {@code EntryReader} enhanced-for modeling the count + * pre-pass at {@code TraceMapperV0_4:95} — adds a CONSTANT per-call cost (+56 custom / +80 dense, + * identical at 7 and 12 tags): that flat-vs-tagCount signature is the {@code EntryReaderIterator} + * OBJECT, NOT per-tag Entry — the iterator reuses a dense flyweight (TagMap:2182/2652). So the + * dense win SURVIVES serialization; the only nit is {@code iterator()} allocating one Iterator per + * call, which {@code forEach} avoids and which can be recycled away. + */ +@State(Scope.Benchmark) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@Warmup(iterations = 2, time = 2) +@Measurement(iterations = 3, time = 2) +@Fork(1) +@Threads(1) +public class DenseStoreAllocBenchmark { + + // Real stored (dense-routed) tag names — a realistic web/db span's known set. + static final String[] KNOWN = + new String[] { + DDTags.BASE_SERVICE, + Tags.VERSION, + Tags.COMPONENT, + Tags.SPAN_KIND, + Tags.HTTP_METHOD, + Tags.HTTP_ROUTE, + Tags.DB_TYPE, + Tags.DB_INSTANCE, + Tags.PEER_HOSTNAME, + Tags.DB_USER, + DDTags.LANGUAGE_TAG_KEY, + Tags.PEER_PORT, + }; + + // today = all custom (all bucket, what ships now); dense = ~70% known + custom (a real span); + // allKnown = 100% known (the trace-tier read-through parent's shape — exercises lazy buckets). + @Param({"today", "dense", "allKnown"}) + String scenario; + + @Param({"7", "12"}) + int tagCount; + + private String[] keys; + private String[] values; + + @Setup(Level.Trial) + public void setup() { + KnownTags.init(); // registers the real (allocation-free) resolver + int knownCount; + if ("allKnown".equals(scenario)) { + knownCount = tagCount; // 100% known (<= KNOWN.length) + } else if ("dense".equals(scenario)) { + knownCount = (tagCount * 7) / 10; // ~70% known + custom + } else { + knownCount = 0; // today: all custom (all bucket) + } + this.keys = new String[tagCount]; + this.values = new String[tagCount]; + for (int i = 0; i < tagCount; i++) { + this.keys[i] = i < knownCount ? KNOWN[i] : "custom.tag." + i; + this.values[i] = "value-" + i; + } + } + + @Benchmark + public TagMap buildMap() { + TagMap m = TagMap.create(16); + for (int i = 0; i < tagCount; i++) { + m.set(keys[i], values[i]); + } + return m; + } + + @Benchmark + public void buildAndSerialize(Blackhole bh) { + TagMap m = TagMap.create(16); + for (int i = 0; i < tagCount; i++) { + m.set(keys[i], values[i]); + } + // forEach: the alloc-free flyweight emit for dense + m.forEach(reader -> bh.consume(reader.objectValue())); + bh.consume(m); + } + + @Benchmark + public void buildAndSerializeViaIterator(Blackhole bh) { + TagMap m = TagMap.create(16); + for (int i = 0; i < tagCount; i++) { + m.set(keys[i], values[i]); + } + // models the REAL serializer's count pre-pass (TraceMapperV0_4:95). The EntryReader iterator + // uses a reused dense flyweight (NO per-tag Entry alloc — TagMap:2182/2652), so the dense win + // SURVIVES; the only extra cost vs forEach is the EntryReaderIterator object itself (a fixed + // per-call cost, constant across tagCount — not per-tag). forEach avoids even that. + for (TagMap.EntryReader reader : m) { + bh.consume(reader.objectValue()); + } + bh.consume(m); + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java new file mode 100644 index 00000000000..7ed9dff633a --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/KnownTagCodec.java @@ -0,0 +1,177 @@ +package datadog.trace.api; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +/** + * Registry for generated tag ID ↔ name resolution. The code generator populates this at tracer init + * via {@link #register(Resolver)}. Once registered, HotSpot CHA devirtualizes and inlines the + * resolver's switch, making {@link #nameOf}/{@link #keyOf} effectively zero-overhead. + */ +public final class KnownTagCodec { + // Plain (non-volatile) fast-path flag: false until a resolver is ever registered. A plain read is + // free and hoistable, unlike a volatile read of `resolver` (costly on weak memory models such as + // ARM). A stale `false` is benign — callers treat the tag as unknown and use the hash buckets, + // which is correct, just unoptimized; the next read after publication takes the slot path. + private static boolean active; + + private static volatile Resolver resolver; + + /** Fast-path gate: true once a resolver has been registered. */ + public static boolean isActive() { + return active; + } + + /* + * tagId bit layout: [63 intercepted] [62-48 globalSerial (15 bits)] [47-32 fieldPos] + * [31-0 nameHash]. Bit 63 (the sign bit) marks a tag the tag interceptor must see, so the check + * is a single {@code tagId < 0}. globalSerial is globally unique per known tag; fieldPos is its + * slot in the global positional layout (TagMap.knownEntries index); nameHash is + * TagMap.Entry#_hash(name) and is layout-independent. Unknown (string-only) tags have the upper + * 32 bits zero. NOTE: TagMap.Entry decodes nameHash inline as (int) tagId on its hot path, so the + * low-32 encoding here must stay in sync with that. + */ + public static int globalSerial(long tagId) { + return (int) ((tagId >>> 48) & 0x7FFF); + } + + /** + * Flag bit (the sign bit) marking a tag the tag interceptor must process — reserved/"virtual" + * tags AND intercepted-but-stored tags (e.g. http.method, which the interceptor side-effects and + * also stores). Encoded in the id so {@code DDSpanContext.setTag(long)} can route with a single + * sign test ({@link #isIntercepted}) instead of resolving the name. Non-intercepted tags (peer.*, + * base.service, …) leave it clear and take the fast store path. Must agree with the interceptor's + * name-based {@code needsIntercept} for every assigned id. + */ + public static final long INTERCEPTED = Long.MIN_VALUE; // 1L << 63 + + /** True if the tagId is flagged for tag-interceptor processing. */ + public static boolean isIntercepted(long tagId) { + return tagId < 0L; + } + + /** Returns the tagId with the {@link #INTERCEPTED} flag set. */ + public static long intercepted(long tagId) { + return tagId | INTERCEPTED; + } + + public static int fieldPos(long tagId) { + return (int) ((tagId >>> 32) & 0xFFFF); + } + + public static int nameHash(long tagId) { + return (int) tagId; + } + + /** + * globalSerial partition. {@code [1, FIRST_STORED_SERIAL)} is reserved for "virtual" tags that + * are specially handled (redirected to span fields or processed by the tag interceptor) and are + * NOT stored in the TagMap — these are hand-assigned in tracer core. {@code [FIRST_STORED_SERIAL, + * ..]} is for generated convention tags that ARE stored (slotted/bucketed). {@code globalSerial + * == 0} means unknown / string-only. Both core and the code generator must agree on this + * boundary. + */ + public static final int FIRST_STORED_SERIAL = 256; + + /** True if the tagId names a reserved "virtual"/specially-handled tag (not stored in the map). */ + public static boolean isReserved(long tagId) { + int globalSerial = globalSerial(tagId); + return globalSerial > 0 && globalSerial < FIRST_STORED_SERIAL; + } + + /** True if the tagId names a generated, map-stored (slotted/bucketed) tag. */ + public static boolean isStored(long tagId) { + return globalSerial(tagId) >= FIRST_STORED_SERIAL; + } + + /** + * Sentinel {@code fieldPos} meaning "no positional slot". It is the maximum value the 16-bit + * fieldPos field can hold, so it always compares {@code >= slotCount()} and routes to the hash + * buckets rather than the fast positional array. Two kinds of tagId use it: + * + *
Reserved serials {@code [1, KnownTagCodec.FIRST_STORED_SERIAL)} name "virtual" tags handled by + * the tag interceptor / span fields and are NOT stored in the {@code TagMap}; their {@code + * fieldPos} is the {@link KnownTagCodec#NO_SLOT} sentinel that is out of slot range, so any + * incidental store routes to the hash buckets rather than a positional slot. Serials {@code >= + * FIRST_STORED_SERIAL} name stored tags that slot/bucket normally (or, with {@code NO_SLOT}, are + * stored bucket-only). + * + *
The resolver registers on class initialization, so simply referencing any constant here makes + * tag-id resolution live before the first span is built. + * + *
Slice-1 note (keyOf substrate): the {@code fieldPos} assignments below (and {@link
+ * #SLOT_COUNT}) describe a single universal positional layout (slots 0..25). That layout is
+ * currently dormant — no dense store consumes {@code fieldPos} yet — and is provisional: the
+ * dense-store slice replaces the universal layout with per-role / per-type sizing (see the
+ * over-provision finding in {@code dense-tagmap-design.md}). {@code keyOf}/{@code nameOf} depend
+ * only on {@code globalSerial} + name, not {@code fieldPos}, so the ids themselves are stable
+ * across any layout scheme.
+ */
+public final class KnownTags {
+ // slot count = (max stored fieldPos) + 1. Stored tags use fieldPos 0..25. PROVISIONAL universal
+ // layout — see the slice-1 note above; the dense-store slice supersedes this with role/type
+ // sizing.
+ static final int SLOT_COUNT = 26;
+
+ // ---- reserved / virtual (tag-interceptor handled, not stored) ----
+ // Reserved tags are always intercepted -> set the INTERCEPTED flag.
+ public static final int ERROR_SERIAL = 1;
+ public static final long ERROR_ID =
+ KnownTagCodec.intercepted(KnownTagCodec.tagId(ERROR_SERIAL, Tags.ERROR));
+
+ // ---- stored (slotted / bucketed) ----
+ public static final int PARENT_ID_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL;
+ public static final long PARENT_ID = KnownTagCodec.tagId(PARENT_ID_SERIAL, 0, DDTags.PARENT_ID);
+
+ // common (process-constant) tags added by InternalTagsAdder to ~every span
+ public static final int BASE_SERVICE_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 1;
+ public static final long BASE_SERVICE_ID =
+ KnownTagCodec.tagId(BASE_SERVICE_SERIAL, 1, DDTags.BASE_SERVICE);
+
+ public static final int VERSION_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 2;
+ public static final long VERSION_ID = KnownTagCodec.tagId(VERSION_SERIAL, 2, Tags.VERSION);
+
+ // build-time-known constant tags merged into defaultSpanTags (see CoreTracer.withTracerTags).
+ // "env" is a base-mixin tag; the *_ENABLED flags are product-mixin tags. Hand-assigned for now.
+ public static final String ENV = "env";
+ public static final int ENV_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 3;
+ public static final long ENV_ID = KnownTagCodec.tagId(ENV_SERIAL, 3, ENV);
+
+ public static final int DJM_ENABLED_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 4;
+ public static final long DJM_ENABLED_ID =
+ KnownTagCodec.tagId(DJM_ENABLED_SERIAL, 4, DDTags.DJM_ENABLED);
+
+ public static final int DSM_ENABLED_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 5;
+ public static final long DSM_ENABLED_ID =
+ KnownTagCodec.tagId(DSM_ENABLED_SERIAL, 5, DDTags.DSM_ENABLED);
+
+ // common tags added by the tag post-processors (RemoteHostnameAdder / IntegrationAdder /
+ // ServiceNameSourceAdder). Not intercepted; stored.
+ public static final int TRACER_HOST_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 6;
+ public static final long TRACER_HOST_ID =
+ KnownTagCodec.tagId(TRACER_HOST_SERIAL, 6, DDTags.TRACER_HOST);
+
+ public static final int INTEGRATION_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 7;
+ public static final long INTEGRATION_ID =
+ KnownTagCodec.tagId(INTEGRATION_SERIAL, 7, DDTags.DD_INTEGRATION);
+
+ public static final int SVC_SRC_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 8;
+ public static final long SVC_SRC_ID = KnownTagCodec.tagId(SVC_SRC_SERIAL, 8, DDTags.DD_SVC_SRC);
+
+ // peer.service tags, read/written by PeerServiceCalculator (post-processor; uses Map put/get that
+ // bypass the interceptor). peer.service is intercepted on the set-path but STORED, so it slots.
+ public static final int PEER_SERVICE_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 9;
+ public static final long PEER_SERVICE_ID =
+ KnownTagCodec.intercepted(KnownTagCodec.tagId(PEER_SERVICE_SERIAL, 9, Tags.PEER_SERVICE));
+
+ public static final int PEER_SERVICE_REMAPPED_FROM_SERIAL =
+ KnownTagCodec.FIRST_STORED_SERIAL + 10;
+ public static final long PEER_SERVICE_REMAPPED_FROM_ID =
+ KnownTagCodec.tagId(PEER_SERVICE_REMAPPED_FROM_SERIAL, 10, DDTags.PEER_SERVICE_REMAPPED_FROM);
+
+ // HTTP tags read by HttpEndpointPostProcessor. http.method/http.url are intercepted-but-stored
+ // (interceptTag side-effects then returns false → stored); http.route is not intercepted. All
+ // stored, so the string set-path slots them via keyOf and the id reads here find them.
+ public static final int HTTP_METHOD_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 11;
+ public static final long HTTP_METHOD_ID =
+ KnownTagCodec.intercepted(KnownTagCodec.tagId(HTTP_METHOD_SERIAL, 11, Tags.HTTP_METHOD));
+
+ public static final int HTTP_ROUTE_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 12;
+ public static final long HTTP_ROUTE_ID =
+ KnownTagCodec.tagId(HTTP_ROUTE_SERIAL, 12, Tags.HTTP_ROUTE);
+
+ public static final int HTTP_URL_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 13;
+ public static final long HTTP_URL_ID =
+ KnownTagCodec.intercepted(KnownTagCodec.tagId(HTTP_URL_SERIAL, 13, Tags.HTTP_URL));
+
+ // peer connection tags set by BaseDecorator.onPeerConnection on ~every client/producer span.
+ // Not intercepted; stored. Slotted (common across client instrumentations).
+ public static final int PEER_HOSTNAME_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 14;
+ public static final long PEER_HOSTNAME_ID =
+ KnownTagCodec.tagId(PEER_HOSTNAME_SERIAL, 14, Tags.PEER_HOSTNAME);
+
+ public static final int PEER_HOST_IPV4_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 15;
+ public static final long PEER_HOST_IPV4_ID =
+ KnownTagCodec.tagId(PEER_HOST_IPV4_SERIAL, 15, Tags.PEER_HOST_IPV4);
+
+ public static final int PEER_HOST_IPV6_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 16;
+ public static final long PEER_HOST_IPV6_ID =
+ KnownTagCodec.tagId(PEER_HOST_IPV6_SERIAL, 16, Tags.PEER_HOST_IPV6);
+
+ public static final int PEER_PORT_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 17;
+ public static final long PEER_PORT_ID = KnownTagCodec.tagId(PEER_PORT_SERIAL, 17, Tags.PEER_PORT);
+
+ // Universal decorator tags — set on ~every span (component/span.kind via Base/Server/Client
+ // decorators, language via ServerDecorator). span.kind is intercepted (setSpanKindOrdinal).
+ public static final int COMPONENT_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 18;
+ public static final long COMPONENT_ID = KnownTagCodec.tagId(COMPONENT_SERIAL, 18, Tags.COMPONENT);
+
+ public static final int SPAN_KIND_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 19;
+ public static final long SPAN_KIND_ID =
+ KnownTagCodec.intercepted(KnownTagCodec.tagId(SPAN_KIND_SERIAL, 19, Tags.SPAN_KIND));
+
+ public static final int LANGUAGE_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 20;
+ public static final long LANGUAGE_ID =
+ KnownTagCodec.tagId(LANGUAGE_SERIAL, 20, DDTags.LANGUAGE_TAG_KEY);
+
+ // JDBC / database-client tags — set on every db span (58% of petclinic spans). Not intercepted
+ // (only db.statement is, and that's handled separately).
+ public static final int DB_TYPE_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 21;
+ public static final long DB_TYPE_ID = KnownTagCodec.tagId(DB_TYPE_SERIAL, 21, Tags.DB_TYPE);
+
+ public static final int DB_INSTANCE_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 22;
+ public static final long DB_INSTANCE_ID =
+ KnownTagCodec.tagId(DB_INSTANCE_SERIAL, 22, Tags.DB_INSTANCE);
+
+ public static final int DB_USER_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 23;
+ public static final long DB_USER_ID = KnownTagCodec.tagId(DB_USER_SERIAL, 23, Tags.DB_USER);
+
+ public static final int DB_OPERATION_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 24;
+ public static final long DB_OPERATION_ID =
+ KnownTagCodec.tagId(DB_OPERATION_SERIAL, 24, Tags.DB_OPERATION);
+
+ public static final int DB_POOL_NAME_SERIAL = KnownTagCodec.FIRST_STORED_SERIAL + 25;
+ public static final long DB_POOL_NAME_ID =
+ KnownTagCodec.tagId(DB_POOL_NAME_SERIAL, 25, Tags.DB_POOL_NAME);
+
+ // Open-addressed name -> id table backing keyOf (data, not a switch): scales flat as the known
+ // set grows, where a generated switch eventually falls off the inline threshold. KEYOF_NAMES and
+ // KEYOF_VALUES are parallel; the table places names by hash and a parallel ids[] by slot.
+ private static final String[] KEYOF_NAMES = {
+ Tags.ERROR,
+ DDTags.PARENT_ID,
+ DDTags.BASE_SERVICE,
+ Tags.VERSION,
+ ENV,
+ DDTags.DJM_ENABLED,
+ DDTags.DSM_ENABLED,
+ DDTags.TRACER_HOST,
+ DDTags.DD_INTEGRATION,
+ DDTags.DD_SVC_SRC,
+ Tags.PEER_SERVICE,
+ DDTags.PEER_SERVICE_REMAPPED_FROM,
+ Tags.HTTP_METHOD,
+ Tags.HTTP_ROUTE,
+ Tags.HTTP_URL,
+ Tags.PEER_HOSTNAME,
+ Tags.PEER_HOST_IPV4,
+ Tags.PEER_HOST_IPV6,
+ Tags.PEER_PORT,
+ Tags.COMPONENT,
+ Tags.SPAN_KIND,
+ DDTags.LANGUAGE_TAG_KEY,
+ Tags.DB_TYPE,
+ Tags.DB_INSTANCE,
+ Tags.DB_USER,
+ Tags.DB_OPERATION,
+ Tags.DB_POOL_NAME,
+ };
+
+ private static final long[] KEYOF_VALUES = {
+ ERROR_ID,
+ PARENT_ID,
+ BASE_SERVICE_ID,
+ VERSION_ID,
+ ENV_ID,
+ DJM_ENABLED_ID,
+ DSM_ENABLED_ID,
+ TRACER_HOST_ID,
+ INTEGRATION_ID,
+ SVC_SRC_ID,
+ PEER_SERVICE_ID,
+ PEER_SERVICE_REMAPPED_FROM_ID,
+ HTTP_METHOD_ID,
+ HTTP_ROUTE_ID,
+ HTTP_URL_ID,
+ PEER_HOSTNAME_ID,
+ PEER_HOST_IPV4_ID,
+ PEER_HOST_IPV6_ID,
+ PEER_PORT_ID,
+ COMPONENT_ID,
+ SPAN_KIND_ID,
+ LANGUAGE_ID,
+ DB_TYPE_ID,
+ DB_INSTANCE_ID,
+ DB_USER_ID,
+ DB_OPERATION_ID,
+ DB_POOL_NAME_ID,
+ };
+
+ // Static-final raw arrays placed by StringIndex.Support: the JIT folds these refs to constants on
+ // the keyOf hot path (the fastest of StringIndex's three usage modes — no instance dereference).
+ private static final int[] KEYOF_HASHES;
+ private static final String[] KEYOF_KEYS;
+ private static final long[] KEYOF_IDS;
+
+ static {
+ StringIndex.Data data = StringIndex.Support.create(KEYOF_NAMES);
+ long[] ids = new long[data.names.length];
+ for (int j = 0; j < KEYOF_NAMES.length; j++) {
+ ids[StringIndex.Support.indexOf(data.hashes, data.names, KEYOF_NAMES[j])] = KEYOF_VALUES[j];
+ }
+ KEYOF_HASHES = data.hashes;
+ KEYOF_KEYS = data.names;
+ KEYOF_IDS = ids;
+ }
+
+ static final KnownTagCodec.Resolver RESOLVER =
+ new KnownTagCodec.Resolver() {
+ @Override
+ public String nameOf(long tagId) {
+ switch (KnownTagCodec.globalSerial(tagId)) {
+ case ERROR_SERIAL:
+ return Tags.ERROR;
+ case PARENT_ID_SERIAL:
+ return DDTags.PARENT_ID;
+ case BASE_SERVICE_SERIAL:
+ return DDTags.BASE_SERVICE;
+ case VERSION_SERIAL:
+ return Tags.VERSION;
+ case ENV_SERIAL:
+ return ENV;
+ case DJM_ENABLED_SERIAL:
+ return DDTags.DJM_ENABLED;
+ case DSM_ENABLED_SERIAL:
+ return DDTags.DSM_ENABLED;
+ case TRACER_HOST_SERIAL:
+ return DDTags.TRACER_HOST;
+ case INTEGRATION_SERIAL:
+ return DDTags.DD_INTEGRATION;
+ case SVC_SRC_SERIAL:
+ return DDTags.DD_SVC_SRC;
+ case PEER_SERVICE_SERIAL:
+ return Tags.PEER_SERVICE;
+ case PEER_SERVICE_REMAPPED_FROM_SERIAL:
+ return DDTags.PEER_SERVICE_REMAPPED_FROM;
+ case HTTP_METHOD_SERIAL:
+ return Tags.HTTP_METHOD;
+ case HTTP_ROUTE_SERIAL:
+ return Tags.HTTP_ROUTE;
+ case HTTP_URL_SERIAL:
+ return Tags.HTTP_URL;
+ case PEER_HOSTNAME_SERIAL:
+ return Tags.PEER_HOSTNAME;
+ case PEER_HOST_IPV4_SERIAL:
+ return Tags.PEER_HOST_IPV4;
+ case PEER_HOST_IPV6_SERIAL:
+ return Tags.PEER_HOST_IPV6;
+ case PEER_PORT_SERIAL:
+ return Tags.PEER_PORT;
+ case COMPONENT_SERIAL:
+ return Tags.COMPONENT;
+ case SPAN_KIND_SERIAL:
+ return Tags.SPAN_KIND;
+ case LANGUAGE_SERIAL:
+ return DDTags.LANGUAGE_TAG_KEY;
+ case DB_TYPE_SERIAL:
+ return Tags.DB_TYPE;
+ case DB_INSTANCE_SERIAL:
+ return Tags.DB_INSTANCE;
+ case DB_USER_SERIAL:
+ return Tags.DB_USER;
+ case DB_OPERATION_SERIAL:
+ return Tags.DB_OPERATION;
+ case DB_POOL_NAME_SERIAL:
+ return Tags.DB_POOL_NAME;
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public int slotCount() {
+ return SLOT_COUNT;
+ }
+
+ @Override
+ public long keyOf(String name) {
+ int slot = StringIndex.Support.indexOf(KEYOF_HASHES, KEYOF_KEYS, name);
+ return slot < 0 ? 0L : KEYOF_IDS[slot];
+ }
+ };
+
+ static {
+ KnownTagCodec.register(RESOLVER);
+ }
+
+ /**
+ * Forces resolver registration. Merely invoking this static method runs {@code Disjoint from {@link #buckets} by construction: known-ness is global ({@code keyOf} is
+ * deterministic), so a known tag is ALWAYS dense and never bucketed, and vice-versa. That
+ * disjointness keeps read-through shadow checks within-region — a parent dense entry can only be
+ * shadowed by a local dense entry of the same id, a parent bucket entry only by a local bucket
+ * entry — so the existing bucket read-through code is unchanged.
+ *
+ * {@link #size} counts bucket entries only; {@link #knownCount} counts dense entries; the
+ * local total is {@code size + knownCount}.
+ */
+ private long[] knownIds;
+
+ private Object[] knownValues;
+ private int knownCount;
+
+ private static final int KNOWN_INIT_CAP =
+ 12; // generous per-type max stopgap; exact per-type sizing comes with the tag registry
+
/**
* Optional frozen parent for read-through (level-split phase 1). When non-null, reads that miss
* the local buckets fall through to the parent; a local entry shadows the parent's (local-wins).
@@ -1038,8 +1070,9 @@ public TagMap() {
* 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];
+ // Start on the shared empty buckets; materializeBuckets() COWs to a private power-of-two array
+ // on the first custom-tag write. All-known maps never allocate buckets.
+ this.buckets = EMPTY_BUCKETS;
this.size = 0;
this.frozen = false;
this.parent = parent;
@@ -1060,17 +1093,24 @@ public boolean isOptimized() {
@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();
}
/**
* Exact count of parent entries not shadowed locally or tombstoned (the read-through addition).
*/
private int visibleParentCount() {
+ int count = 0;
+ // parent dense entries not shadowed by a local dense entry / tombstoned
+ long[] parentIds = this.parent.knownIds;
+ int parentKnownCount = this.parent.knownCount;
+ for (int i = 0; i < parentKnownCount; ++i) {
+ if (!this.parentDenseHidden(parentIds[i])) count++;
+ }
Object[] parentBuckets = this.parent.buckets;
Object[] thisBuckets = this.buckets;
- int count = 0;
for (int i = 0; i < parentBuckets.length; ++i) {
Object parentBucket = parentBuckets[i];
Object localBucket = thisBuckets[i];
@@ -1094,7 +1134,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;
@@ -1110,12 +1150,15 @@ public boolean isEmpty() {
}
public boolean isDefinitelyEmpty() {
- return this.size == 0 && (this.parent == null || this.parent.isDefinitelyEmpty());
+ return this.size == 0
+ && this.knownCount == 0
+ && (this.parent == null || this.parent.isDefinitelyEmpty());
}
public int estimateSize() {
- // Upper bound: local + parent, ignoring read-through shadowing/removals (over-counts).
- return this.parent == null ? this.size : this.size + this.parent.estimateSize();
+ // Upper bound: local (buckets + dense) + parent, ignoring shadowing/removals (over-counts).
+ int local = this.size + this.knownCount;
+ return this.parent == null ? local : local + this.parent.estimateSize();
}
@Deprecated
@@ -1241,8 +1284,15 @@ public Entry getEntry(String tag) {
return parent.getEntry(tag);
}
- /** 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 (dense then buckets) — no read-through. */
private Entry getLocalEntry(String tag) {
+ // Known tags live in the dense store; resolve identity and check there first. keyOf is a no-op
+ // (returns 0 -> isStored false) until a resolver is registered, so this is inert in production.
+ long id = KnownTagCodec.keyOf(tag);
+ if (KnownTagCodec.isStored(id)) {
+ Object known = this.knownRawValue(id);
+ return known == null ? null : Entry.newAnyEntry(tag, known);
+ }
Object[] thisBuckets = this.buckets;
int hash = TagMap.Entry._hash(tag);
return findInBucket(thisBuckets[hash & (thisBuckets.length - 1)], hash, tag);
@@ -1276,10 +1326,94 @@ private boolean parentEntryVisibleInBucket(Object localBucket, Entry parentEntry
== null; // not shadowed by a local entry
}
+ // ---- dense known-tag store (see the knownIds field doc)
+ // ----------------------------------------
+
+ /**
+ * Linear scan of the dense store for {@code tagId}, returning its index or -1. Ids are canonical
+ * (the only way one enters is {@link KnownTagCodec#keyOf} or a {@code KnownTags} constant, both
+ * canonical), so a full {@code long} compare is exact and cheaper than extracting globalSerial.
+ */
+ private int knownIndexOf(long tagId) {
+ long[] ids = this.knownIds;
+ int n = this.knownCount;
+ for (int i = 0; i < n; ++i) {
+ if (ids[i] == tagId) return i;
+ }
+ return -1;
+ }
+
+ private void ensureKnownCapacity() {
+ 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);
+ }
+ }
+
+ /**
+ * Stores a known tag's value densely (no {@link Entry} alloc). Overwrites in place when present
+ * (returning the prior value materialized as an Entry, per the {@code Map} contract — usually
+ * discarded by {@code set}); otherwise appends, growing x2 as needed.
+ */
+ private Entry putKnownValue(long tagId, Object value) {
+ int i = this.knownIndexOf(tagId);
+ if (i >= 0) {
+ Object prior = this.knownValues[i];
+ this.knownValues[i] = value;
+ return materializeKnown(tagId, prior);
+ }
+ this.ensureKnownCapacity();
+ int slot = this.knownCount++;
+ this.knownIds[slot] = tagId;
+ this.knownValues[slot] = value;
+ return null;
+ }
+
+ /** Raw dense value for {@code tagId}, or {@code null} when absent (no Entry, no boxing). */
+ private Object knownRawValue(long tagId) {
+ int i = this.knownIndexOf(tagId);
+ return i < 0 ? null : this.knownValues[i];
+ }
+
+ /**
+ * Removes a known tag from the dense store (swap-with-last), returning the prior Entry or null.
+ */
+ private Entry removeKnown(long tagId) {
+ int i = this.knownIndexOf(tagId);
+ 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;
+ return materializeKnown(tagId, prior);
+ }
+
+ /** Materializes a transient Entry for a dense (id, value) pair — only on explicit get/iterate. */
+ private static Entry materializeKnown(long tagId, Object value) {
+ return Entry.newAnyEntry(KnownTagCodec.nameOf(tagId), value);
+ }
+
+ /**
+ * Whether a parent dense entry is hidden through this child: shadowed by a local dense entry of
+ * the same id, or tombstoned. (Disjointness means a parent dense entry can't be shadowed by a
+ * local bucket entry — known tags never bucket — so no bucket check is needed here.)
+ */
+ private boolean parentDenseHidden(long tagId) {
+ if (this.knownIndexOf(tagId) >= 0) return true; // shadowed by a local dense entry
+ return this.removedFromParent != null
+ && this.removedFromParent.contains(KnownTagCodec.nameOf(tagId)); // tombstoned
+ }
+
@Deprecated
@Override
public Object put(String tag, Object value) {
- TagMap.Entry entry = this.getAndSet(Entry.newAnyEntry(tag, value));
+ TagMap.Entry entry = this.getAndSet(tag, value);
return entry == null ? null : entry.objectValue();
}
@@ -1290,39 +1424,75 @@ public void set(TagMap.EntryReader newEntryReader) {
this.getAndSet(newEntryReader.entry());
}
+ // The set(String, ...) family delegates to the matching getAndSet(String, ...) overload, which
+ // routes known tags to the dense store BEFORE constructing any Entry (so a known-tag set
+ // allocates no Entry). The discarded return is free on the common first-set path (prior == null).
public void set(String tag, Object value) {
- this.getAndSet(Entry.newAnyEntry(tag, value));
+ this.getAndSet(tag, value);
}
public void set(String tag, CharSequence value) {
- this.getAndSet(Entry.newObjectEntry(tag, value));
+ this.getAndSet(tag, value);
}
public void set(String tag, boolean value) {
- this.getAndSet(Entry.newBooleanEntry(tag, value));
+ this.getAndSet(tag, value);
}
public void set(String tag, int value) {
- this.getAndSet(Entry.newIntEntry(tag, value));
+ this.getAndSet(tag, value);
}
public void set(String tag, long value) {
- this.getAndSet(Entry.newLongEntry(tag, value));
+ this.getAndSet(tag, value);
}
public void set(String tag, float value) {
- this.getAndSet(Entry.newFloatEntry(tag, value));
+ this.getAndSet(tag, value);
}
public void set(String tag, double value) {
- this.getAndSet(Entry.newDoubleEntry(tag, value));
+ this.getAndSet(tag, value);
}
public Entry getAndSet(Entry newEntry) {
if (newEntry == null) {
return null;
}
+ // Entry-based path (set(EntryReader), entry-sharing). The Entry is already constructed by the
+ // caller, so a known tag keeps its value densely and drops the Entry. The hot string/typed
+ // setters route to dense BEFORE constructing an Entry (see set/getAndSet(String, ...)) so a
+ // known-tag set allocates no Entry at all.
+ long id = KnownTagCodec.keyOf(newEntry.tag);
+ return KnownTagCodec.isStored(id)
+ ? this.getAndSetKnown(id, newEntry.tag, newEntry.objectValue())
+ : this.getAndSetBucket(newEntry);
+ }
+
+ /**
+ * Stores a known tag's (resolved id, value) densely with NO Entry retained — the alloc win.
+ * Returns the prior value materialized as an Entry (Map contract); {@code set} discards it.
+ */
+ private Entry getAndSetKnown(long id, String tag, Object value) {
+ this.checkWriteAccess();
+ if (this.removedFromParent != null) {
+ this.removedFromParent.remove(tag);
+ }
+ return this.putKnownValue(id, value);
+ }
+
+ /** Copy-on-write the shared empty buckets to a private array on the first bucket write. */
+ private Object[] materializeBuckets() {
+ Object[] b = this.buckets;
+ if (b == EMPTY_BUCKETS) {
+ b = new Object[1 << 4];
+ this.buckets = b;
+ }
+ return b;
+ }
+ /** Stores an entry in the hash buckets — the unknown/custom-tag path. */
+ private Entry getAndSetBucket(Entry newEntry) {
this.checkWriteAccess();
// Re-setting a key clears any read-through tombstone for it (the new value overrides the
@@ -1331,7 +1501,7 @@ public Entry getAndSet(Entry newEntry) {
this.removedFromParent.remove(newEntry.tag);
}
- Object[] thisBuckets = this.buckets;
+ Object[] thisBuckets = this.materializeBuckets();
int newHash = newEntry.hash();
int bucketIndex = newHash & (thisBuckets.length - 1);
@@ -1376,32 +1546,56 @@ public Entry getAndSet(Entry newEntry) {
return null;
}
+ // Each getAndSet(String, ...) resolves keyOf FIRST: a known tag stores its value densely with no
+ // Entry (boxing the primitive only on this branch); a custom tag falls back to the typed Entry
+ // (no boxing for primitives, preserving the bucket store's no-box property).
public Entry getAndSet(String tag, Object value) {
- return this.getAndSet(Entry.newAnyEntry(tag, value));
+ long id = KnownTagCodec.keyOf(tag);
+ return KnownTagCodec.isStored(id)
+ ? this.getAndSetKnown(id, tag, value)
+ : this.getAndSetBucket(Entry.newAnyEntry(tag, value));
}
public Entry getAndSet(String tag, CharSequence value) {
- return this.getAndSet(Entry.newObjectEntry(tag, value));
+ long id = KnownTagCodec.keyOf(tag);
+ return KnownTagCodec.isStored(id)
+ ? this.getAndSetKnown(id, tag, value)
+ : this.getAndSetBucket(Entry.newObjectEntry(tag, value));
}
public TagMap.Entry getAndSet(String tag, boolean value) {
- return this.getAndSet(Entry.newBooleanEntry(tag, value));
+ long id = KnownTagCodec.keyOf(tag);
+ return KnownTagCodec.isStored(id)
+ ? this.getAndSetKnown(id, tag, Boolean.valueOf(value))
+ : this.getAndSetBucket(Entry.newBooleanEntry(tag, value));
}
public TagMap.Entry getAndSet(String tag, int value) {
- return this.getAndSet(Entry.newIntEntry(tag, value));
+ long id = KnownTagCodec.keyOf(tag);
+ return KnownTagCodec.isStored(id)
+ ? this.getAndSetKnown(id, tag, Integer.valueOf(value))
+ : this.getAndSetBucket(Entry.newIntEntry(tag, value));
}
public TagMap.Entry getAndSet(String tag, long value) {
- return this.getAndSet(Entry.newLongEntry(tag, value));
+ long id = KnownTagCodec.keyOf(tag);
+ return KnownTagCodec.isStored(id)
+ ? this.getAndSetKnown(id, tag, Long.valueOf(value))
+ : this.getAndSetBucket(Entry.newLongEntry(tag, value));
}
public TagMap.Entry getAndSet(String tag, float value) {
- return this.getAndSet(Entry.newFloatEntry(tag, value));
+ long id = KnownTagCodec.keyOf(tag);
+ return KnownTagCodec.isStored(id)
+ ? this.getAndSetKnown(id, tag, Float.valueOf(value))
+ : this.getAndSetBucket(Entry.newFloatEntry(tag, value));
}
public TagMap.Entry getAndSet(String tag, double value) {
- return this.getAndSet(Entry.newDoubleEntry(tag, value));
+ long id = KnownTagCodec.keyOf(tag);
+ return KnownTagCodec.isStored(id)
+ ? this.getAndSetKnown(id, tag, Double.valueOf(value))
+ : this.getAndSetBucket(Entry.newDoubleEntry(tag, value));
}
public void putAll(Map extends String, ? extends Object> map) {
@@ -1435,7 +1629,9 @@ public void putAll(TagMap that) {
}
private void putAllOptimizedMap(TagMap that) {
- if (this.size == 0) {
+ // "empty" must consider BOTH local regions — a map with only dense entries has size == 0 but is
+ // not empty, and putAllIntoEmptyMap would clobber its dense store.
+ if (this.size == 0 && this.knownCount == 0) {
this.putAllIntoEmptyMap(that);
} else {
this.putAllMerge(that);
@@ -1443,7 +1639,9 @@ private void putAllOptimizedMap(TagMap that) {
}
private void putAllMerge(TagMap that) {
- Object[] thisBuckets = this.buckets;
+ // COW our buckets only if the source has bucket entries to merge in; otherwise the loop below
+ // writes nothing and the shared empty buckets stay shared.
+ Object[] thisBuckets = (that.size > 0) ? this.materializeBuckets() : this.buckets;
Object[] thatBuckets = that.buckets;
// Since TagMap-s don't support expansion, buckets are perfectly aligned
@@ -1554,33 +1752,49 @@ private void putAllMerge(TagMap that) {
}
}
}
+
+ // merge the source's dense known-tag entries; incoming clobbers existing (same as buckets)
+ for (int i = 0; i < that.knownCount; ++i) {
+ this.putKnownValue(that.knownIds[i], that.knownValues[i]);
+ }
}
/*
* Specially optimized version of putAll for the common case of destination map being empty
*/
private void putAllIntoEmptyMap(TagMap that) {
- Object[] thisBuckets = this.buckets;
- Object[] thatBuckets = that.buckets;
-
- // Check against both thisBuckets.length && thatBuckets.length is to help the JIT do bound check
- // elimination
- for (int i = 0; i < thisBuckets.length && i < thatBuckets.length; ++i) {
- Object thatBucket = thatBuckets[i];
-
- // faster to explicitly null check first, then do instanceof
- if (thatBucket == null) {
- // do nothing
- } else if (thatBucket instanceof BucketGroup) {
- // if it is a BucketGroup, then need to clone
- BucketGroup thatGroup = (BucketGroup) thatBucket;
+ // Only copy buckets (and COW ours) when the source actually has bucket entries; an all-known
+ // source leaves us on the shared empty buckets.
+ if (that.size > 0) {
+ Object[] thisBuckets = this.materializeBuckets();
+ Object[] thatBuckets = that.buckets;
+
+ // Check against both thisBuckets.length && thatBuckets.length is to help the JIT do bound
+ // check elimination
+ for (int i = 0; i < thisBuckets.length && i < thatBuckets.length; ++i) {
+ Object thatBucket = thatBuckets[i];
+
+ // faster to explicitly null check first, then do instanceof
+ if (thatBucket == null) {
+ // do nothing
+ } else if (thatBucket instanceof BucketGroup) {
+ // if it is a BucketGroup, then need to clone
+ BucketGroup thatGroup = (BucketGroup) thatBucket;
- thisBuckets[i] = thatGroup.cloneChain();
- } else { // if ( thatBucket instanceof Entry )
- thisBuckets[i] = thatBucket;
+ thisBuckets[i] = thatGroup.cloneChain();
+ } else { // if ( thatBucket instanceof Entry )
+ thisBuckets[i] = thatBucket;
+ }
}
+ this.size = that.size;
+ }
+
+ // clone the dense known-tag store (values are immutable boxes/objects -> safe to share refs)
+ if (that.knownCount > 0) {
+ this.knownIds = Arrays.copyOf(that.knownIds, that.knownIds.length);
+ this.knownValues = Arrays.copyOf(that.knownValues, that.knownValues.length);
+ this.knownCount = that.knownCount;
}
- this.size = that.size;
}
public void fillMap(Map super String, Object> map) {
@@ -1599,6 +1813,9 @@ public void fillMap(Map super String, Object> 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 super String, ? super String> stringMap) {
@@ -1617,6 +1834,11 @@ public void fillStringMap(Map super String, ? super String> 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
@@ -1658,8 +1880,13 @@ public Entry getAndRemove(String tag) {
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 id = KnownTagCodec.keyOf(tag);
+ if (KnownTagCodec.isStored(id)) {
+ return this.removeKnown(id);
+ }
+
Object[] thisBuckets = this.buckets;
int hash = TagMap.Entry._hash(tag);
@@ -1727,6 +1954,15 @@ public Stream Stored tags (globalSerial ≥ {@code FIRST_STORED_SERIAL}) route to the dense store; reserved
+ * tags (e.g. {@code error}) and arbitrary tags stay in the hash buckets. Behavior must be
+ * observationally identical to the bucket store.
+ */
+class TagMapDenseForkedTest {
+
+ // stored (dense-routed) tags
+ static final String BASE_SERVICE = DDTags.BASE_SERVICE;
+ static final String COMPONENT = Tags.COMPONENT;
+ static final String DB_TYPE = Tags.DB_TYPE;
+ static final String HTTP_METHOD = Tags.HTTP_METHOD; // stored + intercepted
+ static final String DB_INSTANCE = Tags.DB_INSTANCE;
+ // arbitrary (bucket-routed) tags
+ static final String CUSTOM_A = "custom.tag.a";
+ static final String CUSTOM_B = "custom.tag.b";
+
+ @BeforeAll
+ static void registerResolver() {
+ // referencing any KnownTags constant triggers its Uses a synthetic prefix resolver ({@code known-N} -> stored / dense, anything else -> bucket)
+ * rather than the real {@link KnownTags}: it gives an UNBOUNDED known key space, so the dense array
+ * actually grows past its initial capacity and the linear scan gets long, and it lets each test pin
+ * the known/custom ratio. The three regimes exercise paths the mixed run alone would miss:
+ *
+ * Forked (isolated JVM) because resolver registration is a global static with no un-register.
+ */
+class TagMapDenseFuzzForkedTest {
+ 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;
+
+ // unbounded synthetic key spaces — large enough to grow the dense array past cap-8 several times
+ static final int KNOWN_SPACE = 48;
+ static final int CUSTOM_SPACE = 48;
+
+ enum Regime {
+ KNOWN_ONLY,
+ CUSTOM_ONLY,
+ MIXED
+ }
+
+ /**
+ * Synthetic resolver: {@code known-N} -> stored id (serial = FIRST_STORED_SERIAL + N); else 0.
+ */
+ static final KnownTagCodec.Resolver FUZZ_RESOLVER =
+ new KnownTagCodec.Resolver() {
+ @Override
+ public long keyOf(String name) {
+ if (name.startsWith("known-")) {
+ int n = Integer.parseInt(name.substring("known-".length()));
+ return KnownTagCodec.tagId(KnownTagCodec.FIRST_STORED_SERIAL + n, name);
+ }
+ return 0L;
+ }
+
+ @Override
+ public String nameOf(long tagId) {
+ int serial = KnownTagCodec.globalSerial(tagId);
+ return serial >= KnownTagCodec.FIRST_STORED_SERIAL
+ ? "known-" + (serial - KnownTagCodec.FIRST_STORED_SERIAL)
+ : null;
+ }
+
+ @Override
+ public int slotCount() {
+ return 0; // positional unused
+ }
+ };
+
+ @BeforeAll
+ static void registerResolver() {
+ KnownTagCodec.register(FUZZ_RESOLVER);
+ assertTrue(KnownTagCodec.isActive(), "resolver must be live");
+ assertTrue(KnownTagCodec.isStored(KnownTagCodec.keyOf("known-0")), "known- routes dense");
+ assertFalse(
+ KnownTagCodec.isStored(KnownTagCodec.keyOf("custom-0")), "custom- stays in buckets");
+ // round-trip the synthetic encoding
+ long id = KnownTagCodec.keyOf("known-7");
+ assertTrue("known-7".equals(KnownTagCodec.nameOf(id)), "name<->id round-trips");
+ }
+
+ @Test
+ void knownOnlyFuzz() {
+ runRegime(Regime.KNOWN_ONLY);
+ }
+
+ @Test
+ void customOnlyFuzz() {
+ runRegime(Regime.CUSTOM_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));
+
+ HashMapmap */
public static final TagMap fromMap(Map
+ *
+ *
+ *