Skip to content

Commit 1f257aa

Browse files
dougqhclaude
andcommitted
Wire mergedTracerTags as a read-through parent at span build (level-split phase 1)
Attach the trace's merged tracer tags to each span's TagMap as a frozen read-through parent (via TagMap.createFromParent) at span construction, instead of copying them into every span. The span sees the shared tags on read and only stores its own local tags, so the common trace-level bundle is held once per trace rather than duplicated per span. - CoreTracer builds the frozen merged-tracer-tags parent once; config version is kept out of that bundle. - DDSpanContext attaches the parent at construction (fixed, no re-parenting). - Adds TagMapReadThroughBenchmark (copy-down vs read-through, -prof gc). Stacked on the read-through mechanism (#11789), which builds on the folded final-class TagMap (#11967). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b58bcc0 commit 1f257aa

3 files changed

Lines changed: 173 additions & 7 deletions

File tree

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

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2206,18 +2206,31 @@ protected static final DDSpanContext buildSpanContext(
22062206
propagationTags,
22072207
tracer.profilingContextIntegration,
22082208
tracer.injectBaggageAsTags,
2209-
tracer.injectLinksAsTags);
2209+
tracer.injectLinksAsTags,
2210+
mergedTracerTagsNeedsIntercept ? null : mergedTracerTags);
22102211

22112212
// By setting the tags on the context we apply decorators to any tags that have been set via
22122213
// the builder. This is the order that the tags were added previously, but maybe the `tags`
22132214
// set in the builder should come last, so that they override other tags.
2214-
context.setAllTags(mergedTracerTags, mergedTracerTagsNeedsIntercept);
2215+
//
2216+
// mergedTracerTags is trace-level shared state and the precedence floor (everything below
2217+
// overrides it). When it carries no interceptable tags it is attached as a read-through
2218+
// PARENT at construction (shared by reference, no per-span copy). When it does need
2219+
// interception, copy its entries in (the interceptor's per-span side-effects can't be
2220+
// shared by reference).
2221+
if (mergedTracerTagsNeedsIntercept) {
2222+
context.setAllTags(mergedTracerTags, true);
2223+
}
22152224
context.setAllTags(tagLedger);
22162225
context.setAllTags(coreTags, coreTagsNeedsIntercept);
22172226
context.setAllTags(rootSpanTags, rootSpanTagsNeedsIntercept);
22182227
context.setAllTags(contextualTags);
2219-
// remove version here since will be done later on the postProcessor.
2220-
// it will allow knowing if it will be set manually or not
2228+
// Version is added later by the postProcessor (InternalTagsAdder), only if not already set
2229+
// during the request. Config version is kept out of the trace-level bundle (see
2230+
// withTracerTags), so this removal now only wipes a version set via the span builder —
2231+
// keeping
2232+
// the existing semantics where a builder-set version is replaced by the config version. Under
2233+
// read-through this is a cheap local removal (version isn't in the parent, so no tombstone).
22212234
context.removeTag(Tags.VERSION);
22222235
return context;
22232236
}
@@ -2448,6 +2461,13 @@ static TagMap withTracerTags(
24482461
Map<String, ?> userSpanTags, Config config, TraceConfig traceConfig) {
24492462
final TagMap result = TagMap.create(userSpanTags.size() + 5);
24502463
result.putAll(userSpanTags);
2464+
// Version is conditionally managed by InternalTagsAdder (added only when service == DD_SERVICE
2465+
// and not set during the request), so keep it OUT of the trace-level bundle. This matters under
2466+
// read-through: the bundle becomes a shared parent, and a per-span removeTag(VERSION) on a key
2467+
// that lived in the parent would mint a per-span tombstone. With version excluded here, the
2468+
// per-span removeTag (retained, to wipe a builder-set version) is a cheap local op, never a
2469+
// tombstone. Behavior is unchanged: version was applied-then-removed at build today.
2470+
result.remove(Tags.VERSION);
24512471
if (null != config) { // static
24522472
if (!config.getEnv().isEmpty()) {
24532473
result.set("env", config.getEnv());

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

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,8 @@ public DDSpanContext(
243243
propagationTags,
244244
ProfilingContextIntegration.NoOp.INSTANCE,
245245
true,
246-
true);
246+
true,
247+
null);
247248
}
248249

249250
public DDSpanContext(
@@ -293,9 +294,11 @@ public DDSpanContext(
293294
propagationTags,
294295
ProfilingContextIntegration.NoOp.INSTANCE,
295296
injectBaggageAsTags,
296-
injectLinksAsTags);
297+
injectLinksAsTags,
298+
null);
297299
}
298300

