diff --git a/dd-java-agent/agent-profiling/profiling-ddprof/build.gradle b/dd-java-agent/agent-profiling/profiling-ddprof/build.gradle index 2fa61e6d34b..fe99122c3fc 100644 --- a/dd-java-agent/agent-profiling/profiling-ddprof/build.gradle +++ b/dd-java-agent/agent-profiling/profiling-ddprof/build.gradle @@ -1,5 +1,6 @@ plugins { id 'com.gradleup.shadow' + id 'me.champeau.jmh' } ext { @@ -41,6 +42,19 @@ dependencies { } +jmh { + jmhVersion = libs.versions.jmh.get() + duplicateClassesStrategy = DuplicatesStrategy.EXCLUDE + failOnError = false + forceGC = true + if (project.hasProperty('jmhIncludes')) { + includes = [project.jmhIncludes] + } + if (project.hasProperty('jmhProf')) { + profilers = [project.jmhProf] + } +} + configurations.configureEach { resolutionStrategy.cacheChangingModulesFor 0, 'seconds' } diff --git a/dd-java-agent/agent-profiling/profiling-ddprof/src/jmh/java/com/datadog/profiling/ddprof/AppContextSnapshotBenchmark.java b/dd-java-agent/agent-profiling/profiling-ddprof/src/jmh/java/com/datadog/profiling/ddprof/AppContextSnapshotBenchmark.java new file mode 100644 index 00000000000..53dd2ed9932 --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-ddprof/src/jmh/java/com/datadog/profiling/ddprof/AppContextSnapshotBenchmark.java @@ -0,0 +1,87 @@ +package com.datadog.profiling.ddprof; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +/** + * Verifies that the ScopeStack pool approach (AppContextSnapshot.copyFrom into a pre-allocated + * slot) is allocation-free on the save/restore hot path. + * + *
The {@code deepStack} benchmark uses {@code stackDepth=16} to trigger a one-time resize + * (default pool size is 8) and confirm that subsequent iterations at that depth are still + * allocation-free once the pool has grown. + * + *
Run with: ./gradlew :dd-java-agent:agent-profiling:profiling-ddprof:jmh + * -PjmhIncludes=AppContextSnapshotBenchmark -PjmhProf=gc + * + *
Expected: gc.alloc.rate.norm ≈ 0 B/op for save, restore, and deepStack (steady state).
+ */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 3, time = 1)
+@Measurement(iterations = 5, time = 1)
+@Fork(1)
+@State(Scope.Thread)
+public class AppContextSnapshotBenchmark {
+
+ @Param({"2", "8"})
+ int attrCount;
+
+ /**
+ * Stack depth for the deepStack benchmark — 16 forces one resize past the default 8-slot pool.
+ */
+ @Param({"8", "16"})
+ int stackDepth;
+
+ private DatadogProfiler.AppContextSnapshot source;
+ private DatadogProfiler.AppContextSnapshot slot;
+ private DatadogProfiler.ScopeStack stack;
+
+ @Setup
+ public void setup() {
+ source = new DatadogProfiler.AppContextSnapshot(attrCount);
+ for (int i = 0; i < attrCount; i++) {
+ byte[] utf8 = ("value-" + i).getBytes(StandardCharsets.UTF_8);
+ source.record(i, i + 1, utf8, "value-" + i);
+ }
+ slot = new DatadogProfiler.AppContextSnapshot(attrCount);
+ stack = new DatadogProfiler.ScopeStack(attrCount);
+ }
+
+ /** ScopeStack save: copies current snapshot into a pre-allocated pool slot (zero alloc). */
+ @Benchmark
+ public void save() {
+ slot.copyFrom(source);
+ }
+
+ /** ScopeStack restore: copies pool slot back into the live snapshot (zero alloc). */
+ @Benchmark
+ public void restore() {
+ source.copyFrom(slot);
+ }
+
+ /**
+ * Borrow {@code stackDepth} slots then release them all. At {@code stackDepth=16} the pool
+ * resizes during warmup; steady-state measurement confirms zero allocation after growth.
+ */
+ @Benchmark
+ public void deepStack() {
+ for (int i = 0; i < stackDepth; i++) {
+ stack.borrow().copyFrom(source);
+ }
+ for (int i = 0; i < stackDepth; i++) {
+ stack.release();
+ }
+ }
+}
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..0005e8ea659 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
@@ -44,10 +44,12 @@
import datadog.trace.bootstrap.instrumentation.api.TaskWrapper;
import datadog.trace.util.TempLocationManager;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
@@ -108,6 +110,133 @@ public static DatadogProfiler newInstance(ConfigProvider configProvider) {
private final List Fast path (span activation): uses the pre-computed constant IDs and UTF-8 bytes from {@link
+ * #recordAppContextValue} in a single {@code setContextValuesByIdAndBytes} call — no String
+ * allocation, no hash lookup.
+ *
+ * Fallback (span deactivation via {@code setContext(0,0,0,0)}): {@code clearContextDirect}
+ * calls {@code detach()} but not {@code attach()}, leaving the thread's {@code validOffset=0}.
+ * {@code setContextValuesByIdAndBytes} returns {@code false} in that state, so we fall back to
+ * individual {@code setContextValue} calls which go through the proper detach/attach cycle.
+ */
+ public void reapplyAppContext() {
+ if (!hasAppContext) {
+ return;
+ }
+ AppContextSnapshot snapshot = appContextValues.get();
+ if (snapshot == null) {
+ return;
+ }
+ try {
+ if (!contextSetter.setContextValuesByIdAndBytes(snapshot.ids(), snapshot.utf8())) {
+ // validOffset=0 after clearContextDirect (setContext(0,0,0,0) path) — fall back to
+ // individual writes which go through the proper detach/attach cycle.
+ int remaining = snapshot.nonZeroCount();
+ for (int i = 0; i < isAppOffset.length && remaining > 0; i++) {
+ String s = snapshot.stringAt(i);
+ if (s != null) {
+ contextSetter.setContextValue(i, s);
+ remaining--;
+ }
+ }
+ }
+ } catch (Throwable e) {
+ log.debug("Failed to reapply context", e);
+ }
+ }
+
+ /** Clears the per-thread app-context snapshot. Used in tests and internally. */
+ void clearAppContextSnapshot() {
+ appContextValues.remove();
+ scopeStack.remove();
+ contextScratch.remove();
+ }
+
+ /**
+ * Immediately writes {@code snapshot} into the ddprof native slots for every app-context offset,
+ * clearing any offset not present in the snapshot. Reads the restored state from the per-thread
+ * {@code appContextValues} TL (already updated by {@link #restoreAppContext}) rather than from
+ * the saved slot directly, because {@link ScopeStack#release()} resets the slot before this
+ * method is called. Called by {@link DatadogProfilingScope#close} so that native state matches
+ * the restored Java-side snapshot right away, without waiting for the next span activation.
+ */
+ void syncNativeAppContext() {
+ if (!hasAppContext || contextSetter == null) {
+ return;
+ }
+ AppContextSnapshot snapshot = appContextValues.get();
+ try {
+ for (int i = 0; i < isAppOffset.length; i++) {
+ if (!isAppOffset[i]) {
+ continue;
+ }
+ String value = snapshot != null ? snapshot.stringAt(i) : null;
+ if (value != null) {
+ contextSetter.setContextValue(i, value);
+ } else {
+ contextSetter.clearContextValue(i);
+ }
+ }
+ } catch (Throwable e) {
+ log.debug("Failed to sync native app context on scope close", e);
+ }
+ }
+
+ /**
+ * Returns a copy of the current app-context snapshot for later restoration, or {@code null} if
+ * nothing is set. Called by {@link DatadogProfilingScope} on construction to implement
+ * save/restore across scope boundaries.
+ */
+ AppContextSnapshot saveAppContext() {
+ AppContextSnapshot current = appContextValues.get();
+ if (current == null || current.isEmpty()) {
+ return null;
+ }
+ ScopeStack stack = scopeStack.get();
+ if (stack == null) {
+ stack = new ScopeStack(isAppOffset.length);
+ scopeStack.set(stack);
+ }
+ AppContextSnapshot slot = stack.borrow();
+ slot.copyFrom(current);
+ return slot;
+ }
+
+ /**
+ * Restores a previously saved app-context snapshot. If {@code saved} is {@code null} the
+ * ThreadLocal is removed, otherwise the current snapshot is overwritten with {@code saved}.
+ * Called by {@link DatadogProfilingScope#close()}.
+ */
+ void restoreAppContext(AppContextSnapshot saved) {
+ if (saved == null) {
+ appContextValues.remove();
+ } else {
+ AppContextSnapshot current = appContextValues.get();
+ if (current == null) {
+ current = new AppContextSnapshot(isAppOffset.length);
+ appContextValues.set(current);
+ }
+ current.copyFrom(saved);
+ ScopeStack stack = scopeStack.get();
+ if (stack != null) {
+ stack.release();
+ }
+ }
+ }
+
+ private void recordAppContextValue(int offset, String value) {
+ if (!hasAppContext || offset < 0 || offset >= isAppOffset.length || !isAppOffset[offset]) {
+ return;
+ }
+ AppContextSnapshot snapshot = appContextValues.get();
+ if (value == null) {
+ if (snapshot == null) {
+ return;
+ }
+ snapshot.clear(offset);
+ if (snapshot.isEmpty()) {
+ appContextValues.remove();
+ }
+ return;
+ }
+ if (snapshot == null) {
+ snapshot = new AppContextSnapshot(isAppOffset.length);
+ appContextValues.set(snapshot);
+ }
+ if (!value.equals(snapshot.stringAt(offset))) {
+ byte[] utf8Bytes = value.getBytes(StandardCharsets.UTF_8);
+ // ContextSetter has no single-slot readback API; snapshotTags fills all slots at once.
+ // The scratch array is per-thread and reused across calls, so this is allocation-free.
+ int[] scratch = contextScratch.get();
+ contextSetter.snapshotTags(scratch);
+ snapshot.record(offset, scratch[offset], utf8Bytes, value);
+ }
+ }
+
private void debugLogging(long localRootSpanId) {
if (detailedDebugLogging && log.isDebugEnabled()) {
log.debug("localRootSpanId={}", localRootSpanId, new Throwable());
diff --git a/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilerContextSetter.java b/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilerContextSetter.java
index 58c9fbfd9cb..4bde713bcb6 100644
--- a/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilerContextSetter.java
+++ b/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilerContextSetter.java
@@ -11,7 +11,7 @@ public DatadogProfilerContextSetter(String attribute, DatadogProfiler profiler)
this.profiler = profiler;
}
- public void set(CharSequence value) {
+ public void set(String value) {
profiler.setContextValue(offset, value);
}
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..65462a46bce 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
@@ -41,13 +41,15 @@ public void close() {
public void activate(Object context) {
if (context instanceof ProfilerContext) {
ProfilerContext profilerContext = (ProfilerContext) context;
+ // setSpanContext() calls reapplyAppContext() internally, so no explicit call is needed.
DDPROF.setSpanContext(
profilerContext.getRootSpanId(),
profilerContext.getSpanId(),
profilerContext.getTraceIdHigh(),
profilerContext.getTraceIdLow());
- DDPROF.setContextValue(SPAN_NAME_INDEX, profilerContext.getOperationName());
- DDPROF.setContextValue(RESOURCE_NAME_INDEX, profilerContext.getResourceName());
+ DDPROF.setContextValue(SPAN_NAME_INDEX, profilerContext.getOperationName().toString());
+ DDPROF.setContextValue(
+ RESOURCE_NAME_INDEX, profilerContext.getResourceName().toString());
}
}
};
diff --git a/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilingScope.java b/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilingScope.java
index 848769cb091..27454cdc851 100644
--- a/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilingScope.java
+++ b/dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilingScope.java
@@ -5,9 +5,16 @@
public class DatadogProfilingScope implements ProfilingScope {
private final DatadogProfiler profiler;
+ // Snapshot of the app-managed context slots at scope creation (those registered via
+ // ProfilingContextAttribute). Restored on close() so ambient values set before this
+ // scope are not lost when the scope exits.
+ // Span context slots (managed by DatadogProfilingIntegration) are not snapshotted
+ // here; their lifecycle is controlled by the tracer's activate/deactivate path.
+ private final DatadogProfiler.AppContextSnapshot savedAppContext;
public DatadogProfilingScope(DatadogProfiler profiler) {
this.profiler = profiler;
+ this.savedAppContext = profiler.saveAppContext();
}
@Override
@@ -36,7 +43,15 @@ public void clearContextValue(ProfilingContextAttribute attribute) {
@Override
public void close() {
- // ddprof 1.41.0 removed the int-encoding setter; snapshot/restore of tag
- // context across nested scopes is no longer supported by the library.
+ // Restores the app-managed context slots that were active when this scope opened.
+ // Span context slots are NOT touched here; they are managed independently by the
+ // tracer via DatadogProfilingIntegration.activate()/close().
+ //
+ // Prior to ddprof 1.45.0 this method was a no-op: ddprof 1.41.0 removed the
+ // int-encoding setter that the previous snapshot/restore relied on, leaving no
+ // supported API for restoring context. The new setContextValuesByIdAndBytes API
+ // (1.45.0) makes targeted per-slot restore possible again — for app slots only.
+ profiler.restoreAppContext(savedAppContext);
+ profiler.syncNativeAppContext();
}
}
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..2be7d129e68 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
@@ -1,7 +1,9 @@
package com.datadog.profiling.ddprof;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -193,6 +195,244 @@ public void testContextRegistration() {
assertFalse(Arrays.equals(snapshot2, profiler.snapshot()));
}
}
+
+ // setSpanContext wipes all custom slots and automatically calls reapplyAppContext() to restore
+ // them.
+ int fooOffset = profiler.offsetOf("foo");
+ fooSetter.set("reapply-me");
+ assertNotEquals(0, profiler.snapshot()[fooOffset]);
+
+ profiler.setSpanContext(1L, 1L, 0L, 1L);
+ assertNotEquals(0, profiler.snapshot()[fooOffset]);
+
+ profiler.reapplyAppContext();
+ assertNotEquals(0, profiler.snapshot()[fooOffset]);
+
+ // Scenario A: clearContextValue must clear the snapshot so reapply has nothing to restore
+ profiler.clearContextValue("foo");
+ assertEquals(0, profiler.snapshot()[fooOffset], "clearContextValue must clear ddprof slot");
+ profiler.reapplyAppContext();
+ assertEquals(
+ 0,
+ profiler.snapshot()[fooOffset],
+ "after clearContextValue, reapplyAppContext must not restore foo");
+
+ // Scenario B: scope opened when snapshot is null — close() restores null (pre-scope state)
+ {
+ DatadogProfilingScope scope = new DatadogProfilingScope(profiler);
+ scope.setContextValue("foo", "scope-val");
+ assertNotEquals(0, profiler.snapshot()[fooOffset], "foo must be live inside scope");
+ scope.close();
+ }
+ profiler.setSpanContext(1L, 1L, 0L, 1L);
+ profiler.reapplyAppContext();
+ assertEquals(
+ 0,
+ profiler.snapshot()[fooOffset],
+ "scope.close() restores pre-scope snapshot (null here), so reapply has nothing to restore");
+
+ // Scenario B2: scope.close() immediately clears native slot — no span re-activation needed
+ {
+ DatadogProfilingScope scope = new DatadogProfilingScope(profiler);
+ scope.setContextValue("foo", "immediate-clear-val");
+ assertNotEquals(0, profiler.snapshot()[fooOffset], "foo must be live inside scope");
+ scope.close();
+ assertEquals(
+ 0,
+ profiler.snapshot()[fooOffset],
+ "scope.close() must immediately clear native slot without waiting for reapplyAppContext");
+ }
+
+ // Scenario B3: scope.close() immediately restores prior context to native slot
+ fooSetter.set("outer-val");
+ int outerEncoding = profiler.snapshot()[fooOffset];
+ assertNotEquals(0, outerEncoding, "outer foo must be live before inner scope");
+ {
+ DatadogProfilingScope scope = new DatadogProfilingScope(profiler);
+ scope.setContextValue("foo", "inner-val");
+ int innerEncoding = profiler.snapshot()[fooOffset];
+ assertNotEquals(outerEncoding, innerEncoding, "inner scope must change native slot");
+ scope.close();
+ assertEquals(
+ outerEncoding,
+ profiler.snapshot()[fooOffset],
+ "scope.close() must immediately restore prior native slot value");
+ }
+ profiler.clearContextValue("foo");
+
+ // Scenario C: reapplyAppContext is idempotent
+ fooSetter.set("idempotent-value");
+ profiler.setSpanContext(1L, 1L, 0L, 1L);
+ profiler.reapplyAppContext();
+ int afterFirst = profiler.snapshot()[fooOffset];
+ assertNotEquals(0, afterFirst, "reapplyAppContext must restore foo");
+ profiler.reapplyAppContext();
+ assertEquals(
+ afterFirst,
+ profiler.snapshot()[fooOffset],
+ "calling reapplyAppContext twice must produce the same result");
+
+ // Scenario D: re-activation after child activation restores app attr
+ int parentEncoding = profiler.snapshot()[fooOffset];
+ assertNotEquals(0, parentEncoding, "foo must be set before child activation");
+ profiler.setSpanContext(2L, 2L, 0L, 2L);
+ assertEquals(
+ parentEncoding,
+ profiler.snapshot()[fooOffset],
+ "setSpanContext auto-reapplies app context on child activation");
+ profiler.setSpanContext(1L, 1L, 0L, 1L);
+ profiler.reapplyAppContext();
+ assertEquals(
+ parentEncoding,
+ profiler.snapshot()[fooOffset],
+ "re-activation + reapply must restore parent app attr");
+
+ // Scenario E: app attr set in child survives into next activation
+ profiler.setSpanContext(2L, 2L, 0L, 2L);
+ fooSetter.set("child-val");
+ int childEncoding = profiler.snapshot()[fooOffset];
+ assertNotEquals(0, childEncoding, "foo must be set in child context");
+ profiler.setSpanContext(1L, 1L, 0L, 1L);
+ assertEquals(
+ childEncoding,
+ profiler.snapshot()[fooOffset],
+ "setSpanContext auto-reapplies app context (child-val) on re-activation");
+ profiler.reapplyAppContext();
+ assertEquals(
+ childEncoding,
+ profiler.snapshot()[fooOffset],
+ "ThreadLocal ambient value must survive into the next activation");
+
+ // Scenario F: app attributes are visible after the last span scope closes.
+ // clearSpanContext() wipes all custom slots and automatically calls reapplyAppContext(),
+ // restoring app attrs immediately.
+ fooSetter.set("pre-close-val");
+ assertNotEquals(0, profiler.snapshot()[fooOffset], "foo must be live before clearSpanContext");
+ profiler.clearSpanContext();
+ assertNotEquals(
+ 0, profiler.snapshot()[fooOffset], "clearSpanContext must auto-reapply app context");
+ profiler.reapplyAppContext();
+ assertNotEquals(
+ 0,
+ profiler.snapshot()[fooOffset],
+ "reapplyAppContext after clearSpanContext must restore foo");
+
+ // Scenario G: no app value set — clearSpanContext + reapplyAppContext leaves slot empty
+ profiler.clearContextValue("foo");
+ profiler.clearAppContextSnapshot();
+ profiler.clearSpanContext();
+ profiler.reapplyAppContext();
+ assertEquals(
+ 0,
+ profiler.snapshot()[fooOffset],
+ "reapplyAppContext with no snapshot must leave foo at 0");
+
+ // Scenario H: scope.close() restores ambient context set before scope was opened
+ fooSetter.set("ambient-val");
+ // Activate a span so reapplyAppContext can write the value (validOffset=1 after
+ // setSpanContext).
+ profiler.setSpanContext(1L, 1L, 0L, 1L);
+ profiler.reapplyAppContext();
+ int ambientEncoding = profiler.snapshot()[fooOffset];
+ assertNotEquals(0, ambientEncoding, "ambient foo must be live");
+ {
+ DatadogProfilingScope scope = new DatadogProfilingScope(profiler);
+ scope.setContextValue("foo", "scope-override");
+ assertNotEquals(
+ ambientEncoding, profiler.snapshot()[fooOffset], "scope must override ambient");
+ scope.close(); // must restore ambient snapshot, not nuke it
+ }
+ profiler.setSpanContext(1L, 1L, 0L, 1L);
+ profiler.reapplyAppContext();
+ assertEquals(
+ ambientEncoding,
+ profiler.snapshot()[fooOffset],
+ "scope.close() must restore ambient context, not clear it");
+
+ // Clean up after Scenario H so Acceptance tests start from a neutral state.
+ profiler.clearContextValue("foo");
+ profiler.clearSpanContext();
+
+ // Acceptance 1: reapply happens automatically inside setSpanContext — no manual call needed.
+ fooSetter.set("auto-reapply-val");
+ assertNotEquals(0, profiler.snapshot()[fooOffset], "foo must be live before setSpanContext");
+ profiler.setSpanContext(1L, 1L, 0L, 1L);
+ // Deliberately NO profiler.reapplyAppContext() here.
+ assertNotEquals(
+ 0,
+ profiler.snapshot()[fooOffset],
+ "Acceptance 1: setSpanContext must auto-restore app slot without explicit reapplyAppContext");
+ profiler.clearContextValue("foo");
+
+ // Acceptance 2: reapply happens automatically inside clearSpanContext — no manual call needed.
+ fooSetter.set("clear-reapply-val");
+ assertNotEquals(0, profiler.snapshot()[fooOffset], "foo must be live before clearSpanContext");
+ profiler.clearSpanContext();
+ // Deliberately NO profiler.reapplyAppContext() here.
+ assertNotEquals(
+ 0,
+ profiler.snapshot()[fooOffset],
+ "Acceptance 2: clearSpanContext must auto-restore app slot without explicit reapplyAppContext");
+ profiler.clearContextValue("foo");
+
+ // Acceptance 3: no app value set — clearSpanContext leaves slot empty.
+ profiler.clearContextValue("foo");
+ profiler.clearAppContextSnapshot();
+ profiler.clearSpanContext();
+ // Deliberately NO profiler.reapplyAppContext() here.
+ assertEquals(
+ 0,
+ profiler.snapshot()[fooOffset],
+ "Acceptance 3: clearSpanContext with no app value must leave foo at 0");
+
+ // Acceptance 4: nonZeroCount accuracy — cleared snapshot is considered empty so a new
+ // scope's save/restore does not leak a stale entry.
+ fooSetter.set("v1");
+ assertNotEquals(0, profiler.snapshot()[fooOffset], "foo must be live after set");
+ profiler.clearContextValue("foo");
+ assertEquals(0, profiler.snapshot()[fooOffset], "foo must be 0 after clearContextValue");
+ {
+ DatadogProfilingScope scope4 = new DatadogProfilingScope(profiler);
+ scope4.setContextValue("foo", "v2");
+ assertNotEquals(0, profiler.snapshot()[fooOffset], "foo must be live inside scope4");
+ scope4.close();
+ }
+ assertEquals(
+ 0,
+ profiler.snapshot()[fooOffset],
+ "Acceptance 4: scope.close() must restore pre-scope empty state; nonZeroCount must not drift");
+
+ // Acceptance 5: clearAppContextSnapshot() fully resets per-thread state so no stale value
+ // leaks through.
+ fooSetter.set("leak-check");
+ assertNotEquals(0, profiler.snapshot()[fooOffset], "foo must be live before reset");
+ profiler.clearContextValue("foo");
+ profiler.clearAppContextSnapshot();
+ {
+ DatadogProfilingScope scope5 = new DatadogProfilingScope(profiler);
+ scope5.setContextValue("foo", "after-reset");
+ assertNotEquals(0, profiler.snapshot()[fooOffset], "foo must be live inside scope5");
+ scope5.close();
+ }
+ profiler.setSpanContext(1L, 1L, 0L, 1L);
+ // Deliberately NO profiler.reapplyAppContext() here — fold-in suffices.
+ assertEquals(
+ 0,
+ profiler.snapshot()[fooOffset],
+ "Acceptance 5: after clearAppContextSnapshot, scope.close() restores empty state; no stale value leaks");
+
+ // Acceptance 6: restoreAppContext does not throw when scope stack is absent on the restoring
+ // thread. The guard is verified by the absence of an exception on the normal close path.
+ profiler.clearSpanContext();
+ profiler.clearContextValue("foo");
+ profiler.clearAppContextSnapshot();
+ assertDoesNotThrow(
+ () -> {
+ DatadogProfilingScope scope6 = new DatadogProfilingScope(profiler);
+ scope6.setContextValue("foo", "guard-val");
+ scope6.close();
+ },
+ "Acceptance 6: DatadogProfilingScope.close() must not throw even when scopeStack is absent");
}
private static ConfigProvider configProvider(
diff --git a/dd-java-agent/ddprof-lib/gradle.lockfile b/dd-java-agent/ddprof-lib/gradle.lockfile
index 145897cbafb..916c5f949dc 100644
--- a/dd-java-agent/ddprof-lib/gradle.lockfile
+++ b/dd-java-agent/ddprof-lib/gradle.lockfile
@@ -5,7 +5,7 @@
ch.qos.logback:logback-classic:1.2.13=testCompileClasspath,testRuntimeClasspath
ch.qos.logback:logback-core:1.2.13=testCompileClasspath,testRuntimeClasspath
com.datadoghq:dd-javac-plugin-client:0.2.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
-com.datadoghq:ddprof:1.44.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
+com.datadoghq:ddprof:1.45.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.javaparser:javaparser-core:3.25.6=codenarc
com.github.spotbugs:spotbugs-annotations:4.9.8=compileClasspath,spotbugs
com.github.spotbugs:spotbugs:4.9.8=spotbugs
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 6ed047edda2..dc40da9c734 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -9,7 +9,7 @@ jsr305 = "3.0.2"
spotbugs_annotations = "4.9.8"
# DataDog libs and forks
-ddprof = "1.44.0"
+ddprof = "1.45.0"
dogstatsd = "4.4.5"
okhttp = "3.12.15" # Datadog fork to support Java 7