diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a583190fcb3..e8574c97991 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -68,6 +68,10 @@ variables: description: "Enable flaky tests" value: "false" + JAVA_PROFILER_REF: + description: "When non-empty, clone DataDog/java-profiler at this Git ref (branch or tag), build ddprof, and use it as ddprof.jar for Gradle jobs instead of the Maven dependency." + value: "paul.fournillon/wallclock_precheck" + # One pipeline injection package size ratchet OCI_PACKAGE_MAX_SIZE_BYTES: 40_000_000 LIB_INJECTION_IMAGE_MAX_SIZE_BYTES: 40_000_000 @@ -171,9 +175,21 @@ default: echo "Failed to find base ref for PR" >&2 fi +# When build_java_profiler_ddprof ran, its artifact is available at custom-ddprof/ddprof.jar. +# Append root project property expected by dd-java-agent/ddprof-lib/build.gradle. +.inject_custom_ddprof_jar: &inject_custom_ddprof_jar + - | + if [ -f "${CI_PROJECT_DIR}/custom-ddprof/ddprof.jar" ]; then + echo "ddprof.jar=${CI_PROJECT_DIR}/custom-ddprof/ddprof.jar" >> gradle.properties + echo "Using custom ddprof.jar from java-profiler build" + fi + .gradle_build: &gradle_build image: ${BUILDER_IMAGE_REPO}:${BUILDER_IMAGE_VERSION_PREFIX}base stage: build + needs: + - job: build_java_profiler_ddprof + optional: true variables: MAVEN_OPTS: "-Xms256M -Xmx1024M" GRADLE_WORKERS: 6 @@ -223,6 +239,7 @@ default: org.gradle.java.installations.auto-download=false org.gradle.java.installations.fromEnv=$JAVA_HOMES EOF + - *inject_custom_ddprof_jar - mkdir -p .gradle - export GRADLE_USER_HOME=$(pwd)/.gradle # replace maven central part by MAVEN_REPOSITORY_PROXY in .mvn/wrapper/maven-wrapper.properties @@ -298,8 +315,73 @@ dd-octo-sts-pre-release-check: max: 2 when: always +# Builds java-profiler from JAVA_PROFILER_REF and publishes custom-ddprof/ddprof.jar for downstream Gradle jobs. +# Uses :ddprof-lib:assembleReleaseJar (not assembleRelease, which is native-only). JDK 21+ for release + JDK 17+ for Gradle 9. +build_java_profiler_ddprof: + image: ${BUILDER_IMAGE_REPO}:${BUILDER_IMAGE_VERSION_PREFIX}base + stage: build + rules: + - if: '$JAVA_PROFILER_REF =~ /.+/' + when: on_success + variables: + FF_USE_FASTZIP: "true" + CACHE_COMPRESSION_LEVEL: "slowest" + KUBERNETES_CPU_REQUEST: 10 + KUBERNETES_MEMORY_REQUEST: 20Gi + KUBERNETES_MEMORY_LIMIT: 20Gi + before_script: + - | + # java-profiler uses Gradle 9.x; Gradle requires JVM 17+. Builder image default java is often JDK 8. + if [ -n "${JAVA_21_HOME:-}" ] && [ -x "${JAVA_21_HOME}/bin/java" ]; then + export JAVA_HOME="$JAVA_21_HOME" + elif [ -n "${JAVA_17_HOME:-}" ] && [ -x "${JAVA_17_HOME}/bin/java" ]; then + export JAVA_HOME="$JAVA_17_HOME" + else + shopt -s nullglob + for d in /usr/lib/jvm/java-21-* /usr/lib/jvm/temurin-21-* /usr/lib/jvm/java-17-*; do + if [ -x "${d}/bin/java" ]; then + export JAVA_HOME="$d" + break + fi + done + shopt -u nullglob + fi + if [ -z "${JAVA_HOME:-}" ] || ! [ -x "${JAVA_HOME}/bin/java" ]; then + echo "Could not find JDK 17+ for Gradle 9 (set JAVA_21_HOME or JAVA_17_HOME, or install JDK 21 under /usr/lib/jvm)." >&2 + ls -la /usr/lib/jvm 2>/dev/null || true + exit 1 + fi + export PATH="${JAVA_HOME}/bin:${PATH}" + java -version + script: + - | + set -euo pipefail + mkdir -p "${CI_PROJECT_DIR}/custom-ddprof" + SRCDIR="${CI_PROJECT_DIR}/java-profiler-src" + rm -rf "$SRCDIR" + git clone --depth 1 --branch "$JAVA_PROFILER_REF" https://github.com/DataDog/java-profiler.git "$SRCDIR" + cd "$SRCDIR" + chmod +x ./gradlew + ./gradlew --version + # assembleRelease is the native link/assemble task only; the packaged jar is assembleReleaseJar. + ./gradlew :ddprof-lib:assembleReleaseJar -Pskip-tests -Pskip-gtest + JAR=$(find ddprof-lib/build/libs -maxdepth 1 -type f \( -name 'ddprof-*.jar' \) ! -name '*-sources*' ! -name '*-javadoc*' | head -1) + if [ -z "$JAR" ] || [ ! -f "$JAR" ]; then + echo "No ddprof jar found under ddprof-lib/build/libs" >&2 + ls -la ddprof-lib/build/libs 2>/dev/null || ls -laR ddprof-lib/build 2>/dev/null || true + exit 1 + fi + cp "$JAR" "${CI_PROJECT_DIR}/custom-ddprof/ddprof.jar" + ls -la "${CI_PROJECT_DIR}/custom-ddprof/" + artifacts: + when: on_success + paths: + - custom-ddprof/ddprof.jar + build: needs: + - job: build_java_profiler_ddprof + optional: true - job: maven-central-pre-release-check optional: true - job: dd-octo-sts-pre-release-check @@ -410,7 +492,9 @@ publish-artifacts-to-s3: spotless: extends: .gradle_build stage: tests - needs: [] + needs: + - job: build_java_profiler_ddprof + optional: true variables: GRADLE_MEMORY_MAX: 6G script: @@ -420,7 +504,9 @@ spotless: check-instrumentation-naming: extends: .gradle_build stage: tests - needs: [ ] + needs: + - job: build_java_profiler_ddprof + optional: true script: - ./gradlew --version - ./gradlew checkInstrumentationNaming @@ -428,7 +514,9 @@ check-instrumentation-naming: config-inversion-linter: extends: .gradle_build stage: tests - needs: [] + needs: + - job: build_java_profiler_ddprof + optional: true script: - ./gradlew --version - ./gradlew checkConfigurations @@ -437,7 +525,10 @@ test_published_artifacts: extends: .gradle_build image: ${BUILDER_IMAGE_REPO}:${BUILDER_IMAGE_VERSION_PREFIX}7 # Needs Java7 for some tests stage: tests - needs: [ build ] + needs: + - job: build_java_profiler_ddprof + optional: true + - build variables: CACHE_TYPE: "lib" script: @@ -464,7 +555,10 @@ test_published_artifacts: .check_job: extends: .gradle_build - needs: [ build ] + needs: + - job: build_java_profiler_ddprof + optional: true + - build stage: tests variables: CACHE_TYPE: "lib" @@ -500,7 +594,9 @@ test_published_artifacts: check_build_src: extends: .check_job - needs: [] + needs: + - job: build_java_profiler_ddprof + optional: true variables: GRADLE_TARGET: ":buildSrc:build" @@ -535,7 +631,10 @@ check_debugger: muzzle: extends: .gradle_build - needs: [ build_tests ] + needs: + - job: build_java_profiler_ddprof + optional: true + - build_tests stage: tests parallel: matrix: @@ -567,7 +666,10 @@ muzzle: muzzle-dep-report: extends: .gradle_build - needs: [ build_tests ] + needs: + - job: build_java_profiler_ddprof + optional: true + - build_tests stage: tests variables: CACHE_TYPE: "inst" @@ -604,7 +706,10 @@ muzzle-dep-report: extends: .gradle_build image: ${BUILDER_IMAGE_REPO}:${BUILDER_IMAGE_VERSION_PREFIX}$testJvm tags: [ "docker-in-docker:amd64" ] # use docker-in-docker runner for testcontainers - needs: [ build_tests ] + needs: + - job: build_java_profiler_ddprof + optional: true + - build_tests stage: tests variables: GRADLE_PARAMS: "-PskipFlakyTests" @@ -912,7 +1017,10 @@ deploy_to_di_backend:manual: deploy_to_maven_central: extends: .gradle_build stage: publish - needs: [ build ] + needs: + - job: build_java_profiler_ddprof + optional: true + - build variables: CACHE_TYPE: "lib" rules: @@ -940,7 +1048,10 @@ deploy_to_maven_central: deploy_snapshot_with_ddprof_snapshot: extends: .gradle_build stage: publish - needs: [ build ] + needs: + - job: build_java_profiler_ddprof + optional: true + - build variables: CACHE_TYPE: "lib" rules: diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TaskBlockHelper.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TaskBlockHelper.java new file mode 100644 index 00000000000..1696078761b --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TaskBlockHelper.java @@ -0,0 +1,64 @@ +package datadog.trace.bootstrap.instrumentation.java.concurrent; + +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; + +/** Helper for Java-level instrumentation that emits {@code datadog.TaskBlock} intervals. */ +public final class TaskBlockHelper { + static final long MIN_TASK_BLOCK_NANOS = 1_000_000L; + + private TaskBlockHelper() {} + + /** Captured state for a potential blocking interval. */ + public static final class State { + final ProfilingContextIntegration profiling; + final long startTicks; + final long startNanos; + final long spanId; + final long rootSpanId; + final long blocker; + + State( + ProfilingContextIntegration profiling, + long startTicks, + long startNanos, + long spanId, + long rootSpanId, + long blocker) { + this.profiling = profiling; + this.startTicks = startTicks; + this.startNanos = startNanos; + this.spanId = spanId; + this.rootSpanId = rootSpanId; + this.blocker = blocker; + } + } + + public static State capture(long blocker) { + return capture(blocker, AgentTracer.get().getProfilingContext(), AgentTracer.activeSpan()); + } + + static State capture(long blocker, ProfilingContextIntegration profiling, AgentSpan span) { + if (profiling == null || span == null || !(span.context() instanceof ProfilerContext)) { + return null; + } + ProfilerContext context = (ProfilerContext) span.context(); + return new State( + profiling, + profiling.getCurrentTicks(), + System.nanoTime(), + context.getSpanId(), + context.getRootSpanId(), + blocker); + } + + public static void finish(State state) { + if (state == null || System.nanoTime() - state.startNanos < MIN_TASK_BLOCK_NANOS) { + return; + } + state.profiling.recordTaskBlock( + state.startTicks, state.spanId, state.rootSpanId, state.blocker, 0L); + } +} diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/java/concurrent/TaskBlockHelperTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/java/concurrent/TaskBlockHelperTest.groovy new file mode 100644 index 00000000000..374a4a8d40d --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/java/concurrent/TaskBlockHelperTest.groovy @@ -0,0 +1,107 @@ +package datadog.trace.bootstrap.instrumentation.java.concurrent + +import datadog.trace.bootstrap.instrumentation.api.AgentSpan +import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext +import datadog.trace.bootstrap.instrumentation.api.ProfilerContext +import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration +import spock.lang.Specification + +class TaskBlockHelperTest extends Specification { + static final long SPAN_ID = 0xDEADBEEFL + static final long ROOT_SPAN_ID = 0xCAFEBABEL + static final long START_TICKS = 42_000_000L + static final long BLOCKER = 1234L + + def profiling = Mock(ProfilingContextIntegration) + def span = Mock(AgentSpan) + def ctx = Mock(ProfilerSpanContext) + def nonProfilerCtx = Mock(AgentSpanContext) + + def setup() { + span.context() >> ctx + ctx.getSpanId() >> SPAN_ID + ctx.getRootSpanId() >> ROOT_SPAN_ID + profiling.getCurrentTicks() >> START_TICKS + } + + def "capture returns null without profiling context"() { + expect: + TaskBlockHelper.capture(BLOCKER, null, span) == null + } + + def "capture returns null without active span"() { + expect: + TaskBlockHelper.capture(BLOCKER, profiling, null) == null + } + + def "capture returns null when span context is not profiler context"() { + setup: + def nonProfilerSpan = Mock(AgentSpan) + nonProfilerSpan.context() >> nonProfilerCtx + + expect: + TaskBlockHelper.capture(BLOCKER, profiling, nonProfilerSpan) == null + } + + def "capture records active span and entry timing"() { + setup: + long before = System.nanoTime() + + when: + def state = TaskBlockHelper.capture(BLOCKER, profiling, span) + + then: + state != null + state.profiling == profiling + state.startTicks == START_TICKS + state.startNanos >= before + state.startNanos <= System.nanoTime() + state.spanId == SPAN_ID + state.rootSpanId == ROOT_SPAN_ID + state.blocker == BLOCKER + } + + def "finish ignores null state"() { + when: + TaskBlockHelper.finish(null) + + then: + 0 * profiling._ + } + + def "finish ignores too-short intervals"() { + setup: + def state = new TaskBlockHelper.State( + profiling, + START_TICKS, + System.nanoTime() + 60_000_000_000L, + SPAN_ID, + ROOT_SPAN_ID, + BLOCKER) + + when: + TaskBlockHelper.finish(state) + + then: + 0 * profiling.recordTaskBlock(_, _, _, _, _) + } + + def "finish emits task block for eligible interval"() { + setup: + def state = new TaskBlockHelper.State( + profiling, + START_TICKS, + System.nanoTime() - 2 * TaskBlockHelper.MIN_TASK_BLOCK_NANOS, + SPAN_ID, + ROOT_SPAN_ID, + BLOCKER) + + when: + TaskBlockHelper.finish(state) + + then: + 1 * profiling.recordTaskBlock(START_TICKS, SPAN_ID, ROOT_SPAN_ID, BLOCKER, 0L) + } + + private interface ProfilerSpanContext extends AgentSpanContext, ProfilerContext {} +} 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 b1e07b08c32..8abf48b769b 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 @@ -341,6 +341,17 @@ public void recordTraceRoot(long rootSpanId, String endpoint, String operation) } } + /** Monotonic tick count for TaskBlock and wall-clock off-CPU interval timing. */ + public long getCurrentTicks() { + return profiler.getCurrentTicks(); + } + + int encode(CharSequence constant) { + // java-profiler ContextSetter no longer exposes value encoding. + // Keep API contract by returning "not encoded" (0), which callers already handle. + return 0; + } + public int operationNameOffset() { return offsetOf(OPERATION); } @@ -454,4 +465,24 @@ void recordQueueTimeEvent( } } } + + 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); + } + } + + void parkEnter(long spanId, long rootSpanId) { + if (profiler != null) { + profiler.parkEnter(spanId, rootSpanId); + } + } + + void parkExit(long blocker, long unblockingSpanId) { + if (profiler != null) { + profiler.parkExit(blocker, unblockingSpanId); + } + } } 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 00a0358d346..67af75a7afd 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 @@ -71,11 +71,53 @@ public void onDetach() { } } + @Override + public int encode(CharSequence constant) { + return DDPROF.encode(constant); + } + + @Override + public int encodeOperationName(CharSequence constant) { + if (SPAN_NAME_INDEX >= 0) { + return DDPROF.encode(constant); + } + return 0; + } + + @Override + public int encodeResourceName(CharSequence constant) { + if (RESOURCE_NAME_INDEX >= 0) { + return DDPROF.encode(constant); + } + return 0; + } + @Override 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 parkEnter(long spanId, long rootSpanId) { + DDPROF.parkEnter(spanId, rootSpanId); + } + + @Override + public void parkExit(long blocker, long unblockingSpanId) { + DDPROF.parkExit(blocker, unblockingSpanId); + } + public void clearContext() { DDPROF.clearSpanContext(); DDPROF.clearContextValue(SPAN_NAME_INDEX); diff --git a/dd-java-agent/agent-profiling/profiling-ddprof/src/test/java/com/datadog/profiling/ddprof/DatadogProfilerTest.java b/dd-java-agent/agent-profiling/profiling-ddprof/src/test/java/com/datadog/profiling/ddprof/DatadogProfilerTest.java index 55d39ba52a0..80754c7937f 100644 --- a/dd-java-agent/agent-profiling/profiling-ddprof/src/test/java/com/datadog/profiling/ddprof/DatadogProfilerTest.java +++ b/dd-java-agent/agent-profiling/profiling-ddprof/src/test/java/com/datadog/profiling/ddprof/DatadogProfilerTest.java @@ -20,6 +20,7 @@ import java.util.HashSet; import java.util.Properties; import java.util.UUID; +import java.util.concurrent.locks.LockSupport; import java.util.stream.IntStream; import java.util.stream.Stream; import org.junit.jupiter.api.Assumptions; @@ -29,6 +30,8 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.openjdk.jmc.common.item.IItemCollection; +import org.openjdk.jmc.common.item.IItemIterable; +import org.openjdk.jmc.common.item.ItemFilters; import org.openjdk.jmc.flightrecorder.JfrLoaderToolkit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -74,6 +77,47 @@ void test() throws Exception { } } + @Test + void testTaskBlockBridgeMethodsEmitTaskBlockEvents() throws Exception { + assertDoesNotThrow( + () -> DdprofLibraryLoader.jvmAccess().getReasonNotLoaded(), "Profiler not available"); + DatadogProfiler profiler = DatadogProfiler.newInstance(ConfigProvider.getInstance()); + if (profiler.isActive()) { + log.warn("Datadog profiler is already running. Skipping task-block integration test."); + return; + } + + OngoingRecording recording = profiler.start(); + if (recording == null) { + log.warn("Datadog Profiler is not available. Skipping task-block integration test."); + return; + } + + try { + // Direct bridge path (recordTaskBlock -> JavaProfiler.recordTaskBlock0) + long startTicks = profiler.getCurrentTicks(); + LockSupport.parkNanos(3_000_000L); // > 1ms native threshold + profiler.recordTaskBlockEvent(startTicks, 101L, 202L, 303L, 404L); + + // Park path (parkEnter/parkExit -> JavaProfiler.parkEnter0/parkExit0) + profiler.parkEnter(505L, 606L); + LockSupport.parkNanos(3_000_000L); // > 1ms native threshold + profiler.parkExit(707L, 808L); + + RecordingData data = profiler.stop(recording); + assertNotNull(data); + IItemCollection events = JfrLoaderToolkit.loadEvents(data.getStream()); + long taskBlockCount = + events.apply(ItemFilters.type("datadog.TaskBlock")).stream() + .mapToLong(IItemIterable::getItemCount) + .sum(); + + assertTrue(taskBlockCount > 0, "Expected datadog.TaskBlock events from bridge methods"); + } finally { + recording.stop(); + } + } + @ParameterizedTest @MethodSource("profilingModes") void testStartCmd(boolean cpu, boolean wall, boolean alloc, boolean memleak) throws Exception { diff --git a/dd-java-agent/instrumentation/datadog/profiling/lock-support/build.gradle b/dd-java-agent/instrumentation/datadog/profiling/lock-support/build.gradle new file mode 100644 index 00000000000..f2f91cdc7fd --- /dev/null +++ b/dd-java-agent/instrumentation/datadog/profiling/lock-support/build.gradle @@ -0,0 +1,12 @@ +apply from: "$rootDir/gradle/java.gradle" + +muzzle { + pass { + coreJdk() + } +} + +dependencies { + testImplementation libs.bundles.junit5 + testImplementation libs.bundles.mockito +} diff --git a/dd-java-agent/instrumentation/datadog/profiling/lock-support/src/main/java/datadog/trace/instrumentation/locksupport/LockSupportProfilingInstrumentation.java b/dd-java-agent/instrumentation/datadog/profiling/lock-support/src/main/java/datadog/trace/instrumentation/locksupport/LockSupportProfilingInstrumentation.java new file mode 100644 index 00000000000..a892954ce62 --- /dev/null +++ b/dd-java-agent/instrumentation/datadog/profiling/lock-support/src/main/java/datadog/trace/instrumentation/locksupport/LockSupportProfilingInstrumentation.java @@ -0,0 +1,157 @@ +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 as the Java entry point + * for native parked-state tracking. The native profiler uses this state to suppress wall-clock + * signals while the thread is parked and, when the interval belongs to an active span, to emit a + * replacement {@code datadog.TaskBlock} event on {@code parkExit}. + * + *

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

