Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dependencies {

testImplementation libs.bundles.jmc
testImplementation libs.bundles.junit5
testImplementation libs.bundles.mockito
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,15 +332,21 @@ String cmdStartProfiling(Path file) throws IllegalStateException {
return cmdString;
}

public void recordTraceRoot(long rootSpanId, String endpoint, String operation) {
if (!profiler.recordTraceRoot(rootSpanId, endpoint, operation, MAX_NUM_ENDPOINTS)) {
public void recordTraceRoot(
long rootSpanId, long parentSpanId, long startTicks, String endpoint, String operation) {
if (!profiler.recordTraceRoot(
rootSpanId, parentSpanId, startTicks, endpoint, operation, MAX_NUM_ENDPOINTS)) {
log.debug(
"Endpoint event not written because more than {} distinct endpoints have been encountered."
+ " This avoids excessive memory overhead.",
MAX_NUM_ENDPOINTS);
}
}

public long getCurrentTicks() {
return profiler.getCurrentTicks();
}

public int operationNameOffset() {
return offsetOf(OPERATION);
}
Expand Down Expand Up @@ -455,6 +461,34 @@ boolean shouldRecordQueueTimeEvent(long startMillis) {
return System.currentTimeMillis() - startMillis >= queueTimeThresholdMillis;
}

void recordTaskBlockEvent(
long startTicks, long spanId, long rootSpanId, long blocker, long unblockingSpanId) {
if (profiler != null) {
long endTicks = profiler.getCurrentTicks();
profiler.recordTaskBlock(startTicks, endTicks, spanId, rootSpanId, blocker, unblockingSpanId);
}
}

public void recordSpanNodeEvent(
long spanId,
long parentSpanId,
long rootSpanId,
long startNanos,
long durationNanos,
int encodedOperation,
int encodedResource) {
if (profiler != null) {
profiler.recordSpanNode(
spanId,
parentSpanId,
rootSpanId,
startNanos,
durationNanos,
encodedOperation,
encodedResource);
}
}

void recordQueueTimeEvent(
long startTicks,
Object task,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,31 @@ public String name() {
return "ddprof";
}

@Override
public long getCurrentTicks() {
return DDPROF.getCurrentTicks();
}

@Override
public void recordTaskBlock(
long startTicks, long spanId, long rootSpanId, long blocker, long unblockingSpanId) {
DDPROF.recordTaskBlockEvent(startTicks, spanId, rootSpanId, blocker, unblockingSpanId);
}

@Override
public void onSpanFinished(AgentSpan span) {
if (span == null || !(span.context() instanceof ProfilerContext)) return;
ProfilerContext ctx = (ProfilerContext) span.context();
DDPROF.recordSpanNodeEvent(
ctx.getSpanId(),
ctx.getParentSpanId(),
ctx.getRootSpanId(),
span.getStartTime(),
span.getDurationNano(),
ctx.getEncodedOperationName(),
ctx.getEncodedResourceName());
}

public void clearContext() {
DDPROF.clearSpanContext();
DDPROF.clearContextValue(SPAN_NAME_INDEX);
Expand All @@ -115,15 +140,25 @@ public void onRootSpanFinished(AgentSpan rootSpan, EndpointTracker tracker) {
CharSequence resourceName = rootSpan.getResourceName();
CharSequence operationName = rootSpan.getOperationName();
if (resourceName != null && operationName != null) {
long startTicks =
(tracker instanceof RootSpanTracker) ? ((RootSpanTracker) tracker).startTicks : 0L;
long parentSpanId = 0L;
if (rootSpan.context() instanceof ProfilerContext) {
parentSpanId = ((ProfilerContext) rootSpan.context()).getParentSpanId();
}
DDPROF.recordTraceRoot(
rootSpan.getSpanId(), resourceName.toString(), operationName.toString());
rootSpan.getSpanId(),
parentSpanId,
startTicks,
resourceName.toString(),
operationName.toString());
}
}
}

@Override
public EndpointTracker onRootSpanStarted(AgentSpan rootSpan) {
return NoOpEndpointTracker.INSTANCE;
return new RootSpanTracker(DDPROF.getCurrentTicks());
}

@Override
Expand All @@ -135,12 +170,14 @@ public Timing start(TimerType type) {
}

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

public static final NoOpEndpointTracker INSTANCE = new NoOpEndpointTracker();
RootSpanTracker(long startTicks) {
this.startTicks = startTicks;
}

@Override
public void endpointWritten(AgentSpan span) {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package com.datadog.profiling.ddprof;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext;
import datadog.trace.bootstrap.instrumentation.api.ProfilerContext;
import org.junit.jupiter.api.Test;

/**
* Tests for {@link DatadogProfilingIntegration#onSpanFinished(AgentSpan)}.
*
* <p>Because {@link DatadogProfiler} wraps a native library, we verify the filtering logic and
* dispatch path without asserting on the native event itself. Native calls simply must not throw
* (the {@code if (profiler != null)} guard inside {@link DatadogProfiler} protects them on systems
* where the native library is unavailable).
*/
class DatadogProfilerSpanNodeTest {

/**
* When the span's context does NOT implement {@link ProfilerContext}, {@code onSpanFinished}
* should be a no-op and must not throw.
*/
@Test
void onSpanFinished_nonProfilerContext_isNoOp() {
DatadogProfilingIntegration integration = new DatadogProfilingIntegration();
AgentSpan span = mock(AgentSpan.class);
AgentSpanContext ctx = mock(AgentSpanContext.class); // plain context, NOT a ProfilerContext
when(span.context()).thenReturn(ctx);

assertDoesNotThrow(() -> integration.onSpanFinished(span));
}

/**
* When the span's context DOES implement {@link ProfilerContext}, {@code onSpanFinished} extracts
* fields and attempts to emit a SpanNode event. Must not throw regardless of whether the native
* profiler is loaded.
*/
@Test
void onSpanFinished_profilerContext_doesNotThrow() {
DatadogProfilingIntegration integration = new DatadogProfilingIntegration();

// Mockito can create a mock that implements multiple interfaces
AgentSpanContext ctx = mock(AgentSpanContext.class, org.mockito.Answers.RETURNS_DEFAULTS);
ProfilerContext profilerCtx = mock(ProfilerContext.class);

// We need a single object that satisfies both instanceof checks.
// Use a hand-rolled stub instead.
TestContext combinedCtx = new TestContext(42L, 7L, 1L, 3, 5);

AgentSpan span = mock(AgentSpan.class);
when(span.context()).thenReturn(combinedCtx);
when(span.getStartTime()).thenReturn(1_700_000_000_000_000_000L);
when(span.getDurationNano()).thenReturn(1_000_000L);

assertDoesNotThrow(() -> integration.onSpanFinished(span));
}

/** Null span must not throw (guard at top of onSpanFinished). */
@Test
void onSpanFinished_nullSpan_doesNotThrow() {
DatadogProfilingIntegration integration = new DatadogProfilingIntegration();
assertDoesNotThrow(() -> integration.onSpanFinished(null));
}

// ---------------------------------------------------------------------------
// Stub: a single object that satisfies both AgentSpanContext and ProfilerContext
// ---------------------------------------------------------------------------

private static final class TestContext implements AgentSpanContext, ProfilerContext {

private final long spanId;
private final long parentSpanId;
private final long rootSpanId;
private final int encodedOp;
private final int encodedResource;

TestContext(
long spanId, long parentSpanId, long rootSpanId, int encodedOp, int encodedResource) {
this.spanId = spanId;
this.parentSpanId = parentSpanId;
this.rootSpanId = rootSpanId;
this.encodedOp = encodedOp;
this.encodedResource = encodedResource;
}

// ProfilerContext
@Override
public long getSpanId() {
return spanId;
}

@Override
public long getParentSpanId() {
return parentSpanId;
}

@Override
public long getRootSpanId() {
return rootSpanId;
}

@Override
public int getEncodedOperationName() {
return encodedOp;
}

@Override
public CharSequence getOperationName() {
return "test-op";
}

@Override
public int getEncodedResourceName() {
return encodedResource;
}

@Override
public CharSequence getResourceName() {
return "test-resource";
}

// AgentSpanContext
@Override
public datadog.trace.api.DDTraceId getTraceId() {
return datadog.trace.api.DDTraceId.ZERO;
}

@Override
public datadog.trace.bootstrap.instrumentation.api.AgentTraceCollector getTraceCollector() {
return datadog.trace.bootstrap.instrumentation.api.AgentTracer.NoopAgentTraceCollector
.INSTANCE;
}

@Override
public int getSamplingPriority() {
return datadog.trace.api.sampling.PrioritySampling.UNSET;
}

@Override
public Iterable<java.util.Map.Entry<String, String>> baggageItems() {
return java.util.Collections.emptyList();
}

@Override
public datadog.trace.api.datastreams.PathwayContext getPathwayContext() {
return null;
}

@Override
public boolean isRemote() {
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apply from: "$rootDir/gradle/java.gradle"

muzzle {
pass {
coreJdk()
}
}

dependencies {
testImplementation libs.bundles.junit5
}
Loading
Loading