diff --git a/.github/workflows/run-system-tests.yaml b/.github/workflows/run-system-tests.yaml index e7bd8552acd..07771b5d4e7 100644 --- a/.github/workflows/run-system-tests.yaml +++ b/.github/workflows/run-system-tests.yaml @@ -22,6 +22,11 @@ jobs: build: runs-on: group: APM Larger Runners + # Keep in sync with the JAVA_PROFILER_REF default in .gitlab-ci.yml. When non-empty, + # we clone DataDog/java-profiler at this ref, build :ddprof-lib:assembleReleaseJar + # and inject it via -Pddprof.jar= instead of the published Maven artifact. + env: + JAVA_PROFILER_REF: "paul.fournillon/wallclock-taskblock" steps: - name: Checkout repository uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # 6.0.3 @@ -39,17 +44,51 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- + # Mirrors the `build_java_profiler_ddprof` GitLab job: clone java-profiler at + # JAVA_PROFILER_REF, build :ddprof-lib:assembleReleaseJar with JDK 21 (Gradle 9.x), + # and stage the resulting jar under custom-ddprof/ddprof.jar. + # assembleRelease is the native link/assemble task only; the packaged jar is assembleReleaseJar. + - name: Build custom ddprof.jar from java-profiler + if: ${{ env.JAVA_PROFILER_REF != '' }} + run: | + set -euo pipefail + mkdir -p custom-ddprof + SRCDIR="${RUNNER_TEMP}/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 + JAVA_HOME="$JAVA_HOME_21_X64" PATH="$JAVA_HOME_21_X64/bin:$PATH" ./gradlew --version + JAVA_HOME="$JAVA_HOME_21_X64" PATH="$JAVA_HOME_21_X64/bin:$PATH" \ + ./gradlew :ddprof-lib:assembleReleaseJar -Pskip-tests -Pskip-gtest --no-daemon + ) + JAR=$(find "$SRCDIR/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 $SRCDIR/ddprof-lib/build/libs" >&2 + ls -laR "$SRCDIR/ddprof-lib/build" 2>/dev/null || true + exit 1 + fi + cp "$JAR" "$GITHUB_WORKSPACE/custom-ddprof/ddprof.jar" + echo "DDPROF_JAR=$GITHUB_WORKSPACE/custom-ddprof/ddprof.jar" >> "$GITHUB_ENV" + ls -la "$GITHUB_WORKSPACE/custom-ddprof/" + - name: Build dd-trace-java env: ORG_GRADLE_PROJECT_akkaRepositoryToken: ${{ secrets.AKKA_REPO_TOKEN }} run: | + DDPROF_ARG="" + if [ -n "${DDPROF_JAR:-}" ] && [ -f "$DDPROF_JAR" ]; then + echo "Injecting custom ddprof.jar: $DDPROF_JAR" + DDPROF_ARG="-Pddprof.jar=$DDPROF_JAR" + fi GRADLE_OPTS="-Xms2g -Xmx4g -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC" \ JAVA_HOME=$JAVA_HOME_8_X64 \ JAVA_8_HOME=$JAVA_HOME_8_X64 \ JAVA_11_HOME=$JAVA_HOME_11_X64 \ JAVA_17_HOME=$JAVA_HOME_17_X64 \ JAVA_21_HOME=$JAVA_HOME_21_X64 \ - ./gradlew clean :dd-java-agent:shadowJar \ + ./gradlew clean :dd-java-agent:shadowJar $DDPROF_ARG \ --build-cache --parallel --stacktrace --no-daemon --max-workers=4 - name: Upload artifact diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 13fcd80620f..6bc787170a6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -66,6 +66,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-taskblock" + # One pipeline injection package size ratchet OCI_PACKAGE_MAX_SIZE_BYTES: 40_000_000 LIB_INJECTION_IMAGE_MAX_SIZE_BYTES: 40_000_000 @@ -172,9 +176,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 @@ -225,6 +241,7 @@ default: org.gradle.java.installations.fromEnv=$JAVA_HOMES org.gradle.console=colored 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 @@ -307,8 +324,114 @@ 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" + export ORG_GRADLE_PROJECT_mavenRepositoryProxy="$MAVEN_REPOSITORY_PROXY" + export ORG_GRADLE_PROJECT_gradlePluginProxy="$GRADLE_PLUGIN_PROXY" + PROFILER_GRADLE_INIT="${CI_PROJECT_DIR}/java-profiler-init.gradle" + cat > "$PROFILER_GRADLE_INIT" <<'EOF' + def mavenRepositoryProxy = System.getenv('MAVEN_REPOSITORY_PROXY') + def gradlePluginProxy = System.getenv('GRADLE_PLUGIN_PROXY') ?: mavenRepositoryProxy + + def addPluginRepositories = { repositories -> + if (gradlePluginProxy) { + repositories.maven { url = uri(gradlePluginProxy) } + } + if (mavenRepositoryProxy && mavenRepositoryProxy != gradlePluginProxy) { + repositories.maven { url = uri(mavenRepositoryProxy) } + } + } + + def addMavenRepositories = { repositories -> + if (mavenRepositoryProxy) { + repositories.maven { url = uri(mavenRepositoryProxy) } + } + } + + beforeSettings { settings -> + settings.pluginManagement { + repositories { + addPluginRepositories(delegate) + } + } + } + + allprojects { + buildscript { + repositories { + addPluginRepositories(delegate) + } + } + repositories { + addMavenRepositories(delegate) + } + } + EOF + chmod +x ./gradlew + ./gradlew --version + # assembleRelease is the native link/assemble task only; the packaged jar is assembleReleaseJar. + ./gradlew --init-script "$PROFILER_GRADLE_INIT" :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 @@ -422,7 +545,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 CACHE_TYPE: "spotless" @@ -433,7 +558,9 @@ spotless: check-instrumentation-naming: extends: .gradle_build stage: tests - needs: [ ] + needs: + - job: build_java_profiler_ddprof + optional: true script: - ./gradlew --version - ./gradlew checkInstrumentationNaming @@ -441,7 +568,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 @@ -450,7 +579,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: @@ -478,7 +610,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" @@ -514,7 +649,9 @@ test_published_artifacts: check_build_src: extends: .check_job - needs: [] + needs: + - job: build_java_profiler_ddprof + optional: true variables: GRADLE_TARGET: ":buildSrc:build" @@ -553,7 +690,9 @@ 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_tests + - job: build_java_profiler_ddprof + optional: true + - build_tests parallel: matrix: - GRADLE_TARGET: ":instrumentationTest" @@ -638,7 +777,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" @@ -988,7 +1130,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: @@ -1016,7 +1161,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-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 9aedde9e49f..3b28cab316e 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 @@ -13,6 +13,7 @@ import static com.datadog.profiling.ddprof.DatadogProfilerConfig.getWallCollapsing; import static com.datadog.profiling.ddprof.DatadogProfilerConfig.getWallContextFilter; import static com.datadog.profiling.ddprof.DatadogProfilerConfig.getWallInterval; +import static com.datadog.profiling.ddprof.DatadogProfilerConfig.getWallPrecheck; import static com.datadog.profiling.ddprof.DatadogProfilerConfig.isAllocationProfilingEnabled; import static com.datadog.profiling.ddprof.DatadogProfilerConfig.isCpuProfilerEnabled; import static com.datadog.profiling.ddprof.DatadogProfilerConfig.isLiveHeapSizeTrackingEnabled; @@ -44,6 +45,8 @@ import datadog.trace.bootstrap.instrumentation.api.TaskWrapper; import datadog.trace.util.TempLocationManager; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.PosixFilePermissions; @@ -102,6 +105,7 @@ public static DatadogProfiler newInstance(ConfigProvider configProvider) { private final AtomicBoolean recordingFlag = 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; @@ -128,6 +132,12 @@ private DatadogProfiler(ConfigProvider configProvider) { throw new UnsupportedOperationException( "Unable to instantiate datadog profiler", reasonNotLoaded); } + this.taskBlockBridge = new TaskBlockBridge(profiler); + if (getWallPrecheck(configProvider) && !taskBlockBridge.hasTaskBlockFromContextSupport()) { + log.debug( + "TaskBlock profiling bridge methods are unavailable in the loaded ddprof artifact; " + + "Java-level TaskBlock events will be skipped."); + } // TODO enable/disable events by name (e.g. datadog.ExecutionSample), not flag, so configuration // can be consistent with JFR event control @@ -267,7 +277,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)); @@ -313,6 +322,9 @@ String cmdStartProfiling(Path file) throws IllegalStateException { } else { cmd.append(",filter="); } + if (getWallPrecheck(configProvider)) { + cmd.append(",wallprecheck=true"); + } } cmd.append(",loglevel=").append(getLogLevel(configProvider)); if (profilingModes.contains(ALLOCATION) || profilingModes.contains(MEMLEAK)) { @@ -342,6 +354,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); } @@ -455,4 +478,237 @@ void recordQueueTimeEvent( } } } + + public int getCurrentThreadId() { + return profiler != null ? taskBlockBridge.getCurrentThreadId() : -1; + } + + public long getTscFrequency() { + return profiler != null ? taskBlockBridge.getTscFrequency() : 1_000_000_000L; + } + + boolean hasTaskBlockEventSupport() { + return profiler != null && taskBlockBridge.hasTaskBlockEventSupport(); + } + + boolean hasTaskBlockFromContextSupport() { + return profiler != null && taskBlockBridge.hasTaskBlockFromContextSupport(); + } + + long blockEnter(int state) { + if (profiler != null && recordingFlag.get()) { + return taskBlockBridge.blockEnter(state); + } + return 0L; + } + + void blockExit(long token) { + if (token != 0L && profiler != null) { + taskBlockBridge.blockExit(token); + } + } + + void recordTaskBlockEvent(long startTicks, long blocker, long unblockingSpanId) { + if (profiler != null && recordingFlag.get()) { + long endTicks = profiler.getCurrentTicks(); + taskBlockBridge.recordTaskBlock(startTicks, endTicks, blocker, unblockingSpanId); + } + } + + void recordTaskBlockWithContextEvent( + long startTicks, long blocker, long unblockingSpanId, long spanId, long rootSpanId) { + if (profiler != null && recordingFlag.get()) { + long endTicks = profiler.getCurrentTicks(); + taskBlockBridge.recordTaskBlockWithContext( + startTicks, endTicks, blocker, unblockingSpanId, spanId, rootSpanId); + } + } + + void recordTaskBlockFromContextEvent( + int tid, + long startTicks, + long endTicks, + long blocker, + long unblockingSpanId, + long spanId, + long rootSpanId) { + if (profiler != null && recordingFlag.get()) { + taskBlockBridge.recordTaskBlockFromContext( + tid, startTicks, endTicks, blocker, unblockingSpanId, spanId, rootSpanId); + } + } + + void parkEnter() { + // Guard with recordingFlag: stopProfiler() calls LockSupport.parkNanos while waiting for + // the profiler to stop. Without this guard an instrumented park on a tracing thread could call + // native parkEnter0 on a stopping/stopped profiler. + if (profiler != null && recordingFlag.get()) { + taskBlockBridge.parkEnter(); + } + } + + void parkExit(long blocker, long unblockingSpanId) { + if (profiler != null && recordingFlag.get()) { + taskBlockBridge.parkExit(blocker, unblockingSpanId); + } + } + + private static final class TaskBlockBridge { + private static final long DEFAULT_TSC_FREQUENCY = 1_000_000_000L; + + private final JavaProfiler profiler; + private final Method getCurrentThreadId; + private final Method getTscFrequency; + private final Method recordTaskBlock; + private final Method recordTaskBlockWithContext; + private final Method recordTaskBlockFromContext; + private final Method blockEnter; + private final Method blockExit; + private final Method parkEnter; + private final Method parkExit; + + private TaskBlockBridge(JavaProfiler profiler) { + this.profiler = profiler; + this.getCurrentThreadId = method("getCurrentThreadId"); + this.getTscFrequency = method("getTscFrequency"); + this.recordTaskBlock = + method("recordTaskBlock", long.class, long.class, long.class, long.class); + this.recordTaskBlockWithContext = + method( + "recordTaskBlockWithContext", + long.class, + long.class, + long.class, + long.class, + long.class, + long.class); + this.recordTaskBlockFromContext = + method( + "recordTaskBlockFromContext", + int.class, + long.class, + long.class, + long.class, + long.class, + long.class, + long.class); + this.blockEnter = method("blockEnter", int.class); + this.blockExit = method("blockExit", long.class); + this.parkEnter = method("parkEnter"); + this.parkExit = method("parkExit", long.class, long.class); + } + + private int getCurrentThreadId() { + if (getCurrentThreadId == null) { + return -1; + } + return ((Number) invoke(getCurrentThreadId)).intValue(); + } + + private boolean hasTaskBlockEventSupport() { + return recordTaskBlock != null && parkEnter != null && parkExit != null; + } + + private boolean hasTaskBlockFromContextSupport() { + return getCurrentThreadId != null && recordTaskBlockFromContext != null; + } + + private long getTscFrequency() { + if (getTscFrequency == null) { + return DEFAULT_TSC_FREQUENCY; + } + return ((Number) invoke(getTscFrequency)).longValue(); + } + + private void recordTaskBlock( + long startTicks, long endTicks, long blocker, long unblockingSpanId) { + invokeIfPresent(recordTaskBlock, startTicks, endTicks, blocker, unblockingSpanId); + } + + private void recordTaskBlockWithContext( + long startTicks, + long endTicks, + long blocker, + long unblockingSpanId, + long spanId, + long rootSpanId) { + invokeIfPresent( + recordTaskBlockWithContext, + startTicks, + endTicks, + blocker, + unblockingSpanId, + spanId, + rootSpanId); + } + + private void recordTaskBlockFromContext( + int tid, + long startTicks, + long endTicks, + long blocker, + long unblockingSpanId, + long spanId, + long rootSpanId) { + invokeIfPresent( + recordTaskBlockFromContext, + tid, + startTicks, + endTicks, + blocker, + unblockingSpanId, + spanId, + rootSpanId); + } + + private long blockEnter(int state) { + if (blockEnter == null) { + return 0L; + } + return ((Number) invoke(blockEnter, state)).longValue(); + } + + private void blockExit(long token) { + invokeIfPresent(blockExit, token); + } + + private void parkEnter() { + invokeIfPresent(parkEnter); + } + + private void parkExit(long blocker, long unblockingSpanId) { + invokeIfPresent(parkExit, blocker, unblockingSpanId); + } + + private static Method method(String name, Class... parameterTypes) { + try { + return JavaProfiler.class.getMethod(name, parameterTypes); + } catch (NoSuchMethodException ignored) { + return null; + } + } + + private void invokeIfPresent(Method method, Object... args) { + if (method != null) { + invoke(method, args); + } + } + + private Object invoke(Method method, Object... args) { + try { + return method.invoke(profiler, args); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + if (cause instanceof Error) { + throw (Error) cause; + } + throw new IllegalStateException(cause); + } + } + } } 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 60c0d5c2e60..ca7c984ae17 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 @@ -43,6 +43,8 @@ import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_ENABLED; import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_INTERVAL; import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_INTERVAL_DEFAULT; +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_HEAP_ENABLED; import static datadog.trace.api.config.ProfilingConfig.PROFILING_HEAP_TRACK_GENERATIONS; import static datadog.trace.api.config.ProfilingConfig.PROFILING_HEAP_TRACK_GENERATIONS_DEFAULT; @@ -167,6 +169,13 @@ public static boolean getWallContextFilter(ConfigProvider configProvider) { PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER_DEFAULT); } + public static boolean getWallPrecheck(ConfigProvider configProvider) { + return getBoolean( + configProvider, + PROFILING_DATADOG_PROFILER_WALL_PRECHECK, + PROFILING_DATADOG_PROFILER_WALL_PRECHECK_DEFAULT); + } + static boolean isJmethodIDSafe() { return ProfilingSupport.isJmethodIDSafe(); } 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..54d516aaaf3 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,69 @@ 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 long blockEnter(int state) { + return DDPROF.blockEnter(state); + } + + @Override + public void blockExit(long token) { + DDPROF.blockExit(token); + } + + @Override + public void recordTaskBlock(long startTicks, long blocker, long unblockingSpanId) { + DDPROF.recordTaskBlockEvent(startTicks, blocker, unblockingSpanId); + } + + @Override + public void recordTaskBlockWithContext( + long startTicks, long blocker, long unblockingSpanId, long spanId, long rootSpanId) { + DDPROF.recordTaskBlockWithContextEvent( + startTicks, blocker, unblockingSpanId, spanId, rootSpanId); + } + + @Override + public void parkEnter() { + DDPROF.parkEnter(); + } + + @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..6cdf0d4ba1c 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,55 @@ void test() throws Exception { } } + @Test + void testTaskBlockBridgeMethodsEmitTaskBlockEvents() throws Exception { + assertDoesNotThrow( + () -> DdprofLibraryLoader.jvmAccess().getReasonNotLoaded(), "Profiler not available"); + DatadogProfiler profiler = DatadogProfiler.newInstance(ConfigProvider.getInstance()); + Assumptions.assumeTrue( + profiler.hasTaskBlockEventSupport(), + "Loaded ddprof artifact does not expose TaskBlock bridge methods"); + 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; + } + + // The native TaskBlock gate filters events when span_id == 0 (reads from OTEP TLS). + // Set a non-zero span context so both the recordTaskBlock and park paths actually emit. + profiler.setSpanContext(1L /* rootSpanId */, 42L /* spanId */, 0L, 0L); + try { + // Direct bridge path (recordTaskBlock -> JavaProfiler.recordTaskBlock0). Span ids are no + // longer passed across JNI; the native side reads them from OTEP TLS. + long startTicks = profiler.getCurrentTicks(); + LockSupport.parkNanos(3_000_000L); // > 1ms native threshold + profiler.recordTaskBlockEvent(startTicks, 303L, 404L); + + // Park path (parkEnter/parkExit -> JavaProfiler.parkEnter0/parkExit0) + profiler.parkEnter(); + 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 { + profiler.clearSpanContext(); + recording.stop(); + } + } + @ParameterizedTest @MethodSource("profilingModes") void testStartCmd(boolean cpu, boolean wall, boolean alloc, boolean memleak) throws Exception { @@ -102,8 +154,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)); } @ParameterizedTest diff --git a/dd-java-agent/ddprof-lib/src/main/java/datadog/libs/ddprof/DdprofLibraryLoader.java b/dd-java-agent/ddprof-lib/src/main/java/datadog/libs/ddprof/DdprofLibraryLoader.java index b637dc69309..2f9fa3c200a 100644 --- a/dd-java-agent/ddprof-lib/src/main/java/datadog/libs/ddprof/DdprofLibraryLoader.java +++ b/dd-java-agent/ddprof-lib/src/main/java/datadog/libs/ddprof/DdprofLibraryLoader.java @@ -4,16 +4,17 @@ import com.datadoghq.profiler.JavaProfiler; import com.datadoghq.profiler.OTelContext; import datadog.trace.api.config.ProfilingConfig; +import datadog.trace.api.profiling.TaskBlockInstrumentationConfig; import datadog.trace.bootstrap.config.provider.ConfigProvider; import datadog.trace.util.TempLocationManager; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.PosixFilePermissions; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * A wrapper around unified loading of the Datadog profiler and JVM access. It exposes {@linkplain @@ -23,8 +24,6 @@ * constructed, if that's the case. */ public final class DdprofLibraryLoader { - private static final Logger log = LoggerFactory.getLogger(DdprofLibraryLoader.class.getName()); - public abstract static class ComponentHolder { private volatile boolean loaded = false; @@ -129,10 +128,13 @@ private static JavaProfilerHolder initJavaProfiler() { try { ConfigProvider configProvider = ConfigProvider.getInstance(); String scratch = getScratchDir(configProvider); + boolean wallPrecheck = TaskBlockInstrumentationConfig.isWallPrecheckEnabled(configProvider); profiler = - JavaProfiler.getInstance( + createJavaProfiler( configProvider.getString(ProfilingConfig.PROFILING_DATADOG_PROFILER_LIBPATH), - scratch); + scratch, + false, + wallPrecheck); // sanity test - force load Datadog profiler to catch it not being available early profiler.execute("status"); } catch (Throwable t) { @@ -142,6 +144,31 @@ private static JavaProfilerHolder initJavaProfiler() { return new JavaProfilerHolder(profiler, reasonNotLoaded); } + private static JavaProfiler createJavaProfiler( + String libPath, String scratch, boolean delegateMonitorEvents, boolean wallPrecheck) + throws Exception { + try { + Method getInstance = + JavaProfiler.class.getMethod( + "getInstance", String.class, String.class, boolean.class, boolean.class); + return (JavaProfiler) + getInstance.invoke(null, libPath, scratch, delegateMonitorEvents, wallPrecheck); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof Error) { + throw (Error) cause; + } + if (cause instanceof Exception) { + throw (Exception) cause; + } + throw e; + } catch (NoSuchMethodException ignored) { + // Older ddprof artifacts do not support the explicit wallprecheck init flag. Keep the agent + // buildable and fall back to the legacy constructor. + return JavaProfiler.getInstance(libPath, scratch); + } + } + private static JVMAccessHolder initJVMAccess() { ConfigProvider configProvider = ConfigProvider.getInstance(); AtomicReference reasonNotLoaded = new AtomicReference<>(); 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..1863d59404c --- /dev/null +++ b/dd-smoke-tests/profiling-integration-tests/src/test/java/com/datadog/smoketest/profiling/SynchronizedContentionForkedApp.java @@ -0,0 +1,126 @@ +package com.datadog.smoketest.profiling; + +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.util.GlobalTracer; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public final class SynchronizedContentionForkedApp { + private static final int REPETITIONS = 5; + private static final Object BLOCK_LOCK = new Object(); + + public static void main(final String[] args) throws Exception { + SynchronizedContentionForkedApp app = new SynchronizedContentionForkedApp(GlobalTracer.get()); + for (int i = 0; i < REPETITIONS; i++) { + app.runBlockScenario(); + app.runInstanceMethodScenario(); + app.runStaticMethodScenario(); + } + Thread.sleep(1500); + } + + private final Tracer tracer; + private final InstanceLockTarget instanceTarget = new InstanceLockTarget(); + + private SynchronizedContentionForkedApp(final Tracer tracer) { + this.tracer = tracer; + } + + private void runBlockScenario() throws Exception { + CountDownLatch holderIn = new CountDownLatch(1); + CountDownLatch holderOut = new CountDownLatch(1); + Thread holder = + new Thread( + () -> { + synchronized (BLOCK_LOCK) { + holderIn.countDown(); + try { + holderOut.await(2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }, + "sync-block-holder"); + holder.setDaemon(true); + holder.start(); + holderIn.await(); + + Span span = tracer.buildSpan("sync.block").start(); + try (Scope scope = tracer.activateSpan(span)) { + synchronized (BLOCK_LOCK) { + // entry-queue wait is the TaskBlock interval + } + } finally { + span.finish(); + } + holderOut.countDown(); + holder.join(); + } + + private void runInstanceMethodScenario() throws Exception { + CountDownLatch holderIn = new CountDownLatch(1); + CountDownLatch holderOut = new CountDownLatch(1); + Thread holder = + new Thread(() -> instanceTarget.hold(holderIn, holderOut), "sync-instance-holder"); + holder.setDaemon(true); + holder.start(); + holderIn.await(); + + Span span = tracer.buildSpan("sync.instance-method").start(); + try (Scope scope = tracer.activateSpan(span)) { + instanceTarget.contend(); + } finally { + span.finish(); + } + holderOut.countDown(); + holder.join(); + } + + private void runStaticMethodScenario() throws Exception { + CountDownLatch holderIn = new CountDownLatch(1); + CountDownLatch holderOut = new CountDownLatch(1); + Thread holder = + new Thread(() -> StaticLockTarget.hold(holderIn, holderOut), "sync-static-holder"); + holder.setDaemon(true); + holder.start(); + holderIn.await(); + + Span span = tracer.buildSpan("sync.static-method").start(); + try (Scope scope = tracer.activateSpan(span)) { + StaticLockTarget.contend(); + } finally { + span.finish(); + } + holderOut.countDown(); + holder.join(); + } + + static final class InstanceLockTarget { + synchronized void hold(final CountDownLatch in, final CountDownLatch out) { + in.countDown(); + try { + out.await(2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + synchronized void contend() {} + } + + static final class StaticLockTarget { + static synchronized void hold(final CountDownLatch in, final CountDownLatch out) { + in.countDown(); + try { + out.await(2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + static synchronized void contend() {} + } +} 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..549b440e936 --- /dev/null +++ b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/ObjectWaitTaskBlockProfilingTest.java @@ -0,0 +1,350 @@ +package datadog.smoketest; + +import static datadog.smoketest.SmokeTestUtils.agentShadowJar; +import static datadog.smoketest.SmokeTestUtils.buildDirectory; +import static datadog.smoketest.SmokeTestUtils.checkProcessSuccessfullyEnd; +import static datadog.smoketest.SmokeTestUtils.javaPath; +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 static org.openjdk.jmc.common.unit.UnitLookup.PLAIN_TEXT; + +import datadog.trace.api.config.ProfilingConfig; +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.util.GlobalTracer; +import java.io.File; +import java.io.IOException; +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.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +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.JfrLoaderToolkit; +import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; + +/** + * Smoke test for native {@code Object.wait} TaskBlock coverage through the profiler's JVMTI {@code + * MonitorWait}/{@code MonitorWaited} callbacks. + */ +@DisabledOnJ9 +final class ObjectWaitTaskBlockProfilingTest { + private static final byte[] JFR_MAGIC = new byte[] {'F', 'L', 'R', 0}; + private static final IAttribute SPAN_ID = attr("spanId", "spanId", "spanId", NUMBER); + private static final IAttribute LOCAL_ROOT_SPAN_ID = + attr("localRootSpanId", "localRootSpanId", "localRootSpanId", NUMBER); + private static final IAttribute BLOCKER = + attr("blocker", "blocker", "blocker", NUMBER); + private static final IAttribute UNBLOCKING_SPAN_ID = + attr("unblockingSpanId", "unblockingSpanId", "unblockingSpanId", NUMBER); + private static final IAttribute TASK_BLOCK_EMITTED = + attr("numTaskBlockEmitted", "numTaskBlockEmitted", "numTaskBlockEmitted", NUMBER); + private static final IAttribute OPERATION = + attr("_dd.trace.operation", "_dd.trace.operation", "_dd.trace.operation", PLAIN_TEXT); + private static final Path LOG_FILE_BASE = + Paths.get( + buildDirectory(), + "reports", + "testProcess." + ObjectWaitTaskBlockProfilingTest.class.getName()); + + private Path dumpDir; + private Path logFilePath; + + @BeforeEach + void setup(TestInfo testInfo) throws IOException { + Files.createDirectories(LOG_FILE_BASE); + logFilePath = + LOG_FILE_BASE.resolve( + testInfo.getTestMethod().map(method -> method.getName()).orElse("objectWait") + ".log"); + dumpDir = Files.createTempDirectory("dd-profiler-objectwait-"); + } + + @AfterEach + void tearDown() throws IOException { + if (dumpDir != null && Files.exists(dumpDir)) { + Files.walk(dumpDir).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } + } + + @Test + @DisplayName("Object.wait emits span-attributed native TaskBlock events") + void objectWaitsEmitTaskBlockEvents() throws Exception { + Process targetProcess = createProcessBuilder().start(); + + checkProcessSuccessfullyEnd(targetProcess, logFilePath); + + JfrStats stats = loadStats(); + assertTrue(stats.taskBlockCount > 0, "Expected datadog.TaskBlock events"); + assertTrue(stats.taskBlockEmitted > 0, "Expected numTaskBlockEmitted counter"); + assertTrue(stats.taskBlocksWithNonZeroBlocker > 0, "Expected monitor identity to be recorded"); + assertFalse(stats.hasZeroSpanId, "TaskBlock events must have non-zero spanId"); + assertFalse( + stats.hasZeroLocalRootSpanId, "TaskBlock events must have non-zero localRootSpanId"); + assertFalse(stats.hasMissingEventThread, "TaskBlock events must resolve Event Thread"); + assertTrue( + stats.hasExpectedOperation, + "Expected TaskBlock events to include the objectwait.active span operation name"); + // notify/notifyAll are not instrumented, so the unblocking thread is not identified. + assertFalse( + stats.hasNonZeroUnblockingSpanId, + "Object.wait TaskBlocks must report unblockingSpanId == 0 (notify is still native)"); + assertFalse(logHasObjectWaitInstrumentationError(), "Object.wait instrumentation failed"); + } + + private ProcessBuilder createProcessBuilder() { + String templateOverride = + ObjectWaitTaskBlockProfilingTest.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=smoke-test-objectwait-taskblock", + "-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.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"), + ObjectWaitTaskBlockForkedApp.class.getName())); + if (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; + } + + private JfrStats loadStats() throws Exception { + JfrStats stats = new JfrStats(); + List jfrFiles; + try (java.util.stream.Stream files = Files.walk(dumpDir)) { + jfrFiles = + files + .filter(Files::isRegularFile) + .filter(path -> path.toString().endsWith(".jfr")) + .collect(Collectors.toList()); + } + for (Path jfrFile : jfrFiles) { + stats.add(loadEvents(jfrFile)); + } + return stats; + } + + private IItemCollection loadEvents(Path path) { + try { + return JfrLoaderToolkit.loadEvents(extractLastJfrStream(path).toFile()); + } catch (Exception e) { + throw new RuntimeException("Failed to load JFR " + path, e); + } + } + + private Path extractLastJfrStream(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; + } + + private boolean logHasObjectWaitInstrumentationError() throws IOException { + String log = new String(Files.readAllBytes(logFilePath), StandardCharsets.UTF_8); + return log.contains("NoClassDefFoundError") + || log.contains("Failed to handle exception in instrumentation for java.lang.Object"); + } + + public static final class ObjectWaitTaskBlockForkedApp { + private static final int WAIT_ITERATIONS = 20; + private static final long LONG_WAIT_MILLIS = 50L; + private static final Object BLOCKER = new Object(); + + public static void main(String[] args) throws Exception { + ObjectWaitTaskBlockForkedApp app = new ObjectWaitTaskBlockForkedApp(GlobalTracer.get()); + app.runActiveSpanWaits(); + app.runSpanlessWaits(); + app.runTooShortWaits(); + Thread.sleep(1500); + } + + private final Tracer tracer; + + private ObjectWaitTaskBlockForkedApp(Tracer tracer) { + this.tracer = tracer; + } + + private void runActiveSpanWaits() throws InterruptedException { + for (int i = 0; i < WAIT_ITERATIONS; i++) { + Span span = tracer.buildSpan("objectwait.active").start(); + try (Scope scope = tracer.activateSpan(span)) { + synchronized (BLOCKER) { + BLOCKER.wait(LONG_WAIT_MILLIS); + } + } finally { + span.finish(); + } + } + } + + private void runSpanlessWaits() throws InterruptedException { + for (int i = 0; i < WAIT_ITERATIONS; i++) { + synchronized (BLOCKER) { + BLOCKER.wait(LONG_WAIT_MILLIS); + } + } + } + + private void runTooShortWaits() throws InterruptedException { + // Exercises the wait(long, int) path. The native task-block threshold is 1 ms, so a 1-ms + // request typically rounds out just over it, but path coverage matters more than duration + // here; the assertion focuses on callbacks not crashing rather than which side of the + // threshold the interval lands on. + for (int i = 0; i < WAIT_ITERATIONS; i++) { + Span span = tracer.buildSpan("objectwait.too-short").start(); + try (Scope scope = tracer.activateSpan(span)) { + synchronized (BLOCKER) { + BLOCKER.wait(0L, 1); + } + } finally { + span.finish(); + } + } + } + } + + private static final class JfrStats { + private long taskBlockCount; + private long taskBlockEmitted; + private long taskBlocksWithNonZeroBlocker; + private boolean hasZeroSpanId; + private boolean hasZeroLocalRootSpanId; + private boolean hasMissingEventThread; + private boolean hasExpectedOperation; + private boolean hasNonZeroUnblockingSpanId; + + private void add(IItemCollection events) { + addTaskBlocks(events); + addWallClockEpochs(events); + } + + private void addTaskBlocks(IItemCollection events) { + IItemCollection taskBlocks = events.apply(ItemFilters.type("datadog.TaskBlock")); + for (IItemIterable items : taskBlocks) { + IMemberAccessor spanIdAccessor = SPAN_ID.getAccessor(items.getType()); + IMemberAccessor localRootSpanIdAccessor = + LOCAL_ROOT_SPAN_ID.getAccessor(items.getType()); + IMemberAccessor blockerAccessor = BLOCKER.getAccessor(items.getType()); + IMemberAccessor unblockingSpanIdAccessor = + UNBLOCKING_SPAN_ID.getAccessor(items.getType()); + IMemberAccessor eventThreadAccessor = + JdkAttributes.EVENT_THREAD_NAME.getAccessor(items.getType()); + IMemberAccessor operationAccessor = OPERATION.getAccessor(items.getType()); + for (IItem item : items) { + String operation = operationAccessor == null ? null : operationAccessor.getMember(item); + // Filter strictly to events emitted by our forked app; the JVM may emit other + // TaskBlock events (LockSupport from agent code, etc.) that we don't want to mix in. + if (!"objectwait.active".equals(operation) && !"objectwait.too-short".equals(operation)) { + continue; + } + taskBlockCount++; + long spanId = spanIdAccessor.getMember(item).longValue(); + long localRootSpanId = localRootSpanIdAccessor.getMember(item).longValue(); + long blocker = blockerAccessor.getMember(item).longValue(); + long unblockingSpanId = unblockingSpanIdAccessor.getMember(item).longValue(); + String eventThread = + eventThreadAccessor == null ? null : eventThreadAccessor.getMember(item); + hasZeroSpanId |= spanId == 0; + hasZeroLocalRootSpanId |= localRootSpanId == 0; + hasMissingEventThread |= eventThread == null || eventThread.isEmpty(); + hasExpectedOperation |= "objectwait.active".equals(operation); + if (blocker != 0) { + taskBlocksWithNonZeroBlocker++; + } + if (unblockingSpanId != 0) { + hasNonZeroUnblockingSpanId = true; + } + } + } + } + + private void addWallClockEpochs(IItemCollection events) { + IItemCollection epochs = events.apply(ItemFilters.type("datadog.WallClockSamplingEpoch")); + for (IItemIterable items : epochs) { + IMemberAccessor emittedAccessor = + TASK_BLOCK_EMITTED.getAccessor(items.getType()); + for (IItem item : items) { + taskBlockEmitted += emittedAccessor.getMember(item).longValue(); + } + } + } + } +} 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..a2b53652248 --- /dev/null +++ b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/SynchronizedContentionProfilingTest.java @@ -0,0 +1,288 @@ +package datadog.smoketest; + +import static datadog.smoketest.SmokeTestUtils.agentShadowJar; +import static datadog.smoketest.SmokeTestUtils.buildDirectory; +import static datadog.smoketest.SmokeTestUtils.checkProcessSuccessfullyEnd; +import static datadog.smoketest.SmokeTestUtils.javaPath; +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 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.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 org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +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.JfrLoaderToolkit; +import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes; + +/** + * Smoke test for native synchronized-contention TaskBlock coverage. Asserts that block-level {@code + * synchronized(obj){}} and method-level {@code synchronized} contention emit {@code + * datadog.TaskBlock} events with a non-zero {@code blocker} field through the native JVMTI {@code + * MonitorContendedEnter}/{@code MonitorContendedEntered} path. + */ +@DisabledOnJ9 +final class SynchronizedContentionProfilingTest { + private static final byte[] JFR_MAGIC = new byte[] {'F', 'L', 'R', 0}; + private static final IAttribute SPAN_ID = attr("spanId", "spanId", "spanId", NUMBER); + private static final IAttribute LOCAL_ROOT_SPAN_ID = + attr("localRootSpanId", "localRootSpanId", "localRootSpanId", NUMBER); + private static final IAttribute BLOCKER = + attr("blocker", "blocker", "blocker", NUMBER); + private static final IAttribute OPERATION = + attr("_dd.trace.operation", "_dd.trace.operation", "_dd.trace.operation", PLAIN_TEXT); + private static final Path LOG_FILE_BASE = + Paths.get( + buildDirectory(), + "reports", + "testProcess." + SynchronizedContentionProfilingTest.class.getName()); + + private Path dumpDir; + private Path logFilePath; + + @BeforeEach + void setup(TestInfo testInfo) throws IOException { + Files.createDirectories(LOG_FILE_BASE); + logFilePath = + LOG_FILE_BASE.resolve( + testInfo.getTestMethod().map(m -> m.getName()).orElse("syncContention") + ".log"); + dumpDir = Files.createTempDirectory("dd-profiler-synccontention-"); + } + + @AfterEach + void tearDown() throws IOException { + if (dumpDir != null && Files.exists(dumpDir)) { + Files.walk(dumpDir).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } + } + + @Test + @DisplayName( + "synchronized block and method contention emit span-attributed native TaskBlock events") + void synchronizedContentionEmitsTaskBlockEvents() throws Exception { + Process targetProcess = createProcessBuilder().start(); + checkProcessSuccessfullyEnd(targetProcess, logFilePath); + + JfrStats stats = loadStats(); + + // Each scenario must have produced at least one TaskBlock event. + assertTrue( + stats.blockScenarioCount > 0, + "Expected TaskBlock events for synchronized block contention"); + assertTrue( + stats.instanceMethodScenarioCount > 0, + "Expected TaskBlock events for synchronized instance-method contention"); + assertTrue( + stats.staticMethodScenarioCount > 0, + "Expected TaskBlock events for synchronized static-method contention"); + + // Every emitted event must carry a valid span context. + assertFalse(stats.hasZeroSpanId, "TaskBlock events must have non-zero spanId"); + assertFalse( + stats.hasZeroLocalRootSpanId, "TaskBlock events must have non-zero localRootSpanId"); + assertFalse(stats.hasMissingEventThread, "TaskBlock events must resolve Event Thread"); + + // The blocker field must identify the contested monitor (non-zero). + assertTrue(stats.blockersWithNonZeroValue > 0, "Expected non-zero blocker on TaskBlock events"); + + // The three scenarios contend on three distinct locks, so the blocker values must not all be + // identical — this proves the rewriter is recording per-monitor identity, not a constant. + assertTrue( + stats.distinctBlockerValues.size() > 1, + "Expected distinct blocker values across the three contention scenarios"); + + assertFalse( + logHasSynchronizedContentionError(), + "native synchronized-contention TaskBlock path must not produce errors"); + } + + private ProcessBuilder createProcessBuilder() { + String templateOverride = + SynchronizedContentionProfilingTest.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=smoke-test-synccontention-taskblock", + "-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.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"), + com.datadog.smoketest.profiling.SynchronizedContentionForkedApp.class.getName())); + if (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; + } + + private JfrStats loadStats() throws Exception { + JfrStats stats = new JfrStats(); + List jfrFiles; + try (java.util.stream.Stream files = Files.walk(dumpDir)) { + jfrFiles = + files + .filter(Files::isRegularFile) + .filter(path -> path.toString().endsWith(".jfr")) + .collect(Collectors.toList()); + } + for (Path jfrFile : jfrFiles) { + stats.add(loadEvents(jfrFile)); + } + return stats; + } + + private IItemCollection loadEvents(final Path path) { + try { + return JfrLoaderToolkit.loadEvents(extractLastJfrStream(path).toFile()); + } catch (Exception e) { + throw new RuntimeException("Failed to load JFR " + path, e); + } + } + + private Path extractLastJfrStream(final 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(final byte[] data, final 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; + } + + private boolean logHasSynchronizedContentionError() throws IOException { + String log = new String(Files.readAllBytes(logFilePath), StandardCharsets.UTF_8); + return log.contains("NoClassDefFoundError") + || log.contains("Failed to handle exception in instrumentation") + || log.contains("VerifyError"); + } + + // ------------------------------------------------------------------ stats + + private static final class JfrStats { + long blockScenarioCount; + long instanceMethodScenarioCount; + long staticMethodScenarioCount; + long blockersWithNonZeroValue; + final Set distinctBlockerValues = new HashSet<>(); + boolean hasZeroSpanId; + boolean hasZeroLocalRootSpanId; + boolean hasMissingEventThread; + + void add(final IItemCollection events) { + IItemCollection taskBlocks = events.apply(ItemFilters.type("datadog.TaskBlock")); + for (IItemIterable items : taskBlocks) { + IMemberAccessor spanIdAcc = SPAN_ID.getAccessor(items.getType()); + IMemberAccessor rootSpanIdAcc = + LOCAL_ROOT_SPAN_ID.getAccessor(items.getType()); + IMemberAccessor blockerAcc = BLOCKER.getAccessor(items.getType()); + IMemberAccessor operationAcc = OPERATION.getAccessor(items.getType()); + IMemberAccessor threadAcc = + JdkAttributes.EVENT_THREAD_NAME.getAccessor(items.getType()); + for (IItem item : items) { + String op = operationAcc == null ? null : operationAcc.getMember(item); + if (!"sync.block".equals(op) + && !"sync.instance-method".equals(op) + && !"sync.static-method".equals(op)) { + continue; + } + if ("sync.block".equals(op)) blockScenarioCount++; + else if ("sync.instance-method".equals(op)) instanceMethodScenarioCount++; + else staticMethodScenarioCount++; + + if (spanIdAcc != null) { + long spanId = spanIdAcc.getMember(item).longValue(); + hasZeroSpanId |= spanId == 0; + } + if (rootSpanIdAcc != null) { + long rootId = rootSpanIdAcc.getMember(item).longValue(); + hasZeroLocalRootSpanId |= rootId == 0; + } + if (blockerAcc != null) { + long blocker = blockerAcc.getMember(item).longValue(); + if (blocker != 0) { + blockersWithNonZeroValue++; + distinctBlockerValues.add(blocker); + } + } + String thread = threadAcc == null ? null : threadAcc.getMember(item); + hasMissingEventThread |= thread == null || thread.isEmpty(); + } + } + } + } +} 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 4076f4aae30..ac2aed34290 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 @@ -91,6 +91,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 = @@ -119,6 +120,10 @@ public final class ProfilingConfig { "profiling.ddprof.wall.context.filter"; public static final boolean PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER_DEFAULT = true; + 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..7534781d27f --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/profiling/TaskBlockInstrumentationConfig.java @@ -0,0 +1,16 @@ +package datadog.trace.api.profiling; + +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 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 configProvider.getBoolean( + PROFILING_DATADOG_PROFILER_WALL_PRECHECK, PROFILING_DATADOG_PROFILER_WALL_PRECHECK_DEFAULT); + } +} 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..9fe8729f86a 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 @@ -6,6 +6,9 @@ import datadog.trace.api.profiling.*; public interface ProfilingContextIntegration extends Profiling, EndpointCheckpointer, Timer { + /** Native {@code OSThreadState::SLEEPING}; used for span-scoped Thread.sleep precheck state. */ + int BLOCKING_STATE_SLEEPING = 7; + /** * invoked when the profiler is started, implementations must not initialise JFR before this is * called. @@ -34,6 +37,97 @@ 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. Span context is + * captured natively from the OTEP TLS sidecar at JNI entry, matching the {@code recordQueueTime} + * convention. + * + * @param startTicks TSC tick at block entry + * @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 blocker, long unblockingSpanId) {} + + /** + * Variant of {@link #recordTaskBlock} for virtual threads. + * + *