{@code parkEnter} runs even without an active span (span id 0) so the native wall-clock + * precheck can suppress {@code SIGVTALRM} for the whole park interval. TaskBlock JFR emission is + * gated by the profiler on duration and span context. + */ +@AutoService(InstrumenterModule.class) +public class LockSupportProfilingInstrumentation extends InstrumenterModule.Profiling + implements Instrumenter.ForKnownTypes, Instrumenter.HasMethodAdvice { + + public LockSupportProfilingInstrumentation() { + super("lock-support"); + } + + @Override + public String[] knownMatchingTypes() { + return new String[] {"java.util.concurrent.locks.LockSupport"}; + } + + @Override + public String[] muzzleIgnoredClassNames() { + // Static helpers on the advice class produce intra-class references that core-JDK muzzle + // cannot resolve against an empty application classpath. + return new String[] { + getClass().getName() + "$ParkAdvice", + getClass().getName() + "$State", + getClass().getName() + "$ParkState" + }; + } + + @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<>(); + } + + static final class ParkState { + final ProfilingContextIntegration profiling; + final long blockerHash; + final long spanId; + final long rootSpanId; + + ParkState( + ProfilingContextIntegration profiling, long blockerHash, long spanId, long rootSpanId) { + this.profiling = profiling; + this.blockerHash = blockerHash; + this.spanId = spanId; + this.rootSpanId = rootSpanId; + } + } + + public static final class ParkAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static ParkState before(@Advice.Argument(value = 0, optional = true) Object blocker) { + return captureState( + blocker, AgentTracer.get().getProfilingContext(), AgentTracer.activeSpan()); + } + + static ParkState captureState( + Object blocker, ProfilingContextIntegration profiling, AgentSpan span) { + if (profiling == null) { + return null; + } + // Always call parkEnter for signal suppression, even without an active span. + // spanId/rootSpanId = 0 when no active span, and native TaskBlock eligibility filters out + // zero-span intervals at exit. + long spanId = 0L; + long rootSpanId = 0L; + if (span != null && span.context() instanceof ProfilerContext) { + ProfilerContext ctx = (ProfilerContext) span.context(); + spanId = ctx.getSpanId(); + rootSpanId = ctx.getRootSpanId(); + } + profiling.parkEnter(spanId, rootSpanId); + long blockerHash = blocker != null ? System.identityHashCode(blocker) : 0L; + return new ParkState(profiling, blockerHash, spanId, rootSpanId); + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void after(@Advice.Enter ParkState state) { + // Always drain the map entry before any early return. If we returned first, a stale + // unblocking-span ID placed by a prior unpark() would persist and be incorrectly + // attributed to the next TaskBlock event emitted on this thread. + Long unblockingSpanId = State.UNPARKING_SPAN.remove(Thread.currentThread()); + finish(state, unblockingSpanId != null ? unblockingSpanId : 0L); + } + + static void finish(ParkState state, long unblockingSpanId) { + if (state == null) { + return; + } + // parkExit() clears native parked state and records an eligible TaskBlock using the entry + // tick saved by parkEnter(). + state.profiling.parkExit(state.blockerHash, unblockingSpanId); + } + } + + 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 == null || !(span.context() instanceof ProfilerContext)) { + return; + } + ProfilerContext ctx = (ProfilerContext) span.context(); + long effectiveSpanId = ctx.getSpanId(); + State.UNPARKING_SPAN.put(thread, effectiveSpanId); + } + } +} diff --git a/dd-java-agent/instrumentation/datadog/profiling/lock-support/src/test/java/datadog/trace/instrumentation/locksupport/LockSupportProfilingInstrumentationTest.java b/dd-java-agent/instrumentation/datadog/profiling/lock-support/src/test/java/datadog/trace/instrumentation/locksupport/LockSupportProfilingInstrumentationTest.java new file mode 100644 index 00000000000..4904bbeab9f --- /dev/null +++ b/dd-java-agent/instrumentation/datadog/profiling/lock-support/src/test/java/datadog/trace/instrumentation/locksupport/LockSupportProfilingInstrumentationTest.java @@ -0,0 +1,264 @@ +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 static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +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 datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration; +import datadog.trace.instrumentation.locksupport.LockSupportProfilingInstrumentation.ParkState; +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 { + + private static final long SPAN_ID = 1234L; + private static final long ROOT_SPAN_ID = 5678L; + + private interface ProfilerSpanContext extends AgentSpanContext, ProfilerContext {} + + @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 not throw and must not leave entries in {@code UNPARKING_SPAN}. + * It does call {@code remove(currentThread)}, but on an empty map that is a no-op. + */ + @Test + void parkAdvice_after_null_state_isNoOp() { + LockSupportProfilingInstrumentation.ParkAdvice.after(null); + assertTrue(State.UNPARKING_SPAN.isEmpty()); + } + + @Test + void parkAdvice_captureState_nullProfiling_returnsNull() { + AgentSpan span = mock(AgentSpan.class); + + assertNull( + LockSupportProfilingInstrumentation.ParkAdvice.captureState(new Object(), null, span)); + } + + @Test + void parkAdvice_captureState_spanless_callsParkEnterWithZeroIds() { + ProfilingContextIntegration profiling = mock(ProfilingContextIntegration.class); + + ParkState state = + LockSupportProfilingInstrumentation.ParkAdvice.captureState(new Object(), profiling, null); + + assertNotNull(state); + assertEquals(0L, state.spanId); + assertEquals(0L, state.rootSpanId); + verify(profiling).parkEnter(0L, 0L); + } + + @Test + void parkAdvice_captureState_activeSpan_callsParkEnterWithSpanIds() { + ProfilingContextIntegration profiling = mock(ProfilingContextIntegration.class); + AgentSpan span = mock(AgentSpan.class); + ProfilerSpanContext context = mock(ProfilerSpanContext.class); + Object blocker = new Object(); + when(span.context()).thenReturn(context); + when(context.getSpanId()).thenReturn(SPAN_ID); + when(context.getRootSpanId()).thenReturn(ROOT_SPAN_ID); + + ParkState state = + LockSupportProfilingInstrumentation.ParkAdvice.captureState(blocker, profiling, span); + + assertNotNull(state); + assertEquals(System.identityHashCode(blocker), state.blockerHash); + assertEquals(SPAN_ID, state.spanId); + assertEquals(ROOT_SPAN_ID, state.rootSpanId); + verify(profiling).parkEnter(SPAN_ID, ROOT_SPAN_ID); + } + + @Test + void parkAdvice_finish_callsOriginalProfilingContext() { + ProfilingContextIntegration profiling = mock(ProfilingContextIntegration.class); + ParkState state = new ParkState(profiling, 42L, SPAN_ID, ROOT_SPAN_ID); + + LockSupportProfilingInstrumentation.ParkAdvice.finish(state, 99L); + + verify(profiling).parkExit(42L, 99L); + } + + @Test + void parkAdvice_finish_nullState_doesNotTouchProfiling() { + ProfilingContextIntegration profiling = mock(ProfilingContextIntegration.class); + + LockSupportProfilingInstrumentation.ParkAdvice.finish(null, 99L); + + verifyNoInteractions(profiling); + } + + /** + * Regression test for stale-entry misattribution. + * + *

If {@code unpark(t)} is called (inserting an entry into {@code UNPARKING_SPAN}) and thread + * {@code t} then parks without an active span ({@code state == null}), the entry must still be + * drained. Without the fix, it would linger and be incorrectly attributed to the next {@code + * TaskBlock} emitted on that thread. + */ + @Test + void stale_entry_is_drained_when_park_fires_without_active_span() { + Thread t = Thread.currentThread(); + State.UNPARKING_SPAN.put(t, 99L); + + // Simulate park() returning with no active span (state == null) + LockSupportProfilingInstrumentation.ParkAdvice.after(null); + + assertNull( + State.UNPARKING_SPAN.get(t), + "Stale UNPARKING_SPAN entry must be drained even when state is null"); + } + + /** + * If multiple unpark calls race for the same parked thread, the latest span ID should be consumed + * and the entry must still be drained exactly once by ParkAdvice.after(). + */ + @Test + void latest_unparking_span_wins_and_entry_is_drained() { + Thread t = Thread.currentThread(); + State.UNPARKING_SPAN.put(t, 101L); + State.UNPARKING_SPAN.put(t, 202L); + + Long consumed = State.UNPARKING_SPAN.remove(t); + assertNotNull(consumed); + assertEquals(202L, consumed.longValue()); + assertNull(State.UNPARKING_SPAN.get(t), "Entry must be removed after consumption"); + } +} diff --git a/dd-java-agent/instrumentation/datadog/profiling/object-wait/build.gradle b/dd-java-agent/instrumentation/datadog/profiling/object-wait/build.gradle new file mode 100644 index 00000000000..d61e8bd6155 --- /dev/null +++ b/dd-java-agent/instrumentation/datadog/profiling/object-wait/build.gradle @@ -0,0 +1,16 @@ +apply from: "$rootDir/gradle/java.gradle" + +testJvmConstraints { + minJavaVersion = JavaVersion.VERSION_21 +} + +muzzle { + pass { + coreJdk('21') + } +} + +dependencies { + testImplementation libs.bundles.junit5 + testImplementation libs.bundles.mockito +} diff --git a/dd-java-agent/instrumentation/datadog/profiling/object-wait/src/main/java/datadog/trace/instrumentation/objectwait/ObjectWaitProfilingInstrumentation.java b/dd-java-agent/instrumentation/datadog/profiling/object-wait/src/main/java/datadog/trace/instrumentation/objectwait/ObjectWaitProfilingInstrumentation.java new file mode 100644 index 00000000000..615008857d9 --- /dev/null +++ b/dd-java-agent/instrumentation/datadog/profiling/object-wait/src/main/java/datadog/trace/instrumentation/objectwait/ObjectWaitProfilingInstrumentation.java @@ -0,0 +1,86 @@ +package datadog.trace.instrumentation.objectwait; + +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.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.environment.JavaVirtualMachine; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.bootstrap.instrumentation.java.concurrent.TaskBlockHelper; +import net.bytebuddy.asm.Advice; + +/** + * Instruments {@link Object#wait(long)} in JDK 21+ to emit {@code datadog.TaskBlock} JFR events. + * + *

In JDK 21+, {@code wait(long)} is a pure-Java wrapper around the native {@code wait0(long)}, + * so ByteBuddy can add advice to it. In JDK 8-20 the method is declared {@code native} and is not + * instrumented by this class (Approach 1 osThreadState precheck already suppresses SIGVTALRM for + * threads in OBJECT_WAIT state on all JDK versions). + * + *

Only {@code wait(long)} is instrumented: {@code wait()} delegates to {@code wait(0L)} and + * {@code wait(long, int)} delegates to {@code wait(long)}, so all wait variants are covered. + * + *

{@code unblockingSpanId} is always 0 because {@code notify()} and {@code notifyAll()} remain + * {@code native} in JDK 21+ and the notifying thread cannot be identified via BCI. + */ +@AutoService(InstrumenterModule.class) +public class ObjectWaitProfilingInstrumentation extends InstrumenterModule.Profiling + implements Instrumenter.ForBootstrap, Instrumenter.ForKnownTypes, Instrumenter.HasMethodAdvice { + + public ObjectWaitProfilingInstrumentation() { + super("object-wait"); + } + + @Override + public boolean isEnabled() { + return JavaVirtualMachine.isJavaVersionAtLeast(21) && super.isEnabled(); + } + + @Override + public String[] knownMatchingTypes() { + return new String[] {"java.lang.Object"}; + } + + @Override + public String[] muzzleIgnoredClassNames() { + // Static helpers on the advice class produce intra-class references that core-JDK muzzle + // cannot resolve against an empty application classpath. + return new String[] {getClass().getName() + "$WaitAdvice"}; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod() + .and(named("wait")) + .and(takesArguments(1)) + .and(takesArgument(0, long.class)) + .and(isDeclaredBy(named("java.lang.Object"))), + getClass().getName() + "$WaitAdvice"); + } + + public static final class WaitAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static TaskBlockHelper.State before(@Advice.This Object monitor) { + return captureState(monitor); + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void after(@Advice.Enter TaskBlockHelper.State state) { + finish(state); + } + + static TaskBlockHelper.State captureState(Object monitor) { + return TaskBlockHelper.capture(System.identityHashCode(monitor)); + } + + static void finish(TaskBlockHelper.State state) { + TaskBlockHelper.finish(state); + } + } +} diff --git a/dd-java-agent/instrumentation/datadog/profiling/object-wait/src/test/java/datadog/trace/instrumentation/objectwait/ObjectWaitProfilingInstrumentationTest.java b/dd-java-agent/instrumentation/datadog/profiling/object-wait/src/test/java/datadog/trace/instrumentation/objectwait/ObjectWaitProfilingInstrumentationTest.java new file mode 100644 index 00000000000..b556e8bc9e9 --- /dev/null +++ b/dd-java-agent/instrumentation/datadog/profiling/object-wait/src/test/java/datadog/trace/instrumentation/objectwait/ObjectWaitProfilingInstrumentationTest.java @@ -0,0 +1,63 @@ +package datadog.trace.instrumentation.objectwait; + +import static org.mockito.Mockito.verify; + +import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration; +import datadog.trace.bootstrap.instrumentation.java.concurrent.TaskBlockHelper; +import datadog.trace.instrumentation.objectwait.ObjectWaitProfilingInstrumentation.WaitAdvice; +import java.lang.reflect.Constructor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Unit tests for {@link ObjectWaitProfilingInstrumentation}. + * + *

Detailed interval eligibility is covered by {@code TaskBlockHelperTest}; this class verifies + * that Object.wait advice delegates to the shared Java-level TaskBlock helper. + */ +@ExtendWith(MockitoExtension.class) +class ObjectWaitProfilingInstrumentationTest { + + private static final long SPAN_ID = 0xDEADBEEFL; + private static final long ROOT_SPAN_ID = 0xCAFEBABEL; + private static final long START_TICKS = 42_000_000L; + private static final long BLOCKER = 1234L; + + @Mock private ProfilingContextIntegration profiling; + + @Test + void after_nullState_doesNotThrow() { + WaitAdvice.after(null); + } + + @Test + void after_eligibleState_emitsTaskBlockWithZeroUnblockingSpanId() throws Exception { + TaskBlockHelper.State state = newState(System.nanoTime() - 2 * taskBlockThresholdNanos()); + + WaitAdvice.after(state); + + verify(profiling).recordTaskBlock(START_TICKS, SPAN_ID, ROOT_SPAN_ID, BLOCKER, 0L); + } + + private TaskBlockHelper.State newState(long startNanos) throws Exception { + Constructor constructor = + TaskBlockHelper.State.class.getDeclaredConstructor( + ProfilingContextIntegration.class, + long.class, + long.class, + long.class, + long.class, + long.class); + constructor.setAccessible(true); + return constructor.newInstance( + profiling, START_TICKS, startNanos, SPAN_ID, ROOT_SPAN_ID, BLOCKER); + } + + private static long taskBlockThresholdNanos() throws Exception { + java.lang.reflect.Field field = TaskBlockHelper.class.getDeclaredField("MIN_TASK_BLOCK_NANOS"); + field.setAccessible(true); + return field.getLong(null); + } +} 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..3e67cf48fa3 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,36 @@ 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 + * @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) {} + + /** + * Called when the current thread is about to enter {@code LockSupport.park*}. Native code can + * suppress wall-clock signals for the park interval and record the start tick for off-CPU + * analysis. + */ + default void parkEnter(long spanId, long rootSpanId) {} + + /** + * Called when the current thread has returned from {@code LockSupport.park*}. Clears the park + * state and may emit a TaskBlock JFR event. + */ + default void parkExit(long blocker, long unblockingSpanId) {} + String name(); final class NoOp implements ProfilingContextIntegration { diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 8db93e05399..eb27a073eef 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -7865,6 +7865,14 @@ "aliases": ["DD_TRACE_INTEGRATION_LIBERTY_ENABLED", "DD_INTEGRATION_LIBERTY_ENABLED"] } ], + "DD_TRACE_LOCK_SUPPORT_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "true", + "aliases": ["DD_TRACE_INTEGRATION_LOCK_SUPPORT_ENABLED", "DD_INTEGRATION_LOCK_SUPPORT_ENABLED"] + } + ], "DD_TRACE_LOG4J_1_ENABLED": [ { "version": "A", @@ -8289,6 +8297,14 @@ "aliases": ["DD_OBFUSCATION_QUERY_STRING_REGEXP"] } ], + "DD_TRACE_OBJECT_WAIT_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "true", + "aliases": ["DD_TRACE_INTEGRATION_OBJECT_WAIT_ENABLED", "DD_INTEGRATION_OBJECT_WAIT_ENABLED"] + } + ], "DD_TRACE_OGNL_ENABLED": [ { "version": "A", diff --git a/settings.gradle.kts b/settings.gradle.kts index bd5aaceffaa..6716528fb8b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -328,6 +328,8 @@ 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", + ":dd-java-agent:instrumentation:datadog:profiling:object-wait", ":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",