From cbba0becb8ecb3b16fa4830285d44dc391122a46 Mon Sep 17 00:00:00 2001 From: Paul Fournillon Date: Thu, 16 Jul 2026 11:38:11 +0200 Subject: [PATCH] feat: add TaskBlock profiler integration plumbing --- .gitlab-ci.yml | 39 +++- .../profiling/ddprof/DatadogProfiler.java | 59 +++-- .../ddprof/DatadogProfilerConfig.java | 5 + .../ddprof/DatadogProfilerRecordingTest.java | 9 + .../profiling/ddprof/DatadogProfilerTest.java | 60 +++-- dd-java-agent/ddprof-lib/build.gradle | 15 +- .../datadoghq/profiler/TaskBlockBridge.java | 116 ++++++++++ .../profiler/TaskBlockBridgeTest.java | 34 +++ dd-smoke-tests/build.gradle | 34 ++- .../profiling-integration-tests/build.gradle | 4 + .../profiling/NativeIoTaskBlockForkedApp.java | 53 +++++ .../SynchronizedContentionForkedApp.java | 100 ++++++++ .../NativeIoTaskBlockProfilingTest.java | 109 +++++++++ .../ObjectWaitTaskBlockProfilingTest.java | 177 ++++++++++++++ .../datadog/smoketest/SmokeTestUtils.java | 2 +- .../SynchronizedContentionProfilingTest.java | 121 ++++++++++ .../TaskBlockProfilingTestSupport.java | 216 ++++++++++++++++++ .../trace/api/config/ProfilingConfig.java | 7 +- .../TaskBlockInstrumentationConfig.java | 64 ++++++ .../TaskBlockInstrumentationConfigTest.java | 118 ++++++++++ metadata/supported-configurations.json | 18 +- 21 files changed, 1306 insertions(+), 54 deletions(-) create mode 100644 dd-java-agent/ddprof-lib/src/main/java/com/datadoghq/profiler/TaskBlockBridge.java create mode 100644 dd-java-agent/ddprof-lib/src/test/java/com/datadoghq/profiler/TaskBlockBridgeTest.java create mode 100644 dd-smoke-tests/profiling-integration-tests/src/test/java/com/datadog/smoketest/profiling/NativeIoTaskBlockForkedApp.java create mode 100644 dd-smoke-tests/profiling-integration-tests/src/test/java/com/datadog/smoketest/profiling/SynchronizedContentionForkedApp.java create mode 100644 dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/NativeIoTaskBlockProfilingTest.java create mode 100644 dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/ObjectWaitTaskBlockProfilingTest.java create mode 100644 dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/SynchronizedContentionProfilingTest.java create mode 100644 dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/TaskBlockProfilingTestSupport.java create mode 100644 internal-api/src/main/java/datadog/trace/api/profiling/TaskBlockInstrumentationConfig.java create mode 100644 internal-api/src/test/java/datadog/trace/api/profiling/TaskBlockInstrumentationConfigTest.java diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b29ab7a669e..026702eb7a2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -64,6 +64,9 @@ variables: NON_DEFAULT_JVMS: description: "Enable tests on JVMs that are not the default" value: "false" + DDPROF_VERSION: + description: "ddprof version embedded in the agent artifact used by downstream tests" + value: "1.47.0-paul.fournillon_taskblock-native-io-SNAPSHOT" RUN_FLAKY_TESTS: description: "Enable flaky tests" value: "false" @@ -343,14 +346,22 @@ build: script: - if [ $CI_PIPELINE_SOURCE == "schedule" ] ; then ./gradlew resolveAndLockAll --write-locks $GRADLE_ARGS; fi - ./gradlew --version - - ./gradlew clean :dd-java-agent:shadowJar :dd-java-agent:check :dd-trace-api:jar :dd-trace-ot:shadowJar -PskipTests -x spotlessCheck $GRADLE_ARGS - - echo UPSTREAM_TRACER_VERSION=$(java -jar workspace/dd-java-agent/build/libs/*.jar) >> upstream.env + - ./gradlew clean :dd-java-agent:shadowJar :dd-java-agent:check :dd-trace-api:jar :dd-trace-ot:shadowJar -Pddprof.version="$DDPROF_VERSION" -PskipTests -x spotlessCheck $GRADLE_ARGS + - AGENT_JARS=$(find workspace/dd-java-agent/build/libs -maxdepth 1 -type f -name 'dd-java-agent-*.jar' ! -name '*-javadoc.jar' ! -name '*-sources.jar' -print) + - test "$(printf '%s\n' "$AGENT_JARS" | grep -c .)" -eq 1 + - AGENT_JAR="$AGENT_JARS" + - AGENT_SHA256=$(sha256sum "$AGENT_JAR" | cut -d' ' -f1) + - echo "$AGENT_SHA256 $(basename "$AGENT_JAR")" > "$AGENT_JAR.sha256" + - echo UPSTREAM_TRACER_VERSION=$(java -jar "$AGENT_JAR") >> upstream.env - echo "BUILD_JOB_NAME=$CI_JOB_NAME" >> build.env - echo "BUILD_JOB_ID=$CI_JOB_ID" >> build.env + - echo "SMOKETEST_AGENT_JAR=$AGENT_JAR" >> build.env + - echo "SMOKETEST_AGENT_SHA256=$AGENT_SHA256" >> build.env artifacts: when: always paths: - 'workspace/dd-java-agent/build/libs/*.jar' + - 'workspace/dd-java-agent/build/libs/*.sha256' - 'workspace/dd-trace-api/build/libs/*.jar' - 'workspace/dd-trace-ot/build/libs/*.jar' - 'upstream.env' @@ -575,6 +586,8 @@ muzzle: # Keep matrix vars exact and in build_tests declaration order: # https://docs.gitlab.com/ci/yaml/#needsparallelmatrix needs: &needs_build_tests_inst + - job: build + artifacts: true - job: build_tests parallel: matrix: @@ -660,7 +673,11 @@ 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 + artifacts: true + - job: build_tests + artifacts: false stage: tests variables: <<: *tier_m_variables @@ -695,6 +712,8 @@ muzzle-dep-report: - *prepare_test_env - export GRADLE_OPTS="-Dorg.gradle.jvmargs='-Xms$GRADLE_MEMORY_MIN -Xmx$GRADLE_MEMORY_MAX $PROFILER_COMMAND -XX:ErrorFile=/tmp/hs_err_pid%p.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp -Djava.util.prefs.userRoot=/tmp/.java/.userPrefs-${CI_JOB_ID}' -Ddatadog.forkedMinHeapSize=128M -Ddatadog.forkedMaxHeapSize=1024M" - ./gradlew --version + - test -n "$SMOKETEST_AGENT_JAR" && test -n "$SMOKETEST_AGENT_SHA256" && test -f "$SMOKETEST_AGENT_JAR" + - echo "$SMOKETEST_AGENT_SHA256 $SMOKETEST_AGENT_JAR" | sha256sum -c - - ./gradlew $GRADLE_TARGET $GRADLE_PARAMS -PtestJvm=$testJvm -Pslot=$CI_NODE_INDEX/$CI_NODE_TOTAL $GRADLE_ARGS --continue || $CONTINUE_ON_FAILURE after_script: - *restore_pretest_env @@ -742,7 +761,9 @@ muzzle-dep-report: image: ${BUILDER_IMAGE_REPO}:${BUILDER_IMAGE_VERSION_PREFIX}$testJvm tags: [ "docker-in-docker:arm64" ] stage: tests-arm64 - needs: [] + needs: + - job: build + artifacts: true variables: <<: *tier_m_variables DEFAULT_TEST_JVMS: /^(8|11|17|21|25|27|tip)$/ # Java 27 TODO: remove 27 after GA (tip will move to 27) @@ -821,6 +842,8 @@ muzzle-dep-report: - export GRADLE_OPTS="-Dorg.gradle.jvmargs='-Xshare:off -Xms$GRADLE_MEMORY_MIN -Xmx$GRADLE_MEMORY_MAX -XX:ErrorFile=/tmp/hs_err_pid%p.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp -Djava.util.prefs.userRoot=/tmp/.java/.userPrefs-${CI_JOB_ID}' -Ddatadog.forkedMinHeapSize=128M -Ddatadog.forkedMaxHeapSize=1024M" - export GRADLE_ARGS="--build-cache --stacktrace --no-daemon --parallel --max-workers=$GRADLE_WORKERS" - ./gradlew --version + - test -n "$SMOKETEST_AGENT_JAR" && test -n "$SMOKETEST_AGENT_SHA256" && test -f "$SMOKETEST_AGENT_JAR" + - echo "$SMOKETEST_AGENT_SHA256 $SMOKETEST_AGENT_JAR" | sha256sum -c - script: - *gitlab_base_ref_params - ./gradlew $GRADLE_TARGET $GRADLE_PARAMS -PtestJvm=$testJvm -Pslot=$CI_NODE_INDEX/$CI_NODE_TOTAL $GRADLE_ARGS --continue @@ -919,6 +942,8 @@ test_base: # Keep matrix vars exact and in build_tests declaration order: # https://docs.gitlab.com/ci/yaml/#needsparallelmatrix needs: + - job: build + artifacts: true - job: build_tests parallel: matrix: @@ -970,6 +995,8 @@ test_inst_latest: # Keep matrix vars exact and in build_tests declaration order: # https://docs.gitlab.com/ci/yaml/#needsparallelmatrix needs: + - job: build + artifacts: true - job: build_tests parallel: matrix: @@ -1038,6 +1065,8 @@ test_profiling: # Keep matrix vars exact and in build_tests declaration order: # https://docs.gitlab.com/ci/yaml/#needsparallelmatrix needs: + - job: build + artifacts: true - job: build_tests parallel: matrix: @@ -1092,6 +1121,8 @@ test_smoke: # Keep matrix vars exact and in build_tests declaration order: # https://docs.gitlab.com/ci/yaml/#needsparallelmatrix needs: &needs_build_tests_smoke + - job: build + artifacts: true - job: build_tests parallel: matrix: 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 df1b75789db..2ef8a76ba53 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 @@ -1,3 +1,4 @@ +// Copyright 2026 Datadog, Inc. package com.datadog.profiling.ddprof; import static com.datadog.profiling.ddprof.DatadogProfilerConfig.enableJMethodIDOptim; @@ -36,6 +37,7 @@ import com.datadog.profiling.utils.ProfilingMode; import com.datadoghq.profiler.ContextSetter; import com.datadoghq.profiler.JavaProfiler; +import com.datadoghq.profiler.TaskBlockBridge; import datadog.environment.JavaVirtualMachine; import datadog.libs.ddprof.DdprofLibraryLoader; import datadog.trace.api.config.ProfilingConfig; @@ -101,9 +103,12 @@ public static DatadogProfiler newInstance(ConfigProvider configProvider) { return new DatadogProfiler(configProvider); } - private final AtomicBoolean recordingFlag = new AtomicBoolean(false); + // JavaProfiler owns one process-wide native recording; all wrapper instances must observe the + // same lifecycle so early-created integrations can dispatch hooks while the controller records. + private static final AtomicBoolean RECORDING = new AtomicBoolean(false); private final ConfigProvider configProvider; private final JavaProfiler profiler; + private final TaskBlockBridge taskBlockBridge; private final Set profilingModes = EnumSet.noneOf(ProfilingMode.class); private final ContextSetter contextSetter; @@ -244,6 +249,7 @@ void reset() { private DatadogProfiler(ConfigProvider configProvider) { this.configProvider = configProvider; this.profiler = DdprofLibraryLoader.javaProfiler().getComponent(); + this.taskBlockBridge = new TaskBlockBridge(profiler); this.detailedDebugLogging = configProvider.getBoolean( PROFILING_DETAILED_DEBUG_LOGGING, PROFILING_DETAILED_DEBUG_LOGGING_DEFAULT); @@ -355,7 +361,7 @@ public RecordingData stop(OngoingRecording recording) { /** A call-back from {@linkplain DatadogProfilerRecording#stop()} */ void stopProfiler() { - if (recordingFlag.compareAndSet(true, false)) { + if (RECORDING.compareAndSet(true, false)) { profiler.stop(); if (isActive()) { log.debug("Profiling is still active. Waiting to stop."); @@ -385,7 +391,7 @@ String executeProfilerCmd(String cmd) throws IOException { } Path newRecording() throws IOException, IllegalStateException { - if (recordingFlag.compareAndSet(false, true)) { + if (RECORDING.compareAndSet(false, true)) { Path recFile = Files.createTempFile(recordingsPath, "dd-profiler-", ".jfr"); String cmd = cmdStartProfiling(recFile); try { @@ -397,7 +403,7 @@ Path newRecording() throws IOException, IllegalStateException { } else { log.warn("Unable to start Datadog profiler recording: {}", e.getMessage()); } - recordingFlag.set(false); + RECORDING.set(false); throw e; } return recFile; @@ -421,7 +427,6 @@ String cmdStartProfiling(Path file) throws IllegalStateException { } StringBuilder cmd = new StringBuilder("start,jfr"); cmd.append(",file=").append(file.toAbsolutePath()); - cmd.append(",loglevel=").append(getLogLevel(configProvider)); cmd.append(",jstackdepth=").append(getStackDepth(configProvider)); cmd.append(",cstack=").append(getCStack(configProvider)); cmd.append(",safemode=").append(getSafeMode(configProvider)); @@ -464,15 +469,13 @@ String cmdStartProfiling(Path file) throws IllegalStateException { cmd.append('~'); } cmd.append(getWallInterval(configProvider)).append('m'); - // ddprof quirk: if filter parameter is omitted, it defaults to "0" (enabled), - // not empty string (disabled). When enabled without tracing, no threads are added - // to the filter, resulting in zero samples. We must explicitly pass filter= (empty) - // to disable filtering and sample all threads. - if (getWallContextFilter(configProvider)) { - cmd.append(",filter=0"); - } else { - cmd.append(",filter="); - } + boolean contextScope = getWallContextFilter(configProvider); + // Emit both protocols during the compatibility window. Older ddprof versions ignore the + // unknown wallscope option and use filter; newer versions use wallscope while continuing to + // accept filter. This cannot be feature-probed because unknown options are warnings, not + // execution errors. + cmd.append(contextScope ? ",filter=0,wallscope=context" : ",filter=,wallscope=all"); + cmd.append(",wallprecheck=true"); } cmd.append(",loglevel=").append(getLogLevel(configProvider)); if (profilingModes.contains(ALLOCATION) || profilingModes.contains(MEMLEAK)) { @@ -534,6 +537,34 @@ public void clearSpanContext() { reapplyAppContext(); } + boolean hasParkTaskBlockSupport() { + return taskBlockBridge.hasParkSupport(); + } + + boolean parkEnter() { + if (!RECORDING.get()) { + return false; + } + taskBlockBridge.parkEnter(); + return true; + } + + void parkExit(long blocker, long unblockingSpanId) { + taskBlockBridge.parkExit(blocker, unblockingSpanId); + } + + boolean hasSynchronousTaskBlockSupport() { + return taskBlockBridge.hasSynchronousTaskBlockSupport(); + } + + long beginTaskBlock(int state) { + return RECORDING.get() ? taskBlockBridge.beginTaskBlock(state) : 0L; + } + + boolean endTaskBlock(long token, long blocker, long unblockingSpanId) { + return token != 0L && taskBlockBridge.endTaskBlock(token, blocker, unblockingSpanId); + } + public boolean setContextValue(int offset, String value) { if (contextSetter != null && offset >= 0 && value != null) { try { diff --git a/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilerConfig.java b/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilerConfig.java index 8582e27c853..f9d5a17aaf0 100644 --- a/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilerConfig.java +++ b/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilerConfig.java @@ -58,6 +58,7 @@ import com.datadog.profiling.controller.ProfilingSupport; import datadog.environment.JavaVirtualMachine; import datadog.trace.api.config.ProfilingConfig; +import datadog.trace.api.profiling.TaskBlockInstrumentationConfig; import datadog.trace.bootstrap.config.provider.ConfigProvider; import java.lang.management.ManagementFactory; import java.util.Collections; @@ -169,6 +170,10 @@ public static boolean getWallContextFilter(ConfigProvider configProvider) { PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER_DEFAULT); } + public static boolean getWallPrecheck(ConfigProvider configProvider) { + return TaskBlockInstrumentationConfig.isWallPrecheckEnabled(configProvider); + } + static boolean isJmethodIDSafe() { return ProfilingSupport.isJmethodIDSafe(); } diff --git a/dd-java-agent/agent-profiling/profiling-ddprof/src/test/java/com/datadog/profiling/ddprof/DatadogProfilerRecordingTest.java b/dd-java-agent/agent-profiling/profiling-ddprof/src/test/java/com/datadog/profiling/ddprof/DatadogProfilerRecordingTest.java index 79d6985508e..6ae99538a0f 100644 --- a/dd-java-agent/agent-profiling/profiling-ddprof/src/test/java/com/datadog/profiling/ddprof/DatadogProfilerRecordingTest.java +++ b/dd-java-agent/agent-profiling/profiling-ddprof/src/test/java/com/datadog/profiling/ddprof/DatadogProfilerRecordingTest.java @@ -1,3 +1,4 @@ +// Copyright 2026 Datadog, Inc. package com.datadog.profiling.ddprof; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @@ -72,4 +73,12 @@ void testSnapshot() throws Exception { assertNotNull(inputStream); assertTrue(inputStream.available() > 0); } + + @Test + void recordingLifecycleIsVisibleAcrossProfilerWrappers() { + DatadogProfiler integrationProfiler = DatadogProfiler.newInstance(); + + assertTrue(integrationProfiler.parkEnter()); + integrationProfiler.parkExit(0L, 0L); + } } 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 6dcb3e9d231..82fb85e7757 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 @@ -69,7 +69,7 @@ void test() throws Exception { IItemCollection events = JfrLoaderToolkit.loadEvents(data.getStream()); assertTrue(events.hasItems()); } finally { - recording.stop(); + recording.close(); } } else { log.warn("Datadog Profiler is not available. Skipping test."); @@ -104,8 +104,7 @@ void testStartCmd(boolean cpu, boolean wall, boolean alloc, boolean memleak) thr private static Stream profilingModes() { return IntStream.range(0, 1 << 4) .mapToObj( - x -> - Arguments.of((x & 0x1000) != 0, (x & 0x100) != 0, (x & 0x10) != 0, (x & 0x1) != 0)); + x -> Arguments.of((x & 0x8) != 0, (x & 0x4) != 0, (x & 0x2) != 0, (x & 0x1) != 0)); } @Test @@ -127,7 +126,11 @@ void testStartCmdEnableJMethodIDOptim() throws Exception { @ParameterizedTest @MethodSource("wallContextFilterModes") - void testWallContextFilter(boolean tracingEnabled, boolean contextFilterEnabled) + void testWallScopeCompatibilityOptions( + boolean tracingEnabled, + boolean contextFilterEnabled, + String expectedFilter, + String expectedWallScope) throws Exception { // Skip test if profiler native library is not available (e.g., on macOS) try { @@ -153,33 +156,42 @@ void testWallContextFilter(boolean tracingEnabled, boolean contextFilterEnabled) String cmd = profiler.cmdStartProfiling(targetFile); assertTrue(cmd.contains("wall="), "Command should contain wall profiling: " + cmd); + assertTrue(cmd.contains(",filter=" + expectedFilter + ",wallscope="), cmd); + assertTrue(cmd.contains(",wallscope=" + expectedWallScope), cmd); + assertTrue(cmd.contains(",wallprecheck=true"), cmd); + } - if (tracingEnabled && contextFilterEnabled) { - assertTrue( - cmd.contains(",filter=0"), - "Command should contain ',filter=0' when tracing and context filter are enabled: " + cmd); - } else { - assertTrue( - cmd.contains(",filter="), - "Command should contain ',filter=' when tracing is disabled or context filter is disabled: " - + cmd); - if (cmd.contains(",filter=0")) { - throw new AssertionError( - "Command should not contain ',filter=0' when tracing is disabled or context filter is disabled: " - + cmd); + @Test + void testWallPrecheckIsAlwaysEnabled() throws Exception { + try { + Throwable reason = DdprofLibraryLoader.jvmAccess().getReasonNotLoaded(); + if (reason != null) { + Assumptions.assumeTrue(false, "Profiler not available: " + reason.getMessage()); } + } catch (Throwable e) { + Assumptions.assumeTrue(false, "Profiler not available: " + e.getMessage()); } + + Properties props = new Properties(); + props.put(ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_ENABLED, "true"); + props.put(ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_PRECHECK, "false"); + + DatadogProfiler profiler = + DatadogProfiler.newInstance(ConfigProvider.withPropertiesOverride(props)); + + Path targetFile = Paths.get("/tmp/target.jfr"); + String cmd = profiler.cmdStartProfiling(targetFile); + + assertTrue(cmd.contains("wall="), cmd); + assertTrue(cmd.contains(",wallprecheck=true"), cmd); } private static Stream wallContextFilterModes() { return Stream.of( - Arguments.of(true, true), // tracing enabled, context filter enabled -> filter=0 - Arguments.of(true, false), // tracing enabled, context filter disabled -> filter= - Arguments.of( - false, true), // tracing disabled, context filter enabled -> filter= (tracing disabled - // overrides) - Arguments.of(false, false) // tracing disabled, context filter disabled -> filter= - ); + Arguments.of(true, true, "0", "context"), + Arguments.of(true, false, "", "all"), + Arguments.of(false, true, "", "all"), + Arguments.of(false, false, "", "all")); } @Test diff --git a/dd-java-agent/ddprof-lib/build.gradle b/dd-java-agent/ddprof-lib/build.gradle index f6d653e3bbd..c8e9e19de61 100644 --- a/dd-java-agent/ddprof-lib/build.gradle +++ b/dd-java-agent/ddprof-lib/build.gradle @@ -7,14 +7,27 @@ plugins { apply from: "$rootDir/gradle/java.gradle" +// These classes are native-library loading and compatibility glue. Their meaningful coverage comes +// from profiler smoke tests; per-class Jacoco verification is not stable across CI JVM/OS splits. +excludedClassesCoverage += ['datadog.libs.ddprof.DdprofLibraryLoader*', 'com.datadoghq.profiler.TaskBlockBridge',] + +def ddprofVersion = project.findProperty('ddprof.version') + dependencies { // This module provides the ddprof library as an api dependency // so that other modules can easily depend on it. - implementation project.hasProperty('ddprof.jar') ? files(project.findProperty('ddprof.jar')) : libs.ddprof + implementation project.hasProperty('ddprof.jar') ? files(project.findProperty('ddprof.jar')) : + ddprofVersion != null ? "com.datadoghq:ddprof:${ddprofVersion}" : libs.ddprof api project(':internal-api') api project(':dd-trace-api') } +if (ddprofVersion?.endsWith('-SNAPSHOT')) { + configurations.configureEach { + resolutionStrategy.cacheChangingModulesFor 0, 'seconds' + } +} + tasks.named("shadowJar", ShadowJar) { dependencies deps.excludeShared exclude '**/*.debug' diff --git a/dd-java-agent/ddprof-lib/src/main/java/com/datadoghq/profiler/TaskBlockBridge.java b/dd-java-agent/ddprof-lib/src/main/java/com/datadoghq/profiler/TaskBlockBridge.java new file mode 100644 index 00000000000..040c6d9df04 --- /dev/null +++ b/dd-java-agent/ddprof-lib/src/main/java/com/datadoghq/profiler/TaskBlockBridge.java @@ -0,0 +1,116 @@ +// Copyright 2026 Datadog, Inc. +package com.datadoghq.profiler; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; + +/** + * Compatibility bridge for TaskBlock hooks added to newer ddprof artifacts. + * + *

This class deliberately lives in the profiler package: that gives its lookup access to the + * package-private park hooks without making those hooks public. Hook names are resolved from + * strings so the agent remains binary-compatible with older ddprof artifacts. Resolved method + * handles are cached and invoked directly on the hot path. + */ +public final class TaskBlockBridge { + private static final MethodHandle PARK_ENTER = findVirtual("parkEnter", void.class); + private static final MethodHandle PARK_EXIT = + findVirtual("parkExit", void.class, long.class, long.class); + private static final MethodHandle BEGIN_TASK_BLOCK = + findVirtual("beginTaskBlock", long.class, int.class); + private static final MethodHandle END_TASK_BLOCK = + findVirtual("endTaskBlock", boolean.class, long.class, long.class, long.class); + + private final JavaProfiler profiler; + + /** Creates a bridge bound to the loaded profiler singleton. */ + public TaskBlockBridge(JavaProfiler profiler) { + this.profiler = profiler; + } + + /** Returns whether both native park hooks are available. */ + public boolean hasParkSupport() { + return PARK_ENTER != null && PARK_EXIT != null; + } + + /** Returns whether the synchronous paired TaskBlock API is available. */ + public boolean hasSynchronousTaskBlockSupport() { + return BEGIN_TASK_BLOCK != null && END_TASK_BLOCK != null; + } + + /** Invokes the native park-entry hook when it is available. */ + public boolean parkEnter() { + if (!hasParkSupport()) { + return false; + } + try { + PARK_ENTER.invokeExact(profiler); + return true; + } catch (Throwable throwable) { + throw propagate(throwable); + } + } + + /** Invokes the native park-exit hook when it is available. */ + public void parkExit(long blocker, long unblockingSpanId) { + if (!hasParkSupport()) { + return; + } + try { + PARK_EXIT.invokeExact(profiler, blocker, unblockingSpanId); + } catch (Throwable throwable) { + throw propagate(throwable); + } + } + + /** Begins a synchronous TaskBlock interval, returning {@code 0} when unsupported. */ + public long beginTaskBlock(int state) { + if (!hasSynchronousTaskBlockSupport()) { + return 0L; + } + try { + return (long) BEGIN_TASK_BLOCK.invokeExact(profiler, state); + } catch (Throwable throwable) { + throw propagate(throwable); + } + } + + /** Ends a synchronous TaskBlock interval when the paired API is available. */ + public boolean endTaskBlock(long token, long blocker, long unblockingSpanId) { + if (!hasSynchronousTaskBlockSupport()) { + return false; + } + try { + return (boolean) END_TASK_BLOCK.invokeExact(profiler, token, blocker, unblockingSpanId); + } catch (Throwable throwable) { + throw propagate(throwable); + } + } + + private static MethodHandle findVirtual( + String methodName, Class returnType, Class... parameterTypes) { + return findVirtual(JavaProfiler.class, methodName, returnType, parameterTypes); + } + + static MethodHandle findVirtual( + Class targetClass, String methodName, Class returnType, Class... parameterTypes) { + try { + return MethodHandles.lookup() + .findVirtual(targetClass, methodName, MethodType.methodType(returnType, parameterTypes)); + } catch (NoSuchMethodException | IllegalAccessException ignored) { + return null; + } + } + + private static RuntimeException propagate(Throwable throwable) { + if (throwable instanceof RuntimeException) { + return (RuntimeException) throwable; + } + if (throwable instanceof Error) { + throw (Error) throwable; + } + return new IllegalStateException( + "Unexpected checked exception from ddprof TaskBlock hook", throwable); + } +} diff --git a/dd-java-agent/ddprof-lib/src/test/java/com/datadoghq/profiler/TaskBlockBridgeTest.java b/dd-java-agent/ddprof-lib/src/test/java/com/datadoghq/profiler/TaskBlockBridgeTest.java new file mode 100644 index 00000000000..a0f92a7ca9d --- /dev/null +++ b/dd-java-agent/ddprof-lib/src/test/java/com/datadoghq/profiler/TaskBlockBridgeTest.java @@ -0,0 +1,34 @@ +// Copyright 2026 Datadog, Inc. +package com.datadoghq.profiler; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +class TaskBlockBridgeTest { + @Test + void resolvesPackagePrivateHooksFromProfilerPackage() { + assertNotNull(TaskBlockBridge.findVirtual(SupportedHooks.class, "parkEnter", void.class)); + assertNotNull( + TaskBlockBridge.findVirtual(SupportedHooks.class, "beginTaskBlock", long.class, int.class)); + } + + @Test + void missingHooksAreReportedWithoutFailingClassInitialization() { + assertNull(TaskBlockBridge.findVirtual(UnsupportedHooks.class, "parkEnter", void.class)); + assertNull( + TaskBlockBridge.findVirtual( + UnsupportedHooks.class, "beginTaskBlock", long.class, int.class)); + } + + static final class SupportedHooks { + void parkEnter() {} + + long beginTaskBlock(int state) { + return state; + } + } + + static final class UnsupportedHooks {} +} diff --git a/dd-smoke-tests/build.gradle b/dd-smoke-tests/build.gradle index 15e070ea84d..8a78225c778 100644 --- a/dd-smoke-tests/build.gradle +++ b/dd-smoke-tests/build.gradle @@ -37,16 +37,34 @@ subprojects { Project subProj -> subProj.evaluationDependsOn ':dd-java-agent' subProj.tasks.withType(Test).configureEach { subTask -> - def agentTaskRef = project(':dd-java-agent').tasks.named('shadowJar', ShadowJar) - dependsOn agentTaskRef - // Tests depend on this to know where to run things and what agent jar to use jvmArgs "-Ddatadog.smoketest.builddir=${buildDir}" - jvmArgumentProviders.add(new CommandLineArgumentProvider() { - @Override - Iterable asArguments() { - return agentTaskRef.map { ["-Ddatadog.smoketest.agent.shadowJar.path=${it.archiveFile.get()}"] }.get() + def prebuiltAgentPath = providers.environmentVariable('SMOKETEST_AGENT_JAR') + if (prebuiltAgentPath.isPresent()) { + def prebuiltAgent = prebuiltAgentPath.map { rootProject.file(it) } + inputs.file(prebuiltAgent) + .withPropertyName('smokeTestAgentJar') + .withPathSensitivity(PathSensitivity.NONE) + doFirst { + if (!prebuiltAgent.get().isFile()) { + throw new GradleException("Prebuilt smoke-test agent does not exist: ${prebuiltAgent.get()}") } - }) + } + jvmArgumentProviders.add(new CommandLineArgumentProvider() { + @Override + Iterable asArguments() { + return ["-Ddatadog.smoketest.agent.shadowJar.path=${prebuiltAgent.get().absolutePath}"] + } + }) + } else { + def agentTaskRef = project(':dd-java-agent').tasks.named('shadowJar', ShadowJar) + dependsOn agentTaskRef + jvmArgumentProviders.add(new CommandLineArgumentProvider() { + @Override + Iterable asArguments() { + return agentTaskRef.map { ["-Ddatadog.smoketest.agent.shadowJar.path=${it.archiveFile.get()}"] }.get() + } + }) + } } } diff --git a/dd-smoke-tests/profiling-integration-tests/build.gradle b/dd-smoke-tests/profiling-integration-tests/build.gradle index 74fe60e6b82..be0506df3e2 100644 --- a/dd-smoke-tests/profiling-integration-tests/build.gradle +++ b/dd-smoke-tests/profiling-integration-tests/build.gradle @@ -38,6 +38,10 @@ dependencies { tasks.withType(Test).configureEach { dependsOn "shadowJar" + inputs.file(providers.environmentVariable("TEST_LIBASYNC")) + .withPropertyName("testLibAsync") + .withPathSensitivity(PathSensitivity.NONE) + .optional() def shadowJarTask = tasks.named('shadowJar', ShadowJar) jvmArgumentProviders.add(new CommandLineArgumentProvider() { @Override diff --git a/dd-smoke-tests/profiling-integration-tests/src/test/java/com/datadog/smoketest/profiling/NativeIoTaskBlockForkedApp.java b/dd-smoke-tests/profiling-integration-tests/src/test/java/com/datadog/smoketest/profiling/NativeIoTaskBlockForkedApp.java new file mode 100644 index 00000000000..92ce4897734 --- /dev/null +++ b/dd-smoke-tests/profiling-integration-tests/src/test/java/com/datadog/smoketest/profiling/NativeIoTaskBlockForkedApp.java @@ -0,0 +1,53 @@ +// Copyright 2026 Datadog, Inc. +package com.datadog.smoketest.profiling; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; + +/** Forked workload that blocks a platform thread in native accept and socket-read calls. */ +public final class NativeIoTaskBlockForkedApp { + private static final int ITERATIONS = 20; + + public static void main(String[] args) throws Exception { + Thread.currentThread().setName("native-io-spanless"); + for (int i = 0; i < ITERATIONS; i++) { + runBlockingAcceptAndRead(); + } + Thread.sleep(1500); + } + + private static void runBlockingAcceptAndRead() throws Exception { + try (ServerSocket server = new ServerSocket(0, 1, InetAddress.getLoopbackAddress())) { + Thread client = + new Thread( + () -> { + try { + Thread.sleep(50L); + try (Socket socket = + new Socket(InetAddress.getLoopbackAddress(), server.getLocalPort())) { + Thread.sleep(50L); + OutputStream output = socket.getOutputStream(); + output.write(1); + output.flush(); + } + } catch (Exception e) { + throw new IllegalStateException(e); + } + }, + "native-io-client"); + client.start(); + + try (Socket accepted = server.accept()) { + accepted.setSoTimeout(5000); + InputStream input = accepted.getInputStream(); + if (input.read() != 1) { + throw new IllegalStateException("Unexpected socket payload"); + } + } + client.join(); + } + } +} diff --git a/dd-smoke-tests/profiling-integration-tests/src/test/java/com/datadog/smoketest/profiling/SynchronizedContentionForkedApp.java b/dd-smoke-tests/profiling-integration-tests/src/test/java/com/datadog/smoketest/profiling/SynchronizedContentionForkedApp.java new file mode 100644 index 00000000000..bbef973747f --- /dev/null +++ b/dd-smoke-tests/profiling-integration-tests/src/test/java/com/datadog/smoketest/profiling/SynchronizedContentionForkedApp.java @@ -0,0 +1,100 @@ +// Copyright 2026 Datadog, Inc. +package com.datadog.smoketest.profiling; + +import java.util.concurrent.CountDownLatch; + +/** Forked workload for native monitor-contention TaskBlock smoke coverage. */ +public final class SynchronizedContentionForkedApp { + private static final int REPETITIONS = 10; + private static final Object BLOCK_LOCK = new Object(); + + public static void main(final String[] args) throws Exception { + SynchronizedContentionForkedApp app = new SynchronizedContentionForkedApp(); + for (int i = 0; i < REPETITIONS; i++) { + app.runBlockScenario(); + app.runInstanceMethodScenario(); + app.runStaticMethodScenario(); + } + Thread.sleep(1500); + } + + private final InstanceLockTarget instanceTarget = new InstanceLockTarget(); + + private void runBlockScenario() throws Exception { + CountDownLatch holderIn = new CountDownLatch(1); + Thread holder = + new Thread( + () -> { + synchronized (BLOCK_LOCK) { + holderIn.countDown(); + sleepWhileHoldingMonitor(); + } + }, + "sync-block-holder"); + holder.start(); + holderIn.await(); + + Thread contender = + new Thread( + () -> { + synchronized (BLOCK_LOCK) { + // entry-queue wait is the TaskBlock interval + } + }, + "sync-block-spanless"); + contender.start(); + contender.join(); + holder.join(); + } + + private void runInstanceMethodScenario() throws Exception { + CountDownLatch holderIn = new CountDownLatch(1); + Thread holder = new Thread(() -> instanceTarget.hold(holderIn), "sync-instance-holder"); + holder.start(); + holderIn.await(); + + Thread contender = new Thread(instanceTarget::contend, "sync-instance-spanless"); + contender.start(); + contender.join(); + holder.join(); + } + + private void runStaticMethodScenario() throws Exception { + CountDownLatch holderIn = new CountDownLatch(1); + Thread holder = new Thread(() -> StaticLockTarget.hold(holderIn), "sync-static-holder"); + holder.start(); + holderIn.await(); + + Thread contender = new Thread(StaticLockTarget::contend, "sync-static-spanless"); + contender.start(); + contender.join(); + holder.join(); + } + + private static void sleepWhileHoldingMonitor() { + try { + Thread.sleep(50L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } + } + + static final class InstanceLockTarget { + synchronized void hold(final CountDownLatch in) { + in.countDown(); + sleepWhileHoldingMonitor(); + } + + synchronized void contend() {} + } + + static final class StaticLockTarget { + static synchronized void hold(final CountDownLatch in) { + in.countDown(); + sleepWhileHoldingMonitor(); + } + + static synchronized void contend() {} + } +} diff --git a/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/NativeIoTaskBlockProfilingTest.java b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/NativeIoTaskBlockProfilingTest.java new file mode 100644 index 00000000000..21f38c7316f --- /dev/null +++ b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/NativeIoTaskBlockProfilingTest.java @@ -0,0 +1,109 @@ +// Copyright 2026 Datadog, Inc. +package datadog.smoketest; + +import static datadog.smoketest.SmokeTestUtils.checkProcessSuccessfullyEnd; +import static datadog.smoketest.TaskBlockProfilingTestSupport.BLOCKER; +import static datadog.smoketest.TaskBlockProfilingTestSupport.LOCAL_ROOT_SPAN_ID; +import static datadog.smoketest.TaskBlockProfilingTestSupport.SPAN_ID; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.Path; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.openjdk.jmc.common.item.IItem; +import org.openjdk.jmc.common.item.IItemCollection; +import org.openjdk.jmc.common.item.IItemIterable; +import org.openjdk.jmc.common.item.IMemberAccessor; +import org.openjdk.jmc.common.item.ItemFilters; +import org.openjdk.jmc.common.unit.IQuantity; +import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; + +/** Linux smoke coverage for TaskBlocks emitted by native socket-I/O interposition. */ +@DisabledOnJ9 +@EnabledOnOs(OS.LINUX) +final class NativeIoTaskBlockProfilingTest { + private Path dumpDir; + private Path logFilePath; + + @BeforeEach + void setup(TestInfo testInfo) throws IOException { + logFilePath = + TaskBlockProfilingTestSupport.buildLogFilePath( + NativeIoTaskBlockProfilingTest.class, testInfo, "nativeIo"); + dumpDir = TaskBlockProfilingTestSupport.createDumpDir("dd-profiler-nativeio-"); + } + + @AfterEach + void tearDown() throws IOException { + TaskBlockProfilingTestSupport.deleteRecursively(dumpDir); + } + + @Test + void blockingNativeSocketIoEmitsSpanlessTaskBlocks() throws Exception { + Process targetProcess = + TaskBlockProfilingTestSupport.createTaskBlockProcessBuilder( + "smoke-test-nativeio-taskblock", + com.datadog.smoketest.profiling.NativeIoTaskBlockForkedApp.class.getName(), + dumpDir, + logFilePath) + .start(); + checkProcessSuccessfullyEnd(targetProcess, logFilePath); + + JfrStats stats = new JfrStats(); + for (IItemCollection events : TaskBlockProfilingTestSupport.loadDumpedEvents(dumpDir)) { + stats.add(events); + } + + assertTrue(stats.count > 0, "Expected TaskBlocks from blocking native socket I/O"); + assertTrue(stats.nonZeroBlockerCount > 0, "Expected encoded native I/O blocker identity"); + assertFalse(stats.hasNonZeroSpanId, "Spanless native I/O TaskBlocks must keep spanId zero"); + assertFalse( + stats.hasNonZeroLocalRootSpanId, + "Spanless native I/O TaskBlocks must keep root spanId zero"); + assertFalse(stats.hasMissingEventThread, "TaskBlock events must resolve Event Thread"); + assertFalse( + TaskBlockProfilingTestSupport.logContainsAny( + logFilePath, + "native I/O hooks disabled", + "NoClassDefFoundError", + "Failed to handle exception"), + "native I/O TaskBlock path failed"); + } + + private static final class JfrStats { + long count; + long nonZeroBlockerCount; + boolean hasNonZeroSpanId; + boolean hasNonZeroLocalRootSpanId; + boolean hasMissingEventThread; + + void add(IItemCollection events) { + for (IItemIterable items : events.apply(ItemFilters.type("datadog.TaskBlock"))) { + TaskBlockProfilingTestSupport.assertFinalTaskBlockSchema(items); + IMemberAccessor spanId = SPAN_ID.getAccessor(items.getType()); + IMemberAccessor rootSpanId = + LOCAL_ROOT_SPAN_ID.getAccessor(items.getType()); + IMemberAccessor blocker = BLOCKER.getAccessor(items.getType()); + IMemberAccessor eventThread = + JdkAttributes.EVENT_THREAD_NAME.getAccessor(items.getType()); + for (IItem item : items) { + String thread = eventThread.getMember(item); + if (!"native-io-spanless".equals(thread)) { + continue; + } + count++; + nonZeroBlockerCount += blocker.getMember(item).longValue() == 0 ? 0 : 1; + hasNonZeroSpanId |= spanId.getMember(item).longValue() != 0; + hasNonZeroLocalRootSpanId |= rootSpanId.getMember(item).longValue() != 0; + hasMissingEventThread |= thread.isEmpty(); + } + } + } + } +} diff --git a/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/ObjectWaitTaskBlockProfilingTest.java b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/ObjectWaitTaskBlockProfilingTest.java new file mode 100644 index 00000000000..f6cc89a05a8 --- /dev/null +++ b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/ObjectWaitTaskBlockProfilingTest.java @@ -0,0 +1,177 @@ +// Copyright 2026 Datadog, Inc. +package datadog.smoketest; + +import static datadog.smoketest.SmokeTestUtils.checkProcessSuccessfullyEnd; +import static datadog.smoketest.TaskBlockProfilingTestSupport.BLOCKER; +import static datadog.smoketest.TaskBlockProfilingTestSupport.LOCAL_ROOT_SPAN_ID; +import static datadog.smoketest.TaskBlockProfilingTestSupport.SPAN_ID; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.openjdk.jmc.common.item.Attribute.attr; +import static org.openjdk.jmc.common.unit.UnitLookup.NUMBER; + +import datadog.trace.api.Trace; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.openjdk.jmc.common.IMCFrame; +import org.openjdk.jmc.common.IMCStackTrace; +import org.openjdk.jmc.common.item.IAttribute; +import org.openjdk.jmc.common.item.IItem; +import org.openjdk.jmc.common.item.IItemCollection; +import org.openjdk.jmc.common.item.IItemIterable; +import org.openjdk.jmc.common.item.IMemberAccessor; +import org.openjdk.jmc.common.item.ItemFilters; +import org.openjdk.jmc.common.unit.IQuantity; +import org.openjdk.jmc.flightrecorder.JfrAttributes; +import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; + +/** Smoke coverage for native {@code Object.wait} TaskBlock emission and context exclusion. */ +@DisabledOnJ9 +@EnabledOnOs(OS.LINUX) +final class ObjectWaitTaskBlockProfilingTest { + private static final IAttribute UNBLOCKING_SPAN_ID = + attr("unblockingSpanId", "unblockingSpanId", "unblockingSpanId", NUMBER); + + private Path dumpDir; + private Path logFilePath; + + @BeforeEach + void setup(TestInfo testInfo) throws IOException { + logFilePath = + TaskBlockProfilingTestSupport.buildLogFilePath( + ObjectWaitTaskBlockProfilingTest.class, testInfo, "objectWait"); + dumpDir = TaskBlockProfilingTestSupport.createDumpDir("dd-profiler-objectwait-"); + } + + @AfterEach + void tearDown() throws IOException { + TaskBlockProfilingTestSupport.deleteRecursively(dumpDir); + } + + @Test + void spanlessObjectWaitEmitsWhileTracedObjectWaitDoesNot() throws Exception { + Process targetProcess = + TaskBlockProfilingTestSupport.createTaskBlockProcessBuilder( + "smoke-test-objectwait-taskblock", + ObjectWaitTaskBlockForkedApp.class.getName(), + dumpDir, + logFilePath) + .start(); + checkProcessSuccessfullyEnd(targetProcess, logFilePath); + + JfrStats stats = new JfrStats(); + for (IItemCollection events : TaskBlockProfilingTestSupport.loadDumpedEvents(dumpDir)) { + stats.add(events); + } + + assertTrue(stats.spanlessCount > 0, "Expected Object.wait TaskBlocks outside trace context"); + assertTrue(stats.nonZeroBlockerCount > 0, "Expected monitor identity on Object.wait events"); + assertFalse(stats.hasNonZeroSpanId, "Spanless TaskBlocks must keep spanId zero"); + assertFalse(stats.hasNonZeroLocalRootSpanId, "Spanless TaskBlocks must keep root spanId zero"); + assertFalse(stats.hasNonZeroUnblockingSpanId, "Object.wait has no attributed notifier hook"); + assertFalse(stats.hasMissingEventThread, "TaskBlock events must resolve Event Thread"); + assertFalse( + stats.hasTracedWaitEvent, + "Traced waits must not emit TaskBlock events: " + stats.tracedWaitStacks); + assertFalse( + TaskBlockProfilingTestSupport.logContainsAny( + logFilePath, "NoClassDefFoundError", "Failed to handle exception in instrumentation"), + "Object.wait TaskBlock path failed"); + } + + public static final class ObjectWaitTaskBlockForkedApp { + private static final int ITERATIONS = 20; + private static final Object BLOCKER = new Object(); + + public static void main(String[] args) throws Exception { + Thread spanless = + new Thread(ObjectWaitTaskBlockForkedApp::waitRepeatedly, "objectwait-spanless"); + spanless.start(); + spanless.join(); + + Thread traced = + new Thread(ObjectWaitTaskBlockForkedApp::waitRepeatedlyWithTrace, "objectwait-traced"); + traced.start(); + traced.join(); + Thread.sleep(1500); + } + + private static void waitRepeatedly() { + try { + for (int i = 0; i < ITERATIONS; i++) { + synchronized (BLOCKER) { + BLOCKER.wait(50L); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } + } + + @Trace + private static void waitRepeatedlyWithTrace() { + waitRepeatedly(); + } + } + + private static final class JfrStats { + long spanlessCount; + long nonZeroBlockerCount; + boolean hasNonZeroSpanId; + boolean hasNonZeroLocalRootSpanId; + boolean hasNonZeroUnblockingSpanId; + boolean hasMissingEventThread; + boolean hasTracedWaitEvent; + final List tracedWaitStacks = new ArrayList<>(); + + void add(IItemCollection events) { + for (IItemIterable items : events.apply(ItemFilters.type("datadog.TaskBlock"))) { + TaskBlockProfilingTestSupport.assertFinalTaskBlockSchema(items); + IMemberAccessor spanId = SPAN_ID.getAccessor(items.getType()); + IMemberAccessor rootSpanId = + LOCAL_ROOT_SPAN_ID.getAccessor(items.getType()); + IMemberAccessor blocker = BLOCKER.getAccessor(items.getType()); + IMemberAccessor unblockingSpanId = + UNBLOCKING_SPAN_ID.getAccessor(items.getType()); + IMemberAccessor eventThread = + JdkAttributes.EVENT_THREAD_NAME.getAccessor(items.getType()); + IMemberAccessor stackTrace = + JfrAttributes.EVENT_STACKTRACE.getAccessor(items.getType()); + for (IItem item : items) { + String thread = eventThread.getMember(item); + if ("objectwait-traced".equals(thread)) { + IMCStackTrace trace = stackTrace.getMember(item); + for (IMCFrame frame : trace.getFrames()) { + if (ObjectWaitTaskBlockForkedApp.class + .getName() + .equals(frame.getMethod().getType().getFullName()) + && "waitRepeatedly".equals(frame.getMethod().getMethodName())) { + hasTracedWaitEvent = true; + tracedWaitStacks.add(trace.toString()); + break; + } + } + } + if (!"objectwait-spanless".equals(thread)) { + continue; + } + spanlessCount++; + hasNonZeroSpanId |= spanId.getMember(item).longValue() != 0; + hasNonZeroLocalRootSpanId |= rootSpanId.getMember(item).longValue() != 0; + hasNonZeroUnblockingSpanId |= unblockingSpanId.getMember(item).longValue() != 0; + nonZeroBlockerCount += blocker.getMember(item).longValue() == 0 ? 0 : 1; + hasMissingEventThread |= thread.isEmpty(); + } + } + } + } +} diff --git a/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/SmokeTestUtils.java b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/SmokeTestUtils.java index d1a13d80fb5..2ffb9bda0f2 100644 --- a/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/SmokeTestUtils.java +++ b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/SmokeTestUtils.java @@ -67,7 +67,7 @@ static ProcessBuilder createProcessBuilder( + ProfilingConfig.PROFILING_TEMPLATE_OVERRIDE_FILE + "=" + templateOverride)); - if (System.getenv("TEST_LIBASYNC") != null) { + if (System.getenv("SMOKETEST_AGENT_JAR") == null && System.getenv("TEST_LIBASYNC") != null) { command.add( "-Ddd." + ProfilingConfig.PROFILING_DATADOG_PROFILER_LIBPATH diff --git a/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/SynchronizedContentionProfilingTest.java b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/SynchronizedContentionProfilingTest.java new file mode 100644 index 00000000000..2ac1fb496c5 --- /dev/null +++ b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/SynchronizedContentionProfilingTest.java @@ -0,0 +1,121 @@ +// Copyright 2026 Datadog, Inc. +package datadog.smoketest; + +import static datadog.smoketest.SmokeTestUtils.checkProcessSuccessfullyEnd; +import static datadog.smoketest.TaskBlockProfilingTestSupport.BLOCKER; +import static datadog.smoketest.TaskBlockProfilingTestSupport.LOCAL_ROOT_SPAN_ID; +import static datadog.smoketest.TaskBlockProfilingTestSupport.SPAN_ID; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.openjdk.jmc.common.item.IItem; +import org.openjdk.jmc.common.item.IItemCollection; +import org.openjdk.jmc.common.item.IItemIterable; +import org.openjdk.jmc.common.item.IMemberAccessor; +import org.openjdk.jmc.common.item.ItemFilters; +import org.openjdk.jmc.common.unit.IQuantity; +import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; + +/** Smoke coverage for native synchronized-contention TaskBlock emission. */ +@DisabledOnJ9 +@EnabledOnOs(OS.LINUX) +final class SynchronizedContentionProfilingTest { + private Path dumpDir; + private Path logFilePath; + + @BeforeEach + void setup(TestInfo testInfo) throws IOException { + logFilePath = + TaskBlockProfilingTestSupport.buildLogFilePath( + SynchronizedContentionProfilingTest.class, testInfo, "syncContention"); + dumpDir = TaskBlockProfilingTestSupport.createDumpDir("dd-profiler-synccontention-"); + } + + @AfterEach + void tearDown() throws IOException { + TaskBlockProfilingTestSupport.deleteRecursively(dumpDir); + } + + @Test + void spanlessSynchronizedContentionEmitsNativeTaskBlocks() throws Exception { + Process targetProcess = + TaskBlockProfilingTestSupport.createTaskBlockProcessBuilder( + "smoke-test-synccontention-taskblock", + com.datadog.smoketest.profiling.SynchronizedContentionForkedApp.class.getName(), + dumpDir, + logFilePath) + .start(); + checkProcessSuccessfullyEnd(targetProcess, logFilePath); + + JfrStats stats = new JfrStats(); + for (IItemCollection events : TaskBlockProfilingTestSupport.loadDumpedEvents(dumpDir)) { + stats.add(events); + } + + assertTrue(stats.blockScenarioCount > 0, "Expected synchronized-block TaskBlocks"); + assertTrue(stats.instanceMethodScenarioCount > 0, "Expected instance-method TaskBlocks"); + assertTrue(stats.staticMethodScenarioCount > 0, "Expected static-method TaskBlocks"); + assertFalse(stats.hasNonZeroSpanId, "Spanless TaskBlocks must keep spanId zero"); + assertFalse(stats.hasNonZeroLocalRootSpanId, "Spanless TaskBlocks must keep root spanId zero"); + assertTrue(stats.blockersWithNonZeroValue > 0, "Expected monitor identity on TaskBlocks"); + assertTrue(stats.distinctBlockerValues.size() > 1, "Expected per-monitor blocker identities"); + assertFalse(stats.hasMissingEventThread, "TaskBlock events must resolve Event Thread"); + assertFalse( + TaskBlockProfilingTestSupport.logContainsAny( + logFilePath, "NoClassDefFoundError", "Failed to handle exception", "VerifyError"), + "native synchronized-contention TaskBlock path failed"); + } + + private static final class JfrStats { + long blockScenarioCount; + long instanceMethodScenarioCount; + long staticMethodScenarioCount; + long blockersWithNonZeroValue; + final Set distinctBlockerValues = new HashSet<>(); + boolean hasNonZeroSpanId; + boolean hasNonZeroLocalRootSpanId; + boolean hasMissingEventThread; + + void add(IItemCollection events) { + for (IItemIterable items : events.apply(ItemFilters.type("datadog.TaskBlock"))) { + TaskBlockProfilingTestSupport.assertFinalTaskBlockSchema(items); + IMemberAccessor spanId = SPAN_ID.getAccessor(items.getType()); + IMemberAccessor rootSpanId = + LOCAL_ROOT_SPAN_ID.getAccessor(items.getType()); + IMemberAccessor blocker = BLOCKER.getAccessor(items.getType()); + IMemberAccessor eventThread = + JdkAttributes.EVENT_THREAD_NAME.getAccessor(items.getType()); + for (IItem item : items) { + String thread = eventThread.getMember(item); + if ("sync-block-spanless".equals(thread)) { + blockScenarioCount++; + } else if ("sync-instance-spanless".equals(thread)) { + instanceMethodScenarioCount++; + } else if ("sync-static-spanless".equals(thread)) { + staticMethodScenarioCount++; + } else { + continue; + } + hasNonZeroSpanId |= spanId.getMember(item).longValue() != 0; + hasNonZeroLocalRootSpanId |= rootSpanId.getMember(item).longValue() != 0; + long blockerValue = blocker.getMember(item).longValue(); + if (blockerValue != 0) { + blockersWithNonZeroValue++; + distinctBlockerValues.add(blockerValue); + } + hasMissingEventThread |= thread.isEmpty(); + } + } + } + } +} diff --git a/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/TaskBlockProfilingTestSupport.java b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/TaskBlockProfilingTestSupport.java new file mode 100644 index 00000000000..3e806930b62 --- /dev/null +++ b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/TaskBlockProfilingTestSupport.java @@ -0,0 +1,216 @@ +// Copyright 2026 Datadog, Inc. +package datadog.smoketest; + +import static datadog.smoketest.SmokeTestUtils.agentShadowJar; +import static datadog.smoketest.SmokeTestUtils.buildDirectory; +import static datadog.smoketest.SmokeTestUtils.javaPath; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.openjdk.jmc.common.item.Attribute.attr; +import static org.openjdk.jmc.common.unit.UnitLookup.NUMBER; +import static org.openjdk.jmc.common.unit.UnitLookup.PLAIN_TEXT; + +import datadog.trace.api.config.ProfilingConfig; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.TestInfo; +import org.openjdk.jmc.common.item.IAttribute; +import org.openjdk.jmc.common.item.IItemCollection; +import org.openjdk.jmc.common.item.IItemIterable; +import org.openjdk.jmc.common.unit.IQuantity; +import org.openjdk.jmc.flightrecorder.JfrLoaderToolkit; + +/** Shared plumbing for native TaskBlock profiling smoke tests (JFR-loading, forked-JVM launch). */ +final class TaskBlockProfilingTestSupport { + static final byte[] JFR_MAGIC = new byte[] {'F', 'L', 'R', 0}; + static final IAttribute SPAN_ID = attr("spanId", "spanId", "spanId", NUMBER); + static final IAttribute LOCAL_ROOT_SPAN_ID = + attr("localRootSpanId", "localRootSpanId", "localRootSpanId", NUMBER); + static final IAttribute BLOCKER = attr("blocker", "blocker", "blocker", NUMBER); + static final IAttribute OPERATION = + attr("_dd.trace.operation", "_dd.trace.operation", "_dd.trace.operation", PLAIN_TEXT); + private static final Set REQUIRED_TASK_BLOCK_FIELDS = + new HashSet<>( + Arrays.asList( + "startTime", + "duration", + "eventThread", + "blocker", + "unblockingSpanId", + "stackTrace", + "observedBlockingState", + "spanId", + "localRootSpanId")); + + private TaskBlockProfilingTestSupport() {} + + static Path buildLogFilePath(Class testClass, TestInfo testInfo, String defaultName) + throws IOException { + Path logFileBase = Paths.get(buildDirectory(), "reports", "testProcess." + testClass.getName()); + Files.createDirectories(logFileBase); + return logFileBase.resolve( + testInfo.getTestMethod().map(Method::getName).orElse(defaultName) + ".log"); + } + + static Path createDumpDir(String prefix) throws IOException { + return Files.createTempDirectory(prefix); + } + + static void deleteRecursively(Path dir) throws IOException { + if (dir != null && Files.exists(dir)) { + Files.walk(dir).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } + } + + static ProcessBuilder createTaskBlockProcessBuilder( + String serviceName, String mainClassName, Path dumpDir, Path logFilePath) throws IOException { + String templateOverride = + TaskBlockProfilingTestSupport.class.getClassLoader().getResource("overrides.jfp").getFile(); + List command = + new ArrayList<>( + Arrays.asList( + javaPath(), + "-Xmx" + System.getProperty("datadog.forkedMaxHeapSize", "1024M"), + "-Xms" + System.getProperty("datadog.forkedMinHeapSize", "64M"), + "-javaagent:" + agentShadowJar(), + "-XX:ErrorFile=/tmp/hs_err_pid%p.log", + "-Ddd.service.name=" + serviceName, + "-Ddd.env=smoketest", + "-Ddd.version=99", + "-Ddd.profiling.enabled=true", + "-Ddd.profiling.ddprof.enabled=true", + "-Ddd." + ProfilingConfig.PROFILING_AUXILIARY_TYPE + "=async", + "-Ddd." + ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_ENABLED + "=true", + "-Ddd." + ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_INTERVAL + "=10ms", + "-Ddd." + ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER + "=false", + "-Ddd.profiling.agentless=false", + "-Ddd.profiling.start-delay=0", + "-Ddd." + ProfilingConfig.PROFILING_START_FORCE_FIRST + "=true", + "-Ddd.profiling.upload.period=1", + "-Ddd.profiling.hotspots.enabled=true", + "-Ddd." + ProfilingConfig.PROFILING_CONTEXT_ATTRIBUTES_SPAN_NAME_ENABLED + "=true", + "-Ddd.profiling.debug.dump_path=" + dumpDir, + "-Ddd." + ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_PRECHECK + "=true", + "-Ddatadog.slf4j.simpleLogger.defaultLogLevel=debug", + "-Dorg.slf4j.simpleLogger.defaultLogLevel=debug", + "-XX:+IgnoreUnrecognizedVMOptions", + "-XX:+UnlockCommercialFeatures", + "-XX:+FlightRecorder", + "-Ddd." + ProfilingConfig.PROFILING_TEMPLATE_OVERRIDE_FILE + "=" + templateOverride, + "-cp", + System.getProperty("java.class.path"), + mainClassName)); + if (System.getenv("SMOKETEST_AGENT_JAR") == null && System.getenv("TEST_LIBASYNC") != null) { + command.add( + command.size() - 3, + "-Ddd." + + ProfilingConfig.PROFILING_DATADOG_PROFILER_LIBPATH + + "=" + + System.getenv("TEST_LIBASYNC")); + } + + ProcessBuilder processBuilder = new ProcessBuilder(command); + processBuilder.directory(new File(buildDirectory())); + processBuilder.environment().put("JAVA_HOME", System.getProperty("java.home")); + processBuilder.redirectErrorStream(true); + processBuilder.redirectOutput(ProcessBuilder.Redirect.to(logFilePath.toFile())); + return processBuilder; + } + + static List loadDumpedEvents(Path dumpDir) throws IOException { + List jfrFiles; + try (Stream files = Files.walk(dumpDir)) { + jfrFiles = + files + .filter(Files::isRegularFile) + .filter(path -> path.toString().endsWith(".jfr")) + .sorted() + .collect(Collectors.toList()); + } + List events = new ArrayList<>(); + List loadFailures = new ArrayList<>(); + for (Path jfrFile : jfrFiles) { + try { + events.add(loadEvents(dumpDir, jfrFile)); + } catch (RuntimeException e) { + loadFailures.add(e); + } + } + assertTrue( + !events.isEmpty(), + () -> "No readable JFR files found in " + dumpDir + "; failures=" + loadFailures); + return events; + } + + static boolean logContainsAny(Path logFilePath, String... needles) throws IOException { + String log = new String(Files.readAllBytes(logFilePath), StandardCharsets.UTF_8); + for (String needle : needles) { + if (log.contains(needle)) { + return true; + } + } + return false; + } + + static void assertFinalTaskBlockSchema(IItemIterable items) { + Set actualFields = new HashSet<>(); + items + .getType() + .getAccessorKeys() + .keySet() + .forEach(accessorKey -> actualFields.add(accessorKey.getIdentifier())); + assertTrue( + actualFields.containsAll(REQUIRED_TASK_BLOCK_FIELDS), + "TaskBlock schema is missing fields. required=" + + REQUIRED_TASK_BLOCK_FIELDS + + ", actual=" + + actualFields); + } + + private static IItemCollection loadEvents(Path dumpDir, Path path) { + try { + return JfrLoaderToolkit.loadEvents(extractLastJfrStream(dumpDir, path).toFile()); + } catch (Exception e) { + throw new RuntimeException("Failed to load JFR " + path, e); + } + } + + private static Path extractLastJfrStream(Path dumpDir, Path path) throws IOException { + byte[] data = Files.readAllBytes(path); + int lastMagic = lastIndexOf(data, JFR_MAGIC); + if (lastMagic <= 0) { + return path; + } + Path extracted = dumpDir.resolve(path.getFileName() + ".ddprof.jfr"); + Files.write(extracted, Arrays.copyOfRange(data, lastMagic, data.length)); + return extracted; + } + + private static int lastIndexOf(byte[] data, byte[] needle) { + for (int i = data.length - needle.length; i >= 0; i--) { + boolean match = true; + for (int j = 0; j < needle.length; j++) { + if (data[i + j] != needle[j]) { + match = false; + break; + } + } + if (match) { + return i; + } + } + return -1; + } +} diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java index 6d60ab686a7..d1b817b6fdb 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java @@ -97,6 +97,7 @@ public final class ProfilingConfig { public static final String PROFILING_DATADOG_PROFILER_SCRATCH = "profiling.ddprof.scratch"; public static final String PROFILING_DATADOG_PROFILER_LIBPATH = "profiling.ddprof.debug.lib"; + public static final String PROFILING_DATADOG_PROFILER_ALLOC_ENABLED = "profiling.ddprof.alloc.enabled"; public static final String PROFILING_DATADOG_PROFILER_ALLOC_INTERVAL = @@ -123,7 +124,11 @@ public final class ProfilingConfig { public static final String PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER = "profiling.ddprof.wall.context.filter"; - public static final boolean PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER_DEFAULT = true; + public static final boolean PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER_DEFAULT = false; + + public static final String PROFILING_DATADOG_PROFILER_WALL_PRECHECK = + "profiling.ddprof.wall.precheck"; + public static final boolean PROFILING_DATADOG_PROFILER_WALL_PRECHECK_DEFAULT = false; public static final String PROFILING_DATADOG_PROFILER_SCHEDULING_EVENT = "profiling.experimental.ddprof.scheduling.event"; diff --git a/internal-api/src/main/java/datadog/trace/api/profiling/TaskBlockInstrumentationConfig.java b/internal-api/src/main/java/datadog/trace/api/profiling/TaskBlockInstrumentationConfig.java new file mode 100644 index 00000000000..9c9f21553be --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/profiling/TaskBlockInstrumentationConfig.java @@ -0,0 +1,64 @@ +// Copyright 2026 Datadog, Inc. +package datadog.trace.api.profiling; + +import static datadog.environment.JavaVirtualMachine.isJ9; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER_DEFAULT; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_ENABLED; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_PRECHECK; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_PRECHECK_DEFAULT; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_ULTRA_MINIMAL; +import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_ENABLED; + +import datadog.trace.api.Config; +import datadog.trace.bootstrap.config.provider.ConfigProvider; + +/** Shared configuration gates for Java-level {@code datadog.TaskBlock} instrumentations. */ +public final class TaskBlockInstrumentationConfig { + private TaskBlockInstrumentationConfig() {} + + public static boolean isWallPrecheckEnabled(final ConfigProvider configProvider) { + return getDdprofBoolean( + configProvider, + PROFILING_DATADOG_PROFILER_WALL_PRECHECK, + PROFILING_DATADOG_PROFILER_WALL_PRECHECK_DEFAULT); + } + + /** Returns whether the effective wall-clock configuration samples all threads. */ + public static boolean isAllThreadWallScope(final ConfigProvider configProvider) { + boolean tracingEnabled = configProvider.getBoolean(TRACE_ENABLED, true); + return !tracingEnabled + || !getDdprofBoolean( + configProvider, + PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER, + PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER_DEFAULT); + } + + /** + * Common enablement gate shared by TaskBlock instrumentations. + * + *

The producer is useful only when ddprof and its effective wall-clock engine are enabled, + * wall precheck is requested, and the wall profiler samples all threads. In context-only scope, + * the existing context filter already excludes out-of-context threads. + */ + public static boolean isEnabled(final Config config, final ConfigProvider configProvider) { + return config.isDatadogProfilerEnabled() + && isWallClockProfilerEnabled(configProvider) + && isWallPrecheckEnabled(configProvider) + && isAllThreadWallScope(configProvider); + } + + private static boolean isWallClockProfilerEnabled(final ConfigProvider configProvider) { + boolean ultraMinimal = configProvider.getBoolean(PROFILING_ULTRA_MINIMAL, false); + boolean tracingEnabled = configProvider.getBoolean(TRACE_ENABLED, true); + boolean enabledByDefault = !ultraMinimal && tracingEnabled && !isJ9(); + return getDdprofBoolean( + configProvider, PROFILING_DATADOG_PROFILER_WALL_ENABLED, enabledByDefault); + } + + private static boolean getDdprofBoolean( + ConfigProvider configProvider, String key, boolean defaultValue) { + return configProvider.getBoolean( + key, configProvider.getBoolean(key.replace(".ddprof.", ".async."), defaultValue)); + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/profiling/TaskBlockInstrumentationConfigTest.java b/internal-api/src/test/java/datadog/trace/api/profiling/TaskBlockInstrumentationConfigTest.java new file mode 100644 index 00000000000..dfb223dfb55 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/profiling/TaskBlockInstrumentationConfigTest.java @@ -0,0 +1,118 @@ +// Copyright 2026 Datadog, Inc. +package datadog.trace.api.profiling; + +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_ENABLED; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_PRECHECK; +import static datadog.trace.api.config.ProfilingConfig.PROFILING_ULTRA_MINIMAL; +import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_ENABLED; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import datadog.trace.api.Config; +import datadog.trace.bootstrap.config.provider.ConfigProvider; +import java.util.Properties; +import org.junit.jupiter.api.Test; + +class TaskBlockInstrumentationConfigTest { + + @Test + void enabledRequiresEverySharedGate() { + Config config = config(true); + + assertTrue( + TaskBlockInstrumentationConfig.isEnabled( + config, configProvider(true, true, true, false, false))); + assertFalse( + TaskBlockInstrumentationConfig.isEnabled( + config(false), configProvider(true, true, true, false, false))); + assertFalse( + TaskBlockInstrumentationConfig.isEnabled( + config, configProvider(false, true, true, false, false))); + assertFalse( + TaskBlockInstrumentationConfig.isEnabled( + config, configProvider(true, false, true, false, false))); + assertTrue( + TaskBlockInstrumentationConfig.isEnabled( + config, configProvider(true, true, false, false, false))); + assertFalse( + TaskBlockInstrumentationConfig.isEnabled( + config, configProvider(true, true, true, true, false))); + } + + @Test + void disabledTracingForcesAllThreadScope() { + Properties properties = new Properties(); + properties.setProperty(TRACE_ENABLED, "false"); + properties.setProperty(PROFILING_DATADOG_PROFILER_WALL_ENABLED, "true"); + properties.setProperty(PROFILING_DATADOG_PROFILER_WALL_PRECHECK, "true"); + properties.setProperty(PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER, "true"); + + assertTrue( + TaskBlockInstrumentationConfig.isEnabled( + config(true), ConfigProvider.withPropertiesOverride(properties))); + assertTrue(TaskBlockInstrumentationConfig.isAllThreadWallScope(configProvider(properties))); + } + + @Test + void disabledTracingDisablesWallByDefault() { + Properties properties = new Properties(); + properties.setProperty(TRACE_ENABLED, "false"); + properties.setProperty(PROFILING_DATADOG_PROFILER_WALL_PRECHECK, "true"); + + assertFalse( + TaskBlockInstrumentationConfig.isEnabled( + config(true), ConfigProvider.withPropertiesOverride(properties))); + } + + @Test + void defaultsDoNotEnableTaskBlockInstrumentation() { + assertFalse( + TaskBlockInstrumentationConfig.isEnabled( + config(true), ConfigProvider.withPropertiesOverride(new Properties()))); + assertTrue( + TaskBlockInstrumentationConfig.isAllThreadWallScope( + ConfigProvider.withPropertiesOverride(new Properties()))); + } + + @Test + void asyncAliasesRemainSupported() { + Properties properties = new Properties(); + properties.setProperty("profiling.async.wall.enabled", "true"); + properties.setProperty("profiling.async.wall.precheck", "true"); + properties.setProperty("profiling.async.wall.context.filter", "false"); + + assertTrue( + TaskBlockInstrumentationConfig.isEnabled( + config(true), ConfigProvider.withPropertiesOverride(properties))); + } + + private static Config config(boolean ddprofEnabled) { + Config config = mock(Config.class); + when(config.isDatadogProfilerEnabled()).thenReturn(ddprofEnabled); + return config; + } + + private static ConfigProvider configProvider( + boolean wallEnabled, + boolean wallPrecheck, + boolean tracingEnabled, + boolean contextFilter, + boolean ultraMinimal) { + Properties properties = new Properties(); + properties.setProperty(PROFILING_DATADOG_PROFILER_WALL_ENABLED, Boolean.toString(wallEnabled)); + properties.setProperty( + PROFILING_DATADOG_PROFILER_WALL_PRECHECK, Boolean.toString(wallPrecheck)); + properties.setProperty(TRACE_ENABLED, Boolean.toString(tracingEnabled)); + properties.setProperty( + PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER, Boolean.toString(contextFilter)); + properties.setProperty(PROFILING_ULTRA_MINIMAL, Boolean.toString(ultraMinimal)); + return configProvider(properties); + } + + private static ConfigProvider configProvider(Properties properties) { + return ConfigProvider.withPropertiesOverride(properties); + } +} diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 733f3736cbb..3b6fba6e674 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -2769,6 +2769,14 @@ "aliases": [] } ], + "DD_PROFILING_ASYNC_WALL_PRECHECK": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": [] + } + ], "DD_PROFILING_AUXILIARY": [ { "version": "A", @@ -2981,7 +2989,7 @@ { "version": "A", "type": "boolean", - "default": "true", + "default": "false", "aliases": [] } ], @@ -2993,6 +3001,14 @@ "aliases": [] } ], + "DD_PROFILING_DDPROF_WALL_PRECHECK": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": [] + } + ], "DD_PROFILING_DDPROF_WALL_INTERVAL_MS": [ { "version": "A",