Skip to content

Commit a484345

Browse files
committed
feat(profiler): emit synthetic SpanNode events for ThreadPoolExecutor worker threads
1 parent ebd23a2 commit a484345

3 files changed

Lines changed: 84 additions & 1 deletion

File tree

dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TPEHelper.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
import datadog.trace.api.InstrumenterConfig;
88
import datadog.trace.bootstrap.ContextStore;
99
import datadog.trace.bootstrap.instrumentation.api.AgentScope;
10+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
11+
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
12+
import datadog.trace.bootstrap.instrumentation.api.ProfilerContext;
1013
import java.util.Set;
1114
import java.util.concurrent.ThreadPoolExecutor;
1215

@@ -25,6 +28,8 @@ public final class TPEHelper {
2528
private static final Set<String> excludedClasses;
2629
// A ThreadLocal to store the Scope between beforeExecute and afterExecute if wrapping is not used
2730
private static final ThreadLocal<AgentScope> threadLocalScope;
31+
// Stores System.nanoTime() at task activation so onTaskDeactivation can compute duration
32+
private static final ThreadLocal<Long> threadLocalActivationNano;
2833

2934
private static final ClassValue<Boolean> WRAP =
3035
GenericClassValue.of(
@@ -42,8 +47,10 @@ public final class TPEHelper {
4247
excludedClasses = config.getTraceThreadPoolExecutorsExclude();
4348
if (useWrapping) {
4449
threadLocalScope = null;
50+
threadLocalActivationNano = null;
4551
} else {
4652
threadLocalScope = new ThreadLocal<>();
53+
threadLocalActivationNano = new ThreadLocal<>();
4754
}
4855
}
4956

@@ -82,7 +89,18 @@ public static AgentScope startScope(ContextStore<Runnable, State> contextStore,
8289
if (task == null || exclude(RUNNABLE, task)) {
8390
return null;
8491
}
85-
return AdviceUtils.startTaskScope(contextStore, task);
92+
AgentScope scope = AdviceUtils.startTaskScope(contextStore, task);
93+
if (scope != null && threadLocalActivationNano != null) {
94+
long startNano = System.nanoTime();
95+
threadLocalActivationNano.set(startNano);
96+
AgentSpan span = scope.span();
97+
if (span != null && span.context() instanceof ProfilerContext) {
98+
AgentTracer.get()
99+
.getProfilingContext()
100+
.onTaskActivation((ProfilerContext) span.context(), startNano);
101+
}
102+
}
103+
return scope;
86104
}
87105

88106
public static void setThreadLocalScope(AgentScope scope, Runnable task) {
@@ -112,6 +130,19 @@ public static void endScope(AgentScope scope, Runnable task) {
112130
if (task == null || exclude(RUNNABLE, task)) {
113131
return;
114132
}
133+
if (scope != null && threadLocalActivationNano != null) {
134+
Long startNano = threadLocalActivationNano.get();
135+
// noinspection ThreadLocalSetWithNull
136+
threadLocalActivationNano.set(null);
137+
if (startNano != null) {
138+
AgentSpan span = scope.span();
139+
if (span != null && span.context() instanceof ProfilerContext) {
140+
AgentTracer.get()
141+
.getProfilingContext()
142+
.onTaskDeactivation((ProfilerContext) span.context(), startNano);
143+
}
144+
}
145+
}
115146
AdviceUtils.endTaskScope(scope);
116147
}
117148

dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilingIntegration.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,40 @@ public void onSpanFinished(AgentSpan span) {
119119
ctx.getEncodedResourceName());
120120
}
121121

122+
// Calibration offset so System.nanoTime() values can be expressed as epoch nanoseconds.
123+
// Computed once at class load to keep activation/deactivation overhead minimal.
124+
private static final long EPOCH_NANOS_OFFSET =
125+
System.currentTimeMillis() * 1_000_000L - System.nanoTime();
126+
127+
@Override
128+
public void onTaskActivation(ProfilerContext profilerContext, long startTicks) {
129+
// startTicks captured by TPEHelper is the authoritative start; nothing to do here.
130+
}
131+
132+
@Override
133+
public void onTaskDeactivation(ProfilerContext profilerContext, long startTicks) {
134+
if (profilerContext == null) {
135+
return;
136+
}
137+
long endNano = System.nanoTime();
138+
long startNano = startTicks; // startTicks carries nanoTime at activation (see TPEHelper)
139+
long durationNanos = endNano - startNano;
140+
if (durationNanos <= 0) {
141+
return;
142+
}
143+
long startNanos = startNano + EPOCH_NANOS_OFFSET;
144+
long syntheticSpanId =
145+
profilerContext.getSpanId() ^ ((long) Thread.currentThread().getId() << 32) ^ startNano;
146+
DDPROF.recordSpanNodeEvent(
147+
syntheticSpanId,
148+
profilerContext.getSpanId(),
149+
profilerContext.getRootSpanId(),
150+
startNanos,
151+
durationNanos,
152+
profilerContext.getEncodedOperationName(),
153+
profilerContext.getEncodedResourceName());
154+
}
155+
122156
public void clearContext() {
123157
DDPROF.clearSpanContext();
124158
DDPROF.clearContextValue(SPAN_NAME_INDEX);

internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/ProfilingContextIntegration.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,24 @@ default void recordTaskBlock(
5858
*/
5959
default void onSpanFinished(AgentSpan span) {}
6060

61+
/**
62+
* Called when a span context continuation activates on a worker thread (task execution start).
63+
* Implementations record the start tick so a synthetic SpanNode can be emitted at deactivation.
64+
*
65+
* @param profilerContext the activated span context
66+
* @param startTicks TSC tick at activation (from {@link #getCurrentTicks()})
67+
*/
68+
default void onTaskActivation(ProfilerContext profilerContext, long startTicks) {}
69+
70+
/**
71+
* Called when a span context continuation deactivates on a worker thread (task execution end).
72+
* Implementations emit a lightweight synthetic SpanNode covering the worker thread interval.
73+
*
74+
* @param profilerContext the deactivated span context
75+
* @param startTicks TSC tick recorded at {@link #onTaskActivation}
76+
*/
77+
default void onTaskDeactivation(ProfilerContext profilerContext, long startTicks) {}
78+
6179
String name();
6280

6381
final class NoOp implements ProfilingContextIntegration {

0 commit comments

Comments
 (0)