diff --git a/.github/workflows/run-system-tests.yaml b/.github/workflows/run-system-tests.yaml index 26160736564..c856d1cf567 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 7.0.0 @@ -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 2ba10349d5a..27e4de8ead8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -68,6 +68,10 @@ variables: description: "Enable flaky tests" value: "false" + JAVA_PROFILER_REF: + description: "When non-empty, clone DataDog/java-profiler at this Git ref (branch or tag), build ddprof, and use it as ddprof.jar for Gradle jobs instead of the Maven dependency." + value: "paul.fournillon/wallclock-taskblock" + # One pipeline injection package size ratchet OCI_PACKAGE_MAX_SIZE_BYTES: 40_000_000 LIB_INJECTION_IMAGE_MAX_SIZE_BYTES: 40_000_000 @@ -197,9 +201,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: <<: *tier_m_variables MAVEN_OPTS: "-Xms256M -Xmx1024M" @@ -245,6 +261,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 # Apache Maven Wrapper supports MVNW_REPOURL for repository-manager downloads: @@ -328,8 +345,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 @@ -441,7 +564,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" @@ -454,7 +579,9 @@ spotless: check-instrumentation-naming: extends: .gradle_build stage: tests - needs: [ ] + needs: + - job: build_java_profiler_ddprof + optional: true script: - ./gradlew --version - ./gradlew checkInstrumentationNaming @@ -462,7 +589,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 @@ -471,7 +600,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: @@ -499,7 +631,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" @@ -535,7 +670,9 @@ test_published_artifacts: check_build_src: extends: .check_job - needs: [] + needs: + - job: build_java_profiler_ddprof + optional: true variables: GRADLE_TARGET: ":buildSrc:build" @@ -574,6 +711,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_java_profiler_ddprof + optional: true - job: build_tests parallel: matrix: @@ -659,7 +798,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: <<: *tier_m_variables @@ -1262,7 +1404,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: @@ -1291,7 +1436,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 0005e8ea659..7005232c47a 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; @@ -409,7 +410,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)); @@ -455,6 +455,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)) { 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..5dd379d01f0 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 @@ -56,6 +56,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; @@ -167,6 +168,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/DatadogProfilerTest.java b/dd-java-agent/agent-profiling/profiling-ddprof/src/test/java/com/datadog/profiling/ddprof/DatadogProfilerTest.java index 2be7d129e68..210119e36ea 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)); } @ParameterizedTest @@ -154,6 +153,42 @@ void testWallContextFilter(boolean tracingEnabled, boolean contextFilterEnabled) } } + @ParameterizedTest + @MethodSource("wallPrecheckModes") + void testWallPrecheck(boolean wallPrecheckEnabled) 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, + Boolean.toString(wallPrecheckEnabled)); + + DatadogProfiler profiler = + DatadogProfiler.newInstance(ConfigProvider.withPropertiesOverride(props)); + + Path targetFile = Paths.get("/tmp/target.jfr"); + String cmd = profiler.cmdStartProfiling(targetFile); + + assertTrue(cmd.contains("wall="), cmd); + if (wallPrecheckEnabled) { + assertTrue(cmd.contains(",wallprecheck=true"), cmd); + } else { + assertFalse(cmd.contains("wallprecheck="), cmd); + } + } + + private static Stream wallPrecheckModes() { + return Stream.of(Arguments.of(true), Arguments.of(false)); + } + private static Stream wallContextFilterModes() { return Stream.of( Arguments.of(true, true), // tracing enabled, context filter enabled -> filter=0 diff --git a/dd-java-agent/ddprof-lib/build.gradle b/dd-java-agent/ddprof-lib/build.gradle index f6d653e3bbd..59b369c0a62 100644 --- a/dd-java-agent/ddprof-lib/build.gradle +++ b/dd-java-agent/ddprof-lib/build.gradle @@ -13,6 +13,8 @@ dependencies { implementation project.hasProperty('ddprof.jar') ? files(project.findProperty('ddprof.jar')) : libs.ddprof api project(':internal-api') api project(':dd-trace-api') + + testImplementation libs.bundles.junit5 } tasks.named("shadowJar", ShadowJar) { 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..ee6cc9a5ad2 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,39 @@ private static JavaProfilerHolder initJavaProfiler() { return new JavaProfilerHolder(profiler, reasonNotLoaded); } + static Method resolveWallPrecheckGetInstance(Class profilerClass) { + try { + return profilerClass.getMethod( + "getInstance", String.class, String.class, boolean.class, boolean.class); + } catch (NoSuchMethodException e) { + return null; + } + } + + private static JavaProfiler createJavaProfiler( + String libPath, String scratch, boolean delegateMonitorEvents, boolean wallPrecheck) + throws Exception { + Method getInstance = resolveWallPrecheckGetInstance(JavaProfiler.class); + if (getInstance == null) { + // 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); + } + try { + 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; + } + } + private static JVMAccessHolder initJVMAccess() { ConfigProvider configProvider = ConfigProvider.getInstance(); AtomicReference reasonNotLoaded = new AtomicReference<>(); diff --git a/dd-java-agent/ddprof-lib/src/test/java/datadog/libs/ddprof/DdprofLibraryLoaderTest.java b/dd-java-agent/ddprof-lib/src/test/java/datadog/libs/ddprof/DdprofLibraryLoaderTest.java new file mode 100644 index 00000000000..5d90dc3d183 --- /dev/null +++ b/dd-java-agent/ddprof-lib/src/test/java/datadog/libs/ddprof/DdprofLibraryLoaderTest.java @@ -0,0 +1,38 @@ +package datadog.libs.ddprof; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.lang.reflect.Method; +import org.junit.jupiter.api.Test; + +class DdprofLibraryLoaderTest { + + @Test + void resolvesFourArgOverloadWhenPresent() throws Exception { + Method m = DdprofLibraryLoader.resolveWallPrecheckGetInstance(WithWallPrecheckSupport.class); + assertNotNull(m); + assertArrayEquals( + new Class[] {String.class, String.class, boolean.class, boolean.class}, + m.getParameterTypes()); + } + + @Test + void returnsNullWhenFourArgOverloadAbsent() { + assertNull( + DdprofLibraryLoader.resolveWallPrecheckGetInstance(WithoutWallPrecheckSupport.class)); + } + + public static final class WithWallPrecheckSupport { + public static Object getInstance(String a, String b, boolean c, boolean d) { + return null; + } + } + + public static final class WithoutWallPrecheckSupport { + public static Object getInstance(String a, String b) { + return null; + } + } +} 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..c5376988622 --- /dev/null +++ b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/ObjectWaitTaskBlockProfilingTest.java @@ -0,0 +1,260 @@ +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.OPERATION; +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 io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.util.GlobalTracer; +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.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.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 IAttribute UNBLOCKING_SPAN_ID = + attr("unblockingSpanId", "unblockingSpanId", "unblockingSpanId", NUMBER); + private static final IAttribute TASK_BLOCK_EMITTED = + attr("numTaskBlockEmitted", "numTaskBlockEmitted", "numTaskBlockEmitted", 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 + @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"); + assertFalse( + stats.hasMissingTaskBlockAttribute, + "TaskBlock events must include spanId, localRootSpanId, blocker, and unblockingSpanId"); + assertFalse( + stats.hasMissingTaskBlockEmittedAttribute, + "WallClockSamplingEpoch events must include numTaskBlockEmitted"); + 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() throws IOException { + return TaskBlockProfilingTestSupport.createTaskBlockProcessBuilder( + "smoke-test-objectwait-taskblock", + ObjectWaitTaskBlockForkedApp.class.getName(), + dumpDir, + logFilePath); + } + + private JfrStats loadStats() throws Exception { + JfrStats stats = new JfrStats(); + for (IItemCollection events : TaskBlockProfilingTestSupport.loadDumpedEvents(dumpDir)) { + stats.add(events); + } + return stats; + } + + private boolean logHasObjectWaitInstrumentationError() throws IOException { + return TaskBlockProfilingTestSupport.logContainsAny( + logFilePath, + "NoClassDefFoundError", + "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 boolean hasMissingTaskBlockAttribute; + private boolean hasMissingTaskBlockEmittedAttribute; + + 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++; + if (spanIdAccessor != null) { + long spanId = spanIdAccessor.getMember(item).longValue(); + hasZeroSpanId |= spanId == 0; + } else { + hasMissingTaskBlockAttribute = true; + } + if (localRootSpanIdAccessor != null) { + long localRootSpanId = localRootSpanIdAccessor.getMember(item).longValue(); + hasZeroLocalRootSpanId |= localRootSpanId == 0; + } else { + hasMissingTaskBlockAttribute = true; + } + if (blockerAccessor != null) { + long blocker = blockerAccessor.getMember(item).longValue(); + if (blocker != 0) { + taskBlocksWithNonZeroBlocker++; + } + } else { + hasMissingTaskBlockAttribute = true; + } + if (unblockingSpanIdAccessor != null) { + long unblockingSpanId = unblockingSpanIdAccessor.getMember(item).longValue(); + if (unblockingSpanId != 0) { + hasNonZeroUnblockingSpanId = true; + } + } else { + hasMissingTaskBlockAttribute = true; + } + String eventThread = + eventThreadAccessor == null ? null : eventThreadAccessor.getMember(item); + hasMissingEventThread |= eventThread == null || eventThread.isEmpty(); + hasExpectedOperation |= "objectwait.active".equals(operation); + } + } + } + + 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()); + if (emittedAccessor == null) { + hasMissingTaskBlockEmittedAttribute = true; + continue; + } + 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..dacf73968ce --- /dev/null +++ b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/SynchronizedContentionProfilingTest.java @@ -0,0 +1,170 @@ +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.OPERATION; +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.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +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 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 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 + @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() throws IOException { + return TaskBlockProfilingTestSupport.createTaskBlockProcessBuilder( + "smoke-test-synccontention-taskblock", + com.datadog.smoketest.profiling.SynchronizedContentionForkedApp.class.getName(), + dumpDir, + logFilePath); + } + + private JfrStats loadStats() throws Exception { + JfrStats stats = new JfrStats(); + for (IItemCollection events : TaskBlockProfilingTestSupport.loadDumpedEvents(dumpDir)) { + stats.add(events); + } + return stats; + } + + private boolean logHasSynchronizedContentionError() throws IOException { + return TaskBlockProfilingTestSupport.logContainsAny( + logFilePath, + "NoClassDefFoundError", + "Failed to handle exception in instrumentation", + "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-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..507654abcf8 --- /dev/null +++ b/dd-smoke-tests/profiling-integration-tests/src/test/java/datadog/smoketest/TaskBlockProfilingTestSupport.java @@ -0,0 +1,174 @@ +package datadog.smoketest; + +import static datadog.smoketest.SmokeTestUtils.agentShadowJar; +import static datadog.smoketest.SmokeTestUtils.buildDirectory; +import static datadog.smoketest.SmokeTestUtils.javaPath; +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.List; +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.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 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.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("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")) + .collect(Collectors.toList()); + } + List events = new ArrayList<>(); + for (Path jfrFile : jfrFiles) { + events.add(loadEvents(dumpDir, jfrFile)); + } + 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; + } + + 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 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/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 2ac540f99fb..86f80223a75 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -2985,6 +2985,14 @@ "aliases": [] } ], + "DD_PROFILING_DDPROF_WALL_PRECHECK": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": [] + } + ], "DD_PROFILING_DDPROF_WALL_INTERVAL_MS": [ { "version": "A",