Virtual threads are multiplexed on OS carrier threads; the native OTEP TLS sidecar is + * carrier-scoped and cannot be trusted between capture (block entry) and emit (block exit). Java + * call sites that detect a virtual thread must capture span/root ids at block entry and pass them + * here explicitly so the native deferred-capture path can use them instead of the TLS sidecar. + * This virtual-thread path intentionally carries span/root ids only, not custom profiling context + * attributes. + * + * @param startTicks TSC tick captured at block entry + * @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 + * @param spanId span ID captured at block-entry time + * @param rootSpanId root span ID captured at block-entry time + */ + default void recordTaskBlockWithContext( + long startTicks, long blocker, long unblockingSpanId, long spanId, long rootSpanId) {} + + /** + * Returns the OS-level native thread ID for the calling thread, or {@code -1} if unavailable. + * Implementations may pre-cache this value in thread-local storage on {@link #onAttach()} to + * avoid repeated JNI round-trips on the hot path. + */ + default int getCurrentThreadId() { + return -1; + } + + /** + * Marks the current platform thread as entering a span-scoped blocking interval that may be used + * by the native wall-clock timer to skip later signals after the first MethodSample in the run. + * + * @return an opaque token to pass to {@link #blockExit(long)}, or {@code 0} when no native state + * was armed + */ + default long blockEnter(int state) { + return 0L; + } + + /** Clears a native blocked interval previously armed by {@link #blockEnter(int)}. */ + default void blockExit(long token) {} + + /** + * Enqueues a TaskBlock interval for asynchronous recording off the critical request path. The + * actual JFR write is performed by a background drain thread; the calling thread only pays the + * cost of a non-blocking queue offer. + * + *

Called from the {@code Thread.sleep} instrumentation finish path for platform threads. Other + * paths use the synchronous {@link #recordTaskBlock} / {@link #recordTaskBlockWithContext} + * methods instead. + * + * @param startTicks TSC tick captured at sleep entry + * @param durationNanos wall-clock duration of the sleep in nanoseconds + * @param blocker identity hash of the blocking object, or 0 for sleeps + * @param spanId span ID captured at sleep entry + * @param rootSpanId root span ID captured at sleep entry + */ + default void enqueueTaskBlock( + long startTicks, long durationNanos, long blocker, long spanId, long rootSpanId) {} + + /** + * Called when the current thread is about to enter {@code LockSupport.park*}. The native profiler + * snapshots the OTEP TLS span context, records the start tick for {@code datadog.TaskBlock} + * emission on unpark, and arms native blocked-run state for wall-clock pre-send suppression after + * the first MethodSample in the park run. When {@code wallprecheck} is disabled (the default), + * wall-clock signals are still delivered to parked threads. + */ + default void parkEnter() {} + + /** + * 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/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..cf4641672d8 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/profiling/TaskBlockInstrumentationConfigTest.java @@ -0,0 +1,33 @@ +package datadog.trace.api.profiling; + +import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_PRECHECK; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import datadog.trace.bootstrap.config.provider.ConfigProvider; +import java.util.Properties; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class TaskBlockInstrumentationConfigTest { + + @ParameterizedTest + @MethodSource("wallPrecheckModes") + void isWallPrecheckEnabledReadsProfilerWallPrecheckFlag(boolean wallPrecheck, boolean expected) { + ConfigProvider configProvider = configProvider(wallPrecheck); + + assertEquals(expected, TaskBlockInstrumentationConfig.isWallPrecheckEnabled(configProvider)); + } + + private static Stream wallPrecheckModes() { + return Stream.of(Arguments.of(true, true), Arguments.of(false, false)); + } + + private static ConfigProvider configProvider(boolean wallPrecheck) { + Properties properties = new Properties(); + properties.setProperty( + PROFILING_DATADOG_PROFILER_WALL_PRECHECK, Boolean.toString(wallPrecheck)); + return ConfigProvider.withPropertiesOverride(properties); + } +} diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 93e70b13a35..cf9a732effd 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -2753,6 +2753,14 @@ "aliases": [] } ], + "DD_PROFILING_ASYNC_WALL_PRECHECK": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": [] + } + ], "DD_PROFILING_ASYNC_WALL_INTERVAL_MS": [ { "version": "A", @@ -2985,6 +2993,14 @@ "aliases": [] } ], + "DD_PROFILING_DDPROF_WALL_PRECHECK": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": [] + } + ], "DD_PROFILING_DDPROF_WALL_INTERVAL_MS": [ { "version": "A",