From b865b0fcb47c67843f987f05b21030a30607b0bf Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 24 Jun 2026 21:54:18 -0400 Subject: [PATCH 1/5] Resolve TagInterceptor tags via StringIndex (handlerId + handleIntercept) Replaces the two switch(String) chains (needsIntercept + interceptTag) with a static StringIndex over the fixed tags + a slot-aligned id array (StringIndex.mapValues), so per-tag dispatch is one open-addressed probe (== fast path) -> tableswitch on a dense id. Split-by-tags stay a per-instance Set check (empty-short-circuited) folded into handlerId as ID_SPLIT_SERVICE. 0 == skip. interceptTag is now the handlerId+handleIntercept convenience. Behavior preserved (TagInterceptorTest 121/0). Adds StringIndex.mapValues and a TagInterceptorBenchmark (needsIntercept, both paths). Co-Authored-By: Claude Opus 4.8 --- .../TagInterceptorBenchmark.java | 85 +++++++ .../core/taginterceptor/TagInterceptor.java | 211 +++++++++++++----- .../java/datadog/trace/util/StringIndex.java | 26 +++ 3 files changed, 264 insertions(+), 58 deletions(-) create mode 100644 dd-trace-core/src/jmh/java/datadog/trace/core/taginterceptor/TagInterceptorBenchmark.java diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/taginterceptor/TagInterceptorBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/taginterceptor/TagInterceptorBenchmark.java new file mode 100644 index 00000000000..65b25beaf80 --- /dev/null +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/taginterceptor/TagInterceptorBenchmark.java @@ -0,0 +1,85 @@ +package datadog.trace.core.taginterceptor; + +import datadog.trace.api.DDTags; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +/** + * Benchmark for {@link TagInterceptor#needsIntercept(String)} -- the per-tag gate {@code + * DDSpanContext} runs on every {@code setTag}. This is where the change lives: the old code + * switched over ~22 {@code String} case labels (a {@code hashCode} switch + {@code equals}); the + * new code does one open-addressed {@link datadog.trace.util.StringIndex} probe ({@code ==} fast + * path), falling to the (usually empty) split-tags set only on a miss. + * + *

Two paths, both exercised: miss (the common case -- most tags aren't intercepted, so + * the old switch fell all the way to {@code default}), and hit (an intercepted tag). + * Read-only, so {@code @Threads(8)} is clean (no span, no store to dilute the signal). Tag keys are + * interned literals -- the realistic case. + * + *

Run before (switch) vs after (StringIndex) by toggling TagInterceptor; {@code -prof gc} should + * show ~0 B/op both ways (this proves throughput, not allocation). + * + *

+ *   ./gradlew :dd-trace-core:jmh   # add -prof gc
+ * 
+ */ +@Fork(2) +@Warmup(iterations = 2) +@Measurement(iterations = 5) +@Threads(8) +@State(Scope.Benchmark) +public class TagInterceptorBenchmark { + + private final TagInterceptor interceptor = new TagInterceptor(new RuleFlags()); + + // Intercepted (fixed) tags -- handlerId hits the FIXED index. + static final String[] INTERCEPTED = { + DDTags.RESOURCE_NAME, + DDTags.SERVICE_NAME, + Tags.DB_STATEMENT, + Tags.HTTP_STATUS, + Tags.ERROR, + Tags.SPAN_KIND, + Tags.HTTP_URL, + Tags.SAMPLING_PRIORITY, + }; + + // Ordinary integration/app tags that are NOT intercepted -- the common miss path. + static final String[] NOT_INTERCEPTED = { + "http.useragent", + "db.instance", + "messaging.system", + "component", + "thread.name", + "network.peer.address", + "http.route", + "rpc.method", + }; + + /** Per-thread cursors so threads don't contend on a shared index under {@code @Threads(8)}. */ + @State(Scope.Thread) + public static class Cursor { + int hit; + int miss; + } + + @Benchmark + public boolean intercepted(Cursor cursor) { + int i = cursor.hit; + cursor.hit = (i + 1) % INTERCEPTED.length; + return interceptor.needsIntercept(INTERCEPTED[i]); + } + + @Benchmark + public boolean notIntercepted(Cursor cursor) { + int i = cursor.miss; + cursor.miss = (i + 1) % NOT_INTERCEPTED.length; + return interceptor.needsIntercept(NOT_INTERCEPTED[i]); + } +} 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 64bf017e9db..cf3bec99120 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 @@ -37,6 +37,7 @@ import datadog.trace.bootstrap.instrumentation.api.URIUtils; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import datadog.trace.core.DDSpanContext; +import datadog.trace.util.StringIndex; import java.net.URI; import java.util.Map; import java.util.Set; @@ -47,11 +48,116 @@ public class TagInterceptor { private static final UTF8BytesString NOT_FOUND_RESOURCE_NAME = UTF8BytesString.create("404"); + // Handler ids for the intercept dispatch. 1-based: 0 is the skip sentinel ("not intercepted"), + // which lines up with the empty StringIndex slots that mapValues leaves at 0. + private static final int ID_RESOURCE_NAME = 1; + private static final int ID_DB_STATEMENT = 2; + private static final int ID_SERVICE = 3; // SERVICE_NAME and the legacy "service" + private static final int ID_PEER_SERVICE = 4; + private static final int ID_MANUAL_KEEP = 5; + private static final int ID_MANUAL_DROP = 6; + private static final int ID_ASM_KEEP = 7; + private static final int ID_AI_GUARD_KEEP = 8; + private static final int ID_SAMPLING_PRIORITY = 9; + private static final int ID_PROPAGATED_TRACE_SOURCE = 10; + private static final int ID_PROPAGATED_DEBUG = 11; + private static final int ID_SERVLET_CONTEXT = 12; + private static final int ID_SPAN_TYPE = 13; + private static final int ID_ANALYTICS_SAMPLE_RATE = 14; + private static final int ID_ERROR = 15; + private static final int ID_HTTP_STATUS = 16; + private static final int ID_URL_RESOURCE = 17; // HTTP_METHOD and HTTP_URL + private static final int ID_ORIGIN = 18; + private static final int ID_MEASURED = 19; + private static final int ID_SPAN_KIND = 20; + private static final int ID_SPLIT_SERVICE = 21; // a Config split-by-tag (not a fixed case) + + // Static membership/dispatch index over the compile-time-fixed intercepted tags. Split-by-tags + // are + // Config-driven, so they stay a per-instance check (see handlerId) and never enter this index -- + // which keeps it a plain static (no Config, no init-timing holder). + private static final StringIndex FIXED = + StringIndex.of( + DDTags.RESOURCE_NAME, + Tags.DB_STATEMENT, + DDTags.SERVICE_NAME, + "service", + Tags.PEER_SERVICE, + DDTags.MANUAL_KEEP, + DDTags.MANUAL_DROP, + Tags.ASM_KEEP, + Tags.AI_GUARD_KEEP, + Tags.SAMPLING_PRIORITY, + Tags.PROPAGATED_TRACE_SOURCE, + Tags.PROPAGATED_DEBUG, + SERVLET_CONTEXT, + SPAN_TYPE, + ANALYTICS_SAMPLE_RATE, + Tags.ERROR, + HTTP_STATUS, + HTTP_METHOD, + HTTP_URL, + ORIGIN_KEY, + MEASURED, + Tags.SPAN_KIND); + // Slot-aligned handler ids (built once). The name->id switch runs only at class init. + private static final int[] FIXED_IDS = FIXED.mapValues(TagInterceptor::fixedId); + + private static int fixedId(String tag) { + switch (tag) { + case DDTags.RESOURCE_NAME: + return ID_RESOURCE_NAME; + case Tags.DB_STATEMENT: + return ID_DB_STATEMENT; + case DDTags.SERVICE_NAME: + case "service": + return ID_SERVICE; + case Tags.PEER_SERVICE: + return ID_PEER_SERVICE; + case DDTags.MANUAL_KEEP: + return ID_MANUAL_KEEP; + case DDTags.MANUAL_DROP: + return ID_MANUAL_DROP; + case Tags.ASM_KEEP: + return ID_ASM_KEEP; + case Tags.AI_GUARD_KEEP: + return ID_AI_GUARD_KEEP; + case Tags.SAMPLING_PRIORITY: + return ID_SAMPLING_PRIORITY; + case Tags.PROPAGATED_TRACE_SOURCE: + return ID_PROPAGATED_TRACE_SOURCE; + case Tags.PROPAGATED_DEBUG: + return ID_PROPAGATED_DEBUG; + case SERVLET_CONTEXT: + return ID_SERVLET_CONTEXT; + case SPAN_TYPE: + return ID_SPAN_TYPE; + case ANALYTICS_SAMPLE_RATE: + return ID_ANALYTICS_SAMPLE_RATE; + case Tags.ERROR: + return ID_ERROR; + case HTTP_STATUS: + return ID_HTTP_STATUS; + case HTTP_METHOD: + case HTTP_URL: + return ID_URL_RESOURCE; + case ORIGIN_KEY: + return ID_ORIGIN; + case MEASURED: + return ID_MEASURED; + case Tags.SPAN_KIND: + return ID_SPAN_KIND; + default: + return 0; // unreachable: FIXED only holds the names above + } + } + private final RuleFlags ruleFlags; private final boolean isServiceNameSetByUser; private final boolean splitByServletContext; private final String inferredServiceName; private final Set splitServiceTags; + private final boolean hasSplitTags; // short-circuits the split check when none are configured private final boolean shouldSet404ResourceName; private final boolean shouldSetUrlResourceAsName; @@ -75,6 +181,7 @@ public TagInterceptor( this.isServiceNameSetByUser = isServiceNameSetByUser; this.inferredServiceName = inferredServiceName; this.splitServiceTags = splitServiceTags; + this.hasSplitTags = !splitServiceTags.isEmpty(); this.ruleFlags = ruleFlags; splitByServletContext = splitServiceTags.contains(SERVLET_CONTEXT); @@ -101,106 +208,102 @@ public boolean needsIntercept(Map map) { } public boolean needsIntercept(String tag) { - switch (tag) { - case DDTags.RESOURCE_NAME: - case Tags.DB_STATEMENT: - case DDTags.SERVICE_NAME: - case "service": - case Tags.PEER_SERVICE: - case DDTags.MANUAL_KEEP: - case DDTags.MANUAL_DROP: - case Tags.ASM_KEEP: - case Tags.AI_GUARD_KEEP: - case Tags.SAMPLING_PRIORITY: - case Tags.PROPAGATED_TRACE_SOURCE: - case Tags.PROPAGATED_DEBUG: - case SERVLET_CONTEXT: - case SPAN_TYPE: - case ANALYTICS_SAMPLE_RATE: - case Tags.ERROR: - case HTTP_STATUS: - case HTTP_METHOD: - case HTTP_URL: - case ORIGIN_KEY: - case MEASURED: - case Tags.SPAN_KIND: - return true; + return handlerId(tag) != 0; + } - default: - return splitServiceTags.contains(tag); + /** + * Resolves {@code tag} to a dispatch handler id ({@code ID_*}), or {@code 0} when not + * intercepted. Fixed (compile-time) tags come from the static {@link #FIXED} index; a Config + * split-by-tag -- checked only on a fixed miss -- resolves to {@link #ID_SPLIT_SERVICE}. Fixed + * wins when a tag is both, matching the original switch order. + */ + int handlerId(String tag) { + int slot = FIXED.indexOf(tag); + if (slot >= 0) { + return FIXED_IDS[slot]; } + return hasSplitTags && splitServiceTags.contains(tag) ? ID_SPLIT_SERVICE : 0; } + /** + * Convenience: resolve the handler id and dispatch. {@code id == 0} short-circuits (not ours). + */ public boolean interceptTag(DDSpanContext span, String tag, Object value) { - switch (tag) { - case DDTags.RESOURCE_NAME: + int id = handlerId(tag); + return id != 0 && handleIntercept(span, id, tag, value); + } + + boolean handleIntercept(DDSpanContext span, int id, String tag, Object value) { + switch (id) { + case ID_RESOURCE_NAME: return interceptResourceName(span, value); - case Tags.DB_STATEMENT: + case ID_DB_STATEMENT: return interceptDbStatement(span, value); - case DDTags.SERVICE_NAME: - case "service": + case ID_SERVICE: return interceptServiceName(SERVICE_NAME, span, value); - case Tags.PEER_SERVICE: + case ID_PEER_SERVICE: // we still need to intercept and add this tag when the user manually set span.setTag(DDTags.PEER_SERVICE_SOURCE, Tags.PEER_SERVICE); return interceptServiceName(PEER_SERVICE, span, value); - case DDTags.MANUAL_KEEP: + case ID_MANUAL_KEEP: if (asBoolean(value)) { span.forceKeep(); return true; } return false; - case DDTags.MANUAL_DROP: + case ID_MANUAL_DROP: return interceptSamplingPriority( FORCE_MANUAL_DROP, USER_DROP, SamplingMechanism.MANUAL, span, value); - case Tags.ASM_KEEP: + case ID_ASM_KEEP: if (asBoolean(value)) { span.forceKeep(SamplingMechanism.APPSEC); return true; } return false; - case Tags.AI_GUARD_KEEP: + case ID_AI_GUARD_KEEP: if (asBoolean(value)) { span.forceKeep(SamplingMechanism.AI_GUARD); return true; } return false; - case Tags.SAMPLING_PRIORITY: + case ID_SAMPLING_PRIORITY: return interceptSamplingPriority(span, value); - case Tags.PROPAGATED_TRACE_SOURCE: + case ID_PROPAGATED_TRACE_SOURCE: if (value instanceof Integer) { span.addPropagatedTraceSource((Integer) value); return true; } return false; - case Tags.PROPAGATED_DEBUG: + case ID_PROPAGATED_DEBUG: span.updateDebugPropagation(String.valueOf(value)); return true; - case SERVLET_CONTEXT: + case ID_SERVLET_CONTEXT: return interceptServletContext(span, value); - case SPAN_TYPE: + case ID_SPAN_TYPE: return interceptSpanType(span, value); - case ANALYTICS_SAMPLE_RATE: + case ID_ANALYTICS_SAMPLE_RATE: return interceptAnalyticsSampleRate(span, value); - case Tags.ERROR: + case ID_ERROR: return interceptError(span, value); - case HTTP_STATUS: + case ID_HTTP_STATUS: // not set internally but may come from manual instrumentation return interceptHttpStatusCode(span, value); - case HTTP_METHOD: - case HTTP_URL: + case ID_URL_RESOURCE: return interceptUrlResourceAsNameRule(span, tag, value); - case ORIGIN_KEY: + case ID_ORIGIN: return interceptOrigin(span, value); - case MEASURED: + case ID_MEASURED: return interceptMeasured(span, value); - case Tags.SPAN_KIND: + case ID_SPAN_KIND: // Cache the ordinal for fast isOutbound() checks. // Return false so the value is still stored in unsafeTags for serialization. span.setSpanKindOrdinal(String.valueOf(value)); return false; + case ID_SPLIT_SERVICE: + span.setServiceName(String.valueOf(value), SPLIT_BY_TAGS); + return true; default: - return intercept(span, tag, value); + return false; } } @@ -243,14 +346,6 @@ private static void setResourceFromUrl( } } - private boolean intercept(DDSpanContext span, String tag, Object value) { - if (splitServiceTags.contains(tag)) { - span.setServiceName(String.valueOf(value), SPLIT_BY_TAGS); - return true; - } - return false; - } - private boolean interceptResourceName(DDSpanContext span, Object value) { if (ruleFlags.isEnabled(RESOURCE_NAME)) { if (null == value) { diff --git a/internal-api/src/main/java/datadog/trace/util/StringIndex.java b/internal-api/src/main/java/datadog/trace/util/StringIndex.java index 46411a50fe4..c48d95d674f 100644 --- a/internal-api/src/main/java/datadog/trace/util/StringIndex.java +++ b/internal-api/src/main/java/datadog/trace/util/StringIndex.java @@ -1,5 +1,7 @@ package datadog.trace.util; +import java.util.function.ToIntFunction; + /** * Flat open-addressed name set. Generic — it knows only names. * @@ -66,6 +68,16 @@ public int slots() { return this.slots; } + /** + * Builds a slot-aligned value array: {@code out[indexOf(name)] == fn.applyAsInt(name)} for every + * indexed name. Pair with {@link #indexOf} to use this StringIndex as a string->int map + * without per-lookup hashing. Empty slots hold 0 and are never read ({@code indexOf} returns -1 + * for non-members). + */ + public int[] mapValues(ToIntFunction fn) { + return Support.mapValues(this.names, fn); + } + /** Build-time carrier. Pull the fields into your own (static final) fields; don't keep this. */ public static final class Data { public final int[] hashes; @@ -109,6 +121,20 @@ public static Data create(String... names) { return new Data(hashes, placed); } + /** + * Slot-aligned value array over placed {@code names}; {@code out[slot] = fn(name)} per name. + */ + public static int[] mapValues(String[] names, ToIntFunction fn) { + int[] out = new int[names.length]; + for (int slot = 0; slot < names.length; slot++) { + String name = names[slot]; + if (name != null) { + out[slot] = fn.applyAsInt(name); + } + } + return out; + } + /** Build-time placement. Returns the slot. */ public static int put(int[] hashes, String[] names, String name, int h) { final int mask = hashes.length - 1; From 291c749fdc734062aa7d65a21997a5e0b9b0ec7d Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 24 Jun 2026 22:18:11 -0400 Subject: [PATCH 2/5] Collapse DDSpanContext intercept callers to a single handlerId resolve With LegacyTagMap gone (master), TagMap.isOptimized() is constant -- so precheckIntercept's `!isOptimized() || needsIntercept` reduces to one handlerId lookup. Remove precheckIntercept; every setTag path now resolves tagInterceptor.handlerId(tag) once and calls handleIntercept(handlerId) directly, eliminating the needsIntercept-then-interceptTag double resolve (and the dead isOptimized branch). handlerId/handleIntercept made public; locals renamed id -> handlerId. Co-Authored-By: Claude Opus 4.8 --- .../datadog/trace/core/DDSpanContext.java | 64 ++++++++----------- .../core/taginterceptor/TagInterceptor.java | 10 +-- 2 files changed, 30 insertions(+), 44 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 520311a20c1..6cb3927373f 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 @@ -906,10 +906,11 @@ public void setTag(TagMap.EntryReader entry) { return; } - // pre-check to avoid boxing + // resolve the handler once; id 0 short-circuits without a second lookup + int handlerId = tagInterceptor.handlerId(entry.tag()); boolean intercepted = - precheckIntercept(entry.tag()) - && tagInterceptor.interceptTag(this, entry.tag(), entry.objectValue()); + handlerId != 0 + && tagInterceptor.handleIntercept(this, handlerId, entry.tag(), entry.objectValue()); if (!intercepted) { synchronized (unsafeTags) { unsafeTags.set(entry); @@ -918,31 +919,11 @@ public void setTag(TagMap.EntryReader entry) { } /* - * Uses to determine if there's an opportunity to avoid primitve boxing. - * If the underlying map doesn't support efficient primitives, then boxing is used. - * If the tag may be intercepted, then boxing is also used. + * Used when handlerId determined the tag is intercepted (id != 0): the primitive must be boxed to + * pass to the interceptor. The box is reused -- the optimized TagMap caches it on store. */ - private boolean precheckIntercept(String tag) { - // Usually only a single instanceof TagMap will be loaded, - // so isOptimized is turned into a direct call and then inlines to a constant - // Since isOptimized just returns a constant - doesn't require synchronization - return !unsafeTags.isOptimized() || tagInterceptor.needsIntercept(tag); - } - - /* - * Used when precheckIntercept determines that 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 - */ - private void setBox(String tag, Object box) { - if (!tagInterceptor.interceptTag(this, tag, box)) { + private void setBox(int handlerId, String tag, Object box) { + if (!tagInterceptor.handleIntercept(this, handlerId, tag, box)) { synchronized (unsafeTags) { unsafeTags.set(tag, box); } @@ -953,8 +934,9 @@ public void setTag(final String tag, final boolean value) { if (null == tag) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); + int handlerId = tagInterceptor.handlerId(tag); + if (handlerId != 0) { + this.setBox(handlerId, tag, value); } else { synchronized (unsafeTags) { unsafeTags.set(tag, value); @@ -966,8 +948,9 @@ public void setTag(final String tag, final int value) { if (null == tag) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); + int handlerId = tagInterceptor.handlerId(tag); + if (handlerId != 0) { + this.setBox(handlerId, tag, value); } else { synchronized (unsafeTags) { unsafeTags.set(tag, value); @@ -979,10 +962,11 @@ public void setTag(final String tag, final long value) { if (null == tag) { return; } - // check needsIntercept first to avoid unnecessary boxing - boolean intercepted = - tagInterceptor.needsIntercept(tag) && tagInterceptor.interceptTag(this, tag, value); - if (!intercepted) { + // resolve once; handlerId 0 (not intercepted) stores the primitive without boxing + int handlerId = tagInterceptor.handlerId(tag); + if (handlerId != 0) { + this.setBox(handlerId, tag, value); + } else { synchronized (unsafeTags) { unsafeTags.set(tag, value); } @@ -993,8 +977,9 @@ public void setTag(final String tag, final float value) { if (null == tag) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); + int handlerId = tagInterceptor.handlerId(tag); + if (handlerId != 0) { + this.setBox(handlerId, tag, value); } else { synchronized (unsafeTags) { unsafeTags.set(tag, value); @@ -1006,8 +991,9 @@ public void setTag(final String tag, final double value) { if (null == tag) { return; } - if (precheckIntercept(tag)) { - this.setBox(tag, value); + int handlerId = tagInterceptor.handlerId(tag); + if (handlerId != 0) { + this.setBox(handlerId, tag, value); } else { synchronized (unsafeTags) { unsafeTags.set(tag, value); 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 7aed6ccadfc..b7f5900a2b0 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 @@ -216,7 +216,7 @@ public boolean needsIntercept(String tag) { * split-by-tag -- checked only on a fixed miss -- resolves to {@link #ID_SPLIT_SERVICE}. Fixed * wins when a tag is both, matching the original switch order. */ - int handlerId(String tag) { + public int handlerId(String tag) { int slot = FIXED.indexOf(tag); if (slot >= 0) { return FIXED_IDS[slot]; @@ -228,12 +228,12 @@ int handlerId(String tag) { * Convenience: resolve the handler id and dispatch. {@code id == 0} short-circuits (not ours). */ public boolean interceptTag(DDSpanContext span, String tag, Object value) { - int id = handlerId(tag); - return id != 0 && handleIntercept(span, id, tag, value); + int handlerId = handlerId(tag); + return handlerId != 0 && handleIntercept(span, handlerId, tag, value); } - boolean handleIntercept(DDSpanContext span, int id, String tag, Object value) { - switch (id) { + public boolean handleIntercept(DDSpanContext span, int handlerId, String tag, Object value) { + switch (handlerId) { case ID_RESOURCE_NAME: return interceptResourceName(span, value); case ID_DB_STATEMENT: From 6d7152af5930511ac7dd3382d998647b25543373 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 24 Jun 2026 22:26:48 -0400 Subject: [PATCH 3/5] Add DDSpanContextSetTagBenchmark (span.setTag, intercepted + not) Exercises the caller-collapse path: DDSpan.setTag(String,int) -> the refactored DDSpanContext.setTag, intercepted (http.status_code) and not (app metric), per-thread span, @Threads(8). Pairs with TagInterceptorBenchmark (resolve) to measure the StringIndex reframe + the double->single handlerId collapse. Co-Authored-By: Claude Opus 4.8 --- .../core/DDSpanContextSetTagBenchmark.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 dd-trace-core/src/jmh/java/datadog/trace/core/DDSpanContextSetTagBenchmark.java diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/DDSpanContextSetTagBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/DDSpanContextSetTagBenchmark.java new file mode 100644 index 00000000000..f0e867530ff --- /dev/null +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/DDSpanContextSetTagBenchmark.java @@ -0,0 +1,81 @@ +package datadog.trace.core; + +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.common.writer.Writer; +import java.util.List; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +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; + +/** + * Benchmark for {@code DDSpanContext.setTag} on a primitive value -- the path the caller + * collapse touched. Before: {@code precheckIntercept} (an {@code isOptimized} call + a + * {@code needsIntercept} resolve) and then, when intercepted, a second resolve inside + * {@code interceptTag}. After: one {@code handlerId} resolve, then {@code handleIntercept(handlerId)} + * directly. + * + *

Two paths: intercepted (e.g. {@code http.status_code} -- exercises the double->single + * resolve), and notIntercepted (an app metric -- exercises the dropped {@code isOptimized} + * call; stores the primitive without boxing). Per-thread span so the (mutating) {@code setTag} has no + * cross-thread contention under {@code @Threads(8)}. + * + *

Run before (pre-collapse DDSpanContext) vs after by toggling DDSpanContext.java. + * + *

+ *   ./gradlew :dd-trace-core:jmh   # add -prof gc
+ * 
+ */ +@Fork(2) +@Warmup(iterations = 2) +@Measurement(iterations = 5) +@Threads(8) +public class DDSpanContextSetTagBenchmark { + + private static final CoreTracer TRACER = + CoreTracer.builder().writer(new NoopWriter()).strictTraceWrites(false).build(); + + @State(Scope.Thread) + public static class SpanState { + DDSpan span; + + @Setup + public void setup() { + this.span = (DDSpan) TRACER.startSpan("benchmark", "op"); + } + } + + @Benchmark + public Object intercepted(SpanState s) { + // DDSpan.setTag delegates to the refactored DDSpanContext.setTag(String, int) + return s.span.setTag(Tags.HTTP_STATUS, 200); + } + + @Benchmark + public Object notIntercepted(SpanState s) { + return s.span.setTag("app.queue.depth", 200); + } + + static final class NoopWriter implements Writer { + @Override + public void write(List trace) {} + + @Override + public void start() {} + + @Override + public boolean flush() { + return true; + } + + @Override + public void close() {} + + @Override + public void incrementDropCounts(int spanCount) {} + } +} From ff74b1e421e68474d3eb2dfd8b0ec9e9fe8b10bb Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 25 Jun 2026 10:11:38 -0400 Subject: [PATCH 4/5] Use StringIndex.Support directly in TagInterceptor.handlerId hot path handlerId resolved through the StringIndex instance (FIXED.indexOf), costing an instance-field load per setTag. Lift the placed arrays into static final FIXED_HASHES/FIXED_NAMES and resolve via Support.indexOf so the JIT folds the refs to constants -- the hot path StringIndex recommends (benchmarked ~10-18% faster than the instance). Rename FIXED_IDS -> FIXED_HANDLER_IDS, fixedId -> fixedHandlerId. Instance mapValues kept (the OO/procedural dual API is by design). Co-Authored-By: Claude Opus 4.8 --- .../core/DDSpanContextSetTagBenchmark.java | 14 ++-- .../core/taginterceptor/TagInterceptor.java | 74 ++++++++++--------- .../java/datadog/trace/util/StringIndex.java | 3 +- 3 files changed, 50 insertions(+), 41 deletions(-) diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/DDSpanContextSetTagBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/DDSpanContextSetTagBenchmark.java index f0e867530ff..2ee2ac079e9 100644 --- a/dd-trace-core/src/jmh/java/datadog/trace/core/DDSpanContextSetTagBenchmark.java +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/DDSpanContextSetTagBenchmark.java @@ -14,15 +14,15 @@ /** * Benchmark for {@code DDSpanContext.setTag} on a primitive value -- the path the caller - * collapse touched. Before: {@code precheckIntercept} (an {@code isOptimized} call + a - * {@code needsIntercept} resolve) and then, when intercepted, a second resolve inside - * {@code interceptTag}. After: one {@code handlerId} resolve, then {@code handleIntercept(handlerId)} + * collapse touched. Before: {@code precheckIntercept} (an {@code isOptimized} call + a {@code + * needsIntercept} resolve) and then, when intercepted, a second resolve inside {@code + * interceptTag}. After: one {@code handlerId} resolve, then {@code handleIntercept(handlerId)} * directly. * - *

Two paths: intercepted (e.g. {@code http.status_code} -- exercises the double->single - * resolve), and notIntercepted (an app metric -- exercises the dropped {@code isOptimized} - * call; stores the primitive without boxing). Per-thread span so the (mutating) {@code setTag} has no - * cross-thread contention under {@code @Threads(8)}. + *

Two paths: intercepted (e.g. {@code http.status_code} -- exercises the + * double->single resolve), and notIntercepted (an app metric -- exercises the dropped + * {@code isOptimized} call; stores the primitive without boxing). Per-thread span so the (mutating) + * {@code setTag} has no cross-thread contention under {@code @Threads(8)}. * *

Run before (pre-collapse DDSpanContext) vs after by toggling DDSpanContext.java. * 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 b7f5900a2b0..06e83fb3609 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 @@ -71,38 +71,46 @@ public class TagInterceptor { private static final int ID_SPAN_KIND = 20; private static final int ID_SPLIT_SERVICE = 21; // a Config split-by-tag (not a fixed case) - // Static membership/dispatch index over the compile-time-fixed intercepted tags. Split-by-tags - // are - // Config-driven, so they stay a per-instance check (see handlerId) and never enter this index -- - // which keeps it a plain static (no Config, no init-timing holder). - private static final StringIndex FIXED = - StringIndex.of( - DDTags.RESOURCE_NAME, - Tags.DB_STATEMENT, - DDTags.SERVICE_NAME, - "service", - Tags.PEER_SERVICE, - DDTags.MANUAL_KEEP, - DDTags.MANUAL_DROP, - Tags.ASM_KEEP, - Tags.AI_GUARD_KEEP, - Tags.SAMPLING_PRIORITY, - Tags.PROPAGATED_TRACE_SOURCE, - Tags.PROPAGATED_DEBUG, - SERVLET_CONTEXT, - SPAN_TYPE, - ANALYTICS_SAMPLE_RATE, - Tags.ERROR, - HTTP_STATUS, - HTTP_METHOD, - HTTP_URL, - ORIGIN_KEY, - MEASURED, - Tags.SPAN_KIND); + // Membership/dispatch index over the compile-time-fixed intercepted tags, held as the raw placed + // arrays (not a StringIndex instance) so handlerId's hot-path lookup folds the refs to constants + // via Support.indexOf -- the path StringIndex recommends for hot code. Split-by-tags are + // Config-driven, so they stay a per-instance check (see handlerId) and never enter this index. + private static final int[] FIXED_HASHES; + private static final String[] FIXED_NAMES; // Slot-aligned handler ids (built once). The name->id switch runs only at class init. - private static final int[] FIXED_IDS = FIXED.mapValues(TagInterceptor::fixedId); + private static final int[] FIXED_HANDLER_IDS; + + static { + StringIndex.Data fixed = + StringIndex.Support.create( + DDTags.RESOURCE_NAME, + Tags.DB_STATEMENT, + DDTags.SERVICE_NAME, + "service", + Tags.PEER_SERVICE, + DDTags.MANUAL_KEEP, + DDTags.MANUAL_DROP, + Tags.ASM_KEEP, + Tags.AI_GUARD_KEEP, + Tags.SAMPLING_PRIORITY, + Tags.PROPAGATED_TRACE_SOURCE, + Tags.PROPAGATED_DEBUG, + SERVLET_CONTEXT, + SPAN_TYPE, + ANALYTICS_SAMPLE_RATE, + Tags.ERROR, + HTTP_STATUS, + HTTP_METHOD, + HTTP_URL, + ORIGIN_KEY, + MEASURED, + Tags.SPAN_KIND); + FIXED_HASHES = fixed.hashes; + FIXED_NAMES = fixed.names; + FIXED_HANDLER_IDS = StringIndex.Support.mapValues(FIXED_NAMES, TagInterceptor::fixedHandlerId); + } - private static int fixedId(String tag) { + private static int fixedHandlerId(String tag) { switch (tag) { case DDTags.RESOURCE_NAME: return ID_RESOURCE_NAME; @@ -212,14 +220,14 @@ public boolean needsIntercept(String tag) { /** * Resolves {@code tag} to a dispatch handler id ({@code ID_*}), or {@code 0} when not - * intercepted. Fixed (compile-time) tags come from the static {@link #FIXED} index; a Config + * intercepted. Fixed (compile-time) tags come from the static {@code FIXED_*} index; a Config * split-by-tag -- checked only on a fixed miss -- resolves to {@link #ID_SPLIT_SERVICE}. Fixed * wins when a tag is both, matching the original switch order. */ public int handlerId(String tag) { - int slot = FIXED.indexOf(tag); + int slot = StringIndex.Support.indexOf(FIXED_HASHES, FIXED_NAMES, tag); if (slot >= 0) { - return FIXED_IDS[slot]; + return FIXED_HANDLER_IDS[slot]; } return hasSplitTags && splitServiceTags.contains(tag) ? ID_SPLIT_SERVICE : 0; } diff --git a/internal-api/src/main/java/datadog/trace/util/StringIndex.java b/internal-api/src/main/java/datadog/trace/util/StringIndex.java index c48d95d674f..5d6368c3c2c 100644 --- a/internal-api/src/main/java/datadog/trace/util/StringIndex.java +++ b/internal-api/src/main/java/datadog/trace/util/StringIndex.java @@ -72,7 +72,8 @@ public int slots() { * Builds a slot-aligned value array: {@code out[indexOf(name)] == fn.applyAsInt(name)} for every * indexed name. Pair with {@link #indexOf} to use this StringIndex as a string->int map * without per-lookup hashing. Empty slots hold 0 and are never read ({@code indexOf} returns -1 - * for non-members). + * for non-members). For the hot path, prefer the raw {@link Support#mapValues} + {@link + * Support#indexOf} over {@code static final} arrays (the JIT folds the refs). */ public int[] mapValues(ToIntFunction fn) { return Support.mapValues(this.names, fn); From 431d3eeba6eef4ad80b1faf9483c0e5b86b0007f Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 25 Jun 2026 10:31:57 -0400 Subject: [PATCH 5/5] Stack TagInterceptor on the StringIndex value API; make it final - Consume the StringIndex surface API now in #11660: FIXED_HANDLER_IDS via Support.mapIntValues; handlerId resolves via Support.lookup (id, or 0 on miss -- the not-intercepted sentinel, so no separate slot check). - Make TagInterceptor final (nothing extends it; aids JIT devirtualization). Co-Authored-By: Claude Opus 4.8 --- .../core/taginterceptor/TagInterceptor.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 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 06e83fb3609..850002f234a 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 @@ -43,12 +43,12 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; -public class TagInterceptor { +public final class TagInterceptor { private static final UTF8BytesString NOT_FOUND_RESOURCE_NAME = UTF8BytesString.create("404"); // Handler ids for the intercept dispatch. 1-based: 0 is the skip sentinel ("not intercepted"), - // which lines up with the empty StringIndex slots that mapValues leaves at 0. + // which lines up with the empty StringIndex slots that mapIntValues leaves at 0. private static final int ID_RESOURCE_NAME = 1; private static final int ID_DB_STATEMENT = 2; private static final int ID_SERVICE = 3; // SERVICE_NAME and the legacy "service" @@ -72,9 +72,9 @@ public class TagInterceptor { private static final int ID_SPLIT_SERVICE = 21; // a Config split-by-tag (not a fixed case) // Membership/dispatch index over the compile-time-fixed intercepted tags, held as the raw placed - // arrays (not a StringIndex instance) so handlerId's hot-path lookup folds the refs to constants - // via Support.indexOf -- the path StringIndex recommends for hot code. Split-by-tags are - // Config-driven, so they stay a per-instance check (see handlerId) and never enter this index. + // arrays (not a StringIndex instance) so handlerId's hot-path Support.lookup folds the refs to + // constants -- the path StringIndex recommends for hot code. Split-by-tags are Config-driven, so + // they stay a per-instance check (see handlerId) and never enter this index. private static final int[] FIXED_HASHES; private static final String[] FIXED_NAMES; // Slot-aligned handler ids (built once). The name->id switch runs only at class init. @@ -107,7 +107,8 @@ public class TagInterceptor { Tags.SPAN_KIND); FIXED_HASHES = fixed.hashes; FIXED_NAMES = fixed.names; - FIXED_HANDLER_IDS = StringIndex.Support.mapValues(FIXED_NAMES, TagInterceptor::fixedHandlerId); + FIXED_HANDLER_IDS = + StringIndex.Support.mapIntValues(FIXED_NAMES, TagInterceptor::fixedHandlerId); } private static int fixedHandlerId(String tag) { @@ -225,9 +226,11 @@ public boolean needsIntercept(String tag) { * wins when a tag is both, matching the original switch order. */ public int handlerId(String tag) { - int slot = StringIndex.Support.indexOf(FIXED_HASHES, FIXED_NAMES, tag); - if (slot >= 0) { - return FIXED_HANDLER_IDS[slot]; + // lookup returns the slot's id, or 0 on a miss -- which is exactly the "not fixed" sentinel + // (ids are 1-based), so no separate slot >= 0 check is needed. + int id = StringIndex.Support.lookup(FIXED_HASHES, FIXED_NAMES, FIXED_HANDLER_IDS, tag); + if (id != 0) { + return id; } return hasSplitTags && splitServiceTags.contains(tag) ? ID_SPLIT_SERVICE : 0; }