From 54c578635be3450e119a326278ee85cf8d1584bf Mon Sep 17 00:00:00 2001 From: Paul Fournillon Date: Thu, 26 Mar 2026 19:17:11 +0100 Subject: [PATCH 1/4] feat(profiler): add parentSpanId and getCurrentTicks to profiling interface --- .../datadog/trace/core/DDSpanContext.java | 5 ++++ .../instrumentation/api/ProfilerContext.java | 5 ++++ .../api/ProfilingContextIntegration.java | 24 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index a17d7684a71..47a177dff07 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -372,6 +372,11 @@ public long getRootSpanId() { return getRootSpanContextOrThis().spanId; } + @Override + public long getParentSpanId() { + return parentId; + } + @Override public int getEncodedOperationName() { return encodedOperationName; diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/ProfilerContext.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/ProfilerContext.java index 2fc52a0a073..ddf27ded238 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/ProfilerContext.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/ProfilerContext.java @@ -9,6 +9,11 @@ public interface ProfilerContext { */ long getRootSpanId(); + /** + * @return the span id of the parent span, or 0 if this is the root + */ + long getParentSpanId(); + int getEncodedOperationName(); CharSequence getOperationName(); diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/ProfilingContextIntegration.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/ProfilingContextIntegration.java index 4accced983a..e5d671096c9 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/ProfilingContextIntegration.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/ProfilingContextIntegration.java @@ -34,6 +34,30 @@ default int encodeResourceName(CharSequence constant) { return 0; } + /** Returns the current TSC tick count for the calling thread. */ + default long getCurrentTicks() { + return 0L; + } + + /** + * Emits a TaskBlock event covering a blocking interval on the current thread. + * + * @param startTicks TSC tick at block entry (obtained from {@link #getCurrentTicks()}) + * @param spanId the span ID active when blocking began + * @param rootSpanId the local root span ID active when blocking began + * @param blocker identity hash code of the blocking object, or 0 if none + * @param unblockingSpanId the span ID of the thread that unblocked this thread, or 0 if unknown + */ + default void recordTaskBlock( + long startTicks, long spanId, long rootSpanId, long blocker, long unblockingSpanId) {} + + /** + * Emits a SpanNode event when a span finishes, recording its identity, timing, and encoding. + * + * @param span the finished span + */ + default void onSpanFinished(AgentSpan span) {} + String name(); final class NoOp implements ProfilingContextIntegration { From 8ed9d5791e1ea3b39d2123832f8d97f8cd4b7c9f Mon Sep 17 00:00:00 2001 From: Paul Fournillon Date: Thu, 26 Mar 2026 19:21:19 +0100 Subject: [PATCH 2/4] feat(profiler): wire parentSpanId, startTicks, SpanNode and TaskBlock to DatadogProfiler --- .../profiling-ddprof/build.gradle | 1 + .../profiling/ddprof/DatadogProfiler.java | 38 ++++- .../ddprof/DatadogProfilingIntegration.java | 49 +++++- .../ddprof/DatadogProfilerSpanNodeTest.java | 157 ++++++++++++++++++ 4 files changed, 237 insertions(+), 8 deletions(-) create mode 100644 dd-java-agent/agent-profiling/profiling-ddprof/src/test/java/com/datadog/profiling/ddprof/DatadogProfilerSpanNodeTest.java diff --git a/dd-java-agent/agent-profiling/profiling-ddprof/build.gradle b/dd-java-agent/agent-profiling/profiling-ddprof/build.gradle index 2fa61e6d34b..52ec99874ef 100644 --- a/dd-java-agent/agent-profiling/profiling-ddprof/build.gradle +++ b/dd-java-agent/agent-profiling/profiling-ddprof/build.gradle @@ -38,6 +38,7 @@ dependencies { testImplementation libs.bundles.jmc testImplementation libs.bundles.junit5 + testImplementation libs.bundles.mockito } diff --git a/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfiler.java b/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfiler.java index 3d9325168e7..616f7694402 100644 --- a/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfiler.java +++ b/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfiler.java @@ -332,8 +332,10 @@ 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.", @@ -341,6 +343,10 @@ public void recordTraceRoot(long rootSpanId, String endpoint, String operation) } } + public long getCurrentTicks() { + return profiler.getCurrentTicks(); + } + public int operationNameOffset() { return offsetOf(OPERATION); } @@ -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, diff --git a/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilingIntegration.java b/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilingIntegration.java index f6b09476df4..f4f8d1987ac 100644 --- a/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilingIntegration.java +++ b/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilingIntegration.java @@ -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); @@ -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 @@ -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) {} diff --git a/dd-java-agent/agent-profiling/profiling-ddprof/src/test/java/com/datadog/profiling/ddprof/DatadogProfilerSpanNodeTest.java b/dd-java-agent/agent-profiling/profiling-ddprof/src/test/java/com/datadog/profiling/ddprof/DatadogProfilerSpanNodeTest.java new file mode 100644 index 00000000000..a5cad42552c --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-ddprof/src/test/java/com/datadog/profiling/ddprof/DatadogProfilerSpanNodeTest.java @@ -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)}. + * + *

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> baggageItems() { + return java.util.Collections.emptyList(); + } + + @Override + public datadog.trace.api.datastreams.PathwayContext getPathwayContext() { + return null; + } + + @Override + public boolean isRemote() { + return false; + } + } +} From 93337846d8b8e0f8f62c3efef138d8632c4a763d Mon Sep 17 00:00:00 2001 From: Paul Fournillon Date: Thu, 26 Mar 2026 19:24:30 +0100 Subject: [PATCH 3/4] feat(profiler): emit SpanNode events for all spans on trace completion --- dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index 0b835c77da3..d292352434d 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -1255,6 +1255,9 @@ void write(final SpanList trace) { if (null != rootSpan) { onRootSpanFinished(rootSpan, rootSpan.getEndpointTracker()); } + for (DDSpan span : writtenTrace) { + profilingContextIntegration.onSpanFinished(span); + } } private List interceptCompleteTrace(SpanList originalTrace) { From b5306f176c0c81a0873c602085ffb59761ee0e9b Mon Sep 17 00:00:00 2001 From: Paul Fournillon Date: Fri, 27 Mar 2026 11:24:32 +0100 Subject: [PATCH 4/4] feat(profiler): add LockSupport.park/unpark instrumentation for causal DAG edges --- .../lock-support-profiling/build.gradle | 11 ++ .../LockSupportProfilingInstrumentation.java | 116 +++++++++++++ ...ckSupportProfilingInstrumentationTest.java | 153 ++++++++++++++++++ settings.gradle.kts | 1 + 4 files changed, 281 insertions(+) create mode 100644 dd-java-agent/instrumentation/datadog/profiling/lock-support-profiling/build.gradle create mode 100644 dd-java-agent/instrumentation/datadog/profiling/lock-support-profiling/src/main/java/datadog/trace/instrumentation/locksupport/LockSupportProfilingInstrumentation.java create mode 100644 dd-java-agent/instrumentation/datadog/profiling/lock-support-profiling/src/test/java/datadog/trace/instrumentation/locksupport/LockSupportProfilingInstrumentationTest.java diff --git a/dd-java-agent/instrumentation/datadog/profiling/lock-support-profiling/build.gradle b/dd-java-agent/instrumentation/datadog/profiling/lock-support-profiling/build.gradle new file mode 100644 index 00000000000..9935e818b49 --- /dev/null +++ b/dd-java-agent/instrumentation/datadog/profiling/lock-support-profiling/build.gradle @@ -0,0 +1,11 @@ +apply from: "$rootDir/gradle/java.gradle" + +muzzle { + pass { + coreJdk() + } +} + +dependencies { + testImplementation libs.bundles.junit5 +} diff --git a/dd-java-agent/instrumentation/datadog/profiling/lock-support-profiling/src/main/java/datadog/trace/instrumentation/locksupport/LockSupportProfilingInstrumentation.java b/dd-java-agent/instrumentation/datadog/profiling/lock-support-profiling/src/main/java/datadog/trace/instrumentation/locksupport/LockSupportProfilingInstrumentation.java new file mode 100644 index 00000000000..3c4cb2f5c2a --- /dev/null +++ b/dd-java-agent/instrumentation/datadog/profiling/lock-support-profiling/src/main/java/datadog/trace/instrumentation/locksupport/LockSupportProfilingInstrumentation.java @@ -0,0 +1,116 @@ +package datadog.trace.instrumentation.locksupport; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +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; +import java.util.concurrent.ConcurrentHashMap; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.matcher.ElementMatchers; + +/** + * Instruments {@link java.util.concurrent.locks.LockSupport#park} variants to emit {@code + * datadog.TaskBlock} JFR events. These events record the span, root-span, and duration of every + * blocking interval, enabling critical-path analysis across async handoffs. + * + *

Also instruments {@link java.util.concurrent.locks.LockSupport#unpark} to capture the span ID + * of the unblocking thread, which is then recorded in the TaskBlock event. + * + *

Only fires when a Datadog span is active on the calling thread, so there is no overhead on + * threads that are not part of a traced request. + */ +@AutoService(InstrumenterModule.class) +public class LockSupportProfilingInstrumentation extends InstrumenterModule.Profiling + implements Instrumenter.ForKnownTypes, Instrumenter.HasMethodAdvice { + + public LockSupportProfilingInstrumentation() { + super("lock-support-profiling"); + } + + @Override + public String[] knownMatchingTypes() { + return new String[] {"java.util.concurrent.locks.LockSupport"}; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod() + .and(isStatic()) + .and(nameStartsWith("park")) + .and(isDeclaredBy(named("java.util.concurrent.locks.LockSupport"))), + getClass().getName() + "$ParkAdvice"); + transformer.applyAdvice( + isMethod() + .and(isStatic()) + .and(ElementMatchers.named("unpark")) + .and(isDeclaredBy(named("java.util.concurrent.locks.LockSupport"))), + getClass().getName() + "$UnparkAdvice"); + } + + /** Holds shared state accessible from both {@link ParkAdvice} and {@link UnparkAdvice}. */ + public static final class State { + /** Maps target thread to the span ID of the thread that called {@code unpark()} on it. */ + public static final ConcurrentHashMap UNPARKING_SPAN = new ConcurrentHashMap<>(); + } + + public static final class ParkAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static long[] before(@Advice.Argument(value = 0, optional = true) Object blocker) { + AgentSpan span = AgentTracer.activeSpan(); + if (!(span instanceof ProfilerContext)) { + return null; + } + ProfilerContext ctx = (ProfilerContext) span; + ProfilingContextIntegration profiling = AgentTracer.get().getProfilingContext(); + long startTicks = profiling.getCurrentTicks(); + if (startTicks == 0L) { + // profiler not active + return null; + } + long blockerHash = blocker != null ? System.identityHashCode(blocker) : 0L; + return new long[] {startTicks, ctx.getSpanId(), ctx.getRootSpanId(), blockerHash}; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void after(@Advice.Enter long[] state) { + if (state == null) { + return; + } + Long unblockingSpanId = State.UNPARKING_SPAN.remove(Thread.currentThread()); + AgentTracer.get() + .getProfilingContext() + .recordTaskBlock( + state[0], + state[1], + state[2], + state[3], + unblockingSpanId != null ? unblockingSpanId : 0L); + } + } + + public static final class UnparkAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void before(@Advice.Argument(0) Thread thread) { + if (thread == null) { + return; + } + AgentSpan span = AgentTracer.activeSpan(); + if (!(span instanceof ProfilerContext)) { + return; + } + State.UNPARKING_SPAN.put(thread, ((ProfilerContext) span).getSpanId()); + } + } +} diff --git a/dd-java-agent/instrumentation/datadog/profiling/lock-support-profiling/src/test/java/datadog/trace/instrumentation/locksupport/LockSupportProfilingInstrumentationTest.java b/dd-java-agent/instrumentation/datadog/profiling/lock-support-profiling/src/test/java/datadog/trace/instrumentation/locksupport/LockSupportProfilingInstrumentationTest.java new file mode 100644 index 00000000000..06967b29667 --- /dev/null +++ b/dd-java-agent/instrumentation/datadog/profiling/lock-support-profiling/src/test/java/datadog/trace/instrumentation/locksupport/LockSupportProfilingInstrumentationTest.java @@ -0,0 +1,153 @@ +package datadog.trace.instrumentation.locksupport; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.instrumentation.locksupport.LockSupportProfilingInstrumentation.State; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link LockSupportProfilingInstrumentation}. + * + *

These tests exercise the {@link State} map directly, verifying the mechanism used to + * communicate the unblocking span ID from {@code UnparkAdvice} to {@code ParkAdvice}. + */ +class LockSupportProfilingInstrumentationTest { + + @BeforeEach + void clearState() { + State.UNPARKING_SPAN.clear(); + } + + @AfterEach + void cleanupState() { + State.UNPARKING_SPAN.clear(); + } + + // ------------------------------------------------------------------------- + // State map — basic contract + // ------------------------------------------------------------------------- + + @Test + void state_put_and_remove() { + Thread t = Thread.currentThread(); + long spanId = 12345L; + + State.UNPARKING_SPAN.put(t, spanId); + Long retrieved = State.UNPARKING_SPAN.remove(t); + + assertNotNull(retrieved); + assertEquals(spanId, (long) retrieved); + // After removal the entry should be gone + assertNull(State.UNPARKING_SPAN.get(t)); + } + + @Test + void state_remove_returns_null_when_absent() { + Thread t = new Thread(() -> {}); + assertNull(State.UNPARKING_SPAN.remove(t)); + } + + @Test + void state_is_initially_empty() { + assertTrue(State.UNPARKING_SPAN.isEmpty()); + } + + // ------------------------------------------------------------------------- + // Multithreaded: unpark thread populates map, parked thread reads it + // ------------------------------------------------------------------------- + + /** + * Simulates the UnparkAdvice → ParkAdvice handoff: + * + *

    + *
  1. Thread A (the "parked" thread) blocks on a latch. + *
  2. Thread B (the "unparking" thread) places its span ID in {@code State.UNPARKING_SPAN} for + * Thread A and then releases the latch. + *
  3. Thread A wakes up, reads and removes the span ID from the map. + *
+ */ + @Test + void unparking_spanId_is_visible_to_parked_thread() throws InterruptedException { + long unparkingSpanId = 99887766L; + + CountDownLatch ready = new CountDownLatch(1); + CountDownLatch go = new CountDownLatch(1); + AtomicLong capturedSpanId = new AtomicLong(-1L); + AtomicReference parkedThreadRef = new AtomicReference<>(); + + Thread parkedThread = + new Thread( + () -> { + parkedThreadRef.set(Thread.currentThread()); + ready.countDown(); + try { + go.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // Simulate what ParkAdvice.after does: read and remove unblocking span id + Long unblockingId = State.UNPARKING_SPAN.remove(Thread.currentThread()); + capturedSpanId.set(unblockingId != null ? unblockingId : 0L); + }); + + parkedThread.start(); + ready.await(); // wait for parked thread to register itself + + // Simulate what UnparkAdvice.before does: record unparking span id + State.UNPARKING_SPAN.put(parkedThread, unparkingSpanId); + go.countDown(); // unblock parked thread + + parkedThread.join(2_000); + assertFalse(parkedThread.isAlive(), "Test thread did not finish in time"); + assertEquals( + unparkingSpanId, + capturedSpanId.get(), + "Parked thread should have read the unblocking span id placed by unparking thread"); + } + + /** + * Verifies that if no entry exists for the parked thread (i.e. the thread was unblocked by a + * non-traced thread), the {@code remove} returns {@code null} and the code falls back to 0. + */ + @Test + void no_unparking_entry_yields_zero() throws InterruptedException { + AtomicLong capturedSpanId = new AtomicLong(-1L); + + Thread parkedThread = + new Thread( + () -> { + Long unblockingId = State.UNPARKING_SPAN.remove(Thread.currentThread()); + capturedSpanId.set(unblockingId != null ? unblockingId : 0L); + }); + parkedThread.start(); + parkedThread.join(2_000); + + assertEquals( + 0L, capturedSpanId.get(), "Should fall back to 0 when no unparking span id is recorded"); + } + + // ------------------------------------------------------------------------- + // ParkAdvice.after — null state is a no-op + // ------------------------------------------------------------------------- + + /** + * When {@code ParkAdvice.before} returns {@code null} (profiler not active or no active span), + * {@code ParkAdvice.after} must be a no-op and must not throw. + */ + @Test + void parkAdvice_after_null_state_isNoOp() { + // Should not throw and should not touch State.UNPARKING_SPAN + LockSupportProfilingInstrumentation.ParkAdvice.after(null); + assertTrue(State.UNPARKING_SPAN.isEmpty()); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index dbe66b33670..45918440adb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -325,6 +325,7 @@ include( ":dd-java-agent:instrumentation:datadog:dynamic-instrumentation:span-origin", ":dd-java-agent:instrumentation:datadog:profiling:enable-wallclock-profiling", ":dd-java-agent:instrumentation:datadog:profiling:exception-profiling", + ":dd-java-agent:instrumentation:datadog:profiling:lock-support-profiling", ":dd-java-agent:instrumentation:datadog:tracing:trace-annotation", ":dd-java-agent:instrumentation:datanucleus-4.0.5", ":dd-java-agent:instrumentation:datastax-cassandra:datastax-cassandra-3.0",