From 58d39edd3b8f0f8b3b77f07999ba00eb1e1d70fd Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 25 Jun 2026 19:05:07 -0400 Subject: [PATCH 1/7] Add realistic mixed-tag arm to DDSpanContextSetTagBenchmark Cycles ~1/3 intercepted + ~2/3 ordinary tags through one setTag(String,Object) call site -- closer to production than the single-arm benchmarks, and keeps C2 out of the degenerate single-mode the notIntercepted-only loop locks into (the bimodality probe). Co-Authored-By: Claude Opus 4.8 --- .../core/DDSpanContextSetTagBenchmark.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) 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 2ee2ac079e9..b038d90621b 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 @@ -1,5 +1,6 @@ package datadog.trace.core; +import datadog.trace.api.DDTags; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.common.writer.Writer; import java.util.List; @@ -39,9 +40,52 @@ public class DDSpanContextSetTagBenchmark { private static final CoreTracer TRACER = CoreTracer.builder().writer(new NoopWriter()).strictTraceWrites(false).build(); + // A realistic mix set on one span: ~1/3 intercepted (resource/status/kind/error/service), ~2/3 + // ordinary app/integration tags. Cycling these through a single setTag call site keeps the + // handlerId/handleIntercept dispatch polymorphic and interleaves the store -- closer to + // production + // than the single-arm benchmarks, and it should keep C2 out of the degenerate single-mode that + // the + // notIntercepted-only loop locks into. setTag(String, Object) is the manual-instrumentation path. + private static final String[] MIXED_TAGS = { + DDTags.RESOURCE_NAME, + "http.useragent", + "db.instance", + Tags.HTTP_STATUS, + "component", + "thread.name", + Tags.SPAN_KIND, + "messaging.system", + "network.peer.address", + Tags.ERROR, + "http.route", + DDTags.SERVICE_NAME, + "rpc.method", + "app.queue.depth", + "http.request.content_length" + }; + private static final Object[] MIXED_VALUES = { + "GET /api/users", + "Mozilla/5.0", + "orders", + 200, + "netty", + "worker-1", + "server", + "kafka", + "10.0.0.1", + Boolean.FALSE, + "/api/users/{id}", + "my-service", + "GetUser", + 42, + 1024 + }; + @State(Scope.Thread) public static class SpanState { DDSpan span; + int mixIdx; @Setup public void setup() { @@ -60,6 +104,14 @@ public Object notIntercepted(SpanState s) { return s.span.setTag("app.queue.depth", 200); } + /** Realistic mix of intercepted + non-intercepted tags cycled through one setTag call site. */ + @Benchmark + public Object mixed(SpanState s) { + int i = s.mixIdx; + s.mixIdx = (i + 1 == MIXED_TAGS.length) ? 0 : i + 1; + return s.span.setTag(MIXED_TAGS[i], MIXED_VALUES[i]); + } + static final class NoopWriter implements Writer { @Override public void write(List trace) {} From 830a080c792d87d16de9b3c807e33609a7d854f8 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 25 Jun 2026 20:11:10 -0400 Subject: [PATCH 2/7] Prototype: functional TagHandler[] dispatch for TagInterceptor Replace the 344B handleIntercept tableswitch with a per-instance StringIndex + slot-aligned TagHandler[] (method refs). Split-by-tags fold into the same index (one lookup; per-call splitServiceTags.contains gone). Handlers keep their ruleFlags checks for now -> behavior-identical to the switch (existing TagInterceptorTest green); config-hoist-into-construction is a follow-up. Scratch/experiment off dougqh/taginterceptor-stringindex (#11742). Co-Authored-By: Claude Opus 4.8 --- .../core/taginterceptor/TagInterceptor.java | 349 +++++++----------- .../taginterceptor/TagInterceptorTest.java | 2 +- 2 files changed, 141 insertions(+), 210 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 850002f234a..758d0704c04 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 @@ -38,6 +38,7 @@ import datadog.trace.core.DDSpanContext; import datadog.trace.util.StringIndex; import java.net.URI; +import java.util.HashMap; import java.util.Map; import java.util.Set; import javax.annotation.Nonnull; @@ -47,117 +48,15 @@ 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 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" - 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) - - // 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 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. - 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.mapIntValues(FIXED_NAMES, TagInterceptor::fixedHandlerId); - } - - private static int fixedHandlerId(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 - } + /** + * One intercept behavior, in a uniform shape so every case can live behind a single + * (devirtualizable) call site. Each {@code switch} arm of the old dispatch is now a method + * reference stored slot-aligned in {@link #handlers}; dispatch is an array load + this call rather + * than a 344-byte {@code tableswitch} that overran C2's inline budget. + */ + @FunctionalInterface + public interface TagHandler { + boolean handle(DDSpanContext span, String tag, Object value); } private final RuleFlags ruleFlags; @@ -165,12 +64,17 @@ private static int fixedHandlerId(String tag) { 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; private final boolean jeeSplitByDeployment; + // Per-instance dispatch table: the membership index plus a slot-aligned handler array. Built once + // at construction, so config-driven split-by-tags fold into the same index as the compile-time + // fixed tags -- one lookup covers both, and the per-call splitServiceTags.contains is gone. + private final StringIndex index; + private final TagHandler[] handlers; + public TagInterceptor(RuleFlags ruleFlags) { this( Config.get().isServiceNameSetByUser(), @@ -189,7 +93,6 @@ public TagInterceptor( this.isServiceNameSetByUser = isServiceNameSetByUser; this.inferredServiceName = inferredServiceName; this.splitServiceTags = splitServiceTags; - this.hasSplitTags = !splitServiceTags.isEmpty(); this.ruleFlags = ruleFlags; splitByServletContext = splitServiceTags.contains(SERVLET_CONTEXT); @@ -199,6 +102,43 @@ public TagInterceptor( && ruleFlags.isEnabled(STATUS_404_DECORATOR); shouldSetUrlResourceAsName = ruleFlags.isEnabled(URL_AS_RESOURCE_NAME); this.jeeSplitByDeployment = jeeSplitByDeployment; + + // Assemble the dispatch table. Fixed tags first; split-by-tags only fill slots a fixed tag + // didn't claim (putIfAbsent), preserving the original "fixed wins" precedence. Handlers keep + // their own ruleFlags checks for now (behavior-identical to the old switch); hoisting those + // gates into install/skip decisions here is a clean follow-up. + Map byName = new HashMap<>(); + byName.put(DDTags.RESOURCE_NAME, this::interceptResourceName); + byName.put(Tags.DB_STATEMENT, TagInterceptor::interceptDbStatement); + TagHandler service = this::interceptServiceName; + byName.put(DDTags.SERVICE_NAME, service); + byName.put("service", service); + byName.put(Tags.PEER_SERVICE, this::interceptPeerService); + byName.put(DDTags.MANUAL_KEEP, TagInterceptor::interceptManualKeep); + byName.put(DDTags.MANUAL_DROP, this::interceptManualDrop); + byName.put(Tags.ASM_KEEP, TagInterceptor::interceptAsmKeep); + byName.put(Tags.AI_GUARD_KEEP, TagInterceptor::interceptAiGuardKeep); + byName.put(Tags.SAMPLING_PRIORITY, this::interceptSamplingPriority); + byName.put(Tags.PROPAGATED_TRACE_SOURCE, TagInterceptor::interceptPropagatedTraceSource); + byName.put(Tags.PROPAGATED_DEBUG, TagInterceptor::interceptPropagatedDebug); + byName.put(SERVLET_CONTEXT, this::interceptServletContext); + byName.put(SPAN_TYPE, TagInterceptor::interceptSpanType); + byName.put(ANALYTICS_SAMPLE_RATE, TagInterceptor::interceptAnalyticsSampleRate); + byName.put(Tags.ERROR, TagInterceptor::interceptError); + byName.put(HTTP_STATUS, this::interceptHttpStatusCode); + TagHandler urlResource = this::interceptUrlResourceAsNameRule; + byName.put(HTTP_METHOD, urlResource); + byName.put(HTTP_URL, urlResource); + byName.put(ORIGIN_KEY, TagInterceptor::interceptOrigin); + byName.put(MEASURED, TagInterceptor::interceptMeasured); + byName.put(Tags.SPAN_KIND, TagInterceptor::interceptSpanKind); + for (String splitTag : splitServiceTags) { + byName.putIfAbsent(splitTag, TagInterceptor::interceptSplitService); + } + + String[] names = byName.keySet().toArray(new String[0]); + this.index = StringIndex.of(names); + this.handlers = this.index.mapValues(TagHandler.class, byName::get); } public boolean needsIntercept(TagMap map) { @@ -216,105 +156,30 @@ public boolean needsIntercept(Map map) { } public boolean needsIntercept(String tag) { - return handlerId(tag) != 0; + return index.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 {@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. + * Resolves {@code tag} to a 1-based dispatch slot, or {@code 0} when not intercepted. The slot is + * just {@code indexOf + 1}: the index already unifies the compile-time fixed tags with the + * Config-driven split-by-tags, so a single lookup decides everything. {@link #handleIntercept} + * turns the slot back into a {@link TagHandler}. The 1-based scheme keeps {@code 0} as the + * "store, don't intercept" sentinel that {@code DDSpanContext} relies on to skip boxing. */ public int handlerId(String tag) { - // 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; + return index.indexOf(tag) + 1; } /** - * Convenience: resolve the handler id and dispatch. {@code id == 0} short-circuits (not ours). + * Convenience: resolve the handler and dispatch. A miss short-circuits (not ours). */ public boolean interceptTag(DDSpanContext span, String tag, Object value) { - int handlerId = handlerId(tag); - return handlerId != 0 && handleIntercept(span, handlerId, tag, value); + TagHandler handler = index.lookup(handlers, tag); + return handler != null && handler.handle(span, tag, value); } 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: - return interceptDbStatement(span, value); - case ID_SERVICE: - return interceptServiceName(SERVICE_NAME, span, value); - 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 ID_MANUAL_KEEP: - if (asBoolean(value)) { - span.forceKeep(); - return true; - } - return false; - case ID_MANUAL_DROP: - return interceptSamplingPriority( - FORCE_MANUAL_DROP, USER_DROP, SamplingMechanism.MANUAL, span, value); - case ID_ASM_KEEP: - if (asBoolean(value)) { - span.forceKeep(SamplingMechanism.APPSEC); - return true; - } - return false; - case ID_AI_GUARD_KEEP: - if (asBoolean(value)) { - span.forceKeep(SamplingMechanism.AI_GUARD); - return true; - } - return false; - case ID_SAMPLING_PRIORITY: - return interceptSamplingPriority(span, value); - case ID_PROPAGATED_TRACE_SOURCE: - if (value instanceof Integer) { - span.addPropagatedTraceSource((Integer) value); - return true; - } - return false; - case ID_PROPAGATED_DEBUG: - span.updateDebugPropagation(String.valueOf(value)); - return true; - case ID_SERVLET_CONTEXT: - return interceptServletContext(span, value); - case ID_SPAN_TYPE: - return interceptSpanType(span, value); - case ID_ANALYTICS_SAMPLE_RATE: - return interceptAnalyticsSampleRate(span, value); - case ID_ERROR: - return interceptError(span, value); - case ID_HTTP_STATUS: - // not set internally but may come from manual instrumentation - return interceptHttpStatusCode(span, value); - case ID_URL_RESOURCE: - return interceptUrlResourceAsNameRule(span, tag, value); - case ID_ORIGIN: - return interceptOrigin(span, value); - case ID_MEASURED: - return interceptMeasured(span, value); - 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 false; - } + return handlers[handlerId - 1].handle(span, tag, value); } private boolean interceptUrlResourceAsNameRule(DDSpanContext span, String tag, Object value) { @@ -356,7 +221,7 @@ private static void setResourceFromUrl( } } - private boolean interceptResourceName(DDSpanContext span, Object value) { + private boolean interceptResourceName(DDSpanContext span, String tag, Object value) { if (ruleFlags.isEnabled(RESOURCE_NAME)) { if (null == value) { return false; @@ -371,7 +236,7 @@ private boolean interceptResourceName(DDSpanContext span, Object value) { return false; } - private boolean interceptDbStatement(DDSpanContext span, Object value) { + private static boolean interceptDbStatement(DDSpanContext span, String tag, Object value) { if (value instanceof CharSequence) { CharSequence resourceName = (CharSequence) value; if (resourceName.length() > 0) { @@ -381,12 +246,77 @@ private boolean interceptDbStatement(DDSpanContext span, Object value) { return true; } - private boolean interceptError(DDSpanContext span, Object value) { + private boolean interceptServiceName(DDSpanContext span, String tag, Object value) { + return interceptServiceName(SERVICE_NAME, span, value); + } + + private boolean interceptPeerService(DDSpanContext span, String tag, Object value) { + // 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); + } + + private static boolean interceptManualKeep(DDSpanContext span, String tag, Object value) { + if (asBoolean(value)) { + span.forceKeep(); + return true; + } + return false; + } + + private boolean interceptManualDrop(DDSpanContext span, String tag, Object value) { + return interceptSamplingPriority( + FORCE_MANUAL_DROP, USER_DROP, SamplingMechanism.MANUAL, span, value); + } + + private static boolean interceptAsmKeep(DDSpanContext span, String tag, Object value) { + if (asBoolean(value)) { + span.forceKeep(SamplingMechanism.APPSEC); + return true; + } + return false; + } + + private static boolean interceptAiGuardKeep(DDSpanContext span, String tag, Object value) { + if (asBoolean(value)) { + span.forceKeep(SamplingMechanism.AI_GUARD); + return true; + } + return false; + } + + private static boolean interceptPropagatedTraceSource( + DDSpanContext span, String tag, Object value) { + if (value instanceof Integer) { + span.addPropagatedTraceSource((Integer) value); + return true; + } + return false; + } + + private static boolean interceptPropagatedDebug(DDSpanContext span, String tag, Object value) { + span.updateDebugPropagation(String.valueOf(value)); + return true; + } + + private static boolean interceptSpanKind(DDSpanContext span, String tag, Object value) { + // 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; + } + + private static boolean interceptSplitService(DDSpanContext span, String tag, Object value) { + span.setServiceName(String.valueOf(value), SPLIT_BY_TAGS); + return true; + } + + private static boolean interceptError(DDSpanContext span, String tag, Object value) { span.setErrorFlag(asBoolean(value), ErrorPriorities.DEFAULT); return true; } - private boolean interceptAnalyticsSampleRate(DDSpanContext span, Object value) { + private static boolean interceptAnalyticsSampleRate(DDSpanContext span, String tag, Object value) { Number analyticsSampleRate = getOrTryParse(value); if (null != analyticsSampleRate) { span.setMetric(ANALYTICS_SAMPLE_RATE, analyticsSampleRate); @@ -394,7 +324,7 @@ private boolean interceptAnalyticsSampleRate(DDSpanContext span, Object value) { return true; } - private boolean interceptSpanType(DDSpanContext span, Object value) { + private static boolean interceptSpanType(DDSpanContext span, String tag, Object value) { if (value instanceof CharSequence) { span.setSpanType((CharSequence) value); } else { @@ -428,7 +358,7 @@ private boolean interceptSamplingPriority( return false; } - private boolean interceptSamplingPriority(DDSpanContext span, Object value) { + private boolean interceptSamplingPriority(DDSpanContext span, String tag, Object value) { if (ruleFlags.isEnabled(FORCE_SAMPLING_PRIORITY)) { Number samplingPriority = getOrTryParse(value); if (null != samplingPriority) { @@ -443,7 +373,7 @@ private boolean interceptSamplingPriority(DDSpanContext span, Object value) { return false; } - boolean interceptServletContext(DDSpanContext span, Object value) { + boolean interceptServletContext(DDSpanContext span, String tag, Object value) { // even though this tag is sometimes used to set the service name // (which has the side effect of marking the span as eligible for metrics // in the trace agent) we also want to store it in the tags no matter what, @@ -477,7 +407,7 @@ boolean interceptServletContext(DDSpanContext span, Object value) { return false; } - private boolean interceptHttpStatusCode(DDSpanContext span, Object statusCode) { + private boolean interceptHttpStatusCode(DDSpanContext span, String tag, Object statusCode) { if (statusCode instanceof Number) { span.setHttpStatusCode(((Number) statusCode).shortValue()); if (shouldSet404ResourceName && span.getHttpStatusCode() == 404) { @@ -496,7 +426,8 @@ private boolean interceptHttpStatusCode(DDSpanContext span, Object statusCode) { return false; } - private boolean interceptOrigin(final DDSpanContext span, final Object origin) { + private static boolean interceptOrigin( + final DDSpanContext span, final String tag, final Object origin) { if (origin instanceof CharSequence) { span.setOrigin((CharSequence) origin); } else { @@ -505,7 +436,7 @@ private boolean interceptOrigin(final DDSpanContext span, final Object origin) { return true; } - private static boolean interceptMeasured(DDSpanContext span, Object value) { + private static boolean interceptMeasured(DDSpanContext span, String tag, Object value) { if ((value instanceof Number && ((Number) value).intValue() > 0) || asBoolean(value)) { span.setMeasured(true); return true; 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 6f1e714334e..649b3032b3d 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 @@ -750,7 +750,7 @@ void whenInterceptServletContextExtraServiceProviderIsCalled(String value, Strin new TagInterceptor( true, "my-service", Collections.singleton("servlet.context"), ruleFlags, false); - interceptor.interceptServletContext(mock(DDSpanContext.class), value); + interceptor.interceptServletContext(mock(DDSpanContext.class), "servlet.context", value); verify(extraServiceProvider, times(1)).addService(expected); } finally { From 57193f0de0b687f69c0bdaddcd4d4bbef0742552 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 25 Jun 2026 20:15:20 -0400 Subject: [PATCH 3/7] test harness: -Pjmh.jvmArgs/fork/iterations hooks for jmh task Co-Authored-By: Claude Opus 4.8 --- dd-trace-core/build.gradle | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/dd-trace-core/build.gradle b/dd-trace-core/build.gradle index ee8ef78ee16..ab35cd77bc1 100644 --- a/dd-trace-core/build.gradle +++ b/dd-trace-core/build.gradle @@ -122,6 +122,18 @@ jmh { if (project.hasProperty('jmh.profilers')) { profilers = project.property('jmh.profilers').tokenize(',') } + if (project.hasProperty('jmh.jvmArgs')) { + jvmArgsAppend = project.property('jmh.jvmArgs').tokenize(' ') + } + if (project.hasProperty('jmh.fork')) { + fork = project.property('jmh.fork') as Integer + } + if (project.hasProperty('jmh.warmupIterations')) { + warmupIterations = project.property('jmh.warmupIterations') as Integer + } + if (project.hasProperty('jmh.iterations')) { + iterations = project.property('jmh.iterations') as Integer + } if (project.hasProperty('testJvm')) { def testJvmSpec = new TestJvmSpec(project) jvm = testJvmSpec.javaTestLauncher.map { it.executablePath.asFile.absolutePath } From c468f3b3c37c9a3501b80e65a9ff76ea9dd63bc6 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 25 Jun 2026 20:24:59 -0400 Subject: [PATCH 4/7] Functional dispatch: handler(tag)->TagHandler, inline at each call site Replace the two-phase int handlerId + shared handleIntercept with TagInterceptor.handler(tag) returning the TagHandler (null = not intercepted). DDSpanContext dispatches handler.handle(...) inline at each setTag overload (setBox removed), so every call site owns its handle() invocation and gets an independent type profile -- a tag-stable caller can devirtualize+inline its one handler instead of funneling through one globally-megamorphic site. Primitive overloads keep the box-free non-intercept store (box reused only on the intercept path). interceptTag retained for the setAllTags paths. Co-Authored-By: Claude Opus 4.8 --- .../datadog/trace/core/DDSpanContext.java | 92 ++++++++++++------- .../core/taginterceptor/TagInterceptor.java | 24 ++--- 2 files changed, 67 insertions(+), 49 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 6cb3927373f..9bffd4d18f7 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 @@ -34,6 +34,7 @@ import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import datadog.trace.core.propagation.PropagationTags; import datadog.trace.core.taginterceptor.TagInterceptor; +import datadog.trace.core.taginterceptor.TagInterceptor.TagHandler; import datadog.trace.core.tagprocessor.TagsPostProcessorFactory; import datadog.trace.util.TagsHelper; import java.io.Closeable; @@ -881,7 +882,13 @@ public void setTag(final String tag, final Object value) { } if (null == value) { removeTag(tag); - } else if (!tagInterceptor.interceptTag(this, tag, value)) { + return; + } + // Dispatch inline (not via interceptTag) so this overload owns its handle() call site: each + // caller that inlines setTag gets its own type profile, letting a tag-stable caller + // devirtualize and inline its one handler. + TagHandler handler = tagInterceptor.handler(tag); + if (handler == null || !handler.handle(this, tag, value)) { synchronized (unsafeTags) { unsafeTags.set(tag, value); } @@ -894,7 +901,10 @@ public void setTag(final String tag, final String value) { } if (null == value) { removeTag(tag); - } else if (!tagInterceptor.interceptTag(this, tag, value)) { + return; + } + TagHandler handler = tagInterceptor.handler(tag); + if (handler == null || !handler.handle(this, tag, value)) { synchronized (unsafeTags) { unsafeTags.set(tag, value); } @@ -906,11 +916,9 @@ public void setTag(TagMap.EntryReader entry) { return; } - // resolve the handler once; id 0 short-circuits without a second lookup - int handlerId = tagInterceptor.handlerId(entry.tag()); - boolean intercepted = - handlerId != 0 - && tagInterceptor.handleIntercept(this, handlerId, entry.tag(), entry.objectValue()); + // resolve the handler once; a miss (null) short-circuits without a second lookup + TagHandler handler = tagInterceptor.handler(entry.tag()); + boolean intercepted = handler != null && handler.handle(this, entry.tag(), entry.objectValue()); if (!intercepted) { synchronized (unsafeTags) { unsafeTags.set(entry); @@ -918,25 +926,19 @@ public void setTag(TagMap.EntryReader entry) { } } - /* - * 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 void setBox(int handlerId, String tag, Object box) { - if (!tagInterceptor.handleIntercept(this, handlerId, tag, box)) { - synchronized (unsafeTags) { - unsafeTags.set(tag, box); - } - } - } - public void setTag(final String tag, final boolean value) { if (null == tag) { return; } - int handlerId = tagInterceptor.handlerId(tag); - if (handlerId != 0) { - this.setBox(handlerId, tag, value); + TagHandler handler = tagInterceptor.handler(tag); + if (handler != null) { + // intercepted: box once (the optimized TagMap caches it on store) and dispatch + Object box = value; + if (!handler.handle(this, tag, box)) { + synchronized (unsafeTags) { + unsafeTags.set(tag, box); + } + } } else { synchronized (unsafeTags) { unsafeTags.set(tag, value); @@ -948,9 +950,14 @@ public void setTag(final String tag, final int value) { if (null == tag) { return; } - int handlerId = tagInterceptor.handlerId(tag); - if (handlerId != 0) { - this.setBox(handlerId, tag, value); + TagHandler handler = tagInterceptor.handler(tag); + if (handler != null) { + Object box = value; + if (!handler.handle(this, tag, box)) { + synchronized (unsafeTags) { + unsafeTags.set(tag, box); + } + } } else { synchronized (unsafeTags) { unsafeTags.set(tag, value); @@ -962,10 +969,15 @@ public void setTag(final String tag, final long value) { if (null == tag) { return; } - // resolve once; handlerId 0 (not intercepted) stores the primitive without boxing - int handlerId = tagInterceptor.handlerId(tag); - if (handlerId != 0) { - this.setBox(handlerId, tag, value); + // resolve once; a miss (null) stores the primitive without boxing + TagHandler handler = tagInterceptor.handler(tag); + if (handler != null) { + Object box = value; + if (!handler.handle(this, tag, box)) { + synchronized (unsafeTags) { + unsafeTags.set(tag, box); + } + } } else { synchronized (unsafeTags) { unsafeTags.set(tag, value); @@ -977,9 +989,14 @@ public void setTag(final String tag, final float value) { if (null == tag) { return; } - int handlerId = tagInterceptor.handlerId(tag); - if (handlerId != 0) { - this.setBox(handlerId, tag, value); + TagHandler handler = tagInterceptor.handler(tag); + if (handler != null) { + Object box = value; + if (!handler.handle(this, tag, box)) { + synchronized (unsafeTags) { + unsafeTags.set(tag, box); + } + } } else { synchronized (unsafeTags) { unsafeTags.set(tag, value); @@ -991,9 +1008,14 @@ public void setTag(final String tag, final double value) { if (null == tag) { return; } - int handlerId = tagInterceptor.handlerId(tag); - if (handlerId != 0) { - this.setBox(handlerId, tag, value); + TagHandler handler = tagInterceptor.handler(tag); + if (handler != null) { + Object box = value; + if (!handler.handle(this, tag, box)) { + synchronized (unsafeTags) { + unsafeTags.set(tag, box); + } + } } 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 758d0704c04..aa877be46dd 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 @@ -160,28 +160,24 @@ public boolean needsIntercept(String tag) { } /** - * Resolves {@code tag} to a 1-based dispatch slot, or {@code 0} when not intercepted. The slot is - * just {@code indexOf + 1}: the index already unifies the compile-time fixed tags with the - * Config-driven split-by-tags, so a single lookup decides everything. {@link #handleIntercept} - * turns the slot back into a {@link TagHandler}. The 1-based scheme keeps {@code 0} as the - * "store, don't intercept" sentinel that {@code DDSpanContext} relies on to skip boxing. + * Resolves {@code tag} to its {@link TagHandler}, or {@code null} when not intercepted. A single + * lookup decides everything -- the index unifies the compile-time fixed tags with the + * Config-driven split-by-tags. Returning the handler (rather than an int id routed back through a + * shared {@code handleIntercept}) lets each caller invoke {@code handler.handle(...)} at its own + * site, so each call site gets an independent type profile: a tag-stable caller can devirtualize + * and inline its one handler. {@code null} is the "store, don't intercept" sentinel callers use to + * skip boxing the value. */ - public int handlerId(String tag) { - return index.indexOf(tag) + 1; + public TagHandler handler(String tag) { + return index.lookup(handlers, tag); } - /** - * Convenience: resolve the handler and dispatch. A miss short-circuits (not ours). - */ + /** Convenience: resolve the handler and dispatch. A miss short-circuits (not ours). */ public boolean interceptTag(DDSpanContext span, String tag, Object value) { TagHandler handler = index.lookup(handlers, tag); return handler != null && handler.handle(span, tag, value); } - public boolean handleIntercept(DDSpanContext span, int handlerId, String tag, Object value) { - return handlers[handlerId - 1].handle(span, tag, value); - } - private boolean interceptUrlResourceAsNameRule(DDSpanContext span, String tag, Object value) { if (shouldSetUrlResourceAsName) { if (HTTP_METHOD.equals(tag)) { From 713846f4b1df9b54497ae8b69e518fc5b48c8c24 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 25 Jun 2026 21:03:30 -0400 Subject: [PATCH 5/7] Restore const-folded gate: static-final FIXED arrays for handler() lookup The per-instance StringIndex lookup in handler() lost the constant-folding the #11742 gate had (static-final FIXED_HASHES/FIXED_NAMES), measuring ~40% slower on the monomorphic intercepted path. Move the fixed-tag membership arrays back to static final so Support.indexOf folds the refs to constants; keep the slot-aligned TagHandler[] per-instance (its refs close over config) and check split-by-tags per-instance on a fixed miss (fixed wins), mirroring #11742. Co-Authored-By: Claude Opus 4.8 --- .../core/taginterceptor/TagInterceptor.java | 166 ++++++++++++------ 1 file changed, 116 insertions(+), 50 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 aa877be46dd..515596403e1 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 @@ -38,7 +38,6 @@ import datadog.trace.core.DDSpanContext; import datadog.trace.util.StringIndex; import java.net.URI; -import java.util.HashMap; import java.util.Map; import java.util.Set; import javax.annotation.Nonnull; @@ -69,10 +68,51 @@ public interface TagHandler { private final boolean shouldSetUrlResourceAsName; private final boolean jeeSplitByDeployment; - // Per-instance dispatch table: the membership index plus a slot-aligned handler array. Built once - // at construction, so config-driven split-by-tags fold into the same index as the compile-time - // fixed tags -- one lookup covers both, and the per-call splitServiceTags.contains is gone. - private final StringIndex index; + private final boolean hasSplitTags; // short-circuits the split check when none are configured + + // The compile-time-fixed intercepted tags, held as the raw placed arrays in STATIC FINAL fields + // so handler()'s gate -- Support.indexOf over these -- folds the refs to constants (the hot path + // StringIndex recommends; a per-instance index instead costs a field-load + loses the fold, which + // measured ~40% slower on the monomorphic intercepted path). Split-by-tags are Config-driven, so + // they stay a per-instance check in handler() on a fixed miss and never enter this index. + private static final int[] FIXED_HASHES; + private static final String[] FIXED_NAMES; + + 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; + } + + /** Singleton for Config split-by-tag service tags; returned by handler() on a fixed-index miss. */ + private static final TagHandler SPLIT_SERVICE = TagInterceptor::interceptSplitService; + + // Slot-aligned to FIXED_NAMES: handlers[indexOf(tag)] is tag's handler. Per-instance because some + // handler method refs close over config (ruleFlags, shouldSet404ResourceName, ...); the *lookup* + // over FIXED_HASHES/FIXED_NAMES still const-folds, only handlers[slot] is an instance array load. private final TagHandler[] handlers; public TagInterceptor(RuleFlags ruleFlags) { @@ -103,42 +143,63 @@ public TagInterceptor( shouldSetUrlResourceAsName = ruleFlags.isEnabled(URL_AS_RESOURCE_NAME); this.jeeSplitByDeployment = jeeSplitByDeployment; - // Assemble the dispatch table. Fixed tags first; split-by-tags only fill slots a fixed tag - // didn't claim (putIfAbsent), preserving the original "fixed wins" precedence. Handlers keep - // their own ruleFlags checks for now (behavior-identical to the old switch); hoisting those - // gates into install/skip decisions here is a clean follow-up. - Map byName = new HashMap<>(); - byName.put(DDTags.RESOURCE_NAME, this::interceptResourceName); - byName.put(Tags.DB_STATEMENT, TagInterceptor::interceptDbStatement); - TagHandler service = this::interceptServiceName; - byName.put(DDTags.SERVICE_NAME, service); - byName.put("service", service); - byName.put(Tags.PEER_SERVICE, this::interceptPeerService); - byName.put(DDTags.MANUAL_KEEP, TagInterceptor::interceptManualKeep); - byName.put(DDTags.MANUAL_DROP, this::interceptManualDrop); - byName.put(Tags.ASM_KEEP, TagInterceptor::interceptAsmKeep); - byName.put(Tags.AI_GUARD_KEEP, TagInterceptor::interceptAiGuardKeep); - byName.put(Tags.SAMPLING_PRIORITY, this::interceptSamplingPriority); - byName.put(Tags.PROPAGATED_TRACE_SOURCE, TagInterceptor::interceptPropagatedTraceSource); - byName.put(Tags.PROPAGATED_DEBUG, TagInterceptor::interceptPropagatedDebug); - byName.put(SERVLET_CONTEXT, this::interceptServletContext); - byName.put(SPAN_TYPE, TagInterceptor::interceptSpanType); - byName.put(ANALYTICS_SAMPLE_RATE, TagInterceptor::interceptAnalyticsSampleRate); - byName.put(Tags.ERROR, TagInterceptor::interceptError); - byName.put(HTTP_STATUS, this::interceptHttpStatusCode); - TagHandler urlResource = this::interceptUrlResourceAsNameRule; - byName.put(HTTP_METHOD, urlResource); - byName.put(HTTP_URL, urlResource); - byName.put(ORIGIN_KEY, TagInterceptor::interceptOrigin); - byName.put(MEASURED, TagInterceptor::interceptMeasured); - byName.put(Tags.SPAN_KIND, TagInterceptor::interceptSpanKind); - for (String splitTag : splitServiceTags) { - byName.putIfAbsent(splitTag, TagInterceptor::interceptSplitService); + this.hasSplitTags = !splitServiceTags.isEmpty(); + + // Build the slot-aligned handler array over the static FIXED_NAMES placement. Handlers keep + // their own ruleFlags checks (behavior-identical to the old switch). Split-by-tags are NOT in + // this array -- handler() checks them per-instance on a fixed miss, preserving "fixed wins". + this.handlers = + StringIndex.Support.mapValues(FIXED_NAMES, TagHandler.class, this::fixedHandler); + } + + /** The handler for a compile-time-fixed tag (called once per slot at construction). */ + private TagHandler fixedHandler(String tag) { + switch (tag) { + case DDTags.RESOURCE_NAME: + return this::interceptResourceName; + case Tags.DB_STATEMENT: + return TagInterceptor::interceptDbStatement; + case DDTags.SERVICE_NAME: + case "service": + return this::interceptServiceName; + case Tags.PEER_SERVICE: + return this::interceptPeerService; + case DDTags.MANUAL_KEEP: + return TagInterceptor::interceptManualKeep; + case DDTags.MANUAL_DROP: + return this::interceptManualDrop; + case Tags.ASM_KEEP: + return TagInterceptor::interceptAsmKeep; + case Tags.AI_GUARD_KEEP: + return TagInterceptor::interceptAiGuardKeep; + case Tags.SAMPLING_PRIORITY: + return this::interceptSamplingPriority; + case Tags.PROPAGATED_TRACE_SOURCE: + return TagInterceptor::interceptPropagatedTraceSource; + case Tags.PROPAGATED_DEBUG: + return TagInterceptor::interceptPropagatedDebug; + case SERVLET_CONTEXT: + return this::interceptServletContext; + case SPAN_TYPE: + return TagInterceptor::interceptSpanType; + case ANALYTICS_SAMPLE_RATE: + return TagInterceptor::interceptAnalyticsSampleRate; + case Tags.ERROR: + return TagInterceptor::interceptError; + case HTTP_STATUS: + return this::interceptHttpStatusCode; + case HTTP_METHOD: + case HTTP_URL: + return this::interceptUrlResourceAsNameRule; + case ORIGIN_KEY: + return TagInterceptor::interceptOrigin; + case MEASURED: + return TagInterceptor::interceptMeasured; + case Tags.SPAN_KIND: + return TagInterceptor::interceptSpanKind; + default: + return null; // unreachable: FIXED_NAMES only holds the names above } - - String[] names = byName.keySet().toArray(new String[0]); - this.index = StringIndex.of(names); - this.handlers = this.index.mapValues(TagHandler.class, byName::get); } public boolean needsIntercept(TagMap map) { @@ -156,25 +217,30 @@ public boolean needsIntercept(Map map) { } public boolean needsIntercept(String tag) { - return index.contains(tag); + return handler(tag) != null; } /** - * Resolves {@code tag} to its {@link TagHandler}, or {@code null} when not intercepted. A single - * lookup decides everything -- the index unifies the compile-time fixed tags with the - * Config-driven split-by-tags. Returning the handler (rather than an int id routed back through a - * shared {@code handleIntercept}) lets each caller invoke {@code handler.handle(...)} at its own - * site, so each call site gets an independent type profile: a tag-stable caller can devirtualize - * and inline its one handler. {@code null} is the "store, don't intercept" sentinel callers use to - * skip boxing the value. + * Resolves {@code tag} to its {@link TagHandler}, or {@code null} when not intercepted. The gate + * is a {@code Support.indexOf} over the {@code static final} {@link #FIXED_HASHES}/{@link + * #FIXED_NAMES} -- C2 folds those refs to constants, so the resolve is near-free; a Config + * split-by-tag is checked per-instance only on a fixed miss (fixed wins, matching the original + * order). Returning the handler (rather than an int id routed back through a shared {@code + * handleIntercept}) lets each caller invoke {@code handler.handle(...)} at its own site, so each + * call site gets an independent type profile: a tag-stable caller can devirtualize and inline its + * one handler. {@code null} is the "store, don't intercept" sentinel callers use to skip boxing. */ public TagHandler handler(String tag) { - return index.lookup(handlers, tag); + int slot = StringIndex.Support.indexOf(FIXED_HASHES, FIXED_NAMES, tag); + if (slot >= 0) { + return handlers[slot]; + } + return hasSplitTags && splitServiceTags.contains(tag) ? SPLIT_SERVICE : null; } /** Convenience: resolve the handler and dispatch. A miss short-circuits (not ours). */ public boolean interceptTag(DDSpanContext span, String tag, Object value) { - TagHandler handler = index.lookup(handlers, tag); + TagHandler handler = handler(tag); return handler != null && handler.handle(span, tag, value); } From f4a320930d2671b7fefdfe6c38a62b0d07aef329 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 25 Jun 2026 21:20:53 -0400 Subject: [PATCH 6/7] Fold split-by-tags into StringIndex (per-instance splitIndex) Replace the splitServiceTags HashSet membership check with a per-instance StringIndex (membership only; all split tags map to the SPLIT_SERVICE singleton). Add StringIndex.of(Collection) factory. Per-instance because the injection ctor passes varying split sets to live-simultaneous instances, so a static-folded holder can't represent it; const-fold payoff is nil on this rare post-fixed-miss path anyway. Co-Authored-By: Claude Opus 4.8 --- .../trace/core/taginterceptor/TagInterceptor.java | 9 ++++++--- .../src/main/java/datadog/trace/util/StringIndex.java | 10 ++++++++++ 2 files changed, 16 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 515596403e1..6b6d8b0998f 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 @@ -62,7 +62,10 @@ public interface TagHandler { private final boolean isServiceNameSetByUser; private final boolean splitByServletContext; private final String inferredServiceName; - private final Set splitServiceTags; + // Config split-by-tags as a per-instance StringIndex (membership only; all map to SPLIT_SERVICE). + // Config-driven, so it can't be the folded static gate -- but it's the rare path (most deploys + // have none), gated by hasSplitTags, and checked only on a fixed-index miss. + private final StringIndex splitIndex; private final boolean shouldSet404ResourceName; private final boolean shouldSetUrlResourceAsName; @@ -132,7 +135,7 @@ public TagInterceptor( boolean jeeSplitByDeployment) { this.isServiceNameSetByUser = isServiceNameSetByUser; this.inferredServiceName = inferredServiceName; - this.splitServiceTags = splitServiceTags; + this.splitIndex = StringIndex.of(splitServiceTags); this.ruleFlags = ruleFlags; splitByServletContext = splitServiceTags.contains(SERVLET_CONTEXT); @@ -235,7 +238,7 @@ public TagHandler handler(String tag) { if (slot >= 0) { return handlers[slot]; } - return hasSplitTags && splitServiceTags.contains(tag) ? SPLIT_SERVICE : null; + return hasSplitTags && splitIndex.contains(tag) ? SPLIT_SERVICE : null; } /** Convenience: resolve the handler and dispatch. A miss short-circuits (not ours). */ 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 ec16b51dd3e..3a4398cb79c 100644 --- a/internal-api/src/main/java/datadog/trace/util/StringIndex.java +++ b/internal-api/src/main/java/datadog/trace/util/StringIndex.java @@ -1,6 +1,7 @@ package datadog.trace.util; import java.lang.reflect.Array; +import java.util.Collection; import java.util.function.Function; import java.util.function.ToIntFunction; import java.util.function.ToLongFunction; @@ -59,6 +60,15 @@ public static StringIndex of(String... names) { return new StringIndex(data.hashes, data.names); } + /** + * Convenience instance over a collection of names -- e.g. a Config-driven set resolved at startup, + * where the names aren't compile-time-known so the folded static {@link Support} path isn't an + * option anyway. Empty in, empty (membership-always-false) index out. + */ + public static StringIndex of(Collection names) { + return of(names.toArray(new String[0])); + } + /** Slot of {@code name}, or -1. Delegates to {@link Support} on the instance's arrays. */ public int indexOf(String name) { return Support.indexOf(this.hashes, this.names, name); From 186ed1298597772926867f0559084376f87f8a10 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 25 Jun 2026 22:19:02 -0400 Subject: [PATCH 7/7] Config-hoist single-feature gates into handler install/skip For the cleanly single-feature-gated handlers (resource RESOURCE_NAME, service SERVICE_NAME, manual.drop FORCE_MANUAL_DROP, sampling.priority FORCE_SAMPLING_PRIORITY, url-resource URL_AS_RESOURCE_NAME), decide enabled once at construction: install the handler only if enabled, else leave the slot null (tag stores). The handler bodies drop the per-call ruleFlags.isEnabled branch and become static. peer.service (sets PEER_SERVICE_SOURCE even when disabled), http.status (runtime 404 check), and servlet.context (multi-condition) keep their internal logic and stay instance refs. Behavior-preserving; TagInterceptorTest green. Orthogonal to dispatch shape -- ports to switch too. Co-Authored-By: Claude Opus 4.8 --- .../core/taginterceptor/TagInterceptor.java | 111 +++++++++--------- 1 file changed, 53 insertions(+), 58 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 6b6d8b0998f..c83a7f19588 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 @@ -159,24 +159,30 @@ public TagInterceptor( private TagHandler fixedHandler(String tag) { switch (tag) { case DDTags.RESOURCE_NAME: - return this::interceptResourceName; + // config-hoist: install only if enabled, so the handler body drops the per-call check + // (and stays static). Disabled -> null slot -> handler() returns null -> tag is stored. + return ruleFlags.isEnabled(RESOURCE_NAME) ? TagInterceptor::interceptResourceName : null; case Tags.DB_STATEMENT: return TagInterceptor::interceptDbStatement; case DDTags.SERVICE_NAME: case "service": - return this::interceptServiceName; + return ruleFlags.isEnabled(SERVICE_NAME) ? TagInterceptor::interceptServiceName : null; case Tags.PEER_SERVICE: + // not hoisted: it sets PEER_SERVICE_SOURCE even when PEER_SERVICE is disabled, so the + // enabled check must stay inside the handler (and it stays an instance ref). return this::interceptPeerService; case DDTags.MANUAL_KEEP: return TagInterceptor::interceptManualKeep; case DDTags.MANUAL_DROP: - return this::interceptManualDrop; + return ruleFlags.isEnabled(FORCE_MANUAL_DROP) ? TagInterceptor::interceptManualDrop : null; case Tags.ASM_KEEP: return TagInterceptor::interceptAsmKeep; case Tags.AI_GUARD_KEEP: return TagInterceptor::interceptAiGuardKeep; case Tags.SAMPLING_PRIORITY: - return this::interceptSamplingPriority; + return ruleFlags.isEnabled(FORCE_SAMPLING_PRIORITY) + ? TagInterceptor::interceptSamplingPriority + : null; case Tags.PROPAGATED_TRACE_SOURCE: return TagInterceptor::interceptPropagatedTraceSource; case Tags.PROPAGATED_DEBUG: @@ -193,7 +199,7 @@ private TagHandler fixedHandler(String tag) { return this::interceptHttpStatusCode; case HTTP_METHOD: case HTTP_URL: - return this::interceptUrlResourceAsNameRule; + return shouldSetUrlResourceAsName ? TagInterceptor::interceptUrlResourceAsNameRule : null; case ORIGIN_KEY: return TagInterceptor::interceptOrigin; case MEASURED: @@ -247,17 +253,17 @@ public boolean interceptTag(DDSpanContext span, String tag, Object value) { return handler != null && handler.handle(span, tag, value); } - private boolean interceptUrlResourceAsNameRule(DDSpanContext span, String tag, Object value) { - if (shouldSetUrlResourceAsName) { - if (HTTP_METHOD.equals(tag)) { - final Object url = span.unsafeGetTag(HTTP_URL); - if (url != null) { - setResourceFromUrl(span, value.toString(), url); - } - } else if (HTTP_URL.equals(tag)) { - final Object method = span.unsafeGetTag(HTTP_METHOD); - setResourceFromUrl(span, method != null ? method.toString() : null, value); + // Installed only when URL_AS_RESOURCE_NAME is enabled (shouldSetUrlResourceAsName), so the gate is + // gone from the body. Returns false so the method/url tag is still stored. + private static boolean interceptUrlResourceAsNameRule(DDSpanContext span, String tag, Object value) { + if (HTTP_METHOD.equals(tag)) { + final Object url = span.unsafeGetTag(HTTP_URL); + if (url != null) { + setResourceFromUrl(span, value.toString(), url); } + } else if (HTTP_URL.equals(tag)) { + final Object method = span.unsafeGetTag(HTTP_METHOD); + setResourceFromUrl(span, method != null ? method.toString() : null, value); } return false; } @@ -286,19 +292,17 @@ private static void setResourceFromUrl( } } - private boolean interceptResourceName(DDSpanContext span, String tag, Object value) { - if (ruleFlags.isEnabled(RESOURCE_NAME)) { - if (null == value) { - return false; - } - if (value instanceof CharSequence) { - span.setResourceName((CharSequence) value, ResourceNamePriorities.TAG_INTERCEPTOR); - } else { - span.setResourceName(String.valueOf(value), ResourceNamePriorities.TAG_INTERCEPTOR); - } - return true; + // Installed only when RESOURCE_NAME is enabled, so the gate is gone from the body. + private static boolean interceptResourceName(DDSpanContext span, String tag, Object value) { + if (null == value) { + return false; } - return false; + if (value instanceof CharSequence) { + span.setResourceName((CharSequence) value, ResourceNamePriorities.TAG_INTERCEPTOR); + } else { + span.setResourceName(String.valueOf(value), ResourceNamePriorities.TAG_INTERCEPTOR); + } + return true; } private static boolean interceptDbStatement(DDSpanContext span, String tag, Object value) { @@ -311,8 +315,13 @@ private static boolean interceptDbStatement(DDSpanContext span, String tag, Obje return true; } - private boolean interceptServiceName(DDSpanContext span, String tag, Object value) { - return interceptServiceName(SERVICE_NAME, span, value); + // Installed only when SERVICE_NAME is enabled, so the gate is gone (the peer path below keeps the + // feature-checking overload, since it sets PEER_SERVICE_SOURCE regardless). + private static boolean interceptServiceName(DDSpanContext span, String tag, Object value) { + String serviceName = String.valueOf(value); + span.setServiceName(serviceName); + ServiceNameCollector.get().addService(serviceName); + return true; } private boolean interceptPeerService(DDSpanContext span, String tag, Object value) { @@ -329,9 +338,12 @@ private static boolean interceptManualKeep(DDSpanContext span, String tag, Objec return false; } - private boolean interceptManualDrop(DDSpanContext span, String tag, Object value) { - return interceptSamplingPriority( - FORCE_MANUAL_DROP, USER_DROP, SamplingMechanism.MANUAL, span, value); + // Installed only when FORCE_MANUAL_DROP is enabled. + private static boolean interceptManualDrop(DDSpanContext span, String tag, Object value) { + if (asBoolean(value)) { + span.setSamplingPriority(USER_DROP, SamplingMechanism.MANUAL); + } + return true; } private static boolean interceptAsmKeep(DDSpanContext span, String tag, Object value) { @@ -408,34 +420,17 @@ boolean interceptServiceName(RuleFlags.Feature feature, DDSpanContext span, Obje return false; } - private boolean interceptSamplingPriority( - RuleFlags.Feature feature, - int samplingPriority, - int samplingMechanism, - DDSpanContext span, - Object value) { - if (ruleFlags.isEnabled(feature)) { - if (asBoolean(value)) { - span.setSamplingPriority(samplingPriority, samplingMechanism); - } - return true; - } - return false; - } - - private boolean interceptSamplingPriority(DDSpanContext span, String tag, Object value) { - if (ruleFlags.isEnabled(FORCE_SAMPLING_PRIORITY)) { - Number samplingPriority = getOrTryParse(value); - if (null != samplingPriority) { - if (samplingPriority.intValue() > 0) { - span.forceKeep(SamplingMechanism.MANUAL); - } else { - span.setSamplingPriority(USER_DROP, SamplingMechanism.MANUAL); - } + // Installed only when FORCE_SAMPLING_PRIORITY is enabled, so the gate is gone from the body. + private static boolean interceptSamplingPriority(DDSpanContext span, String tag, Object value) { + Number samplingPriority = getOrTryParse(value); + if (null != samplingPriority) { + if (samplingPriority.intValue() > 0) { + span.forceKeep(SamplingMechanism.MANUAL); + } else { + span.setSamplingPriority(USER_DROP, SamplingMechanism.MANUAL); } - return true; } - return false; + return true; } boolean interceptServletContext(DDSpanContext span, String tag, Object value) {