Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ variables:
NON_DEFAULT_JVMS:
description: "Enable tests on JVMs that are not the default"
value: "false"
DDPROF_VERSION:
description: "ddprof version embedded in the agent artifact used by downstream tests"
value: "1.47.0-paul.fournillon_taskblock-native-io-SNAPSHOT"
RUN_FLAKY_TESTS:
description: "Enable flaky tests"
value: "false"
Expand Down Expand Up @@ -343,14 +346,22 @@ build:
script:
- if [ $CI_PIPELINE_SOURCE == "schedule" ] ; then ./gradlew resolveAndLockAll --write-locks $GRADLE_ARGS; fi
- ./gradlew --version
- ./gradlew clean :dd-java-agent:shadowJar :dd-java-agent:check :dd-trace-api:jar :dd-trace-ot:shadowJar -PskipTests -x spotlessCheck $GRADLE_ARGS
- echo UPSTREAM_TRACER_VERSION=$(java -jar workspace/dd-java-agent/build/libs/*.jar) >> upstream.env
- ./gradlew clean :dd-java-agent:shadowJar :dd-java-agent:check :dd-trace-api:jar :dd-trace-ot:shadowJar -Pddprof.version="$DDPROF_VERSION" -PskipTests -x spotlessCheck $GRADLE_ARGS
- AGENT_JARS=$(find workspace/dd-java-agent/build/libs -maxdepth 1 -type f -name 'dd-java-agent-*.jar' ! -name '*-javadoc.jar' ! -name '*-sources.jar' -print)
- test "$(printf '%s\n' "$AGENT_JARS" | grep -c .)" -eq 1
- AGENT_JAR="$AGENT_JARS"
- AGENT_SHA256=$(sha256sum "$AGENT_JAR" | cut -d' ' -f1)
- echo "$AGENT_SHA256 $(basename "$AGENT_JAR")" > "$AGENT_JAR.sha256"
- echo UPSTREAM_TRACER_VERSION=$(java -jar "$AGENT_JAR") >> upstream.env
- echo "BUILD_JOB_NAME=$CI_JOB_NAME" >> build.env
- echo "BUILD_JOB_ID=$CI_JOB_ID" >> build.env
- echo "SMOKETEST_AGENT_JAR=$AGENT_JAR" >> build.env
- echo "SMOKETEST_AGENT_SHA256=$AGENT_SHA256" >> build.env
artifacts:
when: always
paths:
- 'workspace/dd-java-agent/build/libs/*.jar'
- 'workspace/dd-java-agent/build/libs/*.sha256'
- 'workspace/dd-trace-api/build/libs/*.jar'
- 'workspace/dd-trace-ot/build/libs/*.jar'
- 'upstream.env'
Expand Down Expand Up @@ -575,6 +586,8 @@ muzzle:
# Keep matrix vars exact and in build_tests declaration order:
# https://docs.gitlab.com/ci/yaml/#needsparallelmatrix
needs: &needs_build_tests_inst
- job: build
artifacts: true
- job: build_tests
parallel:
matrix:
Expand Down Expand Up @@ -660,7 +673,11 @@ muzzle-dep-report:
extends: .gradle_build
image: ${BUILDER_IMAGE_REPO}:${BUILDER_IMAGE_VERSION_PREFIX}$testJvm
tags: [ "docker-in-docker:amd64" ] # use docker-in-docker runner for testcontainers
needs: [ build_tests ]
needs:
- job: build
artifacts: true
- job: build_tests
artifacts: false
stage: tests
variables:
<<: *tier_m_variables
Expand Down Expand Up @@ -695,6 +712,8 @@ muzzle-dep-report:
- *prepare_test_env
- export GRADLE_OPTS="-Dorg.gradle.jvmargs='-Xms$GRADLE_MEMORY_MIN -Xmx$GRADLE_MEMORY_MAX $PROFILER_COMMAND -XX:ErrorFile=/tmp/hs_err_pid%p.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp -Djava.util.prefs.userRoot=/tmp/.java/.userPrefs-${CI_JOB_ID}' -Ddatadog.forkedMinHeapSize=128M -Ddatadog.forkedMaxHeapSize=1024M"
- ./gradlew --version
- test -n "$SMOKETEST_AGENT_JAR" && test -n "$SMOKETEST_AGENT_SHA256" && test -f "$SMOKETEST_AGENT_JAR"
- echo "$SMOKETEST_AGENT_SHA256 $SMOKETEST_AGENT_JAR" | sha256sum -c -
- ./gradlew $GRADLE_TARGET $GRADLE_PARAMS -PtestJvm=$testJvm -Pslot=$CI_NODE_INDEX/$CI_NODE_TOTAL $GRADLE_ARGS --continue || $CONTINUE_ON_FAILURE
after_script:
- *restore_pretest_env
Expand Down Expand Up @@ -742,7 +761,9 @@ muzzle-dep-report:
image: ${BUILDER_IMAGE_REPO}:${BUILDER_IMAGE_VERSION_PREFIX}$testJvm
tags: [ "docker-in-docker:arm64" ]
stage: tests-arm64
needs: []
needs:
- job: build
artifacts: true
variables:
<<: *tier_m_variables
DEFAULT_TEST_JVMS: /^(8|11|17|21|25|27|tip)$/ # Java 27 TODO: remove 27 after GA (tip will move to 27)
Expand Down Expand Up @@ -821,6 +842,8 @@ muzzle-dep-report:
- export GRADLE_OPTS="-Dorg.gradle.jvmargs='-Xshare:off -Xms$GRADLE_MEMORY_MIN -Xmx$GRADLE_MEMORY_MAX -XX:ErrorFile=/tmp/hs_err_pid%p.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp -Djava.util.prefs.userRoot=/tmp/.java/.userPrefs-${CI_JOB_ID}' -Ddatadog.forkedMinHeapSize=128M -Ddatadog.forkedMaxHeapSize=1024M"
- export GRADLE_ARGS="--build-cache --stacktrace --no-daemon --parallel --max-workers=$GRADLE_WORKERS"
- ./gradlew --version
- test -n "$SMOKETEST_AGENT_JAR" && test -n "$SMOKETEST_AGENT_SHA256" && test -f "$SMOKETEST_AGENT_JAR"
- echo "$SMOKETEST_AGENT_SHA256 $SMOKETEST_AGENT_JAR" | sha256sum -c -
script:
- *gitlab_base_ref_params
- ./gradlew $GRADLE_TARGET $GRADLE_PARAMS -PtestJvm=$testJvm -Pslot=$CI_NODE_INDEX/$CI_NODE_TOTAL $GRADLE_ARGS --continue
Expand Down Expand Up @@ -919,6 +942,8 @@ test_base:
# Keep matrix vars exact and in build_tests declaration order:
# https://docs.gitlab.com/ci/yaml/#needsparallelmatrix
needs:
- job: build
artifacts: true
- job: build_tests
parallel:
matrix:
Expand Down Expand Up @@ -970,6 +995,8 @@ test_inst_latest:
# Keep matrix vars exact and in build_tests declaration order:
# https://docs.gitlab.com/ci/yaml/#needsparallelmatrix
needs:
- job: build
artifacts: true
- job: build_tests
parallel:
matrix:
Expand Down Expand Up @@ -1038,6 +1065,8 @@ test_profiling:
# Keep matrix vars exact and in build_tests declaration order:
# https://docs.gitlab.com/ci/yaml/#needsparallelmatrix
needs:
- job: build
artifacts: true
- job: build_tests
parallel:
matrix:
Expand Down Expand Up @@ -1092,6 +1121,8 @@ test_smoke:
# Keep matrix vars exact and in build_tests declaration order:
# https://docs.gitlab.com/ci/yaml/#needsparallelmatrix
needs: &needs_build_tests_smoke
- job: build
artifacts: true
- job: build_tests
parallel:
matrix:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Copyright 2026 Datadog, Inc.
package com.datadog.profiling.ddprof;

import static com.datadog.profiling.ddprof.DatadogProfilerConfig.enableJMethodIDOptim;
Expand Down Expand Up @@ -36,6 +37,7 @@
import com.datadog.profiling.utils.ProfilingMode;
import com.datadoghq.profiler.ContextSetter;
import com.datadoghq.profiler.JavaProfiler;
import com.datadoghq.profiler.TaskBlockBridge;
import datadog.environment.JavaVirtualMachine;
import datadog.libs.ddprof.DdprofLibraryLoader;
import datadog.trace.api.config.ProfilingConfig;
Expand Down Expand Up @@ -101,9 +103,12 @@ public static DatadogProfiler newInstance(ConfigProvider configProvider) {
return new DatadogProfiler(configProvider);
}

private final AtomicBoolean recordingFlag = new AtomicBoolean(false);
// JavaProfiler owns one process-wide native recording; all wrapper instances must observe the
// same lifecycle so early-created integrations can dispatch hooks while the controller records.
private static final AtomicBoolean RECORDING = new AtomicBoolean(false);
private final ConfigProvider configProvider;
private final JavaProfiler profiler;
private final TaskBlockBridge taskBlockBridge;
private final Set<ProfilingMode> profilingModes = EnumSet.noneOf(ProfilingMode.class);

private final ContextSetter contextSetter;
Expand Down Expand Up @@ -244,6 +249,7 @@ void reset() {
private DatadogProfiler(ConfigProvider configProvider) {
this.configProvider = configProvider;
this.profiler = DdprofLibraryLoader.javaProfiler().getComponent();
this.taskBlockBridge = new TaskBlockBridge(profiler);
this.detailedDebugLogging =
configProvider.getBoolean(
PROFILING_DETAILED_DEBUG_LOGGING, PROFILING_DETAILED_DEBUG_LOGGING_DEFAULT);
Expand Down Expand Up @@ -355,7 +361,7 @@ public RecordingData stop(OngoingRecording recording) {

/** A call-back from {@linkplain DatadogProfilerRecording#stop()} */
void stopProfiler() {
if (recordingFlag.compareAndSet(true, false)) {
if (RECORDING.compareAndSet(true, false)) {
profiler.stop();
if (isActive()) {
log.debug("Profiling is still active. Waiting to stop.");
Expand Down Expand Up @@ -385,7 +391,7 @@ String executeProfilerCmd(String cmd) throws IOException {
}

Path newRecording() throws IOException, IllegalStateException {
if (recordingFlag.compareAndSet(false, true)) {
if (RECORDING.compareAndSet(false, true)) {
Path recFile = Files.createTempFile(recordingsPath, "dd-profiler-", ".jfr");
String cmd = cmdStartProfiling(recFile);
try {
Expand All @@ -397,7 +403,7 @@ Path newRecording() throws IOException, IllegalStateException {
} else {
log.warn("Unable to start Datadog profiler recording: {}", e.getMessage());
}
recordingFlag.set(false);
RECORDING.set(false);
throw e;
}
return recFile;
Expand All @@ -421,7 +427,6 @@ String cmdStartProfiling(Path file) throws IllegalStateException {
}
StringBuilder cmd = new StringBuilder("start,jfr");
cmd.append(",file=").append(file.toAbsolutePath());
cmd.append(",loglevel=").append(getLogLevel(configProvider));
cmd.append(",jstackdepth=").append(getStackDepth(configProvider));
cmd.append(",cstack=").append(getCStack(configProvider));
cmd.append(",safemode=").append(getSafeMode(configProvider));
Expand Down Expand Up @@ -464,15 +469,13 @@ String cmdStartProfiling(Path file) throws IllegalStateException {
cmd.append('~');
}
cmd.append(getWallInterval(configProvider)).append('m');
// ddprof quirk: if filter parameter is omitted, it defaults to "0" (enabled),
// not empty string (disabled). When enabled without tracing, no threads are added
// to the filter, resulting in zero samples. We must explicitly pass filter= (empty)
// to disable filtering and sample all threads.
if (getWallContextFilter(configProvider)) {
cmd.append(",filter=0");
} else {
cmd.append(",filter=");
}
boolean contextScope = getWallContextFilter(configProvider);
// Emit both protocols during the compatibility window. Older ddprof versions ignore the
// unknown wallscope option and use filter; newer versions use wallscope while continuing to
// accept filter. This cannot be feature-probed because unknown options are warnings, not
// execution errors.
cmd.append(contextScope ? ",filter=0,wallscope=context" : ",filter=,wallscope=all");
cmd.append(",wallprecheck=true");
}
cmd.append(",loglevel=").append(getLogLevel(configProvider));
if (profilingModes.contains(ALLOCATION) || profilingModes.contains(MEMLEAK)) {
Expand Down Expand Up @@ -534,6 +537,34 @@ public void clearSpanContext() {
reapplyAppContext();
}

boolean hasParkTaskBlockSupport() {
return taskBlockBridge.hasParkSupport();
}

boolean parkEnter() {
if (!RECORDING.get()) {
return false;
}
taskBlockBridge.parkEnter();
return true;
}

void parkExit(long blocker, long unblockingSpanId) {
taskBlockBridge.parkExit(blocker, unblockingSpanId);
}

boolean hasSynchronousTaskBlockSupport() {
return taskBlockBridge.hasSynchronousTaskBlockSupport();
}

long beginTaskBlock(int state) {
return RECORDING.get() ? taskBlockBridge.beginTaskBlock(state) : 0L;
}

boolean endTaskBlock(long token, long blocker, long unblockingSpanId) {
return token != 0L && taskBlockBridge.endTaskBlock(token, blocker, unblockingSpanId);
}

public boolean setContextValue(int offset, String value) {
if (contextSetter != null && offset >= 0 && value != null) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
import com.datadog.profiling.controller.ProfilingSupport;
import datadog.environment.JavaVirtualMachine;
import datadog.trace.api.config.ProfilingConfig;
import datadog.trace.api.profiling.TaskBlockInstrumentationConfig;
import datadog.trace.bootstrap.config.provider.ConfigProvider;
import java.lang.management.ManagementFactory;
import java.util.Collections;
Expand Down Expand Up @@ -169,6 +170,10 @@ public static boolean getWallContextFilter(ConfigProvider configProvider) {
PROFILING_DATADOG_PROFILER_WALL_CONTEXT_FILTER_DEFAULT);
}

public static boolean getWallPrecheck(ConfigProvider configProvider) {
return TaskBlockInstrumentationConfig.isWallPrecheckEnabled(configProvider);
}

static boolean isJmethodIDSafe() {
return ProfilingSupport.isJmethodIDSafe();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Copyright 2026 Datadog, Inc.
package com.datadog.profiling.ddprof;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
Expand Down Expand Up @@ -72,4 +73,12 @@ void testSnapshot() throws Exception {
assertNotNull(inputStream);
assertTrue(inputStream.available() > 0);
}

@Test
void recordingLifecycleIsVisibleAcrossProfilerWrappers() {
DatadogProfiler integrationProfiler = DatadogProfiler.newInstance();

assertTrue(integrationProfiler.parkEnter());
integrationProfiler.parkExit(0L, 0L);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down Expand Up @@ -104,8 +104,7 @@ void testStartCmd(boolean cpu, boolean wall, boolean alloc, boolean memleak) thr
private static Stream<Arguments> profilingModes() {
return IntStream.range(0, 1 << 4)
.mapToObj(
x ->
Arguments.of((x & 0x1000) != 0, (x & 0x100) != 0, (x & 0x10) != 0, (x & 0x1) != 0));
x -> Arguments.of((x & 0x8) != 0, (x & 0x4) != 0, (x & 0x2) != 0, (x & 0x1) != 0));
}

@Test
Expand All @@ -127,7 +126,11 @@ void testStartCmdEnableJMethodIDOptim() throws Exception {

@ParameterizedTest
@MethodSource("wallContextFilterModes")
void testWallContextFilter(boolean tracingEnabled, boolean contextFilterEnabled)
void testWallScopeCompatibilityOptions(
boolean tracingEnabled,
boolean contextFilterEnabled,
String expectedFilter,
String expectedWallScope)
throws Exception {
// Skip test if profiler native library is not available (e.g., on macOS)
try {
Expand All @@ -153,33 +156,42 @@ void testWallContextFilter(boolean tracingEnabled, boolean contextFilterEnabled)
String cmd = profiler.cmdStartProfiling(targetFile);

assertTrue(cmd.contains("wall="), "Command should contain wall profiling: " + cmd);
assertTrue(cmd.contains(",filter=" + expectedFilter + ",wallscope="), cmd);
assertTrue(cmd.contains(",wallscope=" + expectedWallScope), cmd);
assertTrue(cmd.contains(",wallprecheck=true"), cmd);
}

if (tracingEnabled && contextFilterEnabled) {
assertTrue(
cmd.contains(",filter=0"),
"Command should contain ',filter=0' when tracing and context filter are enabled: " + cmd);
} else {
assertTrue(
cmd.contains(",filter="),
"Command should contain ',filter=' when tracing is disabled or context filter is disabled: "
+ cmd);
if (cmd.contains(",filter=0")) {
throw new AssertionError(
"Command should not contain ',filter=0' when tracing is disabled or context filter is disabled: "
+ cmd);
@Test
void testWallPrecheckIsAlwaysEnabled() throws Exception {
try {
Throwable reason = DdprofLibraryLoader.jvmAccess().getReasonNotLoaded();
if (reason != null) {
Assumptions.assumeTrue(false, "Profiler not available: " + reason.getMessage());
}
} catch (Throwable e) {
Assumptions.assumeTrue(false, "Profiler not available: " + e.getMessage());
}

Properties props = new Properties();
props.put(ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_ENABLED, "true");
props.put(ProfilingConfig.PROFILING_DATADOG_PROFILER_WALL_PRECHECK, "false");

DatadogProfiler profiler =
DatadogProfiler.newInstance(ConfigProvider.withPropertiesOverride(props));

Path targetFile = Paths.get("/tmp/target.jfr");
String cmd = profiler.cmdStartProfiling(targetFile);

assertTrue(cmd.contains("wall="), cmd);
assertTrue(cmd.contains(",wallprecheck=true"), cmd);
}

private static Stream<Arguments> wallContextFilterModes() {
return Stream.of(
Arguments.of(true, true), // tracing enabled, context filter enabled -> filter=0
Arguments.of(true, false), // tracing enabled, context filter disabled -> filter=
Arguments.of(
false, true), // tracing disabled, context filter enabled -> filter= (tracing disabled
// overrides)
Arguments.of(false, false) // tracing disabled, context filter disabled -> filter=
);
Arguments.of(true, true, "0", "context"),
Arguments.of(true, false, "", "all"),
Arguments.of(false, true, "", "all"),
Arguments.of(false, false, "", "all"));
}

@Test
Expand Down
Loading