301+
/** Back-compat ctor (no read-through parent); delegates with a null parent. */
299302
public DDSpanContext(
300303
final DDTraceId traceId,
301304
final long spanId,
@@ -322,6 +325,62 @@ public DDSpanContext(
322325
final ProfilingContextIntegration profilingContextIntegration,
323326
final boolean injectBaggageAsTags,
324327
final boolean injectLinksAsTags) {
328+
this(
329+
traceId,
330+
spanId,
331+
parentId,
332+
parentServiceName,
333+
serviceNameSource,
334+
serviceName,
335+
operationName,
336+
resourceName,
337+
samplingPriority,
338+
origin,
339+
baggageItems,
340+
w3cBaggage,
341+
errorFlag,
342+
spanType,
343+
tagsSize,
344+
traceCollector,
345+
requestContextDataAppSec,
346+
requestContextDataIast,
347+
CiVisibilityContextData,
348+
pathwayContext,
349+
disableSamplingMechanismValidation,
350+
propagationTags,
351+
profilingContextIntegration,
352+
injectBaggageAsTags,
353+
injectLinksAsTags,
354+
null);
355+
}
356+
357+
public DDSpanContext(
358+
final DDTraceId traceId,
359+
final long spanId,
360+
final long parentId,
361+
final CharSequence parentServiceName,
362+
final CharSequence serviceNameSource,
363+
final String serviceName,
364+
final CharSequence operationName,
365+
final CharSequence resourceName,
366+
final int samplingPriority,
367+
final CharSequence origin,
368+
final Map<String, String> baggageItems,
369+
final Baggage w3cBaggage,
370+
final boolean errorFlag,
371+
final CharSequence spanType,
372+
final int tagsSize,
373+
final TraceCollector traceCollector,
374+
final Object requestContextDataAppSec,
375+
final Object requestContextDataIast,
376+
final Object CiVisibilityContextData,
377+
final PathwayContext pathwayContext,
378+
final boolean disableSamplingMechanismValidation,
379+
final PropagationTags propagationTags,
380+
final ProfilingContextIntegration profilingContextIntegration,
381+
final boolean injectBaggageAsTags,
382+
final boolean injectLinksAsTags,
383+
final TagMap readThroughParent) {
325384

326385
assert traceCollector != null;
327386
this.traceCollector = traceCollector;
@@ -350,7 +409,10 @@ public DDSpanContext(
350409
// The +1 is the magic number from the tags below that we set at the end,
351410
// and "* 4 / 3" is to make sure that we don't resize immediately
352411
final int capacity = Math.max((tagsSize <= 0 ? 3 : (tagsSize + 1)) * 4 / 3, 8);
353-
this.unsafeTags = TagMap.create(capacity);
412+
this.unsafeTags =
413+
readThroughParent != null
414+
? TagMap.createFromParent(readThroughParent)
415+
: TagMap.create(capacity);
354416

355417
// must set this before setting the service and resource names below
356418
this.profilingContextIntegration = profilingContextIntegration;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package datadog.trace.util;
2+
3+
import datadog.trace.api.TagMap;
4+
import java.util.concurrent.TimeUnit;
5+
import org.openjdk.jmh.annotations.Benchmark;
6+
import org.openjdk.jmh.annotations.BenchmarkMode;
7+
import org.openjdk.jmh.annotations.Fork;
8+
import org.openjdk.jmh.annotations.Level;
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.Param;
13+
import org.openjdk.jmh.annotations.Scope;
14+
import org.openjdk.jmh.annotations.Setup;
15+
import org.openjdk.jmh.annotations.State;
16+
import org.openjdk.jmh.annotations.Threads;
17+
import org.openjdk.jmh.annotations.Warmup;
18+
19+
/**
20+
* Models span-build tag assembly with vs without read-through of the shared trace-level bundle.
21+
*
22+
* <ul>
23+
* <li><b>copyDown</b> — today's path: {@code putAll} the (frozen) trace-level bundle into the
24+
* fresh span map, then set the span-specific tags. {@code putAll}-into-empty shares the
25+
* frozen entry references (bucket-clone), so this does NOT allocate new Entry objects for the
26+
* trace tags — its cost is cloned {@code BucketGroup}s plus the collisions caused by the
27+
* trace tags sharing the local buckets with the span tags.
28+
* <li><b>readThrough</b> — attach the frozen bundle as a read-through parent; only the
29+
* span-specific tags are stored locally.
30+
* </ul>
31+
*
32+
* <p>Run with {@code -prof gc}; the B/op delta is the per-span allocation read-through saves. Both
33+
* arms set the same span tags, so the delta isolates the trace-bundle handling. {@code
34+
* traceTagCount} sweeps the bundle size — the win scales with it (more trace tags → more cloned
35+
* BucketGroups and local collisions avoided). {@code traceTagCount = 7} ≈ a realistic
36+
* mergedTracerTags (env, version, language, runtime-id, a propagation tag, a couple global tags).
37+
*/
38+
@State(Scope.Benchmark)
39+
@BenchmarkMode(Mode.Throughput)
40+
@OutputTimeUnit(TimeUnit.SECONDS)
41+
@Warmup(iterations = 5, time = 2)
42+
@Measurement(iterations = 5, time = 2)
43+
@Fork(3)
44+
@Threads(8)
45+
public class TagMapReadThroughBenchmark {
46+
47+
@Param({"3", "7", "15"})
48+
int traceTagCount;
49+
50+
private TagMap traceTags;
51+
52+
@Setup(Level.Trial)
53+
public void setup() {
54+
TagMap m = TagMap.create(Math.max(16, traceTagCount * 2));
55+
for (int i = 0; i < traceTagCount; i++) {
56+
m.set("_dd.trace.tag." + i, "trace-value-" + i);
57+
}
58+
this.traceTags = m.freeze();
59+
}
60+
61+
@Benchmark
62+
public TagMap copyDown() {
63+
TagMap m = TagMap.create(16);
64+
m.putAll(traceTags); // putAll-into-empty: shares frozen entries, clones BucketGroups
65+
setSpanTags(m);
66+
return m;
67+
}
68+
69+
@Benchmark
70+
public TagMap readThrough() {
71+
// no copy; trace tags read through the shared frozen parent (fixed at construction)
72+
TagMap m = TagMap.createFromParent(traceTags);
73+
setSpanTags(m);
74+
return m;
75+
}
76+
77+
private static void setSpanTags(TagMap m) {
78+
m.set("http.method", "GET");
79+
m.set("http.url", "/api/checkout/cart");
80+
m.set("component", "spring-web-controller");
81+
m.set("span.kind", "server");
82+
m.set("http.status_code", 200);
83+
}
84+
}

0 commit comments

Comments
 (0)