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 @@ -447,29 +453,65 @@ public void recordSetting(String name, String value, String unit) {
profiler.recordSetting(name, value, unit);
}

public QueueTimeTracker newQueueTimeTracker() {
return new QueueTimeTracker(this, profiler.getCurrentTicks());
public QueueTimeTracker newQueueTimeTracker(long submittingSpanId) {
return new QueueTimeTracker(this, profiler.getCurrentTicks(), submittingSpanId);
}

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,
Class<?> scheduler,
Class<?> queueType,
int queueLength,
Thread origin) {
Thread origin,
long submittingSpanId) {
if (profiler != null) {
// note: because this type traversal can update secondary_super_cache (see JDK-8180450)
// we avoid doing this unless we are absolutely certain we will record the event
Class<?> taskType = TaskWrapper.getUnwrappedType(task);
if (taskType != null) {
long endTicks = profiler.getCurrentTicks();
profiler.recordQueueTime(
startTicks, endTicks, taskType, scheduler, queueType, queueLength, origin);
startTicks,
endTicks,
taskType,
scheduler,
queueType,
queueLength,
origin,
submittingSpanId);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import datadog.trace.api.profiling.ProfilingScope;
import datadog.trace.api.profiling.Timing;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
import datadog.trace.bootstrap.instrumentation.api.ProfilerContext;
import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration;

Expand Down Expand Up @@ -93,6 +94,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,32 +141,47 @@ 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
public Timing start(TimerType type) {
if (IS_PROFILING_QUEUEING_TIME_ENABLED && type == TimerType.QUEUEING) {
return DDPROF.newQueueTimeTracker();
AgentSpan span = AgentTracer.activeSpan();
long submittingSpanId =
(span instanceof ProfilerContext) ? ((ProfilerContext) span).getSpanId() : 0L;
return DDPROF.newQueueTimeTracker(submittingSpanId);
}
return Timing.NoOp.INSTANCE;
}

/**
* 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
Expand Up @@ -9,18 +9,20 @@ public class QueueTimeTracker implements QueueTiming {
private final Thread origin;
private final long startTicks;
private final long startMillis;
private final long submittingSpanId;
private WeakReference<Object> weakTask;
// FIXME this can be eliminated by altering the instrumentation
// since it is known when the item is polled from the queue
private Class<?> scheduler;
private Class<?> queue;
private int queueLength;

public QueueTimeTracker(DatadogProfiler profiler, long startTicks) {
public QueueTimeTracker(DatadogProfiler profiler, long startTicks, long submittingSpanId) {
this.profiler = profiler;
this.origin = Thread.currentThread();
this.startTicks = startTicks;
this.startMillis = System.currentTimeMillis();
this.submittingSpanId = submittingSpanId;
}

@Override
Expand Down Expand Up @@ -49,7 +51,8 @@ public void report() {
Object task = this.weakTask.get();
if (task != null) {
// indirection reduces shallow size of the tracker instance
profiler.recordQueueTimeEvent(startTicks, task, scheduler, queue, queueLength, origin);
profiler.recordQueueTimeEvent(
startTicks, task, scheduler, queue, queueLength, origin, submittingSpanId);
}
}

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