Skip to content

Commit 0cf53f8

Browse files
dougqhclaude
andcommitted
Pre-merge immutable tag sources to reduce tag-merge overhead in span creation
buildSpanContext() called setAllTags() 5 times per span, with two of those sources (mergedTracerTags, localRootSpanTags) being immutable across all spans. Every span redundantly re-merged them plus ran a per-span removeTag(Tags.VERSION). This change pre-merges those frozen tag sources into a preMergedRootSpanBase template at ConfigSnapshot creation time and initializes each span's TagMap via copy from the template. Root spans go from 5 setAllTags + removeTag to 3 setAllTags; child spans go from 2 effective setAllTags to 1. tag: no release note tag: ai generated Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 693449b commit 0cf53f8

3 files changed

Lines changed: 168 additions & 23 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package datadog.trace.core;
2+
3+
import static java.util.concurrent.TimeUnit.MICROSECONDS;
4+
5+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
6+
import org.openjdk.jmh.annotations.Benchmark;
7+
import org.openjdk.jmh.annotations.BenchmarkMode;
8+
import org.openjdk.jmh.annotations.Fork;
9+
import org.openjdk.jmh.annotations.Measurement;
10+
import org.openjdk.jmh.annotations.Mode;
11+
import org.openjdk.jmh.annotations.OutputTimeUnit;
12+
import org.openjdk.jmh.annotations.Scope;
13+
import org.openjdk.jmh.annotations.State;
14+
import org.openjdk.jmh.annotations.Threads;
15+
import org.openjdk.jmh.annotations.Warmup;
16+
17+
/**
18+
* Benchmark measuring the tag-merge optimization in buildSpanContext.
19+
*
20+
* <p>Compares root span creation (which uses the pre-merged template of mergedTracerTags +
21+
* localRootSpanTags) versus child span creation (which uses mergedTracerTags as template).
22+
*/
23+
@State(Scope.Benchmark)
24+
@Warmup(iterations = 3)
25+
@Measurement(iterations = 5)
26+
@BenchmarkMode(Mode.Throughput)
27+
@Threads(8)
28+
@OutputTimeUnit(MICROSECONDS)
29+
@Fork(value = 1)
30+
public class TagMergeBenchmark {
31+
static final CoreTracer TRACER = CoreTracer.builder().build();
32+
33+
@Benchmark
34+
public AgentSpan rootSpan() {
35+
return TRACER.startSpan("foo", "bar");
36+
}
37+
38+
@Benchmark
39+
public AgentSpan childSpan() {
40+
AgentSpan root = TRACER.startSpan("foo", "bar");
41+
return TRACER.startSpan("foo", "child", root.context());
42+
}
43+
}

dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,17 @@ private CoreTracer(
688688
this.defaultSpanTags = defaultSpanTags;
689689
this.defaultSpanTagsNeedsIntercept = this.tagInterceptor.needsIntercept(this.defaultSpanTags);
690690

691+
// Initialize localRootSpanTags before dynamicConfig so ConfigSnapshot can pre-merge them
692+
if (profilingContextIntegration != ProfilingContextIntegration.NoOp.INSTANCE) {
693+
TagMap tmp = TagMap.fromMap(localRootSpanTags);
694+
tmp.put(PROFILING_CONTEXT_ENGINE, profilingContextIntegration.name());
695+
this.localRootSpanTags = tmp.freeze();
696+
} else {
697+
this.localRootSpanTags = TagMap.fromMapImmutable(localRootSpanTags);
698+
}
699+
this.localRootSpanTagsNeedIntercept =
700+
this.tagInterceptor.needsIntercept(this.localRootSpanTags);
701+
691702
this.dynamicConfig =
692703
DynamicConfig.create(ConfigSnapshot::new)
693704
.setTracingEnabled(true) // implied by installation of CoreTracer
@@ -866,16 +877,7 @@ private CoreTracer(
866877
this.injectBaggageAsTags = injectBaggageAsTags;
867878
this.flushOnClose = flushOnClose;
868879
this.allowInferredServices = SpanNaming.instance().namingSchema().allowInferredServices();
869-
if (profilingContextIntegration != ProfilingContextIntegration.NoOp.INSTANCE) {
870-
TagMap tmp = TagMap.fromMap(localRootSpanTags);
871-
tmp.put(PROFILING_CONTEXT_ENGINE, profilingContextIntegration.name());
872-
this.localRootSpanTags = tmp.freeze();
873-
} else {
874-
this.localRootSpanTags = TagMap.fromMapImmutable(localRootSpanTags);
875-
}
876-
877-
this.localRootSpanTagsNeedIntercept =
878-
this.tagInterceptor.needsIntercept(this.localRootSpanTags);
880+
// localRootSpanTags already initialized above (before dynamicConfig)
879881
if (serviceDiscoveryFactory != null) {
880882
AgentTaskScheduler.get()
881883
.schedule(
@@ -2073,14 +2075,23 @@ protected static final DDSpanContext buildSpanContext(
20732075
operationName = resourceName;
20742076
}
20752077

2076-
final TagMap mergedTracerTags = traceConfig.mergedTracerTags;
2077-
boolean mergedTracerTagsNeedsIntercept = traceConfig.mergedTracerTagsNeedsIntercept;
2078+
// Use pre-merged template for root spans, mergedTracerTags template for child spans
2079+
final TagMap tagBase;
2080+
final boolean tagBaseNeedsIntercept;
2081+
if (rootSpanTags != null) {
2082+
// Root span: use pre-merged template (mergedTracerTags + rootSpanTags - Tags.VERSION)
2083+
tagBase = traceConfig.preMergedRootSpanBase;
2084+
tagBaseNeedsIntercept = traceConfig.preMergedRootSpanBaseNeedsIntercept;
2085+
} else {
2086+
// Child span: use mergedTracerTags as template
2087+
tagBase = traceConfig.mergedTracerTags;
2088+
tagBaseNeedsIntercept = traceConfig.mergedTracerTagsNeedsIntercept;
2089+
}
20782090

20792091
final int tagsSize =
2080-
mergedTracerTags.size()
2092+
tagBase.size()
20812093
+ (null == tagLedger ? 0 : tagLedger.estimateSize())
20822094
+ (null == coreTags ? 0 : coreTags.size())
2083-
+ (null == rootSpanTags ? 0 : rootSpanTags.size())
20842095
+ (null == contextualTags ? 0 : contextualTags.size());
20852096

20862097
if (builderRequestContextDataAppSec != null) {
@@ -2111,6 +2122,7 @@ protected static final DDSpanContext buildSpanContext(
21112122
errorFlag,
21122123
spanType,
21132124
tagsSize,
2125+
tagBase,
21142126
parentTraceCollector,
21152127
requestContextDataAppSec,
21162128
requestContextDataIast,
@@ -2121,17 +2133,16 @@ protected static final DDSpanContext buildSpanContext(
21212133
tracer.profilingContextIntegration,
21222134
tracer.injectBaggageAsTags);
21232135

2136+
// Apply tag intercept for the template tags if needed
2137+
if (tagBaseNeedsIntercept) {
2138+
context.interceptAllTags(tagBase);
2139+
}
21242140
// By setting the tags on the context we apply decorators to any tags that have been set via
21252141
// the builder. This is the order that the tags were added previously, but maybe the `tags`
21262142
// set in the builder should come last, so that they override other tags.
2127-
context.setAllTags(mergedTracerTags, mergedTracerTagsNeedsIntercept);
21282143
context.setAllTags(tagLedger);
21292144
context.setAllTags(coreTags, coreTagsNeedsIntercept);
2130-
context.setAllTags(rootSpanTags, rootSpanTagsNeedsIntercept);
21312145
context.setAllTags(contextualTags);
2132-
// remove version here since will be done later on the postProcessor.
2133-
// it will allow knowing if it will be set manually or not
2134-
context.removeTag(Tags.VERSION);
21352146
return context;
21362147
}
21372148
}
@@ -2327,6 +2338,11 @@ protected class ConfigSnapshot extends DynamicConfig.Snapshot {
23272338
final TagMap mergedTracerTags;
23282339
final boolean mergedTracerTagsNeedsIntercept;
23292340

2341+
/** Pre-merged template for root spans: mergedTracerTags + localRootSpanTags - Tags.VERSION */
2342+
final TagMap preMergedRootSpanBase;
2343+
2344+
final boolean preMergedRootSpanBaseNeedsIntercept;
2345+
23302346
protected ConfigSnapshot(
23312347
DynamicConfig<ConfigSnapshot>.Builder builder, ConfigSnapshot oldSnapshot) {
23322348
super(builder, oldSnapshot);
@@ -2351,6 +2367,14 @@ protected ConfigSnapshot(
23512367
mergedTracerTagsNeedsIntercept =
23522368
CoreTracer.this.tagInterceptor.needsIntercept(mergedTracerTags);
23532369
}
2370+
2371+
// Pre-merge root span base: mergedTracerTags + localRootSpanTags - Tags.VERSION
2372+
TagMap rootBase = mergedTracerTags.copy();
2373+
rootBase.putAll(CoreTracer.this.localRootSpanTags);
2374+
rootBase.remove(Tags.VERSION);
2375+
preMergedRootSpanBase = rootBase.freeze();
2376+
preMergedRootSpanBaseNeedsIntercept =
2377+
mergedTracerTagsNeedsIntercept || CoreTracer.this.localRootSpanTagsNeedIntercept;
23542378
}
23552379
}
23562380

dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,60 @@ public DDSpanContext(
288288
final PropagationTags propagationTags,
289289
final ProfilingContextIntegration profilingContextIntegration,
290290
final boolean injectBaggageAsTags) {
291+
this(
292+
traceId,
293+
spanId,
294+
parentId,
295+
parentServiceName,
296+
serviceNameSource,
297+
serviceName,
298+
operationName,
299+
resourceName,
300+
samplingPriority,
301+
origin,
302+
baggageItems,
303+
w3cBaggage,
304+
errorFlag,
305+
spanType,
306+
tagsSize,
307+
null, // tagBase
308+
traceCollector,
309+
requestContextDataAppSec,
310+
requestContextDataIast,
311+
CiVisibilityContextData,
312+
pathwayContext,
313+
disableSamplingMechanismValidation,
314+
propagationTags,
315+
profilingContextIntegration,
316+
injectBaggageAsTags);
317+
}
318+
319+
public DDSpanContext(
320+
final DDTraceId traceId,
321+
final long spanId,
322+
final long parentId,
323+
final CharSequence parentServiceName,
324+
final CharSequence serviceNameSource,
325+
final String serviceName,
326+
final CharSequence operationName,
327+
final CharSequence resourceName,
328+
final int samplingPriority,
329+
final CharSequence origin,
330+
final Map<String, String> baggageItems,
331+
final Baggage w3cBaggage,
332+
final boolean errorFlag,
333+
final CharSequence spanType,
334+
final int tagsSize,
335+
final TagMap tagBase,
336+
final TraceCollector traceCollector,
337+
final Object requestContextDataAppSec,
338+
final Object requestContextDataIast,
339+
final Object CiVisibilityContextData,
340+
final PathwayContext pathwayContext,
341+
final boolean disableSamplingMechanismValidation,
342+
final PropagationTags propagationTags,
343+
final ProfilingContextIntegration profilingContextIntegration,
344+
final boolean injectBaggageAsTags) {
291345

292346
assert traceCollector != null;
293347
this.traceCollector = traceCollector;
@@ -313,10 +367,14 @@ public DDSpanContext(
313367
assert pathwayContext != null;
314368
this.pathwayContext = pathwayContext;
315369

316-
// The +1 is the magic number from the tags below that we set at the end,
317-
// and "* 4 / 3" is to make sure that we don't resize immediately
318-
final int capacity = Math.max((tagsSize <= 0 ? 3 : (tagsSize + 1)) * 4 / 3, 8);
319-
this.unsafeTags = TagMap.create(capacity);
370+
if (tagBase != null) {
371+
this.unsafeTags = tagBase.copy();
372+
} else {
373+
// The +1 is the magic number from the tags below that we set at the end,
374+
// and "* 4 / 3" is to make sure that we don't resize immediately
375+
final int capacity = Math.max((tagsSize <= 0 ? 3 : (tagsSize + 1)) * 4 / 3, 8);
376+
this.unsafeTags = TagMap.create(capacity);
377+
}
320378

321379
// must set this before setting the service and resource names below
322380
this.profilingContextIntegration = profilingContextIntegration;
@@ -990,6 +1048,26 @@ void setAllTags(final Map<String, ?> map) {
9901048
}
9911049
}
9921050

1051+
/**
1052+
* Runs the tag interceptor over tags already present in unsafeTags (from a template copy). Tags
1053+
* that are intercepted are removed from unsafeTags and processed by the interceptor. This avoids
1054+
* re-adding tags that are already present from the template.
1055+
*/
1056+
void interceptAllTags(final TagMap templateTags) {
1057+
synchronized (unsafeTags) {
1058+
templateTags.forEach(
1059+
this,
1060+
(ctx, tagEntry) -> {
1061+
String tag = tagEntry.tag();
1062+
Object value = tagEntry.objectValue();
1063+
1064+
if (ctx.tagInterceptor.interceptTag(ctx, tag, value)) {
1065+
ctx.unsafeTags.remove(tag);
1066+
}
1067+
});
1068+
}
1069+
}
1070+
9931071
void unsafeSetTag(final String tag, final Object value) {
9941072
unsafeTags.set(tag, value);
9951073
}

0 commit comments

Comments
 (0)