diff --git a/dd-java-agent/agent-bootstrap/build.gradle b/dd-java-agent/agent-bootstrap/build.gradle
index 7a59e3d7aca..d626307ed64 100644
--- a/dd-java-agent/agent-bootstrap/build.gradle
+++ b/dd-java-agent/agent-bootstrap/build.gradle
@@ -5,12 +5,17 @@ plugins {
id 'idea'
}
+apply from: "$rootDir/gradle/tries.gradle"
+
// The shadowJar of this project will be injected into the JVM's bootstrap classloader
tasks.named("compileJava", JavaCompile) {
configureCompiler(it, 8, JavaVersion.VERSION_1_8, "Need access to sun.* packages")
+ dependsOn 'generateClassNameTries'
}
+tasks.named("sourcesJar") { dependsOn 'generateClassNameTries' }
+
// FIXME: Improve test coverage.
minimumBranchCoverage = 0.0
minimumInstructionCoverage = 0.0
diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/RunnableWrapper.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/RunnableWrapper.java
index 5d32250ae4c..54570a00559 100644
--- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/RunnableWrapper.java
+++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/RunnableWrapper.java
@@ -1,11 +1,10 @@
package datadog.trace.bootstrap.instrumentation.java.concurrent;
+import datadog.trace.bootstrap.FieldBackedContextAccessor;
import datadog.trace.bootstrap.instrumentation.java.concurrent.ExcludeFilter.ExcludeType;
/**
- * This is used to wrap lambda runnables so we can apply field-injection. RunnableWrapper can be
- * transformed to add the necessary context-store fields, while lambdas currently cannot until the
- * issue reported in https://github.com/raphw/byte-buddy/issues/558 is addressed.
+ * Wraps anonymous Runnable classes that were not field-injected.
*
*
We also make this class final to stop instrumentations from extending it in their injected
* helper classes, because if this class is loaded during helper injection then we can miss the
@@ -25,9 +24,11 @@ public void run() {
}
public static Runnable wrapIfNeeded(final Runnable task) {
- if (!(task instanceof RunnableWrapper) && !ExcludeFilter.exclude(ExcludeType.RUNNABLE, task)) {
- // We wrap only lambdas' anonymous classes and if given object has not already been wrapped.
- // Anonymous classes have '/' in class name which is not allowed in 'normal' classes.
+ // Field-injected tasks are already instrumented and must retain their identity.
+ if (!(task instanceof RunnableWrapper)
+ && !(task instanceof FieldBackedContextAccessor)
+ && !ExcludeFilter.exclude(ExcludeType.RUNNABLE, task)) {
+ // Hidden lambda class names contain '/'.
final String className = task.getClass().getName();
if (className.indexOf('/', className.lastIndexOf('.')) > 0) {
return new RunnableWrapper(task);
diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TPEHelper.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TPEHelper.java
index e95a6580849..9a81f06faeb 100644
--- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TPEHelper.java
+++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TPEHelper.java
@@ -8,6 +8,7 @@
import datadog.trace.api.InstrumenterConfig;
import datadog.trace.api.Platform;
import datadog.trace.bootstrap.ContextStore;
+import datadog.trace.bootstrap.FieldBackedContextAccessor;
import java.util.Set;
import java.util.concurrent.ThreadPoolExecutor;
@@ -22,7 +23,7 @@ public final class TPEHelper {
// If legacy is enabled, we will try to propagate via wrapping, if not we will try to propagate
// via storing the state in the existing field in the Runnable
private static final boolean useWrapping;
- // A ThreadPoolExecutor with one of these types will newer be propagated/wrapped
+ // A ThreadPoolExecutor with one of these types will never be propagated/wrapped
private static final Set excludedClasses;
// A ThreadLocal to store the Scope between beforeExecute and afterExecute if wrapping is not used
private static final ThreadLocal threadLocalScope;
@@ -30,10 +31,11 @@ public final class TPEHelper {
private static final ClassValue WRAP =
GenericClassValue.of(
input -> {
+ if (FieldBackedContextAccessor.class.isAssignableFrom(input)) {
+ return false;
+ }
String className = input.getName();
- // We should always wrap anonymous lambda classes since we can't inject fields into
- // them, and they can never be anything more than a _pure_ Runnable. They have '/' in
- // their class name which is not allowed in 'normal' classes.
+ // Wrap anonymous lambda classes that were not field-injected.
return className.indexOf('/', className.lastIndexOf('.')) > 0;
});
diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/lang/invoke/LambdaTransformer.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/lang/invoke/LambdaTransformer.java
new file mode 100644
index 00000000000..daf9525a1dd
--- /dev/null
+++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/lang/invoke/LambdaTransformer.java
@@ -0,0 +1,12 @@
+package datadog.trace.bootstrap.instrumentation.java.lang.invoke;
+
+/** Transforms a generated lambda class before it is defined. */
+public interface LambdaTransformer {
+ /**
+ * @param slashClassName internal (slash-separated) name of the generated lambda class
+ * @param targetClass the class declaring the lambda
+ * @param classBytes the freshly generated lambda class bytes
+ * @return the transformed bytes, or {@code null}/the original bytes if unchanged
+ */
+ byte[] transform(String slashClassName, Class> targetClass, byte[] classBytes);
+}
diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/lang/invoke/LambdaTransformerHelper.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/lang/invoke/LambdaTransformerHelper.java
new file mode 100644
index 00000000000..5054e657589
--- /dev/null
+++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/lang/invoke/LambdaTransformerHelper.java
@@ -0,0 +1,64 @@
+package datadog.trace.bootstrap.instrumentation.java.lang.invoke;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Transforms eligible lambda bytes before definition, falling back to the original on failure. */
+public final class LambdaTransformerHelper {
+ private static final Logger log = LoggerFactory.getLogger(LambdaTransformerHelper.class);
+
+ // Agent transformation may itself create lambdas.
+ private static final ThreadLocal TRANSFORMING = new ThreadLocal<>();
+
+ private LambdaTransformerHelper() {}
+
+ /**
+ * @param classBytes the generated lambda class bytes
+ * @param lambdaClassName internal (slash-separated) name of the generated lambda class
+ * @param targetClass the class declaring the lambda
+ * @param interfaceClass the functional interface implemented by the lambda
+ * @return possibly transformed bytes; the original bytes on any failure
+ */
+ public static byte[] transform(
+ byte[] classBytes, String lambdaClassName, Class> targetClass, Class> interfaceClass) {
+ try {
+ // Only exact allowlisted interfaces enter the transformer.
+ if (interfaceClass == null || LambdaInterfaceNameTrie.apply(interfaceClass.getName()) != 1) {
+ return classBytes;
+ }
+ LambdaTransformer transformer = LambdaTransformerHolder.get();
+ if (transformer == null) {
+ log.debug("Lambda {} skipped: no transformer registered", lambdaClassName);
+ return classBytes;
+ }
+ if (targetClass == null) {
+ log.debug("Lambda {} skipped: no target class", lambdaClassName);
+ return classBytes;
+ }
+ // Skip lambdas declared by the agent itself to avoid self-instrumentation and recursion.
+ String targetName = targetClass.getName();
+ if (targetName.startsWith("datadog.") || targetName.startsWith("net.bytebuddy.")) {
+ log.debug("Lambda {} skipped: declared by the agent", lambdaClassName);
+ return classBytes;
+ }
+ if (Boolean.TRUE.equals(TRANSFORMING.get())) {
+ log.debug("Lambda {} skipped: re-entrant transform", lambdaClassName);
+ return classBytes;
+ }
+ TRANSFORMING.set(Boolean.TRUE);
+ try {
+ byte[] result = transformer.transform(lambdaClassName, targetClass, classBytes);
+ if (result == null) {
+ log.debug("Lambda {} not transformed", lambdaClassName);
+ return classBytes;
+ }
+ return result;
+ } finally {
+ TRANSFORMING.set(Boolean.FALSE);
+ }
+ } catch (Throwable e) {
+ log.debug("Lambda {} skipped: {}", lambdaClassName, e.toString());
+ return classBytes;
+ }
+ }
+}
diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/lang/invoke/LambdaTransformerHolder.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/lang/invoke/LambdaTransformerHolder.java
new file mode 100644
index 00000000000..9c596a659e4
--- /dev/null
+++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/lang/invoke/LambdaTransformerHolder.java
@@ -0,0 +1,19 @@
+package datadog.trace.bootstrap.instrumentation.java.lang.invoke;
+
+/**
+ * Holds the {@link LambdaTransformer} registered by the agent installer. Lives on the bootstrap
+ * class path so it is reachable from instrumented {@code java.lang.invoke} code.
+ */
+public final class LambdaTransformerHolder {
+ private static volatile LambdaTransformer transformer;
+
+ private LambdaTransformerHolder() {}
+
+ public static void set(LambdaTransformer transformer) {
+ LambdaTransformerHolder.transformer = transformer;
+ }
+
+ public static LambdaTransformer get() {
+ return transformer;
+ }
+}
diff --git a/dd-java-agent/agent-bootstrap/src/main/resources/datadog/trace/bootstrap/instrumentation/java/lang/invoke/lambda_interface_name.trie b/dd-java-agent/agent-bootstrap/src/main/resources/datadog/trace/bootstrap/instrumentation/java/lang/invoke/lambda_interface_name.trie
new file mode 100644
index 00000000000..93487386424
--- /dev/null
+++ b/dd-java-agent/agent-bootstrap/src/main/resources/datadog/trace/bootstrap/instrumentation/java/lang/invoke/lambda_interface_name.trie
@@ -0,0 +1,7 @@
+# Generates 'LambdaInterfaceNameTrie.java'
+
+# Exact functional interfaces whose generated lambda classes should be sent through the agent's
+# matching and transformation pipeline. Keep this list narrow: the lookup runs for every lambda
+# linkage in the application.
+
+1 java.lang.Runnable
diff --git a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java
index 3a8c7065362..501f97866c6 100644
--- a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java
+++ b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java
@@ -5,6 +5,7 @@
import static datadog.trace.agent.tooling.bytebuddy.matcher.GlobalIgnoresMatcher.globalIgnoresMatcher;
import static net.bytebuddy.matcher.ElementMatchers.isDefaultFinalizer;
+import datadog.environment.JavaVirtualMachine;
import datadog.environment.SystemProperties;
import datadog.trace.agent.tooling.bytebuddy.SharedTypePools;
import datadog.trace.agent.tooling.bytebuddy.iast.TaintableRedefinitionStrategyListener;
@@ -19,6 +20,9 @@
import datadog.trace.api.telemetry.IntegrationsCollector;
import datadog.trace.bootstrap.FieldBackedContextAccessor;
import datadog.trace.bootstrap.instrumentation.java.concurrent.ExcludeFilter;
+import datadog.trace.bootstrap.instrumentation.java.lang.invoke.LambdaTransformer;
+import datadog.trace.bootstrap.instrumentation.java.lang.invoke.LambdaTransformerHelper;
+import datadog.trace.bootstrap.instrumentation.java.lang.invoke.LambdaTransformerHolder;
import datadog.trace.bootstrap.instrumentation.java.module.JpmsHelper;
import datadog.trace.util.AgentTaskScheduler;
import de.thetaphi.forbiddenapis.SuppressForbidden;
@@ -35,6 +39,7 @@
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.function.BooleanSupplier;
+import java.util.function.Function;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.type.TypeDescription;
@@ -147,7 +152,7 @@ public static ClassFileTransformer installBytebuddyAgent(
agentBuilder =
agentBuilder
.disableClassFormatChanges()
- .assureReadEdgeTo(inst, FieldBackedContextAccessor.class)
+ .assureReadEdgeTo(inst, FieldBackedContextAccessor.class, LambdaTransformerHelper.class)
.with(AgentStrategies.transformerDecorator())
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.with(AgentStrategies.rediscoveryStrategy())
@@ -253,12 +258,65 @@ public void applied(Iterable instrumentationNames) {
InstrumenterState.resetDefaultState();
try {
- return transformerBuilder.installOn(inst);
+ ClassFileTransformer classFileTransformer = transformerBuilder.installOn(inst);
+ registerLambdaTransformer(classFileTransformer);
+ return classFileTransformer;
} finally {
SharedTypePools.endInstall();
}
}
+ /** Registers the installed class-file transformer for generated lambdas. */
+ private static void registerLambdaTransformer(final ClassFileTransformer classFileTransformer) {
+ LambdaTransformer lambdaTransformer = newLambdaTransformer(classFileTransformer);
+ if (null != lambdaTransformer) {
+ LambdaTransformerHolder.set(lambdaTransformer);
+ }
+ }
+
+ /**
+ * Java 9+ requires the module-aware transformer for injected read edges. Failure must disable
+ * lambda transformation rather than fall back to the module-less overload.
+ */
+ @SuppressWarnings("unchecked")
+ private static LambdaTransformer newLambdaTransformer(
+ final ClassFileTransformer classFileTransformer) {
+ if (JavaVirtualMachine.isJavaVersionAtLeast(9)) {
+ try {
+ Function factory =
+ (Function)
+ Instrumenter.class
+ .getClassLoader()
+ .loadClass("datadog.trace.agent.tooling.bytebuddy.DDJava9LambdaTransformer")
+ .getField("FACTORY")
+ .get(null);
+ return factory.apply(classFileTransformer);
+ } catch (Throwable e) {
+ log.debug("Problem loading Java 9 lambda transformer, disabling lambda field-injection", e);
+ return null;
+ }
+ }
+ // Avoid invoking the instrumented metafactory while installing its transformer.
+ return new LambdaTransformer() {
+ @Override
+ public byte[] transform(String slashClassName, Class> targetClass, byte[] classBytes) {
+ TypePoolFacade.beginLambdaTransform();
+ try {
+ return classFileTransformer.transform(
+ targetClass.getClassLoader(),
+ slashClassName,
+ null,
+ targetClass.getProtectionDomain(),
+ classBytes);
+ } catch (Throwable ignored) {
+ return null;
+ } finally {
+ TypePoolFacade.endLambdaTransform();
+ }
+ }
+ };
+ }
+
/** Returns an iterable that combines the original sequence with any discovered extensions. */
private static Iterable withExtensions(Iterable initial) {
String extensionsPath = InstrumenterConfig.get().getTraceExtensionsPath();
diff --git a/dd-java-agent/agent-installer/src/main/java11/datadog/trace/agent/tooling/bytebuddy/DDJava9LambdaTransformer.java b/dd-java-agent/agent-installer/src/main/java11/datadog/trace/agent/tooling/bytebuddy/DDJava9LambdaTransformer.java
new file mode 100644
index 00000000000..1ffa6640693
--- /dev/null
+++ b/dd-java-agent/agent-installer/src/main/java11/datadog/trace/agent/tooling/bytebuddy/DDJava9LambdaTransformer.java
@@ -0,0 +1,43 @@
+package datadog.trace.agent.tooling.bytebuddy;
+
+import datadog.trace.agent.tooling.bytebuddy.outline.TypePoolFacade;
+import datadog.trace.bootstrap.instrumentation.java.lang.invoke.LambdaTransformer;
+import java.lang.instrument.ClassFileTransformer;
+import java.util.function.Function;
+
+/** Routes generated lambdas through the module-aware Java 9+ transformer overload. */
+public final class DDJava9LambdaTransformer implements LambdaTransformer {
+
+ /** Read reflectively by the agent installer, which cannot name {@link Module} itself. */
+ public static final Function FACTORY =
+ new Function() {
+ @Override
+ public LambdaTransformer apply(ClassFileTransformer classFileTransformer) {
+ return new DDJava9LambdaTransformer(classFileTransformer);
+ }
+ };
+
+ private final ClassFileTransformer classFileTransformer;
+
+ public DDJava9LambdaTransformer(ClassFileTransformer classFileTransformer) {
+ this.classFileTransformer = classFileTransformer;
+ }
+
+ @Override
+ public byte[] transform(String slashClassName, Class> targetClass, byte[] classBytes) {
+ TypePoolFacade.beginLambdaTransform();
+ try {
+ return classFileTransformer.transform(
+ targetClass.getModule(),
+ targetClass.getClassLoader(),
+ slashClassName,
+ null,
+ targetClass.getProtectionDomain(),
+ classBytes);
+ } catch (Throwable ignored) {
+ return null;
+ } finally {
+ TypePoolFacade.endLambdaTransform();
+ }
+ }
+}
diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/Memoizer.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/Memoizer.java
index 6c8d01152ca..b29989fc259 100644
--- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/Memoizer.java
+++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/Memoizer.java
@@ -155,6 +155,8 @@ static final class MemoizingMatcher
@Override
protected boolean doMatch(TypeDescription target) {
String targetName = target.getName();
+ // Same-owner hidden lambdas share a symbolic name. Bypass these caches before supporting
+ // lambda interfaces with different matcher results.
if (noMatchFilter.contains(targetName)
|| "java.lang.Object".equals(targetName)
|| target.isPrimitive()) {
diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/outline/TypeFactory.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/outline/TypeFactory.java
index 51650b3d2b2..6f044d89c27 100644
--- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/outline/TypeFactory.java
+++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/outline/TypeFactory.java
@@ -100,6 +100,8 @@ final class TypeFactory {
boolean createOutlines = OUTLINING_ENABLED;
+ boolean transformingLambda;
+
ClassLoader originalClassLoader;
ClassLoader currentClassLoader;
@@ -159,6 +161,14 @@ void beginTransform(String name, byte[] bytecode) {
}
}
+ void beginLambdaTransform() {
+ transformingLambda = true;
+ }
+
+ void endLambdaTransform() {
+ transformingLambda = false;
+ }
+
/** Once matching is complete we need full descriptions for the actual transformation. */
void enableFullDescriptions() {
createOutlines = false;
@@ -258,8 +268,9 @@ private TypeDescription lookupType(
boolean isOutline = typeParser == outlineTypeParser;
long fromTick = InstrumenterMetrics.tick();
- // existing type description from same classloader?
- SharedTypeInfo sharedType = types.find(name);
+ // Same-owner lambdas share a symbolic name, so build their target from the supplied bytes.
+ SharedTypeInfo sharedType =
+ transformingLambda && name.equals(targetName) ? null : types.find(name);
if (null != sharedType
&& (name.startsWith("java.") || sharedType.sameClassLoader(classLoaderId))) {
InstrumenterMetrics.reuseTypeDescription(fromTick, isOutline);
diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/outline/TypePoolFacade.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/outline/TypePoolFacade.java
index b5b2e3b39a8..f4c08b8cc1c 100644
--- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/outline/TypePoolFacade.java
+++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/outline/TypePoolFacade.java
@@ -49,6 +49,14 @@ public static void beginTransform(String name, byte[] bytecode) {
typeFactory.get().beginTransform(name, bytecode);
}
+ public static void beginLambdaTransform() {
+ typeFactory.get().beginLambdaTransform();
+ }
+
+ public static void endLambdaTransform() {
+ typeFactory.get().endLambdaTransform();
+ }
+
/** Switch to full descriptions, needed for the actual class transformation. */
public static void enableFullDescriptions() {
typeFactory.get().enableFullDescriptions();
diff --git a/dd-java-agent/agent-tooling/src/main/resources/datadog/trace/agent/tooling/bytebuddy/matcher/ignored_class_name.trie b/dd-java-agent/agent-tooling/src/main/resources/datadog/trace/agent/tooling/bytebuddy/matcher/ignored_class_name.trie
index 36cedaf6081..dc90bdf2c60 100644
--- a/dd-java-agent/agent-tooling/src/main/resources/datadog/trace/agent/tooling/bytebuddy/matcher/ignored_class_name.trie
+++ b/dd-java-agent/agent-tooling/src/main/resources/datadog/trace/agent/tooling/bytebuddy/matcher/ignored_class_name.trie
@@ -57,6 +57,8 @@
0 java.lang.Runtime
# allow context tracking for VirtualThread
0 java.lang.VirtualThread
+# allow instrumenting the lambda metafactory to field-inject generated lambda classes
+0 java.lang.invoke.InnerClassLambdaMetafactory
0 java.net.http.*
0 java.net.HttpURLConnection
0 java.net.InetAddress
diff --git a/dd-java-agent/agent-tooling/src/test/java/datadog/trace/agent/tooling/bytebuddy/outline/TypeFactoryTest.java b/dd-java-agent/agent-tooling/src/test/java/datadog/trace/agent/tooling/bytebuddy/outline/TypeFactoryTest.java
new file mode 100644
index 00000000000..e00f938ce3d
--- /dev/null
+++ b/dd-java-agent/agent-tooling/src/test/java/datadog/trace/agent/tooling/bytebuddy/outline/TypeFactoryTest.java
@@ -0,0 +1,57 @@
+package datadog.trace.agent.tooling.bytebuddy.outline;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.concurrent.Callable;
+import net.bytebuddy.ByteBuddy;
+import net.bytebuddy.description.type.TypeDescription;
+import org.junit.jupiter.api.Test;
+
+class TypeFactoryTest {
+ @Test
+ void reusesCachedDescriptionForRegularTransformationTarget() {
+ String name = getClass().getName() + "$RegularTarget";
+
+ assertEquals(
+ Runnable.class.getName(), resolveInterface(name, bytes(name, Runnable.class), false));
+ assertEquals(
+ Runnable.class.getName(), resolveInterface(name, bytes(name, Callable.class), false));
+ }
+
+ @Test
+ void rebuildsLambdaTransformationTargetFromSuppliedBytes() {
+ String name = getClass().getName() + "$LambdaTarget";
+
+ assertEquals(
+ Runnable.class.getName(), resolveInterface(name, bytes(name, Runnable.class), false));
+ assertEquals(
+ Callable.class.getName(), resolveInterface(name, bytes(name, Callable.class), true));
+ }
+
+ private static String resolveInterface(String name, byte[] bytecode, boolean lambda) {
+ TypeFactory typeFactory = TypeFactory.typeFactory.get();
+ typeFactory.switchContext(TypeFactoryTest.class.getClassLoader());
+ if (lambda) {
+ typeFactory.beginLambdaTransform();
+ }
+ typeFactory.beginTransform(name, bytecode);
+ try {
+ TypeDescription type = TypeFactory.findType(name);
+ return type.getInterfaces().getOnly().asErasure().getName();
+ } finally {
+ typeFactory.endTransform();
+ if (lambda) {
+ typeFactory.endLambdaTransform();
+ }
+ }
+ }
+
+ private static byte[] bytes(String name, Class> implementedInterface) {
+ return new ByteBuddy()
+ .subclass(Object.class)
+ .name(name)
+ .implement(implementedInterface)
+ .make()
+ .getBytes();
+ }
+}
diff --git a/dd-java-agent/benchmark/build.gradle b/dd-java-agent/benchmark/build.gradle
index 178eefd2def..bc963b78d8e 100644
--- a/dd-java-agent/benchmark/build.gradle
+++ b/dd-java-agent/benchmark/build.gradle
@@ -38,8 +38,16 @@ jmh {
jmhVersion = libs.versions.jmh.get()
}
+// Copy the agent to a fixed, version-independent path so benchmarks that attach it can name it
+// with a compile-time constant (JMH's @Fork annotation cannot read a system property).
+def agentJarForBenchmarks = tasks.register('agentJarForBenchmarks', Copy) {
+ from project(':dd-java-agent').tasks.named('shadowJar')
+ into layout.buildDirectory.dir('agent')
+ rename { 'dd-java-agent.jar' }
+}
+
tasks.named('jmh') {
- dependsOn ':dd-java-agent:shadowJar'
+ dependsOn agentJarForBenchmarks
}
/*
diff --git a/dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java b/dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java
new file mode 100644
index 00000000000..c1a59f85c77
--- /dev/null
+++ b/dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java
@@ -0,0 +1,161 @@
+package lambdabench;
+
+import datadog.trace.api.Trace;
+import java.lang.invoke.CallSite;
+import java.lang.invoke.LambdaConversionException;
+import java.lang.invoke.LambdaMetafactory;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.Supplier;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+
+/**
+ * Compares lambda {@code Runnable} allocation, execution, and submission with no agent, wrapping,
+ * and field injection.
+ *
+ *
+ *
{@link NoAgent} — baseline, no agent.
+ *
{@link AgentLambdaOff} — agent on, lambda metafactory instrumentation OFF: the lambda is
+ * wrapped on every submit (allocates a {@code Wrapper}).
+ *
{@link AgentLambdaOn} — agent on, lambda metafactory instrumentation ON: the lambda is
+ * field-injected, so no wrapper is allocated and identity is preserved.
+ *
+ *
+ *
{@code runUntracedLambda} isolates the advice cost when no context was attached. With the GC
+ * profiler, {@code allocateCapturingRunnable} isolates the injected field's object size cost while
+ * {@code submitLambda} includes the wrapper allocation tradeoff. Run with:
+ *
+ *