Skip to content

Commit 8ed9d57

Browse files
committed
feat(profiler): wire parentSpanId, startTicks, SpanNode and TaskBlock to DatadogProfiler
1 parent 54c5786 commit 8ed9d57

4 files changed

Lines changed: 237 additions & 8 deletions

File tree

dd-java-agent/agent-profiling/profiling-ddprof/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ dependencies {
3838

3939
testImplementation libs.bundles.jmc
4040
testImplementation libs.bundles.junit5
41+
testImplementation libs.bundles.mockito
4142
}
4243

4344

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

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,15 +332,21 @@ String cmdStartProfiling(Path file) throws IllegalStateException {
332332
return cmdString;
333333
}
334334

335-
public void recordTraceRoot(long rootSpanId, String endpoint, String operation) {
336-
if (!profiler.recordTraceRoot(rootSpanId, endpoint, operation, MAX_NUM_ENDPOINTS)) {
335+
public void recordTraceRoot(
336+
long rootSpanId, long parentSpanId, long startTicks, String endpoint, String operation) {
337+
if (!profiler.recordTraceRoot(
338+
rootSpanId, parentSpanId, startTicks, endpoint, operation, MAX_NUM_ENDPOINTS)) {
337339
log.debug(
338340
"Endpoint event not written because more than {} distinct endpoints have been encountered."
339341
+ " This avoids excessive memory overhead.",
340342
MAX_NUM_ENDPOINTS);
341343
}
342344
}
343345

346+
public long getCurrentTicks() {
347+
return profiler.getCurrentTicks();
348+
}
349+
344350
public int operationNameOffset() {
345351
return offsetOf(OPERATION);
346352
}
@@ -455,6 +461,34 @@ boolean shouldRecordQueueTimeEvent(long startMillis) {
455461
return System.currentTimeMillis() - startMillis >= queueTimeThresholdMillis;
456462
}
457463

464+
void recordTaskBlockEvent(
465+
long startTicks, long spanId, long rootSpanId, long blocker, long unblockingSpanId) {
466+
if (profiler != null) {
467+
long endTicks = profiler.getCurrentTicks();
468+
profiler.recordTaskBlock(startTicks, endTicks, spanId, rootSpanId, blocker, unblockingSpanId);
469+
}
470+
}
471+
472+
public void recordSpanNodeEvent(
473+
long spanId,
474+
long parentSpanId,
475+
long rootSpanId,
476+
long startNanos,
477+
long durationNanos,
478+
int encodedOperation,
479+
int encodedResource) {
480+
if (profiler != null) {
481+
profiler.recordSpanNode(
482+
spanId,
483+
parentSpanId,
484+
rootSpanId,
485+
startNanos,
486+
durationNanos,
487+
encodedOperation,
488+
encodedResource);
489+
}
490+
}
491+
458492
void recordQueueTimeEvent(
459493
long startTicks,
460494
Object task,

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

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,31 @@ public String name() {
9393
return "ddprof";
9494
}
9595

96+
@Override
97+
public long getCurrentTicks() {
98+
return DDPROF.getCurrentTicks();
99+
}
100+
101+
@Override
102+
public void recordTaskBlock(
103+
long startTicks, long spanId, long rootSpanId, long blocker, long unblockingSpanId) {
104+
DDPROF.recordTaskBlockEvent(startTicks, spanId, rootSpanId, blocker, unblockingSpanId);
105+
}
106+
107+
@Override
108+
public void onSpanFinished(AgentSpan span) {
109+
if (span == null || !(span.context() instanceof ProfilerContext)) return;
110+
ProfilerContext ctx = (ProfilerContext) span.context();
111+
DDPROF.recordSpanNodeEvent(
112+
ctx.getSpanId(),
113+
ctx.getParentSpanId(),
114+
ctx.getRootSpanId(),
115+
span.getStartTime(),
116+
span.getDurationNano(),
117+
ctx.getEncodedOperationName(),
118+
ctx.getEncodedResourceName());
119+
}
120+
96121
public void clearContext() {
97122
DDPROF.clearSpanContext();
98123
DDPROF.clearContextValue(SPAN_NAME_INDEX);
@@ -115,15 +140,25 @@ public void onRootSpanFinished(AgentSpan rootSpan, EndpointTracker tracker) {
115140
CharSequence resourceName = rootSpan.getResourceName();
116141
CharSequence operationName = rootSpan.getOperationName();
117142
if (resourceName != null && operationName != null) {
143+
long startTicks =
144+
(tracker instanceof RootSpanTracker) ? ((RootSpanTracker) tracker).startTicks : 0L;
145+
long parentSpanId = 0L;
146+
if (rootSpan.context() instanceof ProfilerContext) {
147+
parentSpanId = ((ProfilerContext) rootSpan.context()).getParentSpanId();
148+
}
118149
DDPROF.recordTraceRoot(
119-
rootSpan.getSpanId(), resourceName.toString(), operationName.toString());
150+
rootSpan.getSpanId(),
151+
parentSpanId,
152+
startTicks,
153+
resourceName.toString(),
154+
operationName.toString());
120155
}
121156
}
122157
}
123158

124159
@Override
125160
public EndpointTracker onRootSpanStarted(AgentSpan rootSpan) {
126-
return NoOpEndpointTracker.INSTANCE;
161+
return new RootSpanTracker(DDPROF.getCurrentTicks());
127162
}
128163

129164
@Override
@@ -135,12 +170,14 @@ public Timing start(TimerType type) {
135170
}
136171

137172
/**
138-
* This implementation is actually stateless, so we don't actually need a tracker object, but
139-
* we'll create a singleton to avoid returning null and risking NPEs elsewhere.
173+
* Captures the TSC tick at root span start so we can emit real duration in the Endpoint event.
140174
*/
141-
private static final class NoOpEndpointTracker implements EndpointTracker {
175+
private static final class RootSpanTracker implements EndpointTracker {
176+
final long startTicks;
142177

143-
public static final NoOpEndpointTracker INSTANCE = new NoOpEndpointTracker();
178+
RootSpanTracker(long startTicks) {
179+
this.startTicks = startTicks;
180+
}
144181

145182
@Override
146183
public void endpointWritten(AgentSpan span) {}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package com.datadog.profiling.ddprof;
2+
3+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
4+
import static org.mockito.Mockito.mock;
5+
import static org.mockito.Mockito.when;
6+
7+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
8+
import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext;
9+
import datadog.trace.bootstrap.instrumentation.api.ProfilerContext;
10+
import org.junit.jupiter.api.Test;
11+
12+
/**
13+
* Tests for {@link DatadogProfilingIntegration#onSpanFinished(AgentSpan)}.
14+
*
15+
* <p>Because {@link DatadogProfiler} wraps a native library, we verify the filtering logic and
16+
* dispatch path without asserting on the native event itself. Native calls simply must not throw
17+
* (the {@code if (profiler != null)} guard inside {@link DatadogProfiler} protects them on systems
18+
* where the native library is unavailable).
19+
*/
20+
class DatadogProfilerSpanNodeTest {
21+
22+
/**
23+
* When the span's context does NOT implement {@link ProfilerContext}, {@code onSpanFinished}
24+
* should be a no-op and must not throw.
25+
*/
26+
@Test
27+
void onSpanFinished_nonProfilerContext_isNoOp() {
28+
DatadogProfilingIntegration integration = new DatadogProfilingIntegration();
29+
AgentSpan span = mock(AgentSpan.class);
30+
AgentSpanContext ctx = mock(AgentSpanContext.class); // plain context, NOT a ProfilerContext
31+
when(span.context()).thenReturn(ctx);
32+
33+
assertDoesNotThrow(() -> integration.onSpanFinished(span));
34+
}
35+
36+
/**
37+
* When the span's context DOES implement {@link ProfilerContext}, {@code onSpanFinished} extracts
38+
* fields and attempts to emit a SpanNode event. Must not throw regardless of whether the native
39+
* profiler is loaded.
40+
*/
41+
@Test
42+
void onSpanFinished_profilerContext_doesNotThrow() {
43+
DatadogProfilingIntegration integration = new DatadogProfilingIntegration();
44+
45+
// Mockito can create a mock that implements multiple interfaces
46+
AgentSpanContext ctx = mock(AgentSpanContext.class, org.mockito.Answers.RETURNS_DEFAULTS);
47+
ProfilerContext profilerCtx = mock(ProfilerContext.class);
48+
49+
// We need a single object that satisfies both instanceof checks.
50+
// Use a hand-rolled stub instead.
51+
TestContext combinedCtx = new TestContext(42L, 7L, 1L, 3, 5);
52+
53+
AgentSpan span = mock(AgentSpan.class);
54+
when(span.context()).thenReturn(combinedCtx);
55+
when(span.getStartTime()).thenReturn(1_700_000_000_000_000_000L);
56+
when(span.getDurationNano()).thenReturn(1_000_000L);
57+
58+
assertDoesNotThrow(() -> integration.onSpanFinished(span));
59+
}
60+
61+
/** Null span must not throw (guard at top of onSpanFinished). */
62+
@Test
63+
void onSpanFinished_nullSpan_doesNotThrow() {
64+
DatadogProfilingIntegration integration = new DatadogProfilingIntegration();
65+
assertDoesNotThrow(() -> integration.onSpanFinished(null));
66+
}
67+
68+
// ---------------------------------------------------------------------------
69+
// Stub: a single object that satisfies both AgentSpanContext and ProfilerContext
70+
// ---------------------------------------------------------------------------
71+
72+
private static final class TestContext implements AgentSpanContext, ProfilerContext {
73+
74+
private final long spanId;
75+
private final long parentSpanId;
76+
private final long rootSpanId;
77+
private final int encodedOp;
78+
private final int encodedResource;
79+
80+
TestContext(
81+
long spanId, long parentSpanId, long rootSpanId, int encodedOp, int encodedResource) {
82+
this.spanId = spanId;
83+
this.parentSpanId = parentSpanId;
84+
this.rootSpanId = rootSpanId;
85+
this.encodedOp = encodedOp;
86+
this.encodedResource = encodedResource;
87+
}
88+
89+
// ProfilerContext
90+
@Override
91+
public long getSpanId() {
92+
return spanId;
93+
}
94+
95+
@Override
96+
public long getParentSpanId() {
97+
return parentSpanId;
98+
}
99+
100+
@Override
101+
public long getRootSpanId() {
102+
return rootSpanId;
103+
}
104+
105+
@Override
106+
public int getEncodedOperationName() {
107+
return encodedOp;
108+
}
109+
110+
@Override
111+
public CharSequence getOperationName() {
112+
return "test-op";
113+
}
114+
115+
@Override
116+
public int getEncodedResourceName() {
117+
return encodedResource;
118+
}
119+
120+
@Override
121+
public CharSequence getResourceName() {
122+
return "test-resource";
123+
}
124+
125+
// AgentSpanContext
126+
@Override
127+
public datadog.trace.api.DDTraceId getTraceId() {
128+
return datadog.trace.api.DDTraceId.ZERO;
129+
}
130+
131+
@Override
132+
public datadog.trace.bootstrap.instrumentation.api.AgentTraceCollector getTraceCollector() {
133+
return datadog.trace.bootstrap.instrumentation.api.AgentTracer.NoopAgentTraceCollector
134+
.INSTANCE;
135+
}
136+
137+
@Override
138+
public int getSamplingPriority() {
139+
return datadog.trace.api.sampling.PrioritySampling.UNSET;
140+
}
141+
142+
@Override
143+
public Iterable<java.util.Map.Entry<String, String>> baggageItems() {
144+
return java.util.Collections.emptyList();
145+
}
146+
147+
@Override
148+
public datadog.trace.api.datastreams.PathwayContext getPathwayContext() {
149+
return null;
150+
}
151+
152+
@Override
153+
public boolean isRemote() {
154+
return false;
155+
}
156+
}
157+
}

0 commit comments

Comments
 (0)