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
@@ -0,0 +1,88 @@
// Copyright 2026 Datadog, Inc.
package datadog.trace.bootstrap.instrumentation.java.concurrent;

import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext;
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
import datadog.trace.bootstrap.instrumentation.api.ProfilerContext;
import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;

/** Helper for profiling {@code LockSupport.park*} intervals from bootstrap instrumentation. */
public final class LockSupportHelper {
/**
* Best-effort association between a parked thread and the most recent {@code unpark} caller's
* active span. Weak keys avoid retaining terminated threads; the synchronized wrapper protects
* calls made concurrently by parked and unparking threads.
*/
static final Map<Thread, Long> UNPARKING_SPAN = Collections.synchronizedMap(new WeakHashMap<>());

private LockSupportHelper() {}

/** State required to balance a park entry accepted by a profiling integration. */
public static final class ParkState {
private final ProfilingContextIntegration profiling;
private final long blockerHash;

ParkState(ProfilingContextIntegration profiling, long blockerHash) {
this.profiling = profiling;
this.blockerHash = blockerHash;
}
}

/** Captures a park entry through the currently installed profiling integration. */
public static ParkState captureState(Object blocker) {
return captureState(blocker, AgentTracer.get().getProfilingContext());
}

static ParkState captureState(Object blocker, ProfilingContextIntegration profiling) {
if (profiling == null) {
return null;
}
try {
if (!profiling.parkEnter()) {
return null;
}
return new ParkState(
profiling,
blocker == null ? 0L : Integer.toUnsignedLong(System.identityHashCode(blocker)));
} catch (Throwable ignored) {
return null;
}
}

/** Drains unpark attribution and balances an accepted park entry. */
public static void finish(ParkState state) {
Long unblockingSpanId = UNPARKING_SPAN.remove(Thread.currentThread());
finish(state, unblockingSpanId == null ? 0L : unblockingSpanId);
}

static void finish(ParkState state, long unblockingSpanId) {
if (state == null) {
return;
}
try {
state.profiling.parkExit(state.blockerHash, unblockingSpanId);
} catch (Throwable ignored) {
}
}

/**
* Records the latest unpark caller for {@code thread}. An untraced call explicitly clears an
* older traced caller so the association follows last-writer semantics.
*/
public static void recordUnpark(Thread thread) {
if (thread == null) {
return;
}
AgentSpan span = AgentTracer.activeSpan();
AgentSpanContext context = span == null ? null : span.spanContext();
if (context instanceof ProfilerContext) {
UNPARKING_SPAN.put(thread, ((ProfilerContext) context).getSpanId());
} else {
UNPARKING_SPAN.remove(thread);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Copyright 2026 Datadog, Inc.
package datadog.trace.bootstrap.instrumentation.java.concurrent;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext;
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
import datadog.trace.bootstrap.instrumentation.api.ProfilerContext;
import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class LockSupportHelperTest {
private interface ProfilerSpanContext extends AgentSpanContext, ProfilerContext {}

private AgentTracer.TracerAPI previousTracer;

@BeforeEach
void setUp() {
previousTracer = AgentTracer.get();
AgentTracer.forceRegister(tracerWithActiveSpan(null));
LockSupportHelper.UNPARKING_SPAN.clear();
}

@AfterEach
void tearDown() {
LockSupportHelper.UNPARKING_SPAN.clear();
AgentTracer.forceRegister(previousTracer);
}

@Test
void nullIntegrationDoesNotAcceptEntry() {
assertNull(LockSupportHelper.captureState(new Object(), null));
}

@Test
void rejectedEntryDoesNotRequireExit() {
ProfilingContextIntegration profiling = mock(ProfilingContextIntegration.class);
when(profiling.parkEnter()).thenReturn(false);

LockSupportHelper.ParkState state = LockSupportHelper.captureState(new Object(), profiling);
LockSupportHelper.finish(state, 17L);

assertNull(state);
verify(profiling).parkEnter();
verify(profiling, never()).parkExit(0L, 17L);
}

@Test
void acceptedEntryIsAlwaysPairedWithExit() {
ProfilingContextIntegration profiling = mock(ProfilingContextIntegration.class);
when(profiling.parkEnter()).thenReturn(true);
Object blocker = new Object();

LockSupportHelper.ParkState state = LockSupportHelper.captureState(blocker, profiling);
LockSupportHelper.finish(state, 23L);

assertNotNull(state);
verify(profiling).parkEnter();
verify(profiling).parkExit(Integer.toUnsignedLong(System.identityHashCode(blocker)), 23L);
}

@Test
void nullBlockerUsesZeroIdentity() {
ProfilingContextIntegration profiling = mock(ProfilingContextIntegration.class);
when(profiling.parkEnter()).thenReturn(true);

LockSupportHelper.finish(LockSupportHelper.captureState(null, profiling), 0L);

verify(profiling).parkExit(0L, 0L);
}

@Test
void entryAndExitFailuresDoNotEscapeInstrumentation() {
ProfilingContextIntegration entryFailure = mock(ProfilingContextIntegration.class);
doThrow(new IllegalStateException("enter")).when(entryFailure).parkEnter();
assertNull(LockSupportHelper.captureState(null, entryFailure));

ProfilingContextIntegration exitFailure = mock(ProfilingContextIntegration.class);
when(exitFailure.parkEnter()).thenReturn(true);
doThrow(new IllegalStateException("exit")).when(exitFailure).parkExit(0L, 0L);
LockSupportHelper.ParkState state = LockSupportHelper.captureState(null, exitFailure);

assertDoesNotThrow(() -> LockSupportHelper.finish(state, 0L));
}

@Test
void finishAlwaysDrainsUnparkAttribution() {
Thread current = Thread.currentThread();
LockSupportHelper.UNPARKING_SPAN.put(current, 31L);

LockSupportHelper.finish(null);

assertNull(LockSupportHelper.UNPARKING_SPAN.get(current));
}

@Test
void latestTracedUnparkWins() {
Thread target = new Thread(() -> {});
installActiveSpan(41L);
LockSupportHelper.recordUnpark(target);
installActiveSpan(42L);

LockSupportHelper.recordUnpark(target);

assertEquals(42L, LockSupportHelper.UNPARKING_SPAN.get(target));
}

@Test
void laterUntracedUnparkClearsOlderTracedCaller() {
Thread target = new Thread(() -> {});
installActiveSpan(51L);
LockSupportHelper.recordUnpark(target);
AgentTracer.forceRegister(tracerWithActiveSpan(null));

LockSupportHelper.recordUnpark(target);

assertNull(LockSupportHelper.UNPARKING_SPAN.get(target));
}

@Test
void nonProfilerSpanContextClearsOlderTracedCaller() {
Thread target = new Thread(() -> {});
installActiveSpan(61L);
LockSupportHelper.recordUnpark(target);
AgentSpan span = mock(AgentSpan.class);
when(span.spanContext()).thenReturn(mock(AgentSpanContext.class));
AgentTracer.forceRegister(tracerWithActiveSpan(span));

LockSupportHelper.recordUnpark(target);

assertNull(LockSupportHelper.UNPARKING_SPAN.get(target));
}

@Test
void nullUnparkTargetIsNoOp() {
installActiveSpan(71L);

LockSupportHelper.recordUnpark(null);

assertEquals(0, LockSupportHelper.UNPARKING_SPAN.size());
}

private static void installActiveSpan(long spanId) {
AgentSpan span = mock(AgentSpan.class);
ProfilerSpanContext context = mock(ProfilerSpanContext.class);
when(span.spanContext()).thenReturn(context);
when(context.getSpanId()).thenReturn(spanId);
AgentTracer.forceRegister(tracerWithActiveSpan(span));
}

private static AgentTracer.TracerAPI tracerWithActiveSpan(AgentSpan span) {
AgentTracer.TracerAPI tracer = mock(AgentTracer.TracerAPI.class);
when(tracer.activeSpan()).thenReturn(span);
return tracer;
}
}
Loading
Loading