Skip to content

Commit 7b8dee3

Browse files
bm1549claude
andcommitted
Skip synchronized(unsafeTags) on owner-thread tag writes
Spans are almost always written by a single thread, so the lock on every setTag/setMetric call is uncontended overhead. This adds a volatile tagWriteState check: if the current thread is the span's creating thread (STATE_OWNER), tag writes skip the lock entirely. Non-owner threads and post-finish writes take the lock and sticky-transition to STATE_SHARED. Long-running spans disable the optimization at construction since the writer thread may read tags on unfinished spans. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5ab378f commit 7b8dee3

File tree

6 files changed

+474
-68
lines changed

6 files changed

+474
-68
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package datadog.trace.core;
2+
3+
import static java.util.concurrent.TimeUnit.MICROSECONDS;
4+
import static java.util.concurrent.TimeUnit.NANOSECONDS;
5+
6+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
7+
import org.openjdk.jmh.annotations.Benchmark;
8+
import org.openjdk.jmh.annotations.BenchmarkMode;
9+
import org.openjdk.jmh.annotations.Fork;
10+
import org.openjdk.jmh.annotations.Level;
11+
import org.openjdk.jmh.annotations.Measurement;
12+
import org.openjdk.jmh.annotations.Mode;
13+
import org.openjdk.jmh.annotations.OutputTimeUnit;
14+
import org.openjdk.jmh.annotations.Scope;
15+
import org.openjdk.jmh.annotations.Setup;
16+
import org.openjdk.jmh.annotations.State;
17+
import org.openjdk.jmh.annotations.Threads;
18+
import org.openjdk.jmh.annotations.Warmup;
19+
20+
@BenchmarkMode(Mode.Throughput)
21+
@Warmup(iterations = 5)
22+
@Measurement(iterations = 5)
23+
@Fork(2)
24+
public class SpanTagBenchmark {
25+
26+
static final CoreTracer TRACER = CoreTracer.builder().build();
27+
28+
@State(Scope.Thread)
29+
public static class SpanPerThread {
30+
AgentSpan span;
31+
32+
@Setup(Level.Invocation)
33+
public void setup() {
34+
span = TRACER.startSpan("benchmark", "tag-benchmark");
35+
}
36+
}
37+
38+
@State(Scope.Benchmark)
39+
public static class SharedSpan {
40+
AgentSpan span;
41+
42+
@Setup(Level.Invocation)
43+
public void setup() {
44+
span = TRACER.startSpan("benchmark", "tag-benchmark-shared");
45+
}
46+
}
47+
48+
@Benchmark
49+
@Threads(1)
50+
@OutputTimeUnit(NANOSECONDS)
51+
public AgentSpan setStringTag_ownerThread(SpanPerThread state) {
52+
state.span.setTag("key", "value");
53+
return state.span;
54+
}
55+
56+
@Benchmark
57+
@Threads(1)
58+
@OutputTimeUnit(NANOSECONDS)
59+
public AgentSpan setIntTag_ownerThread(SpanPerThread state) {
60+
state.span.setTag("key", 42);
61+
return state.span;
62+
}
63+
64+
@Benchmark
65+
@Threads(1)
66+
@OutputTimeUnit(NANOSECONDS)
67+
public AgentSpan setTenTags_ownerThread(SpanPerThread state) {
68+
state.span.setTag("k0", "v0");
69+
state.span.setTag("k1", "v1");
70+
state.span.setTag("k2", "v2");
71+
state.span.setTag("k3", "v3");
72+
state.span.setTag("k4", "v4");
73+
state.span.setTag("k5", 5);
74+
state.span.setTag("k6", 6L);
75+
state.span.setTag("k7", 7.0);
76+
state.span.setTag("k8", true);
77+
state.span.setTag("k9", "v9");
78+
return state.span;
79+
}
80+
81+
@Benchmark
82+
@Threads(8)
83+
@OutputTimeUnit(NANOSECONDS)
84+
public AgentSpan setStringTag_crossThread(SharedSpan state) {
85+
state.span.setTag("key", "value");
86+
return state.span;
87+
}
88+
89+
@Benchmark
90+
@Threads(1)
91+
@OutputTimeUnit(MICROSECONDS)
92+
public AgentSpan fullLifecycle_tenTags(SpanPerThread state) {
93+
state.span.setTag("k0", "v0");
94+
state.span.setTag("k1", "v1");
95+
state.span.setTag("k2", "v2");
96+
state.span.setTag("k3", "v3");
97+
state.span.setTag("k4", "v4");
98+
state.span.setTag("k5", 5);
99+
state.span.setTag("k6", 6L);
100+
state.span.setTag("k7", 7.0);
101+
state.span.setTag("k8", true);
102+
state.span.setTag("k9", "v9");
103+
state.span.finish();
104+
return state.span;
105+
}
106+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ public boolean isFinished() {
155155
private void finishAndAddToTrace(final long durationNano) {
156156
// ensure a min duration of 1
157157
if (DURATION_NANO_UPDATER.compareAndSet(this, 0, Math.max(1, durationNano))) {
158+
context.transitionToShared();
158159
setLongRunningVersion(-this.longRunningVersion);
159160
SpanWrapper wrapper = getWrapper();
160161
if (wrapper != null) {

0 commit comments

Comments
 (0)