From ec68f8e38be6b557ac14c6c8094fd4e3fe104cae Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Mon, 22 Jun 2026 08:57:40 +0200 Subject: [PATCH 01/23] wip --- .../java/concurrent/RunnableWrapper.java | 8 +- .../java/concurrent/TPEHelper.java | 6 + .../java/lang/invoke/LambdaTransformer.java | 19 +++ .../lang/invoke/LambdaTransformerHelper.java | 51 ++++++ .../lang/invoke/LambdaTransformerHolder.java | 19 +++ .../trace/agent/tooling/AgentInstaller.java | 34 +++- .../bytebuddy/matcher/ignored_class_name.trie | 2 + .../java-lambda/java-lambda-1.8/build.gradle | 17 ++ .../LambdaMetafactoryInstrumentation.java | 153 ++++++++++++++++++ .../LambdaMetafactoryInstrumentationTest.java | 148 +++++++++++++++++ .../LambdaMetafactoryIntegrationTest.java | 71 ++++++++ metadata/supported-configurations.json | 8 + settings.gradle.kts | 1 + 13 files changed, 534 insertions(+), 3 deletions(-) create mode 100644 dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/lang/invoke/LambdaTransformer.java create mode 100644 dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/lang/invoke/LambdaTransformerHelper.java create mode 100644 dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/lang/invoke/LambdaTransformerHolder.java create mode 100644 dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/build.gradle create mode 100644 dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java create mode 100644 dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java create mode 100644 dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java 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..54e032c86fb 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,5 +1,6 @@ package datadog.trace.bootstrap.instrumentation.java.concurrent; +import datadog.trace.bootstrap.FieldBackedContextAccessor; import datadog.trace.bootstrap.instrumentation.java.concurrent.ExcludeFilter.ExcludeType; /** @@ -25,7 +26,12 @@ public void run() { } public static Runnable wrapIfNeeded(final Runnable task) { - if (!(task instanceof RunnableWrapper) && !ExcludeFilter.exclude(ExcludeType.RUNNABLE, task)) { + // If the lambda class was field-injected (via the metafactory instrumentation) it carries its + // own context-store field and is instrumented directly, so wrapping is unnecessary and would + // needlessly change the task's identity. + if (!(task instanceof RunnableWrapper) + && !(task instanceof FieldBackedContextAccessor) + && !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. final String className = task.getClass().getName(); 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..8c785837be0 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; @@ -30,6 +31,11 @@ public final class TPEHelper { private static final ClassValue WRAP = GenericClassValue.of( input -> { + // If the lambda class was field-injected (via the metafactory instrumentation) it can + // carry the State context-store field directly, so there is no need to wrap it. + 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 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..82cab203e27 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/lang/invoke/LambdaTransformer.java @@ -0,0 +1,19 @@ +package datadog.trace.bootstrap.instrumentation.java.lang.invoke; + +/** + * Transforms the bytecode of a lambda class generated by {@code + * java.lang.invoke.InnerClassLambdaMetafactory} before it is defined, so the lambda can receive the + * same field-injection and advice the agent applies to ordinary classes. + * + *

The implementation is registered by the agent installer via {@link + * LambdaTransformerHolder#set(LambdaTransformer)} once the main class-file transformer is ready. + */ +public interface LambdaTransformer { + /** + * @param slashClassName internal (slash-separated) name of the generated lambda class + * @param targetClass the class declaring the lambda (used to resolve the defining class loader) + * @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..7a616d39ef5 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/lang/invoke/LambdaTransformerHelper.java @@ -0,0 +1,51 @@ +package datadog.trace.bootstrap.instrumentation.java.lang.invoke; + +/** + * Entry point invoked from instrumented {@code java.lang.invoke.InnerClassLambdaMetafactory}. The + * metafactory instrumentation injects a call to {@link #transform(byte[], String, Class)} right + * after the lambda class bytes are generated and before the class is defined, so the generated + * lambda gets the agent's field-injection and advice applied like an ordinary class. + * + *

The JVM still defines the lambda as a hidden/anonymous class; only the bytes it defines are + * replaced. This never throws: any problem returns the original bytes (mirroring how {@code + * sun.instrument.TransformerManager} swallows {@code ClassFileTransformer} errors). + */ +public final class LambdaTransformerHelper { + // While transforming a lambda the agent / ByteBuddy may itself create lambdas; we must not + // recurse into transformation for those. + private static final ThreadLocal TRANSFORMING = new ThreadLocal<>(); + + private LambdaTransformerHelper() {} + + /** + * @param classBytes the generated lambda class bytes (on the stack from {@code toByteArray()}) + * @param lambdaClassName internal (slash-separated) name of the generated lambda class + * @param targetClass the class declaring the lambda + * @return possibly transformed bytes; the original bytes on any failure + */ + public static byte[] transform(byte[] classBytes, String lambdaClassName, Class targetClass) { + try { + LambdaTransformer transformer = LambdaTransformerHolder.get(); + if (transformer == null || targetClass == null) { + 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.")) { + return classBytes; + } + if (Boolean.TRUE.equals(TRANSFORMING.get())) { + return classBytes; + } + TRANSFORMING.set(Boolean.TRUE); + try { + byte[] result = transformer.transform(lambdaClassName, targetClass, classBytes); + return result != null ? result : classBytes; + } finally { + TRANSFORMING.set(Boolean.FALSE); + } + } catch (Throwable ignored) { + 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-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..7df42a595e5 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 @@ -19,6 +19,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; @@ -147,7 +150,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 +256,39 @@ public void applied(Iterable instrumentationNames) { InstrumenterState.resetDefaultState(); try { - return transformerBuilder.installOn(inst); + ClassFileTransformer classFileTransformer = transformerBuilder.installOn(inst); + registerLambdaTransformer(classFileTransformer); + return classFileTransformer; } finally { SharedTypePools.endInstall(); } } + /** + * Exposes the installed transformer to the {@code InnerClassLambdaMetafactory} instrumentation so + * generated lambda classes can be run through the same matching + field-injection pipeline before + * they are defined. Uses an anonymous class (not a lambda) so this bootstrapping code does not + * itself depend on the metafactory we just instrumented. + */ + private static void registerLambdaTransformer(final ClassFileTransformer classFileTransformer) { + LambdaTransformerHolder.set( + new LambdaTransformer() { + @Override + public byte[] transform(String slashClassName, Class targetClass, byte[] classBytes) { + try { + return classFileTransformer.transform( + targetClass.getClassLoader(), + slashClassName, + null, + targetClass.getProtectionDomain(), + classBytes); + } catch (Throwable ignored) { + return null; + } + } + }); + } + /** 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-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/instrumentation/java/java-lambda/java-lambda-1.8/build.gradle b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/build.gradle new file mode 100644 index 00000000000..6d3c3d445a6 --- /dev/null +++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/build.gradle @@ -0,0 +1,17 @@ +muzzle { + pass { + coreJdk() + } +} + +apply from: "$rootDir/gradle/java.gradle" + +tasks.named("compileJava") { + configureCompiler(it, 8) +} + +dependencies { + testImplementation project(':dd-java-agent:instrumentation:datadog:tracing:trace-annotation') + // executor context propagation across the field-injected lambda + testRuntimeOnly project(':dd-java-agent:instrumentation:java:java-concurrent:java-concurrent-1.8') +} diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java new file mode 100644 index 00000000000..48556a2af52 --- /dev/null +++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java @@ -0,0 +1,153 @@ +package datadog.trace.instrumentation.java.lang.invoke; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.agent.tooling.JavaModuleOpenProvider; +import datadog.trace.bootstrap.instrumentation.java.lang.invoke.LambdaTransformerHelper; +import java.util.Collection; +import java.util.Collections; +import net.bytebuddy.asm.AsmVisitorWrapper; +import net.bytebuddy.description.field.FieldDescription; +import net.bytebuddy.description.field.FieldList; +import net.bytebuddy.description.method.MethodList; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.Implementation; +import net.bytebuddy.jar.asm.ClassVisitor; +import net.bytebuddy.jar.asm.ClassWriter; +import net.bytebuddy.jar.asm.MethodVisitor; +import net.bytebuddy.jar.asm.Opcodes; +import net.bytebuddy.jar.asm.Type; +import net.bytebuddy.pool.TypePool; + +/** + * Instruments {@code java.lang.invoke.InnerClassLambdaMetafactory} so that generated lambda classes + * are routed through the agent's class-file transformer before they are defined. This lets lambda + * {@code Runnable}/{@code Callable} tasks receive field-backed context-store injection (and the + * matching advice) just like ordinary classes, instead of being wrapped. + * + *

The generated bytecode is finalized by a call to ASM's {@code ClassWriter.toByteArray()} (or + * the class-file API {@code build()} on JDK 24). We use an ASM visitor — rather than + * {@code @Advice} — because the transform call must be inserted in the middle of {@code + * spinInnerClass}/{@code generateInnerClass}, right after that call. This mirrors the approach used + * by OpenTelemetry's {@code internal-lambda} instrumentation. + */ +@AutoService(InstrumenterModule.class) +public final class LambdaMetafactoryInstrumentation extends InstrumenterModule.ContextTracking + implements Instrumenter.ForBootstrap, + Instrumenter.ForSingleType, + Instrumenter.HasTypeAdvice, + JavaModuleOpenProvider { + + private static final String METAFACTORY = "java.lang.invoke.InnerClassLambdaMetafactory"; + + public LambdaMetafactoryInstrumentation() { + super("lambda"); + } + + @Override + protected boolean defaultEnabled() { + // Opt-in for the first iteration: enable with DD_TRACE_LAMBDA_ENABLED=true. + return false; + } + + @Override + public String instrumentedType() { + return METAFACTORY; + } + + @Override + public Collection triggerClasses() { + return Collections.singleton(METAFACTORY); + } + + @Override + public void typeAdvice(TypeTransformer transformer) { + transformer.applyAdvice(new MetafactoryVisitorWrapper()); + } + + public static final class MetafactoryVisitorWrapper implements AsmVisitorWrapper { + @Override + public int mergeWriter(int flags) { + return flags | ClassWriter.COMPUTE_MAXS; + } + + @Override + public int mergeReader(int flags) { + return flags; + } + + @Override + public ClassVisitor wrap( + TypeDescription instrumentedType, + ClassVisitor classVisitor, + Implementation.Context implementationContext, + TypePool typePool, + FieldList fields, + MethodList methods, + int writerFlags, + int readerFlags) { + return new MetafactoryClassVisitor(classVisitor, instrumentedType.getInternalName()); + } + } + + private static final class MetafactoryClassVisitor extends ClassVisitor { + private final String slashClassName; + + MetafactoryClassVisitor(ClassVisitor cv, String slashClassName) { + super(Opcodes.ASM7, cv); + this.slashClassName = slashClassName; + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); + // Depending on the JDK version the lambda class is spun in spinInnerClass or + // generateInnerClass, both returning the generated Class. + if (("spinInnerClass".equals(name) || "generateInnerClass".equals(name)) + && "()Ljava/lang/Class;".equals(descriptor)) { + return new MetafactoryMethodVisitor(api, mv, slashClassName); + } + return mv; + } + } + + private static final class MetafactoryMethodVisitor extends MethodVisitor { + private final String slashClassName; + + MetafactoryMethodVisitor(int api, MethodVisitor mv, String slashClassName) { + super(api, mv); + this.slashClassName = slashClassName; + } + + @Override + public void visitMethodInsn( + int opcode, String owner, String name, String descriptor, boolean isInterface) { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + // After the generated bytes are produced (ClassWriter.toByteArray() or, on JDK 24, the + // class-file API build()), insert a call to our transformer. The byte[] is on the stack. + if ((opcode == Opcodes.INVOKEVIRTUAL + && "toByteArray".equals(name) + && "()[B".equals(descriptor)) + || (opcode == Opcodes.INVOKEINTERFACE + && "build".equals(name) + && descriptor.endsWith(")[B"))) { + // stack: ..., byte[] + super.visitVarInsn(Opcodes.ALOAD, 0); + super.visitFieldInsn( + Opcodes.GETFIELD, slashClassName, "lambdaClassName", "Ljava/lang/String;"); + super.visitVarInsn(Opcodes.ALOAD, 0); + // targetClass yields the ClassLoader where the lambda class will be defined. + super.visitFieldInsn(Opcodes.GETFIELD, slashClassName, "targetClass", "Ljava/lang/Class;"); + super.visitMethodInsn( + Opcodes.INVOKESTATIC, + Type.getInternalName(LambdaTransformerHelper.class), + "transform", + "([BLjava/lang/String;Ljava/lang/Class;)[B", + false); + // stack: ..., byte[] (transformed) + } + } + } +} diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java new file mode 100644 index 00000000000..a0a95414ec0 --- /dev/null +++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java @@ -0,0 +1,148 @@ +package datadog.trace.instrumentation.java.lang.invoke; + +import static net.bytebuddy.jar.asm.Opcodes.ACC_PRIVATE; +import static net.bytebuddy.jar.asm.Opcodes.ACC_PUBLIC; +import static net.bytebuddy.jar.asm.Opcodes.ACONST_NULL; +import static net.bytebuddy.jar.asm.Opcodes.ARETURN; +import static net.bytebuddy.jar.asm.Opcodes.ASM7; +import static net.bytebuddy.jar.asm.Opcodes.INVOKEINTERFACE; +import static net.bytebuddy.jar.asm.Opcodes.INVOKESTATIC; +import static net.bytebuddy.jar.asm.Opcodes.INVOKEVIRTUAL; +import static net.bytebuddy.jar.asm.Opcodes.POP; +import static net.bytebuddy.jar.asm.Opcodes.V1_8; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.instrumentation.java.lang.invoke.LambdaMetafactoryInstrumentation.MetafactoryVisitorWrapper; +import java.util.concurrent.atomic.AtomicBoolean; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.jar.asm.ClassReader; +import net.bytebuddy.jar.asm.ClassVisitor; +import net.bytebuddy.jar.asm.ClassWriter; +import net.bytebuddy.jar.asm.MethodVisitor; +import org.junit.jupiter.api.Test; + +class LambdaMetafactoryInstrumentationTest { + + private static final String HELPER = + "datadog/trace/bootstrap/instrumentation/java/lang/invoke/LambdaTransformerHelper"; + + /** + * Builds a class with a single method, transforms it, and reports whether transform() was hit. + */ + private static boolean injectsTransformCall( + String methodName, String methodDescriptor, ClassBody body) { + ClassWriter in = new ClassWriter(0); + in.visit(V1_8, ACC_PUBLIC, "Dummy", null, "java/lang/Object", null); + MethodVisitor mv = in.visitMethod(ACC_PRIVATE, methodName, methodDescriptor, null, null); + mv.visitCode(); + body.write(mv); + mv.visitMaxs(0, 0); + mv.visitEnd(); + in.visitEnd(); + + ClassWriter out = new ClassWriter(0); + ClassVisitor visitor = + new MetafactoryVisitorWrapper() + .wrap( + TypeDescription.ForLoadedType.of(Object.class), out, null, null, null, null, 0, 0); + new ClassReader(in.toByteArray()).accept(visitor, 0); + + AtomicBoolean found = new AtomicBoolean(false); + new ClassReader(out.toByteArray()) + .accept( + new ClassVisitor(ASM7) { + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String sig, String[] ex) { + return new MethodVisitor(ASM7) { + @Override + public void visitMethodInsn( + int opcode, String owner, String name, String desc, boolean itf) { + if (opcode == INVOKESTATIC + && HELPER.equals(owner) + && "transform".equals(name) + && "([BLjava/lang/String;Ljava/lang/Class;)[B".equals(desc)) { + found.set(true); + } + } + }; + } + }, + 0); + return found.get(); + } + + @Test + void injectsAfterToByteArrayInSpinInnerClass() { + assertTrue( + injectsTransformCall( + "spinInnerClass", + "()Ljava/lang/Class;", + mv -> { + mv.visitInsn(ACONST_NULL); + mv.visitMethodInsn( + INVOKEVIRTUAL, "net/bytebuddy/jar/asm/ClassWriter", "toByteArray", "()[B", false); + mv.visitInsn(POP); + mv.visitInsn(ACONST_NULL); + mv.visitInsn(ARETURN); + })); + } + + @Test + void injectsAfterToByteArrayInGenerateInnerClass() { + assertTrue( + injectsTransformCall( + "generateInnerClass", + "()Ljava/lang/Class;", + mv -> { + mv.visitInsn(ACONST_NULL); + mv.visitMethodInsn( + INVOKEVIRTUAL, "net/bytebuddy/jar/asm/ClassWriter", "toByteArray", "()[B", false); + mv.visitInsn(POP); + mv.visitInsn(ACONST_NULL); + mv.visitInsn(ARETURN); + })); + } + + @Test + void injectsAfterBuildOnJdk24() { + assertTrue( + injectsTransformCall( + "spinInnerClass", + "()Ljava/lang/Class;", + mv -> { + mv.visitInsn(ACONST_NULL); + mv.visitMethodInsn( + INVOKEINTERFACE, + "java/lang/classfile/ClassFile", + "build", + "(Ljava/lang/constant/ClassDesc;Ljava/util/function/Consumer;)[B", + true); + mv.visitInsn(POP); + mv.visitInsn(ACONST_NULL); + mv.visitInsn(ARETURN); + })); + } + + @Test + void doesNotInjectInUnrelatedMethod() { + assertFalse( + injectsTransformCall( + "someOtherMethod", + "()Ljava/lang/Class;", + mv -> { + mv.visitInsn(ACONST_NULL); + mv.visitMethodInsn( + INVOKEVIRTUAL, "net/bytebuddy/jar/asm/ClassWriter", "toByteArray", "()[B", false); + mv.visitInsn(POP); + mv.visitInsn(ACONST_NULL); + mv.visitInsn(ARETURN); + })); + } + + @FunctionalInterface + private interface ClassBody { + void write(MethodVisitor mv); + } +} diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java new file mode 100644 index 00000000000..191b23b644c --- /dev/null +++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java @@ -0,0 +1,71 @@ +package testdog.trace.instrumentation.lambda; + +import static datadog.trace.agent.test.assertions.SpanMatcher.span; +import static datadog.trace.agent.test.assertions.TraceMatcher.SORT_BY_START_TIME; +import static datadog.trace.agent.test.assertions.TraceMatcher.trace; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.api.Trace; +import datadog.trace.bootstrap.FieldBackedContextAccessor; +import datadog.trace.bootstrap.instrumentation.java.concurrent.RunnableWrapper; +import datadog.trace.junit.utils.config.WithConfig; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +/** + * End-to-end test that {@code InnerClassLambdaMetafactory} instrumentation makes lambda {@code + * Runnable}s field-injectable (so the agent no longer needs to wrap them) and propagates context + * across an executor. Must live outside {@code datadog.*} so the agent does not ignore the test + * class and its lambdas. + */ +@WithConfig(key = "trace.lambda.enabled", value = "true") +public class LambdaMetafactoryIntegrationTest extends AbstractInstrumentationTest { + + @Test + void lambdaRunnableIsFieldInjectedNotWrapped() { + // Created after the agent is installed in @BeforeAll, so its linkage goes through the + // instrumented metafactory. + Runnable lambda = () -> {}; + + assertTrue( + lambda instanceof FieldBackedContextAccessor, + "lambda Runnable should be field-injected via the metafactory instrumentation"); + // because it is field-injected, wrapping must be skipped and identity preserved + assertSame(lambda, RunnableWrapper.wrapIfNeeded(lambda)); + } + + @Test + void lambdaPropagatesContextAcrossExecutor() throws Exception { + ExecutorService pool = Executors.newSingleThreadExecutor(); + try { + CountDownLatch latch = new CountDownLatch(1); + submitUnderParent(pool, latch); + assertTrue(latch.await(10, TimeUnit.SECONDS), "child task did not run"); + + assertTraces( + trace( + SORT_BY_START_TIME, + span().root().operationName("parent"), + span().childOfPrevious().operationName("lambda-child"))); + } finally { + pool.shutdownNow(); + } + } + + @Trace(operationName = "parent") + void submitUnderParent(ExecutorService pool, CountDownLatch latch) { + pool.execute( + () -> { + child(); + latch.countDown(); + }); + } + + @Trace(operationName = "lambda-child") + void child() {} +} diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 6c3fe354b68..6a97071521b 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -7972,6 +7972,14 @@ "aliases": ["DD_LEGACY_E2E_DURATION_ENABLED"] } ], + "DD_TRACE_LAMBDA_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": ["DD_TRACE_INTEGRATION_LAMBDA_ENABLED", "DD_INTEGRATION_LAMBDA_ENABLED"] + } + ], "DD_TRACE_LETTUCE_4_ASYNC_ENABLED": [ { "version": "A", diff --git a/settings.gradle.kts b/settings.gradle.kts index 14e988af3fe..88316d62a6c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -399,6 +399,7 @@ include( ":dd-java-agent:instrumentation:java:java-concurrent:java-concurrent-21.0", ":dd-java-agent:instrumentation:java:java-concurrent:java-concurrent-25.0", ":dd-java-agent:instrumentation:java:java-io-1.8", + ":dd-java-agent:instrumentation:java:java-lambda:java-lambda-1.8", ":dd-java-agent:instrumentation:java:java-lang:java-lang-1.8", ":dd-java-agent:instrumentation:java:java-lang:java-lang-11.0", ":dd-java-agent:instrumentation:java:java-lang:java-lang-15.0", From 53180533dd1db843ab05c47ae11f1ea1bf7ad17c Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Mon, 22 Jun 2026 09:55:08 +0200 Subject: [PATCH 02/23] perf test --- .../lambdabench/LambdaExecutorBenchmark.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java 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..0395e9919ed --- /dev/null +++ b/dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java @@ -0,0 +1,73 @@ +package lambdabench; + +import datadog.trace.api.Trace; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +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; + +/** + * Measures the executor task-submission path for a lambda {@code Runnable}. Compare the three + * nested fork variants (run with {@code -prof gc}): + * + *

+ * + *

The headline metric is {@code ·gc.alloc.rate.norm} (bytes/op): the delta between {@code + * AgentLambdaOff} and {@code AgentLambdaOn} on {@link #submitLambda} is the wrapper allocation we + * removed. Lives outside the {@code datadog.*} package on purpose — {@code LambdaTransformerHelper} + * skips agent-owned lambdas, and a real application's lambdas are not under {@code datadog.*}. + */ +public class LambdaExecutorBenchmark { + + private static final String AGENT = + "-javaagent:/Users/andrea.marziali/go/src/github.com/DataDog/dd-trace-java/dd-java-agent/build/libs/dd-java-agent-1.64.0-SNAPSHOT.jar"; + + @State(Scope.Benchmark) + public static class ExecutorState { + ExecutorService pool; + + @Setup + public void setup() { + pool = Executors.newSingleThreadExecutor(); + } + + @TearDown + public void tearDown() { + pool.shutdownNow(); + } + } + + /** Submit a lambda Runnable to the executor under an active trace, and wait for it to run. */ + @Benchmark + public void submitLambda(ExecutorState state) throws InterruptedException { + runUnderTrace(state.pool); + } + + @Trace(operationName = "parent") + private void runUnderTrace(ExecutorService pool) throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + // lambda Runnable: this is the task whose propagation strategy (wrap vs field) we compare + pool.execute(latch::countDown); + latch.await(); + } + + @Fork + public static class NoAgent extends LambdaExecutorBenchmark {} + + @Fork(jvmArgsAppend = AGENT) + public static class AgentLambdaOff extends LambdaExecutorBenchmark {} + + @Fork(jvmArgsAppend = {AGENT, "-Ddd.trace.lambda.enabled=true"}) + public static class AgentLambdaOn extends LambdaExecutorBenchmark {} +} From 938c2a5b331fcfee4308e26d9740f692d34218dc Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 26 Aug 2026 10:12:35 +0200 Subject: [PATCH 03/23] Enforce type checks --- .../java-lambda/java-lambda-1.8/build.gradle | 6 +- .../LambdaMetafactoryInstrumentation.java | 71 +++++++++++++++++-- .../LambdaMetafactoryInstrumentationTest.java | 22 ++++++ .../LambdaMetafactoryIntegrationTest.java | 2 +- 4 files changed, 94 insertions(+), 7 deletions(-) diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/build.gradle b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/build.gradle index 6d3c3d445a6..bf836599a3e 100644 --- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/build.gradle +++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/build.gradle @@ -1,11 +1,13 @@ +plugins { + id 'dd-trace-java.module.instrumentation' +} + muzzle { pass { coreJdk() } } -apply from: "$rootDir/gradle/java.gradle" - tasks.named("compileJava") { configureCompiler(it, 8) } diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java index 48556a2af52..fe6cce4872a 100644 --- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java +++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java @@ -11,6 +11,7 @@ import net.bytebuddy.description.field.FieldDescription; import net.bytebuddy.description.field.FieldList; import net.bytebuddy.description.method.MethodList; +import net.bytebuddy.description.type.TypeDefinition; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.implementation.Implementation; import net.bytebuddy.jar.asm.ClassVisitor; @@ -18,7 +19,10 @@ import net.bytebuddy.jar.asm.MethodVisitor; import net.bytebuddy.jar.asm.Opcodes; import net.bytebuddy.jar.asm.Type; +import net.bytebuddy.matcher.ElementMatcher; import net.bytebuddy.pool.TypePool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Instruments {@code java.lang.invoke.InnerClassLambdaMetafactory} so that generated lambda classes @@ -37,10 +41,16 @@ public final class LambdaMetafactoryInstrumentation extends InstrumenterModule.C implements Instrumenter.ForBootstrap, Instrumenter.ForSingleType, Instrumenter.HasTypeAdvice, + Instrumenter.WithTypeStructure, JavaModuleOpenProvider { + private static final Logger log = LoggerFactory.getLogger(LambdaMetafactoryInstrumentation.class); + private static final String METAFACTORY = "java.lang.invoke.InnerClassLambdaMetafactory"; + private static final String LAMBDA_CLASS_NAME_FIELD = "lambdaClassName"; + private static final String TARGET_CLASS_FIELD = "targetClass"; + public LambdaMetafactoryInstrumentation() { super("lambda"); } @@ -61,6 +71,39 @@ public Collection triggerClasses() { return Collections.singleton(METAFACTORY); } + /** + * The injected bytecode reads {@code lambdaClassName} and {@code targetClass} directly. If a JDK + * renames or drops either, the resulting {@code NoSuchFieldError} would break every lambda + * linkage in the JVM, so the type is only transformed when both resolve. {@code targetClass} is + * declared by the superclass, hence the hierarchy walk. + */ + @Override + public ElementMatcher structureMatcher() { + return HasMetafactoryFields.INSTANCE; + } + + private static final class HasMetafactoryFields implements ElementMatcher { + static final HasMetafactoryFields INSTANCE = new HasMetafactoryFields(); + + @Override + public boolean matches(TypeDescription target) { + return declaresField(target, LAMBDA_CLASS_NAME_FIELD, String.class.getName()) + && declaresField(target, TARGET_CLASS_FIELD, Class.class.getName()); + } + + private static boolean declaresField(TypeDescription type, String name, String fieldType) { + for (TypeDefinition current = type; current != null; current = current.getSuperClass()) { + for (FieldDescription field : current.asErasure().getDeclaredFields()) { + if (name.equals(field.getName()) + && fieldType.equals(field.getType().asErasure().getName())) { + return true; + } + } + } + return false; + } + } + @Override public void typeAdvice(TypeTransformer transformer) { transformer.applyAdvice(new MetafactoryVisitorWrapper()); @@ -93,6 +136,7 @@ public ClassVisitor wrap( private static final class MetafactoryClassVisitor extends ClassVisitor { private final String slashClassName; + private boolean injected; MetafactoryClassVisitor(ClassVisitor cv, String slashClassName) { super(Opcodes.ASM7, cv); @@ -107,18 +151,35 @@ public MethodVisitor visitMethod( // generateInnerClass, both returning the generated Class. if (("spinInnerClass".equals(name) || "generateInnerClass".equals(name)) && "()Ljava/lang/Class;".equals(descriptor)) { - return new MetafactoryMethodVisitor(api, mv, slashClassName); + return new MetafactoryMethodVisitor(api, mv, slashClassName, this); } return mv; } + + @Override + public void visitEnd() { + super.visitEnd(); + if (!injected) { + // The fields resolved but the generated bytes are no longer produced where we expect, so + // no lambda will be field-injected and every task falls back to wrapping. + log.debug( + "No injection site found in {}; lambda field-injection is inactive.", slashClassName); + } + } } private static final class MetafactoryMethodVisitor extends MethodVisitor { private final String slashClassName; + private final MetafactoryClassVisitor declaringVisitor; - MetafactoryMethodVisitor(int api, MethodVisitor mv, String slashClassName) { + MetafactoryMethodVisitor( + int api, + MethodVisitor mv, + String slashClassName, + MetafactoryClassVisitor declaringVisitor) { super(api, mv); this.slashClassName = slashClassName; + this.declaringVisitor = declaringVisitor; } @Override @@ -136,10 +197,11 @@ public void visitMethodInsn( // stack: ..., byte[] super.visitVarInsn(Opcodes.ALOAD, 0); super.visitFieldInsn( - Opcodes.GETFIELD, slashClassName, "lambdaClassName", "Ljava/lang/String;"); + Opcodes.GETFIELD, slashClassName, LAMBDA_CLASS_NAME_FIELD, "Ljava/lang/String;"); super.visitVarInsn(Opcodes.ALOAD, 0); // targetClass yields the ClassLoader where the lambda class will be defined. - super.visitFieldInsn(Opcodes.GETFIELD, slashClassName, "targetClass", "Ljava/lang/Class;"); + super.visitFieldInsn( + Opcodes.GETFIELD, slashClassName, TARGET_CLASS_FIELD, "Ljava/lang/Class;"); super.visitMethodInsn( Opcodes.INVOKESTATIC, Type.getInternalName(LambdaTransformerHelper.class), @@ -147,6 +209,7 @@ public void visitMethodInsn( "([BLjava/lang/String;Ljava/lang/Class;)[B", false); // stack: ..., byte[] (transformed) + declaringVisitor.injected = true; } } } diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java index a0a95414ec0..2ed1cfa9632 100644 --- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java +++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java @@ -141,6 +141,28 @@ void doesNotInjectInUnrelatedMethod() { })); } + /** + * Guards the {@code GETFIELD}s emitted by the visitor: if a JDK renames or drops either field + * this fails here rather than as a {@code NoSuchFieldError} in every lambda linkage. {@code + * targetClass} is declared by the superclass, so this also covers the hierarchy walk. + */ + @Test + void structureMatcherAcceptsTheRealMetafactory() throws Exception { + TypeDescription metafactory = + TypeDescription.ForLoadedType.of( + Class.forName("java.lang.invoke.InnerClassLambdaMetafactory")); + + assertTrue(new LambdaMetafactoryInstrumentation().structureMatcher().matches(metafactory)); + } + + @Test + void structureMatcherRejectsTypeWithoutTheFields() { + assertFalse( + new LambdaMetafactoryInstrumentation() + .structureMatcher() + .matches(TypeDescription.ForLoadedType.of(Object.class))); + } + @FunctionalInterface private interface ClassBody { void write(MethodVisitor mv); diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java index 191b23b644c..d512303bf78 100644 --- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java +++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java @@ -10,7 +10,7 @@ import datadog.trace.api.Trace; import datadog.trace.bootstrap.FieldBackedContextAccessor; import datadog.trace.bootstrap.instrumentation.java.concurrent.RunnableWrapper; -import datadog.trace.junit.utils.config.WithConfig; +import datadog.trace.test.junit.utils.config.WithConfig; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; From 0f6dfee89d308f08742c97b5176860cde2110aed Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 26 Aug 2026 11:05:47 +0200 Subject: [PATCH 04/23] Correctly manage JPMS --- .../trace/agent/tooling/AgentInstaller.java | 70 ++++++++++++++----- .../bytebuddy/DDJava9LambdaTransformer.java | 50 +++++++++++++ .../LambdaMetafactoryInstrumentation.java | 8 ++- .../moduleapp/ModuleApplication.java | 3 + .../java11/testdog/moduleapp/LambdaTask.java | 30 ++++++++ .../Java9ModulesLambdaSmokeTest.groovy | 20 ++++++ .../smoketest/Java9ModulesSmokeTest.groovy | 5 ++ 7 files changed, 166 insertions(+), 20 deletions(-) create mode 100644 dd-java-agent/agent-installer/src/main/java11/datadog/trace/agent/tooling/bytebuddy/DDJava9LambdaTransformer.java create mode 100644 dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java create mode 100644 dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaSmokeTest.groovy 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 7df42a595e5..a61e31c0c2a 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; @@ -38,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; @@ -267,26 +269,58 @@ public void applied(Iterable instrumentationNames) { /** * Exposes the installed transformer to the {@code InnerClassLambdaMetafactory} instrumentation so * generated lambda classes can be run through the same matching + field-injection pipeline before - * they are defined. Uses an anonymous class (not a lambda) so this bootstrapping code does not - * itself depend on the metafactory we just instrumented. + * they are defined. Leaving the holder unset disables lambda field-injection, which is always + * safe: tasks then fall back to being wrapped. */ private static void registerLambdaTransformer(final ClassFileTransformer classFileTransformer) { - LambdaTransformerHolder.set( - new LambdaTransformer() { - @Override - public byte[] transform(String slashClassName, Class targetClass, byte[] classBytes) { - try { - return classFileTransformer.transform( - targetClass.getClassLoader(), - slashClassName, - null, - targetClass.getProtectionDomain(), - classBytes); - } catch (Throwable ignored) { - return null; - } - } - }); + LambdaTransformer lambdaTransformer = newLambdaTransformer(classFileTransformer); + if (null != lambdaTransformer) { + LambdaTransformerHolder.set(lambdaTransformer); + } + } + + /** + * On Java 9+ the transformer must be given the module declaring the lambda, otherwise ByteBuddy + * skips the read edge that field-injected classes need and lambdas in named modules become + * undefinable. If that transformer cannot be loaded we return {@code null} rather than falling + * back to the module-less one, which would be the very defect we are avoiding. + * + *

Uses an anonymous class (not a lambda) so this bootstrapping code does not itself depend on + * the metafactory we just instrumented. + */ + @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; + } + } + return new LambdaTransformer() { + @Override + public byte[] transform(String slashClassName, Class targetClass, byte[] classBytes) { + try { + return classFileTransformer.transform( + targetClass.getClassLoader(), + slashClassName, + null, + targetClass.getProtectionDomain(), + classBytes); + } catch (Throwable ignored) { + return null; + } + } + }; } /** Returns an iterable that combines the original sequence with any discovered extensions. */ 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..581abafac9a --- /dev/null +++ b/dd-java-agent/agent-installer/src/main/java11/datadog/trace/agent/tooling/bytebuddy/DDJava9LambdaTransformer.java @@ -0,0 +1,50 @@ +package datadog.trace.agent.tooling.bytebuddy; + +import datadog.trace.bootstrap.instrumentation.java.lang.invoke.LambdaTransformer; +import java.lang.instrument.ClassFileTransformer; +import java.util.function.Function; + +/** + * Routes generated lambda classes through the agent's transformer, passing the module of the class + * that declares the lambda. + * + *

The module-less {@code transform} overload reports {@code JavaModule.UNSUPPORTED} to + * ByteBuddy, which then skips the read edge that field-injected classes need to reach {@code + * FieldBackedContextAccessor}. A lambda declared in a named module would be transformed + * successfully and then fail to define, surfacing as an {@code InternalError} at the lambda's call + * site. + * + *

This class is only used on Java 9+; for Java 8 the module-less overload is complete. + */ +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) { + try { + return classFileTransformer.transform( + targetClass.getModule(), + targetClass.getClassLoader(), + slashClassName, + null, + targetClass.getProtectionDomain(), + classBytes); + } catch (Throwable ignored) { + return null; + } + } +} diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java index fe6cce4872a..5b45e0980c2 100644 --- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java +++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java @@ -82,8 +82,12 @@ public ElementMatcher structureMatcher() { return HasMetafactoryFields.INSTANCE; } - private static final class HasMetafactoryFields implements ElementMatcher { - static final HasMetafactoryFields INSTANCE = new HasMetafactoryFields(); + /** + * Public because the agent loads {@code *Instrumentation} classes in a child class-loader of the + * one that loads this nested class; package-private access across the two is denied. + */ + public static final class HasMetafactoryFields implements ElementMatcher { + public static final HasMetafactoryFields INSTANCE = new HasMetafactoryFields(); @Override public boolean matches(TypeDescription target) { diff --git a/dd-smoke-tests/java9-modules/src/main/java11/datadog/smoketest/moduleapp/ModuleApplication.java b/dd-smoke-tests/java9-modules/src/main/java11/datadog/smoketest/moduleapp/ModuleApplication.java index b7e484b2d7b..41808269b44 100644 --- a/dd-smoke-tests/java9-modules/src/main/java11/datadog/smoketest/moduleapp/ModuleApplication.java +++ b/dd-smoke-tests/java9-modules/src/main/java11/datadog/smoketest/moduleapp/ModuleApplication.java @@ -1,7 +1,10 @@ package datadog.smoketest.moduleapp; +import testdog.moduleapp.LambdaTask; + public class ModuleApplication { public static void main(final String[] args) throws InterruptedException { + LambdaTask.runOnExecutor(); Thread.sleep(600); } } diff --git a/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java b/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java new file mode 100644 index 00000000000..aca6b4e9a8c --- /dev/null +++ b/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java @@ -0,0 +1,30 @@ +package testdog.moduleapp; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * Links a lambda {@code Runnable} from inside the application's named module. + * + *

Lives outside {@code datadog.*} on purpose: the agent skips lambdas declared by classes with + * that prefix to avoid instrumenting itself, so a task in the application's own {@code + * datadog.smoketest} package would never exercise this path. + */ +public final class LambdaTask { + private LambdaTask() {} + + public static void runOnExecutor() throws InterruptedException { + final ExecutorService pool = Executors.newSingleThreadExecutor(); + try { + final CountDownLatch latch = new CountDownLatch(1); + pool.execute(latch::countDown); + if (!latch.await(10, TimeUnit.SECONDS)) { + throw new IllegalStateException("lambda task did not run"); + } + } finally { + pool.shutdownNow(); + } + } +} diff --git a/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaSmokeTest.groovy b/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaSmokeTest.groovy new file mode 100644 index 00000000000..16bfa17bfae --- /dev/null +++ b/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaSmokeTest.groovy @@ -0,0 +1,20 @@ +package datadog.smoketest + +import datadog.environment.JavaVirtualMachine +import datadog.environment.OperatingSystem +import spock.lang.IgnoreIf + +/** + * Runs the modular application with lambda field-injection enabled. The lambda's generated class + * belongs to the application's named module, which reaches the injected types only if the agent + * adds a read edge while transforming it. + */ +@IgnoreIf({ + OperatingSystem.isLinux() && OperatingSystem.architecture().isArm64() && JavaVirtualMachine.isJ9() +}) +class Java9ModulesLambdaSmokeTest extends Java9ModulesSmokeTest { + @Override + protected List extraJvmArgs() { + return ["-Ddd.trace.lambda.enabled=true"] + } +} diff --git a/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesSmokeTest.groovy b/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesSmokeTest.groovy index 57865e57bca..b4a193d5d65 100644 --- a/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesSmokeTest.groovy +++ b/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesSmokeTest.groovy @@ -14,6 +14,10 @@ class Java9ModulesSmokeTest extends AbstractSmokeTest { // Estimate for the amount of time instrumentation plus some extra private static final int TIMEOUT_SECS = 30 + protected List extraJvmArgs() { + return [] + } + @Override ProcessBuilder createProcessBuilder() { String imageDir = System.getProperty("datadog.smoketest.module.image") @@ -23,6 +27,7 @@ class Java9ModulesSmokeTest extends AbstractSmokeTest { List command = new ArrayList<>() command.add(imageDir + "/bin/java") command.addAll(defaultJavaProperties) + command.addAll(extraJvmArgs()) command.addAll((String[]) ["-m", "datadog.smoketest.moduleapp/datadog.smoketest.moduleapp.ModuleApplication"]) ProcessBuilder processBuilder = new ProcessBuilder(command) processBuilder.directory(new File(buildDirectory)) From e34f5fc08fa9aea26ac2724263a9ce09388d3851 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 26 Aug 2026 11:22:51 +0200 Subject: [PATCH 05/23] match the call owner --- .../LambdaMetafactoryInstrumentation.java | 25 +++++++----- .../LambdaMetafactoryInstrumentationTest.java | 40 ++++++++++++++++--- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java index 5b45e0980c2..408a58ae92b 100644 --- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java +++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java @@ -30,11 +30,11 @@ * {@code Runnable}/{@code Callable} tasks receive field-backed context-store injection (and the * matching advice) just like ordinary classes, instead of being wrapped. * - *

The generated bytecode is finalized by a call to ASM's {@code ClassWriter.toByteArray()} (or - * the class-file API {@code build()} on JDK 24). We use an ASM visitor — rather than - * {@code @Advice} — because the transform call must be inserted in the middle of {@code - * spinInnerClass}/{@code generateInnerClass}, right after that call. This mirrors the approach used - * by OpenTelemetry's {@code internal-lambda} instrumentation. + *

The generated bytes come from {@code ClassWriter.toByteArray()} on JDK 8-21 and {@code + * ClassFile.build(...)} on JDK 25-27, one call site per JDK; the switch happened between 21 and 25, + * and 22-24 were not checked. We use an ASM visitor — rather than {@code @Advice} — because the + * transform call must be inserted in the middle of {@code spinInnerClass}/{@code + * generateInnerClass}, right after that call. */ @AutoService(InstrumenterModule.class) public final class LambdaMetafactoryInstrumentation extends InstrumenterModule.ContextTracking @@ -151,8 +151,8 @@ private static final class MetafactoryClassVisitor extends ClassVisitor { public MethodVisitor visitMethod( int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); - // Depending on the JDK version the lambda class is spun in spinInnerClass or - // generateInnerClass, both returning the generated Class. + // Up to JDK 21 the bytes are produced in spinInnerClass; from 25 spinInnerClass delegates + // to generateInnerClass, which produces them. if (("spinInnerClass".equals(name) || "generateInnerClass".equals(name)) && "()Ljava/lang/Class;".equals(descriptor)) { return new MetafactoryMethodVisitor(api, mv, slashClassName, this); @@ -190,14 +190,17 @@ private static final class MetafactoryMethodVisitor extends MethodVisitor { public void visitMethodInsn( int opcode, String owner, String name, String descriptor, boolean isInterface) { super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); - // After the generated bytes are produced (ClassWriter.toByteArray() or, on JDK 24, the - // class-file API build()), insert a call to our transformer. The byte[] is on the stack. + // After the generated bytes are produced, insert a call to our transformer. The byte[] is + // on the stack. Owners are matched loosely so a repackaging only costs the feature, while + // still ruling out unrelated calls such as ByteArrayOutputStream.toByteArray(). if ((opcode == Opcodes.INVOKEVIRTUAL && "toByteArray".equals(name) - && "()[B".equals(descriptor)) + && "()[B".equals(descriptor) + && owner.endsWith("/ClassWriter")) || (opcode == Opcodes.INVOKEINTERFACE && "build".equals(name) - && descriptor.endsWith(")[B"))) { + && descriptor.endsWith(")[B") + && owner.endsWith("/ClassFile"))) { // stack: ..., byte[] super.visitVarInsn(Opcodes.ALOAD, 0); super.visitFieldInsn( diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java index 2ed1cfa9632..4b14256e8e8 100644 --- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java +++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java @@ -82,7 +82,11 @@ void injectsAfterToByteArrayInSpinInnerClass() { mv -> { mv.visitInsn(ACONST_NULL); mv.visitMethodInsn( - INVOKEVIRTUAL, "net/bytebuddy/jar/asm/ClassWriter", "toByteArray", "()[B", false); + INVOKEVIRTUAL, + "jdk/internal/org/objectweb/asm/ClassWriter", + "toByteArray", + "()[B", + false); mv.visitInsn(POP); mv.visitInsn(ACONST_NULL); mv.visitInsn(ARETURN); @@ -98,7 +102,11 @@ void injectsAfterToByteArrayInGenerateInnerClass() { mv -> { mv.visitInsn(ACONST_NULL); mv.visitMethodInsn( - INVOKEVIRTUAL, "net/bytebuddy/jar/asm/ClassWriter", "toByteArray", "()[B", false); + INVOKEVIRTUAL, + "jdk/internal/org/objectweb/asm/ClassWriter", + "toByteArray", + "()[B", + false); mv.visitInsn(POP); mv.visitInsn(ACONST_NULL); mv.visitInsn(ARETURN); @@ -106,7 +114,7 @@ void injectsAfterToByteArrayInGenerateInnerClass() { } @Test - void injectsAfterBuildOnJdk24() { + void injectsAfterBuildOnClassFileApi() { assertTrue( injectsTransformCall( "spinInnerClass", @@ -117,7 +125,9 @@ void injectsAfterBuildOnJdk24() { INVOKEINTERFACE, "java/lang/classfile/ClassFile", "build", - "(Ljava/lang/constant/ClassDesc;Ljava/util/function/Consumer;)[B", + "(Ljava/lang/classfile/constantpool/ClassEntry;" + + "Ljava/lang/classfile/constantpool/ConstantPoolBuilder;" + + "Ljava/util/function/Consumer;)[B", true); mv.visitInsn(POP); mv.visitInsn(ACONST_NULL); @@ -134,7 +144,27 @@ void doesNotInjectInUnrelatedMethod() { mv -> { mv.visitInsn(ACONST_NULL); mv.visitMethodInsn( - INVOKEVIRTUAL, "net/bytebuddy/jar/asm/ClassWriter", "toByteArray", "()[B", false); + INVOKEVIRTUAL, + "jdk/internal/org/objectweb/asm/ClassWriter", + "toByteArray", + "()[B", + false); + mv.visitInsn(POP); + mv.visitInsn(ACONST_NULL); + mv.visitInsn(ARETURN); + })); + } + + @Test + void doesNotInjectOnUnrelatedToByteArrayOwner() { + assertFalse( + injectsTransformCall( + "spinInnerClass", + "()Ljava/lang/Class;", + mv -> { + mv.visitInsn(ACONST_NULL); + mv.visitMethodInsn( + INVOKEVIRTUAL, "java/io/ByteArrayOutputStream", "toByteArray", "()[B", false); mv.visitInsn(POP); mv.visitInsn(ACONST_NULL); mv.visitInsn(ARETURN); From b443c5880d150be38f033acc1f4d030bf566ac83 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 26 Aug 2026 12:22:16 +0200 Subject: [PATCH 06/23] Improve smoke test --- .../java11/testdog/moduleapp/LambdaTask.java | 32 ++++++++++++++++++- .../Java9ModulesLambdaSmokeTest.groovy | 4 +-- .../smoketest/Java9ModulesSmokeTest.groovy | 5 --- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java b/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java index aca6b4e9a8c..22c292a5584 100644 --- a/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java +++ b/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java @@ -1,5 +1,7 @@ package testdog.moduleapp; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -13,13 +15,18 @@ * datadog.smoketest} package would never exercise this path. */ public final class LambdaTask { + private static final String FIELD_BACKED_CONTEXT_ACCESSOR = + "datadog.trace.bootstrap.FieldBackedContextAccessor"; + private LambdaTask() {} public static void runOnExecutor() throws InterruptedException { final ExecutorService pool = Executors.newSingleThreadExecutor(); try { final CountDownLatch latch = new CountDownLatch(1); - pool.execute(latch::countDown); + final Runnable task = latch::countDown; + assertFieldInjection(task, Boolean.getBoolean("dd.trace.lambda.enabled")); + pool.execute(task); if (!latch.await(10, TimeUnit.SECONDS)) { throw new IllegalStateException("lambda task did not run"); } @@ -27,4 +34,27 @@ public static void runOnExecutor() throws InterruptedException { pool.shutdownNow(); } } + + /** + * Checked in both directions: an untransformed lambda is what a silently disabled feature looks + * like, and an injected one with the flag off would mean the flag no longer gates. + */ + private static void assertFieldInjection(final Runnable task, final boolean expected) { + final List interfaces = new ArrayList<>(); + for (final Class type : task.getClass().getInterfaces()) { + interfaces.add(type.getName()); + } + final boolean injected = interfaces.contains(FIELD_BACKED_CONTEXT_ACCESSOR); + if (injected != expected) { + throw new IllegalStateException( + "expected lambda field-injection=" + + expected + + " but was " + + injected + + "; " + + task.getClass().getName() + + " implements " + + interfaces); + } + } } diff --git a/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaSmokeTest.groovy b/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaSmokeTest.groovy index 16bfa17bfae..24b804faebf 100644 --- a/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaSmokeTest.groovy +++ b/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaSmokeTest.groovy @@ -14,7 +14,7 @@ import spock.lang.IgnoreIf }) class Java9ModulesLambdaSmokeTest extends Java9ModulesSmokeTest { @Override - protected List extraJvmArgs() { - return ["-Ddd.trace.lambda.enabled=true"] + def javaProperties() { + return super.javaProperties() + "-Ddd.trace.lambda.enabled=true" } } diff --git a/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesSmokeTest.groovy b/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesSmokeTest.groovy index b4a193d5d65..57865e57bca 100644 --- a/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesSmokeTest.groovy +++ b/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesSmokeTest.groovy @@ -14,10 +14,6 @@ class Java9ModulesSmokeTest extends AbstractSmokeTest { // Estimate for the amount of time instrumentation plus some extra private static final int TIMEOUT_SECS = 30 - protected List extraJvmArgs() { - return [] - } - @Override ProcessBuilder createProcessBuilder() { String imageDir = System.getProperty("datadog.smoketest.module.image") @@ -27,7 +23,6 @@ class Java9ModulesSmokeTest extends AbstractSmokeTest { List command = new ArrayList<>() command.add(imageDir + "/bin/java") command.addAll(defaultJavaProperties) - command.addAll(extraJvmArgs()) command.addAll((String[]) ["-m", "datadog.smoketest.moduleapp/datadog.smoketest.moduleapp.ModuleApplication"]) ProcessBuilder processBuilder = new ProcessBuilder(command) processBuilder.directory(new File(buildDirectory)) From 52137c80e610f9d71c52ee3733c0a6b6ee4f73a0 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 26 Aug 2026 12:50:20 +0200 Subject: [PATCH 07/23] Better logs --- .../lang/invoke/LambdaTransformerHelper.java | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) 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 index 7a616d39ef5..2fb14ebc63a 100644 --- 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 @@ -1,5 +1,8 @@ package datadog.trace.bootstrap.instrumentation.java.lang.invoke; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Entry point invoked from instrumented {@code java.lang.invoke.InnerClassLambdaMetafactory}. The * metafactory instrumentation injects a call to {@link #transform(byte[], String, Class)} right @@ -11,6 +14,8 @@ * sun.instrument.TransformerManager} swallows {@code ClassFileTransformer} errors). */ public final class LambdaTransformerHelper { + private static final Logger log = LoggerFactory.getLogger(LambdaTransformerHelper.class); + // While transforming a lambda the agent / ByteBuddy may itself create lambdas; we must not // recurse into transformation for those. private static final ThreadLocal TRANSFORMING = new ThreadLocal<>(); @@ -26,25 +31,37 @@ private LambdaTransformerHelper() {} public static byte[] transform(byte[] classBytes, String lambdaClassName, Class targetClass) { try { LambdaTransformer transformer = LambdaTransformerHolder.get(); - if (transformer == null || targetClass == null) { + 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); - return result != null ? result : classBytes; + if (result == null) { + log.debug("Lambda {} not transformed", lambdaClassName); + return classBytes; + } + return result; } finally { TRANSFORMING.set(Boolean.FALSE); } - } catch (Throwable ignored) { + } catch (Throwable e) { + log.debug("Lambda {} skipped: {}", lambdaClassName, e.toString()); return classBytes; } } From 3bbe48b72f3087a8edc4b8783de73342ce5b1291 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 26 Aug 2026 13:06:31 +0200 Subject: [PATCH 08/23] Bind agent jar to jmh --- dd-java-agent/benchmark/build.gradle | 10 +++++++++- .../jmh/java/lambdabench/LambdaExecutorBenchmark.java | 6 +++--- 2 files changed, 12 insertions(+), 4 deletions(-) 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 index 0395e9919ed..d7809856d80 100644 --- a/dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java +++ b/dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java @@ -28,10 +28,10 @@ * removed. Lives outside the {@code datadog.*} package on purpose — {@code LambdaTransformerHelper} * skips agent-owned lambdas, and a real application's lambdas are not under {@code datadog.*}. */ -public class LambdaExecutorBenchmark { +public abstract class LambdaExecutorBenchmark { - private static final String AGENT = - "-javaagent:/Users/andrea.marziali/go/src/github.com/DataDog/dd-trace-java/dd-java-agent/build/libs/dd-java-agent-1.64.0-SNAPSHOT.jar"; + // Relative to the JMH working directory, which Gradle sets to this project's directory. + private static final String AGENT = "-javaagent:build/agent/dd-java-agent.jar"; @State(Scope.Benchmark) public static class ExecutorState { From 7af9fc4ed3594bf0f47ce92d46529b4798ed0240 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 26 Aug 2026 13:22:18 +0200 Subject: [PATCH 09/23] Switch to default asm version --- .../java/lang/invoke/LambdaMetafactoryInstrumentation.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java index 408a58ae92b..d84178ca6a4 100644 --- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java +++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java @@ -21,6 +21,7 @@ import net.bytebuddy.jar.asm.Type; import net.bytebuddy.matcher.ElementMatcher; import net.bytebuddy.pool.TypePool; +import net.bytebuddy.utility.OpenedClassReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -143,7 +144,7 @@ private static final class MetafactoryClassVisitor extends ClassVisitor { private boolean injected; MetafactoryClassVisitor(ClassVisitor cv, String slashClassName) { - super(Opcodes.ASM7, cv); + super(OpenedClassReader.ASM_API, cv); this.slashClassName = slashClassName; } From 81421d41319bad024d07d7cc6dc9c253468bb56f Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 26 Aug 2026 15:52:15 +0200 Subject: [PATCH 10/23] skip the cache --- .../trace/agent/tooling/bytebuddy/outline/TypeFactory.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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..89c09524fbc 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 @@ -258,8 +258,11 @@ private TypeDescription lookupType( boolean isOutline = typeParser == outlineTypeParser; long fromTick = InstrumenterMetrics.tick(); - // existing type description from same classloader? - SharedTypeInfo sharedType = types.find(name); + // The class being transformed must always be described from the bytes we were handed. A + // cached description under the same name may belong to a different class - lambda proxies + // generated for one declaring class all share a name - and rebuilding from it would drop + // whatever that description lacks, such as an interface. + SharedTypeInfo sharedType = name.equals(targetName) ? null : types.find(name); if (null != sharedType && (name.startsWith("java.") || sharedType.sameClassLoader(classLoaderId))) { InstrumenterMetrics.reuseTypeDescription(fromTick, isOutline); From c2c7192bb32cb99725c7f2993c8d6e35f6453555 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 26 Aug 2026 16:13:36 +0200 Subject: [PATCH 11/23] add a guard on runnable --- .../lang/invoke/LambdaTransformerHelper.java | 18 +++++++--- .../LambdaMetafactoryInstrumentation.java | 19 ++++++---- .../LambdaMetafactoryInstrumentationTest.java | 35 ++++++++++++++++--- .../LambdaMetafactoryIntegrationTest.java | 11 ++++++ 4 files changed, 69 insertions(+), 14 deletions(-) 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 index 2fb14ebc63a..f51da13cff5 100644 --- 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 @@ -5,9 +5,9 @@ /** * Entry point invoked from instrumented {@code java.lang.invoke.InnerClassLambdaMetafactory}. The - * metafactory instrumentation injects a call to {@link #transform(byte[], String, Class)} right - * after the lambda class bytes are generated and before the class is defined, so the generated - * lambda gets the agent's field-injection and advice applied like an ordinary class. + * metafactory instrumentation injects a call to {@link #transform(byte[], String, Class, Class)} + * right after the lambda class bytes are generated and before the class is defined, so eligible + * lambdas get the agent's field-injection and advice applied like ordinary classes. * *

The JVM still defines the lambda as a hidden/anonymous class; only the bytes it defines are * replaced. This never throws: any problem returns the original bytes (mirroring how {@code @@ -26,10 +26,20 @@ private LambdaTransformerHelper() {} * @param classBytes the generated lambda class bytes (on the stack from {@code toByteArray()}) * @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) { + public static byte[] transform( + byte[] classBytes, + String lambdaClassName, + Class targetClass, + Class interfaceClass) { try { + // Only Runnable lambdas benefit from field-backed executor context propagation. Avoid sending + // every other lambda through the agent's full matching and transformation pipeline. + if (interfaceClass == null || !Runnable.class.isAssignableFrom(interfaceClass)) { + return classBytes; + } LambdaTransformer transformer = LambdaTransformerHolder.get(); if (transformer == null) { log.debug("Lambda {} skipped: no transformer registered", lambdaClassName); diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java index d84178ca6a4..de8f87bd75a 100644 --- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java +++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java @@ -51,6 +51,7 @@ public final class LambdaMetafactoryInstrumentation extends InstrumenterModule.C private static final String LAMBDA_CLASS_NAME_FIELD = "lambdaClassName"; private static final String TARGET_CLASS_FIELD = "targetClass"; + private static final String INTERFACE_CLASS_FIELD = "interfaceClass"; public LambdaMetafactoryInstrumentation() { super("lambda"); @@ -73,10 +74,11 @@ public Collection triggerClasses() { } /** - * The injected bytecode reads {@code lambdaClassName} and {@code targetClass} directly. If a JDK - * renames or drops either, the resulting {@code NoSuchFieldError} would break every lambda - * linkage in the JVM, so the type is only transformed when both resolve. {@code targetClass} is - * declared by the superclass, hence the hierarchy walk. + * The injected bytecode reads {@code lambdaClassName}, {@code targetClass}, and {@code + * interfaceClass} directly. If a JDK renames or drops any of them, the resulting {@code + * NoSuchFieldError} would break every lambda linkage in the JVM, so the type is only transformed + * when all three resolve. The class fields are declared across the hierarchy, hence the hierarchy + * walk. */ @Override public ElementMatcher structureMatcher() { @@ -93,7 +95,8 @@ public static final class HasMetafactoryFields implements ElementMatcher { + transformed.set(true); + return classBytes; + }); + try { + byte[] result = + LambdaTransformerHelper.transform( + originalBytes, "test/Lambda", Object.class, Supplier.class); + + assertSame(originalBytes, result); + assertFalse(transformed.get()); + } finally { + LambdaTransformerHolder.set(null); + } + } + @FunctionalInterface private interface ClassBody { void write(MethodVisitor mv); diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java index d512303bf78..12f56357b92 100644 --- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java +++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java @@ -3,6 +3,7 @@ import static datadog.trace.agent.test.assertions.SpanMatcher.span; import static datadog.trace.agent.test.assertions.TraceMatcher.SORT_BY_START_TIME; import static datadog.trace.agent.test.assertions.TraceMatcher.trace; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -15,6 +16,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import org.junit.jupiter.api.Test; /** @@ -39,6 +41,15 @@ void lambdaRunnableIsFieldInjectedNotWrapped() { assertSame(lambda, RunnableWrapper.wrapIfNeeded(lambda)); } + @Test + void nonRunnableLambdaIsNotTransformed() { + Supplier lambda = Object::new; + + assertFalse( + lambda instanceof FieldBackedContextAccessor, + "non-Runnable lambda should bypass the agent transformer"); + } + @Test void lambdaPropagatesContextAcrossExecutor() throws Exception { ExecutorService pool = Executors.newSingleThreadExecutor(); From 11d07483f233865eb30c98df4d4c85e048377a07 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 26 Aug 2026 16:39:37 +0200 Subject: [PATCH 12/23] Better JMH --- .../lambdabench/LambdaExecutorBenchmark.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java b/dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java index d7809856d80..e340cd9db78 100644 --- a/dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java +++ b/dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java @@ -1,9 +1,16 @@ 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; @@ -33,6 +40,23 @@ public abstract class LambdaExecutorBenchmark { // Relative to the JMH working directory, which Gradle sets to this project's directory. private static final String AGENT = "-javaagent:build/agent/dd-java-agent.jar"; + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + private static final MethodHandle RUNNABLE_TARGET; + private static final MethodHandle SUPPLIER_TARGET; + + static { + try { + RUNNABLE_TARGET = + LOOKUP.findStatic( + LambdaExecutorBenchmark.class, "runTarget", MethodType.methodType(void.class)); + SUPPLIER_TARGET = + LOOKUP.findStatic( + LambdaExecutorBenchmark.class, "supplyTarget", MethodType.methodType(Object.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new ExceptionInInitializerError(e); + } + } + @State(Scope.Benchmark) public static class ExecutorState { ExecutorService pool; @@ -54,6 +78,30 @@ public void submitLambda(ExecutorState state) throws InterruptedException { runUnderTrace(state.pool); } + /** Measures cold linkage of an eligible {@link Runnable} lambda class. */ + @Benchmark + public CallSite linkRunnableLambda() throws LambdaConversionException { + return LambdaMetafactory.metafactory( + LOOKUP, + "run", + MethodType.methodType(Runnable.class), + MethodType.methodType(void.class), + RUNNABLE_TARGET, + MethodType.methodType(void.class)); + } + + /** Measures cold linkage of a non-task lambda, which should bypass the agent transformer. */ + @Benchmark + public CallSite linkSupplierLambda() throws LambdaConversionException { + return LambdaMetafactory.metafactory( + LOOKUP, + "get", + MethodType.methodType(Supplier.class), + MethodType.methodType(Object.class), + SUPPLIER_TARGET, + MethodType.methodType(Object.class)); + } + @Trace(operationName = "parent") private void runUnderTrace(ExecutorService pool) throws InterruptedException { CountDownLatch latch = new CountDownLatch(1); @@ -62,6 +110,12 @@ private void runUnderTrace(ExecutorService pool) throws InterruptedException { latch.await(); } + private static void runTarget() {} + + private static Object supplyTarget() { + return null; + } + @Fork public static class NoAgent extends LambdaExecutorBenchmark {} From 885ffac90a8c2d0ecd04bbeb7cc253ea9b5625a3 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Mon, 31 Aug 2026 11:08:57 +0200 Subject: [PATCH 13/23] refinements --- dd-java-agent/agent-bootstrap/build.gradle | 5 + .../java/concurrent/RunnableWrapper.java | 11 +- .../java/concurrent/TPEHelper.java | 8 +- .../java/lang/invoke/LambdaTransformer.java | 11 +- .../lang/invoke/LambdaTransformerHelper.java | 26 +---- .../lang/invoke/lambda_interface_name.trie | 7 ++ .../trace/agent/tooling/AgentInstaller.java | 20 ++-- .../bytebuddy/DDJava9LambdaTransformer.java | 17 +-- .../tooling/bytebuddy/memoize/Memoizer.java | 2 + .../bytebuddy/outline/TypeFactory.java | 18 ++- .../bytebuddy/outline/TypePoolFacade.java | 8 ++ .../bytebuddy/outline/TypeFactoryTest.java | 57 ++++++++++ .../lambdabench/LambdaExecutorBenchmark.java | 29 +++-- .../java-lambda/java-lambda-1.8/build.gradle | 3 +- .../LambdaMetafactoryInstrumentation.java | 43 ++----- .../LambdaMetafactoryInstrumentationTest.java | 106 ++++++++++++++++-- .../lambda/ClojureAFnIntegrationTest.java | 27 +++++ .../LambdaMetafactoryIntegrationTest.java | 29 +++-- .../java11/testdog/moduleapp/LambdaTask.java | 12 +- .../Java9ModulesLambdaSmokeTest.groovy | 6 +- 20 files changed, 299 insertions(+), 146 deletions(-) create mode 100644 dd-java-agent/agent-bootstrap/src/main/resources/datadog/trace/bootstrap/instrumentation/java/lang/invoke/lambda_interface_name.trie create mode 100644 dd-java-agent/agent-tooling/src/test/java/datadog/trace/agent/tooling/bytebuddy/outline/TypeFactoryTest.java create mode 100644 dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/ClojureAFnIntegrationTest.java 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 54e032c86fb..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 @@ -4,9 +4,7 @@ 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 @@ -26,14 +24,11 @@ public void run() { } public static Runnable wrapIfNeeded(final Runnable task) { - // If the lambda class was field-injected (via the metafactory instrumentation) it carries its - // own context-store field and is instrumented directly, so wrapping is unnecessary and would - // needlessly change the task's identity. + // Field-injected tasks are already instrumented and must retain their identity. if (!(task instanceof RunnableWrapper) && !(task instanceof FieldBackedContextAccessor) && !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. + // 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 8c785837be0..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 @@ -23,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; @@ -31,15 +31,11 @@ public final class TPEHelper { private static final ClassValue WRAP = GenericClassValue.of( input -> { - // If the lambda class was field-injected (via the metafactory instrumentation) it can - // carry the State context-store field directly, so there is no need to wrap it. 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 index 82cab203e27..daf9525a1dd 100644 --- 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 @@ -1,17 +1,10 @@ package datadog.trace.bootstrap.instrumentation.java.lang.invoke; -/** - * Transforms the bytecode of a lambda class generated by {@code - * java.lang.invoke.InnerClassLambdaMetafactory} before it is defined, so the lambda can receive the - * same field-injection and advice the agent applies to ordinary classes. - * - *

The implementation is registered by the agent installer via {@link - * LambdaTransformerHolder#set(LambdaTransformer)} once the main class-file transformer is ready. - */ +/** 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 (used to resolve the defining class loader) + * @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 */ 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 index f51da13cff5..5054e657589 100644 --- 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 @@ -3,41 +3,27 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * Entry point invoked from instrumented {@code java.lang.invoke.InnerClassLambdaMetafactory}. The - * metafactory instrumentation injects a call to {@link #transform(byte[], String, Class, Class)} - * right after the lambda class bytes are generated and before the class is defined, so eligible - * lambdas get the agent's field-injection and advice applied like ordinary classes. - * - *

The JVM still defines the lambda as a hidden/anonymous class; only the bytes it defines are - * replaced. This never throws: any problem returns the original bytes (mirroring how {@code - * sun.instrument.TransformerManager} swallows {@code ClassFileTransformer} errors). - */ +/** 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); - // While transforming a lambda the agent / ByteBuddy may itself create lambdas; we must not - // recurse into transformation for those. + // Agent transformation may itself create lambdas. private static final ThreadLocal TRANSFORMING = new ThreadLocal<>(); private LambdaTransformerHelper() {} /** - * @param classBytes the generated lambda class bytes (on the stack from {@code toByteArray()}) + * @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) { + byte[] classBytes, String lambdaClassName, Class targetClass, Class interfaceClass) { try { - // Only Runnable lambdas benefit from field-backed executor context propagation. Avoid sending - // every other lambda through the agent's full matching and transformation pipeline. - if (interfaceClass == null || !Runnable.class.isAssignableFrom(interfaceClass)) { + // Only exact allowlisted interfaces enter the transformer. + if (interfaceClass == null || LambdaInterfaceNameTrie.apply(interfaceClass.getName()) != 1) { return classBytes; } LambdaTransformer transformer = LambdaTransformerHolder.get(); 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 a61e31c0c2a..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 @@ -266,12 +266,7 @@ public void applied(Iterable instrumentationNames) { } } - /** - * Exposes the installed transformer to the {@code InnerClassLambdaMetafactory} instrumentation so - * generated lambda classes can be run through the same matching + field-injection pipeline before - * they are defined. Leaving the holder unset disables lambda field-injection, which is always - * safe: tasks then fall back to being wrapped. - */ + /** Registers the installed class-file transformer for generated lambdas. */ private static void registerLambdaTransformer(final ClassFileTransformer classFileTransformer) { LambdaTransformer lambdaTransformer = newLambdaTransformer(classFileTransformer); if (null != lambdaTransformer) { @@ -280,13 +275,8 @@ private static void registerLambdaTransformer(final ClassFileTransformer classFi } /** - * On Java 9+ the transformer must be given the module declaring the lambda, otherwise ByteBuddy - * skips the read edge that field-injected classes need and lambdas in named modules become - * undefinable. If that transformer cannot be loaded we return {@code null} rather than falling - * back to the module-less one, which would be the very defect we are avoiding. - * - *

Uses an anonymous class (not a lambda) so this bootstrapping code does not itself depend on - * the metafactory we just instrumented. + * 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( @@ -306,9 +296,11 @@ private static LambdaTransformer newLambdaTransformer( 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(), @@ -318,6 +310,8 @@ public byte[] transform(String slashClassName, Class targetClass, byte[] clas classBytes); } catch (Throwable ignored) { return null; + } finally { + TypePoolFacade.endLambdaTransform(); } } }; 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 index 581abafac9a..1ffa6640693 100644 --- 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 @@ -1,21 +1,11 @@ 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 lambda classes through the agent's transformer, passing the module of the class - * that declares the lambda. - * - *

The module-less {@code transform} overload reports {@code JavaModule.UNSUPPORTED} to - * ByteBuddy, which then skips the read edge that field-injected classes need to reach {@code - * FieldBackedContextAccessor}. A lambda declared in a named module would be transformed - * successfully and then fail to define, surfacing as an {@code InternalError} at the lambda's call - * site. - * - *

This class is only used on Java 9+; for Java 8 the module-less overload is complete. - */ +/** 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. */ @@ -35,6 +25,7 @@ public DDJava9LambdaTransformer(ClassFileTransformer classFileTransformer) { @Override public byte[] transform(String slashClassName, Class targetClass, byte[] classBytes) { + TypePoolFacade.beginLambdaTransform(); try { return classFileTransformer.transform( targetClass.getModule(), @@ -45,6 +36,8 @@ public byte[] transform(String slashClassName, Class targetClass, byte[] clas 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 89c09524fbc..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,11 +268,9 @@ private TypeDescription lookupType( boolean isOutline = typeParser == outlineTypeParser; long fromTick = InstrumenterMetrics.tick(); - // The class being transformed must always be described from the bytes we were handed. A - // cached description under the same name may belong to a different class - lambda proxies - // generated for one declaring class all share a name - and rebuilding from it would drop - // whatever that description lacks, such as an interface. - SharedTypeInfo sharedType = name.equals(targetName) ? null : 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/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/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java b/dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java index e340cd9db78..f2039164eca 100644 --- a/dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java +++ b/dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java @@ -19,8 +19,8 @@ import org.openjdk.jmh.annotations.TearDown; /** - * Measures the executor task-submission path for a lambda {@code Runnable}. Compare the three - * nested fork variants (run with {@code -prof gc}): + * Compares lambda {@code Runnable} allocation and submission with no agent, wrapping, and field + * injection. * *

    *
  • {@link NoAgent} — baseline, no agent. @@ -30,13 +30,18 @@ * field-injected, so no wrapper is allocated and identity is preserved. *
* - *

The headline metric is {@code ·gc.alloc.rate.norm} (bytes/op): the delta between {@code - * AgentLambdaOff} and {@code AgentLambdaOn} on {@link #submitLambda} is the wrapper allocation we - * removed. Lives outside the {@code datadog.*} package on purpose — {@code LambdaTransformerHelper} - * skips agent-owned lambdas, and a real application's lambdas are not under {@code datadog.*}. + *

With the GC profiler, {@code allocateCapturingRunnable} isolates the injected field's object + * size cost while {@code submitLambda} includes the wrapper allocation tradeoff. Run with: + * + *

{@code
+ * ./gradlew :dd-java-agent:benchmark:jmh \
+ *   '-Pjmh.includes=LambdaExecutorBenchmark.*allocateCapturingRunnable' \
+ *   -Pjmh.profilers=gc
+ * }
*/ public abstract class LambdaExecutorBenchmark { + // Must remain outside datadog.* because the helper skips agent-owned lambdas. // Relative to the JMH working directory, which Gradle sets to this project's directory. private static final String AGENT = "-javaagent:build/agent/dd-java-agent.jar"; @@ -72,6 +77,17 @@ public void tearDown() { } } + @State(Scope.Thread) + public static class CapturingLambdaState { + public void run() {} + } + + /** Allocates an untraced capturing Runnable. */ + @Benchmark + public Runnable allocateCapturingRunnable(CapturingLambdaState state) { + return state::run; + } + /** Submit a lambda Runnable to the executor under an active trace, and wait for it to run. */ @Benchmark public void submitLambda(ExecutorState state) throws InterruptedException { @@ -105,7 +121,6 @@ public CallSite linkSupplierLambda() throws LambdaConversionException { @Trace(operationName = "parent") private void runUnderTrace(ExecutorService pool) throws InterruptedException { CountDownLatch latch = new CountDownLatch(1); - // lambda Runnable: this is the task whose propagation strategy (wrap vs field) we compare pool.execute(latch::countDown); latch.await(); } diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/build.gradle b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/build.gradle index bf836599a3e..a47efe00d4a 100644 --- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/build.gradle +++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/build.gradle @@ -14,6 +14,7 @@ tasks.named("compileJava") { dependencies { testImplementation project(':dd-java-agent:instrumentation:datadog:tracing:trace-annotation') - // executor context propagation across the field-injected lambda + testImplementation 'org.clojure:clojure:1.11.1' + // Runs the existing Runnable propagation advice on injected lambdas. testRuntimeOnly project(':dd-java-agent:instrumentation:java:java-concurrent:java-concurrent-1.8') } diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java index de8f87bd75a..3574e694de8 100644 --- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java +++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java @@ -26,16 +26,11 @@ import org.slf4j.LoggerFactory; /** - * Instruments {@code java.lang.invoke.InnerClassLambdaMetafactory} so that generated lambda classes - * are routed through the agent's class-file transformer before they are defined. This lets lambda - * {@code Runnable}/{@code Callable} tasks receive field-backed context-store injection (and the - * matching advice) just like ordinary classes, instead of being wrapped. + * Routes generated lambda bytes through the agent transformer before definition, allowing + * allowlisted interfaces such as {@link Runnable} to receive field injection and advice. * - *

The generated bytes come from {@code ClassWriter.toByteArray()} on JDK 8-21 and {@code - * ClassFile.build(...)} on JDK 25-27, one call site per JDK; the switch happened between 21 and 25, - * and 22-24 were not checked. We use an ASM visitor — rather than {@code @Advice} — because the - * transform call must be inserted in the middle of {@code spinInnerClass}/{@code - * generateInnerClass}, right after that call. + *

An ASM visitor is required because the transform call must be inserted immediately after the + * lambda bytes are generated, in the middle of the metafactory method. */ @AutoService(InstrumenterModule.class) public final class LambdaMetafactoryInstrumentation extends InstrumenterModule.ContextTracking @@ -59,7 +54,6 @@ public LambdaMetafactoryInstrumentation() { @Override protected boolean defaultEnabled() { - // Opt-in for the first iteration: enable with DD_TRACE_LAMBDA_ENABLED=true. return false; } @@ -73,22 +67,13 @@ public Collection triggerClasses() { return Collections.singleton(METAFACTORY); } - /** - * The injected bytecode reads {@code lambdaClassName}, {@code targetClass}, and {@code - * interfaceClass} directly. If a JDK renames or drops any of them, the resulting {@code - * NoSuchFieldError} would break every lambda linkage in the JVM, so the type is only transformed - * when all three resolve. The class fields are declared across the hierarchy, hence the hierarchy - * walk. - */ + /** Require every field read by the injected bytecode. */ @Override public ElementMatcher structureMatcher() { return HasMetafactoryFields.INSTANCE; } - /** - * Public because the agent loads {@code *Instrumentation} classes in a child class-loader of the - * one that loads this nested class; package-private access across the two is denied. - */ + /** Public because this matcher is loaded across agent class-loader boundaries. */ public static final class HasMetafactoryFields implements ElementMatcher { public static final HasMetafactoryFields INSTANCE = new HasMetafactoryFields(); @@ -155,8 +140,7 @@ private static final class MetafactoryClassVisitor extends ClassVisitor { public MethodVisitor visitMethod( int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); - // Up to JDK 21 the bytes are produced in spinInnerClass; from 25 spinInnerClass delegates - // to generateInnerClass, which produces them. + // The byte-generation method changed in JDK 25. if (("spinInnerClass".equals(name) || "generateInnerClass".equals(name)) && "()Ljava/lang/Class;".equals(descriptor)) { return new MetafactoryMethodVisitor(api, mv, slashClassName, this); @@ -168,8 +152,6 @@ public MethodVisitor visitMethod( public void visitEnd() { super.visitEnd(); if (!injected) { - // The fields resolved but the generated bytes are no longer produced where we expect, so - // no lambda will be field-injected and every task falls back to wrapping. log.debug( "No injection site found in {}; lambda field-injection is inactive.", slashClassName); } @@ -194,9 +176,8 @@ private static final class MetafactoryMethodVisitor extends MethodVisitor { public void visitMethodInsn( int opcode, String owner, String name, String descriptor, boolean isInterface) { super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); - // After the generated bytes are produced, insert a call to our transformer. The byte[] is - // on the stack. Owners are matched loosely so a repackaging only costs the feature, while - // still ruling out unrelated calls such as ByteArrayOutputStream.toByteArray(). + // Match repackaged JDK APIs while excluding unrelated byte-array producers. The generated + // byte[] remains on the operand stack after the original call. if ((opcode == Opcodes.INVOKEVIRTUAL && "toByteArray".equals(name) && "()[B".equals(descriptor) @@ -210,11 +191,11 @@ public void visitMethodInsn( super.visitFieldInsn( Opcodes.GETFIELD, slashClassName, LAMBDA_CLASS_NAME_FIELD, "Ljava/lang/String;"); super.visitVarInsn(Opcodes.ALOAD, 0); - // targetClass yields the ClassLoader where the lambda class will be defined. + // Resolves the defining class loader and module. super.visitFieldInsn( Opcodes.GETFIELD, slashClassName, TARGET_CLASS_FIELD, "Ljava/lang/Class;"); super.visitVarInsn(Opcodes.ALOAD, 0); - // interfaceClass lets the helper cheaply reject non-Runnable lambdas before matching. + // Allows the helper to reject non-allowlisted interfaces before full matching. super.visitFieldInsn( Opcodes.GETFIELD, slashClassName, INTERFACE_CLASS_FIELD, "Ljava/lang/Class;"); super.visitMethodInsn( @@ -223,7 +204,7 @@ public void visitMethodInsn( "transform", "([BLjava/lang/String;Ljava/lang/Class;Ljava/lang/Class;)[B", false); - // stack: ..., byte[] (transformed) + // stack: ..., transformed byte[] declaringVisitor.injected = true; } } diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java index b6f42015485..33cd52438f5 100644 --- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java +++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java @@ -10,6 +10,7 @@ import static net.bytebuddy.jar.asm.Opcodes.INVOKEVIRTUAL; import static net.bytebuddy.jar.asm.Opcodes.POP; import static net.bytebuddy.jar.asm.Opcodes.V1_8; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -18,6 +19,7 @@ import datadog.trace.bootstrap.instrumentation.java.lang.invoke.LambdaTransformerHolder; import datadog.trace.instrumentation.java.lang.invoke.LambdaMetafactoryInstrumentation.MetafactoryVisitorWrapper; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.jar.asm.ClassReader; @@ -31,9 +33,6 @@ class LambdaMetafactoryInstrumentationTest { private static final String HELPER = "datadog/trace/bootstrap/instrumentation/java/lang/invoke/LambdaTransformerHelper"; - /** - * Builds a class with a single method, transforms it, and reports whether transform() was hit. - */ private static boolean injectsTransformCall( String methodName, String methodDescriptor, ClassBody body) { ClassWriter in = new ClassWriter(0); @@ -176,12 +175,7 @@ void doesNotInjectOnUnrelatedToByteArrayOwner() { })); } - /** - * Guards the {@code GETFIELD}s emitted by the visitor: if a JDK renames or drops any field this - * fails here rather than as a {@code NoSuchFieldError} in every lambda linkage. {@code - * targetClass} and {@code interfaceClass} are declared by the superclass, so this also covers the - * hierarchy walk. - */ + /** Verifies every field read by the injected bytecode, including inherited fields. */ @Test void structureMatcherAcceptsTheRealMetafactory() throws Exception { TypeDescription metafactory = @@ -220,6 +214,100 @@ void nonRunnableLambdaBypassesTransformer() { } } + @Test + void exactRunnableInterfaceUsesTransformer() { + byte[] originalBytes = new byte[0]; + AtomicBoolean transformed = new AtomicBoolean(); + LambdaTransformerHolder.set( + (className, targetClass, classBytes) -> { + transformed.set(true); + return classBytes; + }); + try { + byte[] result = + LambdaTransformerHelper.transform( + originalBytes, "test/Lambda", Object.class, Runnable.class); + + assertSame(originalBytes, result); + assertTrue(transformed.get()); + } finally { + LambdaTransformerHolder.set(null); + } + } + + @Test + void runnableSubinterfaceBypassesTransformer() { + byte[] originalBytes = new byte[0]; + AtomicBoolean transformed = new AtomicBoolean(); + LambdaTransformerHolder.set( + (className, targetClass, classBytes) -> { + transformed.set(true); + return classBytes; + }); + try { + byte[] result = + LambdaTransformerHelper.transform( + originalBytes, "test/Lambda", Object.class, RunnableSubtype.class); + + assertSame(originalBytes, result); + assertFalse(transformed.get()); + } finally { + LambdaTransformerHolder.set(null); + } + } + + @Test + void transformerFailureFallsBackAndDoesNotPoisonNextLambda() { + byte[] originalBytes = new byte[0]; + byte[] transformedBytes = new byte[1]; + AtomicInteger calls = new AtomicInteger(); + LambdaTransformerHolder.set( + (className, targetClass, classBytes) -> { + if (calls.getAndIncrement() == 0) { + throw new IllegalStateException("expected test failure"); + } + return transformedBytes; + }); + try { + assertSame( + originalBytes, + LambdaTransformerHelper.transform( + originalBytes, "test/Lambda", Object.class, Runnable.class)); + assertSame( + transformedBytes, + LambdaTransformerHelper.transform( + originalBytes, "test/Lambda", Object.class, Runnable.class)); + assertEquals(2, calls.get()); + } finally { + LambdaTransformerHolder.set(null); + } + } + + @Test + void nullTransformFallsBackAndDoesNotPoisonNextLambda() { + byte[] originalBytes = new byte[0]; + byte[] transformedBytes = new byte[1]; + AtomicInteger calls = new AtomicInteger(); + LambdaTransformerHolder.set( + (className, targetClass, classBytes) -> + calls.getAndIncrement() == 0 ? null : transformedBytes); + try { + assertSame( + originalBytes, + LambdaTransformerHelper.transform( + originalBytes, "test/Lambda", Object.class, Runnable.class)); + assertSame( + transformedBytes, + LambdaTransformerHelper.transform( + originalBytes, "test/Lambda", Object.class, Runnable.class)); + assertEquals(2, calls.get()); + } finally { + LambdaTransformerHolder.set(null); + } + } + + private interface RunnableSubtype extends Runnable {} + @FunctionalInterface private interface ClassBody { void write(MethodVisitor mv); diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/ClojureAFnIntegrationTest.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/ClojureAFnIntegrationTest.java new file mode 100644 index 00000000000..704b759166a --- /dev/null +++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/ClojureAFnIntegrationTest.java @@ -0,0 +1,27 @@ +package testdog.trace.instrumentation.lambda; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import clojure.java.api.Clojure; +import clojure.lang.AFn; +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.bootstrap.FieldBackedContextAccessor; +import datadog.trace.test.junit.utils.config.WithConfig; +import org.junit.jupiter.api.Test; + +@WithConfig(key = "trace.lambda.enabled", value = "true") +@WithConfig(key = "trace.runnable.enabled", value = "false") +public class ClojureAFnIntegrationTest extends AbstractInstrumentationTest { + + @Test + void afnIsNotFieldInjected() { + // Runnable instrumentation can be disabled to avoid inflating every AFn; see + // https://github.com/DataDog/dd-trace-java/pull/2925. + Object function = Clojure.var("clojure.core", "eval").invoke(Clojure.read("(fn [] nil)")); + + assertTrue(function instanceof AFn); + assertTrue(function instanceof Runnable); + assertFalse(function instanceof FieldBackedContextAccessor); + } +} diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java index 12f56357b92..5cddcf003dd 100644 --- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java +++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java @@ -3,6 +3,7 @@ import static datadog.trace.agent.test.assertions.SpanMatcher.span; import static datadog.trace.agent.test.assertions.TraceMatcher.SORT_BY_START_TIME; import static datadog.trace.agent.test.assertions.TraceMatcher.trace; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -16,31 +17,41 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; import org.junit.jupiter.api.Test; -/** - * End-to-end test that {@code InnerClassLambdaMetafactory} instrumentation makes lambda {@code - * Runnable}s field-injectable (so the agent no longer needs to wrap them) and propagates context - * across an executor. Must live outside {@code datadog.*} so the agent does not ignore the test - * class and its lambdas. - */ +/** Lambda integration tests outside the ignored {@code datadog.*} prefix. */ @WithConfig(key = "trace.lambda.enabled", value = "true") public class LambdaMetafactoryIntegrationTest extends AbstractInstrumentationTest { @Test void lambdaRunnableIsFieldInjectedNotWrapped() { - // Created after the agent is installed in @BeforeAll, so its linkage goes through the - // instrumented metafactory. + // Link after the agent is installed. Runnable lambda = () -> {}; assertTrue( lambda instanceof FieldBackedContextAccessor, "lambda Runnable should be field-injected via the metafactory instrumentation"); - // because it is field-injected, wrapping must be skipped and identity preserved assertSame(lambda, RunnableWrapper.wrapIfNeeded(lambda)); } + @Test + void sameOwnerRunnableCaptureShapesAreAllFieldInjected() { + AtomicInteger counter = new AtomicInteger(); + int delta = 7; + Runnable[] lambdas = {() -> {}, counter::incrementAndGet, () -> counter.addAndGet(delta)}; + + for (Runnable lambda : lambdas) { + assertTrue( + lambda instanceof FieldBackedContextAccessor, + "every Runnable lambda shape should be field-injected"); + assertSame(lambda, RunnableWrapper.wrapIfNeeded(lambda)); + lambda.run(); + } + assertEquals(8, counter.get()); + } + @Test void nonRunnableLambdaIsNotTransformed() { Supplier lambda = Object::new; diff --git a/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java b/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java index 22c292a5584..1227351a726 100644 --- a/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java +++ b/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java @@ -7,13 +7,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -/** - * Links a lambda {@code Runnable} from inside the application's named module. - * - *

Lives outside {@code datadog.*} on purpose: the agent skips lambdas declared by classes with - * that prefix to avoid instrumenting itself, so a task in the application's own {@code - * datadog.smoketest} package would never exercise this path. - */ +/** Links a Runnable lambda from a named module and outside the ignored {@code datadog.*} prefix. */ public final class LambdaTask { private static final String FIELD_BACKED_CONTEXT_ACCESSOR = "datadog.trace.bootstrap.FieldBackedContextAccessor"; @@ -35,10 +29,6 @@ public static void runOnExecutor() throws InterruptedException { } } - /** - * Checked in both directions: an untransformed lambda is what a silently disabled feature looks - * like, and an injected one with the flag off would mean the flag no longer gates. - */ private static void assertFieldInjection(final Runnable task, final boolean expected) { final List interfaces = new ArrayList<>(); for (final Class type : task.getClass().getInterfaces()) { diff --git a/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaSmokeTest.groovy b/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaSmokeTest.groovy index 24b804faebf..61775f09445 100644 --- a/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaSmokeTest.groovy +++ b/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaSmokeTest.groovy @@ -4,11 +4,7 @@ import datadog.environment.JavaVirtualMachine import datadog.environment.OperatingSystem import spock.lang.IgnoreIf -/** - * Runs the modular application with lambda field-injection enabled. The lambda's generated class - * belongs to the application's named module, which reaches the injected types only if the agent - * adds a read edge while transforming it. - */ +/** Verifies field injection adds the required read edge to a named module. */ @IgnoreIf({ OperatingSystem.isLinux() && OperatingSystem.architecture().isArm64() && JavaVirtualMachine.isJ9() }) From 422b95162724cd08806c8659548f6f6d92fb3ba6 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Mon, 31 Aug 2026 12:59:57 +0200 Subject: [PATCH 14/23] Enable by default --- .../lambdabench/LambdaExecutorBenchmark.java | 31 +++++++++++++++---- .../LambdaMetafactoryInstrumentation.java | 5 --- .../lambda/ClojureAFnIntegrationTest.java | 1 - .../LambdaMetafactoryIntegrationTest.java | 2 -- .../java11/testdog/moduleapp/LambdaTask.java | 3 +- ...ava9ModulesLambdaDisabledSmokeTest.groovy} | 6 ++-- metadata/supported-configurations.json | 2 +- 7 files changed, 31 insertions(+), 19 deletions(-) rename dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/{Java9ModulesLambdaSmokeTest.groovy => Java9ModulesLambdaDisabledSmokeTest.groovy} (58%) diff --git a/dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java b/dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java index f2039164eca..c1a59f85c77 100644 --- a/dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java +++ b/dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java @@ -19,8 +19,8 @@ import org.openjdk.jmh.annotations.TearDown; /** - * Compares lambda {@code Runnable} allocation and submission with no agent, wrapping, and field - * injection. + * Compares lambda {@code Runnable} allocation, execution, and submission with no agent, wrapping, + * and field injection. * *

    *
  • {@link NoAgent} — baseline, no agent. @@ -30,8 +30,9 @@ * field-injected, so no wrapper is allocated and identity is preserved. *
* - *

With the GC profiler, {@code allocateCapturingRunnable} isolates the injected field's object - * size cost while {@code submitLambda} includes the wrapper allocation tradeoff. Run with: + *

{@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: * *

{@code
  * ./gradlew :dd-java-agent:benchmark:jmh \
@@ -82,12 +83,30 @@ public static class CapturingLambdaState {
     public void run() {}
   }
 
+  @State(Scope.Thread)
+  public static class DirectRunState {
+    Runnable runnable;
+    int executions;
+
+    @Setup
+    public void setup() {
+      runnable = () -> executions++;
+    }
+  }
+
   /** Allocates an untraced capturing Runnable. */
   @Benchmark
   public Runnable allocateCapturingRunnable(CapturingLambdaState state) {
     return state::run;
   }
 
+  /** Executes an already-created lambda Runnable without an active trace. */
+  @Benchmark
+  public int runUntracedLambda(DirectRunState state) {
+    state.runnable.run();
+    return state.executions;
+  }
+
   /** Submit a lambda Runnable to the executor under an active trace, and wait for it to run. */
   @Benchmark
   public void submitLambda(ExecutorState state) throws InterruptedException {
@@ -134,9 +153,9 @@ private static Object supplyTarget() {
   @Fork
   public static class NoAgent extends LambdaExecutorBenchmark {}
 
-  @Fork(jvmArgsAppend = AGENT)
+  @Fork(jvmArgsAppend = {AGENT, "-Ddd.trace.lambda.enabled=false"})
   public static class AgentLambdaOff extends LambdaExecutorBenchmark {}
 
-  @Fork(jvmArgsAppend = {AGENT, "-Ddd.trace.lambda.enabled=true"})
+  @Fork(jvmArgsAppend = AGENT)
   public static class AgentLambdaOn extends LambdaExecutorBenchmark {}
 }
diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java
index 3574e694de8..3bfbf95e958 100644
--- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java
+++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java
@@ -52,11 +52,6 @@ public LambdaMetafactoryInstrumentation() {
     super("lambda");
   }
 
-  @Override
-  protected boolean defaultEnabled() {
-    return false;
-  }
-
   @Override
   public String instrumentedType() {
     return METAFACTORY;
diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/ClojureAFnIntegrationTest.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/ClojureAFnIntegrationTest.java
index 704b759166a..a52ac12a473 100644
--- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/ClojureAFnIntegrationTest.java
+++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/ClojureAFnIntegrationTest.java
@@ -10,7 +10,6 @@
 import datadog.trace.test.junit.utils.config.WithConfig;
 import org.junit.jupiter.api.Test;
 
-@WithConfig(key = "trace.lambda.enabled", value = "true")
 @WithConfig(key = "trace.runnable.enabled", value = "false")
 public class ClojureAFnIntegrationTest extends AbstractInstrumentationTest {
 
diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java
index 5cddcf003dd..eeb1f805042 100644
--- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java
+++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java
@@ -12,7 +12,6 @@
 import datadog.trace.api.Trace;
 import datadog.trace.bootstrap.FieldBackedContextAccessor;
 import datadog.trace.bootstrap.instrumentation.java.concurrent.RunnableWrapper;
-import datadog.trace.test.junit.utils.config.WithConfig;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -22,7 +21,6 @@
 import org.junit.jupiter.api.Test;
 
 /** Lambda integration tests outside the ignored {@code datadog.*} prefix. */
-@WithConfig(key = "trace.lambda.enabled", value = "true")
 public class LambdaMetafactoryIntegrationTest extends AbstractInstrumentationTest {
 
   @Test
diff --git a/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java b/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java
index 1227351a726..251a5eb24d9 100644
--- a/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java
+++ b/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java
@@ -19,7 +19,8 @@ public static void runOnExecutor() throws InterruptedException {
     try {
       final CountDownLatch latch = new CountDownLatch(1);
       final Runnable task = latch::countDown;
-      assertFieldInjection(task, Boolean.getBoolean("dd.trace.lambda.enabled"));
+      assertFieldInjection(
+          task, Boolean.parseBoolean(System.getProperty("dd.trace.lambda.enabled", "true")));
       pool.execute(task);
       if (!latch.await(10, TimeUnit.SECONDS)) {
         throw new IllegalStateException("lambda task did not run");
diff --git a/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaSmokeTest.groovy b/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaDisabledSmokeTest.groovy
similarity index 58%
rename from dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaSmokeTest.groovy
rename to dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaDisabledSmokeTest.groovy
index 61775f09445..b5c045d033f 100644
--- a/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaSmokeTest.groovy
+++ b/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaDisabledSmokeTest.groovy
@@ -4,13 +4,13 @@ import datadog.environment.JavaVirtualMachine
 import datadog.environment.OperatingSystem
 import spock.lang.IgnoreIf
 
-/** Verifies field injection adds the required read edge to a named module. */
+/** Verifies lambda transformation can be disabled for a named module. */
 @IgnoreIf({
   OperatingSystem.isLinux() && OperatingSystem.architecture().isArm64() && JavaVirtualMachine.isJ9()
 })
-class Java9ModulesLambdaSmokeTest extends Java9ModulesSmokeTest {
+class Java9ModulesLambdaDisabledSmokeTest extends Java9ModulesSmokeTest {
   @Override
   def javaProperties() {
-    return super.javaProperties() + "-Ddd.trace.lambda.enabled=true"
+    return super.javaProperties() + "-Ddd.trace.lambda.enabled=false"
   }
 }
diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json
index 6a97071521b..c5d035c44a4 100644
--- a/metadata/supported-configurations.json
+++ b/metadata/supported-configurations.json
@@ -7976,7 +7976,7 @@
       {
         "version": "A",
         "type": "boolean",
-        "default": "false",
+        "default": "true",
         "aliases": ["DD_TRACE_INTEGRATION_LAMBDA_ENABLED", "DD_INTEGRATION_LAMBDA_ENABLED"]
       }
     ],

From 6f9490f82f7dcbfc08eff42f7c402353d4e6b475 Mon Sep 17 00:00:00 2001
From: Andrea Marziali 
Date: Mon, 31 Aug 2026 14:21:35 +0200
Subject: [PATCH 15/23] update integration golden file

---
 metadata/agent-jar-checks.properties | 1 +
 1 file changed, 1 insertion(+)

diff --git a/metadata/agent-jar-checks.properties b/metadata/agent-jar-checks.properties
index a7bde3e2525..c02458675a1 100644
--- a/metadata/agent-jar-checks.properties
+++ b/metadata/agent-jar-checks.properties
@@ -110,6 +110,7 @@ expected.integrations = IastInstrumentation,\
   jwt,\
   kafka,\
   kotlin_coroutine,\
+  lambda,\
   lettuce,\
   liberty,\
   log4j,\

From 5742c20ab827d26bbb04c399302543bed41f27cf Mon Sep 17 00:00:00 2001
From: Andrea Marziali 
Date: Mon, 31 Aug 2026 14:40:55 +0200
Subject: [PATCH 16/23] Disable instrumenting lambdas during graal builds

---
 .../java/lang/invoke/LambdaMetafactoryInstrumentation.java  | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java
index 3bfbf95e958..fc1f3faf1b2 100644
--- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java
+++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java
@@ -4,6 +4,7 @@
 import datadog.trace.agent.tooling.Instrumenter;
 import datadog.trace.agent.tooling.InstrumenterModule;
 import datadog.trace.agent.tooling.JavaModuleOpenProvider;
+import datadog.trace.api.Platform;
 import datadog.trace.bootstrap.instrumentation.java.lang.invoke.LambdaTransformerHelper;
 import java.util.Collection;
 import java.util.Collections;
@@ -52,6 +53,11 @@ public LambdaMetafactoryInstrumentation() {
     super("lambda");
   }
 
+  @Override
+  public boolean isEnabled() {
+    return super.isEnabled() && !Platform.isNativeImageBuilder();
+  }
+
   @Override
   public String instrumentedType() {
     return METAFACTORY;

From 5655a6ff3216ab57c75bb03c89b7f91205eae45c Mon Sep 17 00:00:00 2001
From: Andrea Marziali 
Date: Mon, 31 Aug 2026 14:53:42 +0200
Subject: [PATCH 17/23] Support java 8/11 factories

---
 .../LambdaMetafactoryInstrumentation.java     | 30 ++++++++++--
 .../LambdaMetafactoryInstrumentationTest.java | 49 ++++++++++++++++---
 2 files changed, 67 insertions(+), 12 deletions(-)

diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java
index fc1f3faf1b2..c63c9a98b07 100644
--- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java
+++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java
@@ -48,6 +48,7 @@ public final class LambdaMetafactoryInstrumentation extends InstrumenterModule.C
   private static final String LAMBDA_CLASS_NAME_FIELD = "lambdaClassName";
   private static final String TARGET_CLASS_FIELD = "targetClass";
   private static final String INTERFACE_CLASS_FIELD = "interfaceClass";
+  private static final String LEGACY_INTERFACE_CLASS_FIELD = "samBase";
 
   public LambdaMetafactoryInstrumentation() {
     super("lambda");
@@ -82,7 +83,18 @@ public static final class HasMetafactoryFields implements ElementMatcher methods,
         int writerFlags,
         int readerFlags) {
-      return new MetafactoryClassVisitor(classVisitor, instrumentedType.getInternalName());
+      return new MetafactoryClassVisitor(
+          classVisitor,
+          instrumentedType.getInternalName(),
+          HasMetafactoryFields.interfaceClassField(instrumentedType));
     }
   }
 
   private static final class MetafactoryClassVisitor extends ClassVisitor {
     private final String slashClassName;
+    private final String interfaceClassField;
     private boolean injected;
 
-    MetafactoryClassVisitor(ClassVisitor cv, String slashClassName) {
+    MetafactoryClassVisitor(ClassVisitor cv, String slashClassName, String interfaceClassField) {
       super(OpenedClassReader.ASM_API, cv);
       this.slashClassName = slashClassName;
+      this.interfaceClassField = interfaceClassField;
     }
 
     @Override
@@ -144,7 +161,7 @@ public MethodVisitor visitMethod(
       // The byte-generation method changed in JDK 25.
       if (("spinInnerClass".equals(name) || "generateInnerClass".equals(name))
           && "()Ljava/lang/Class;".equals(descriptor)) {
-        return new MetafactoryMethodVisitor(api, mv, slashClassName, this);
+        return new MetafactoryMethodVisitor(api, mv, slashClassName, interfaceClassField, this);
       }
       return mv;
     }
@@ -161,15 +178,18 @@ public void visitEnd() {
 
   private static final class MetafactoryMethodVisitor extends MethodVisitor {
     private final String slashClassName;
+    private final String interfaceClassField;
     private final MetafactoryClassVisitor declaringVisitor;
 
     MetafactoryMethodVisitor(
         int api,
         MethodVisitor mv,
         String slashClassName,
+        String interfaceClassField,
         MetafactoryClassVisitor declaringVisitor) {
       super(api, mv);
       this.slashClassName = slashClassName;
+      this.interfaceClassField = interfaceClassField;
       this.declaringVisitor = declaringVisitor;
     }
 
@@ -198,7 +218,7 @@ public void visitMethodInsn(
         super.visitVarInsn(Opcodes.ALOAD, 0);
         // Allows the helper to reject non-allowlisted interfaces before full matching.
         super.visitFieldInsn(
-            Opcodes.GETFIELD, slashClassName, INTERFACE_CLASS_FIELD, "Ljava/lang/Class;");
+            Opcodes.GETFIELD, slashClassName, interfaceClassField, "Ljava/lang/Class;");
         super.visitMethodInsn(
             Opcodes.INVOKESTATIC,
             Type.getInternalName(LambdaTransformerHelper.class),
diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java
index 33cd52438f5..7b173e0578a 100644
--- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java
+++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java
@@ -47,8 +47,7 @@ private static boolean injectsTransformCall(
     ClassWriter out = new ClassWriter(0);
     ClassVisitor visitor =
         new MetafactoryVisitorWrapper()
-            .wrap(
-                TypeDescription.ForLoadedType.of(Object.class), out, null, null, null, null, 0, 0);
+            .wrap(realMetafactoryDescription(), out, null, null, null, null, 0, 0);
     new ClassReader(in.toByteArray()).accept(visitor, 0);
 
     AtomicBoolean found = new AtomicBoolean(false);
@@ -177,12 +176,27 @@ void doesNotInjectOnUnrelatedToByteArrayOwner() {
 
   /** Verifies every field read by the injected bytecode, including inherited fields. */
   @Test
-  void structureMatcherAcceptsTheRealMetafactory() throws Exception {
-    TypeDescription metafactory =
-        TypeDescription.ForLoadedType.of(
-            Class.forName("java.lang.invoke.InnerClassLambdaMetafactory"));
+  void structureMatcherAcceptsTheRealMetafactory() {
+    assertTrue(
+        new LambdaMetafactoryInstrumentation()
+            .structureMatcher()
+            .matches(realMetafactoryDescription()));
+  }
 
-    assertTrue(new LambdaMetafactoryInstrumentation().structureMatcher().matches(metafactory));
+  @Test
+  void structureMatcherAcceptsCurrentInterfaceField() {
+    assertTrue(
+        new LambdaMetafactoryInstrumentation()
+            .structureMatcher()
+            .matches(TypeDescription.ForLoadedType.of(CurrentMetafactoryFields.class)));
+  }
+
+  @Test
+  void structureMatcherAcceptsLegacyInterfaceField() {
+    assertTrue(
+        new LambdaMetafactoryInstrumentation()
+            .structureMatcher()
+            .matches(TypeDescription.ForLoadedType.of(LegacyMetafactoryFields.class)));
   }
 
   @Test
@@ -193,6 +207,27 @@ void structureMatcherRejectsTypeWithoutTheFields() {
             .matches(TypeDescription.ForLoadedType.of(Object.class)));
   }
 
+  private static TypeDescription realMetafactoryDescription() {
+    try {
+      return TypeDescription.ForLoadedType.of(
+          Class.forName("java.lang.invoke.InnerClassLambdaMetafactory"));
+    } catch (ClassNotFoundException e) {
+      throw new AssertionError(e);
+    }
+  }
+
+  private static final class CurrentMetafactoryFields {
+    private String lambdaClassName;
+    private Class targetClass;
+    private Class interfaceClass;
+  }
+
+  private static final class LegacyMetafactoryFields {
+    private String lambdaClassName;
+    private Class targetClass;
+    private Class samBase;
+  }
+
   @Test
   void nonRunnableLambdaBypassesTransformer() {
     byte[] originalBytes = new byte[0];

From a721063dffaab08844313892d6eff68e8c800566 Mon Sep 17 00:00:00 2001
From: Andrea Marziali 
Date: Mon, 31 Aug 2026 15:31:51 +0200
Subject: [PATCH 18/23] Improve smoke test

---
 .../LambdaMetafactoryDisabledForkedTest.java  | 19 +++++++++++++++++++
 .../java11/testdog/moduleapp/LambdaTask.java  | 13 ++++---------
 ...Java9ModulesLambdaDisabledSmokeTest.groovy | 16 ----------------
 3 files changed, 23 insertions(+), 25 deletions(-)
 create mode 100644 dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryDisabledForkedTest.java
 delete mode 100644 dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaDisabledSmokeTest.groovy

diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryDisabledForkedTest.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryDisabledForkedTest.java
new file mode 100644
index 00000000000..7edd1873b90
--- /dev/null
+++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryDisabledForkedTest.java
@@ -0,0 +1,19 @@
+package testdog.trace.instrumentation.lambda;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import datadog.trace.agent.test.AbstractInstrumentationTest;
+import datadog.trace.bootstrap.FieldBackedContextAccessor;
+import datadog.trace.test.junit.utils.config.WithConfig;
+import org.junit.jupiter.api.Test;
+
+@WithConfig(key = "trace.lambda.enabled", value = "false")
+public class LambdaMetafactoryDisabledForkedTest extends AbstractInstrumentationTest {
+
+  @Test
+  void runnableLambdaIsNotFieldInjected() {
+    Runnable lambda = () -> {};
+
+    assertFalse(lambda instanceof FieldBackedContextAccessor);
+  }
+}
diff --git a/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java b/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java
index 251a5eb24d9..9d8338bd1b8 100644
--- a/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java
+++ b/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java
@@ -19,8 +19,7 @@ public static void runOnExecutor() throws InterruptedException {
     try {
       final CountDownLatch latch = new CountDownLatch(1);
       final Runnable task = latch::countDown;
-      assertFieldInjection(
-          task, Boolean.parseBoolean(System.getProperty("dd.trace.lambda.enabled", "true")));
+      assertFieldInjection(task);
       pool.execute(task);
       if (!latch.await(10, TimeUnit.SECONDS)) {
         throw new IllegalStateException("lambda task did not run");
@@ -30,19 +29,15 @@ public static void runOnExecutor() throws InterruptedException {
     }
   }
 
-  private static void assertFieldInjection(final Runnable task, final boolean expected) {
+  private static void assertFieldInjection(final Runnable task) {
     final List interfaces = new ArrayList<>();
     for (final Class type : task.getClass().getInterfaces()) {
       interfaces.add(type.getName());
     }
     final boolean injected = interfaces.contains(FIELD_BACKED_CONTEXT_ACCESSOR);
-    if (injected != expected) {
+    if (!injected) {
       throw new IllegalStateException(
-          "expected lambda field-injection="
-              + expected
-              + " but was "
-              + injected
-              + "; "
+          "expected lambda field-injection; "
               + task.getClass().getName()
               + " implements "
               + interfaces);
diff --git a/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaDisabledSmokeTest.groovy b/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaDisabledSmokeTest.groovy
deleted file mode 100644
index b5c045d033f..00000000000
--- a/dd-smoke-tests/java9-modules/src/test/groovy/datadog/smoketest/Java9ModulesLambdaDisabledSmokeTest.groovy
+++ /dev/null
@@ -1,16 +0,0 @@
-package datadog.smoketest
-
-import datadog.environment.JavaVirtualMachine
-import datadog.environment.OperatingSystem
-import spock.lang.IgnoreIf
-
-/** Verifies lambda transformation can be disabled for a named module. */
-@IgnoreIf({
-  OperatingSystem.isLinux() && OperatingSystem.architecture().isArm64() && JavaVirtualMachine.isJ9()
-})
-class Java9ModulesLambdaDisabledSmokeTest extends Java9ModulesSmokeTest {
-  @Override
-  def javaProperties() {
-    return super.javaProperties() + "-Ddd.trace.lambda.enabled=false"
-  }
-}

From 1bc6bbcbb0f4dd918a2d58d037902758a0655771 Mon Sep 17 00:00:00 2001
From: Andrea Marziali 
Date: Tue, 1 Sep 2026 10:33:27 +0200
Subject: [PATCH 19/23] Add a separate lambda matcher and restore cache
 correctness

---
 .../java/lang/invoke/LambdaTransformer.java   |  7 +-
 .../lang/invoke/LambdaTransformerHelper.java  |  4 +-
 .../trace/agent/tooling/AgentInstaller.java   |  8 +-
 .../trace/agent/tooling/CombiningMatcher.java | 27 ++++++-
 .../tooling/CombiningTransformerBuilder.java  | 43 ++++++++++-
 .../agent/tooling/LambdaMatchRecorder.java    | 31 ++++++++
 .../bytebuddy/DDJava9LambdaTransformer.java   |  5 +-
 .../agent/tooling/CombiningMatcherTest.java   | 48 ++++++++++++
 .../trace/agent/tooling/Instrumenter.java     |  9 +++
 .../tooling/bytebuddy/memoize/Memoizer.java   | 31 +++++---
 .../bytebuddy/outline/TypeFactory.java        | 37 +++++++---
 .../bytebuddy/outline/TypePoolFacade.java     |  8 +-
 .../bytebuddy/outline/WithLocation.java       |  5 ++
 .../bytebuddy/outline/TypeFactoryTest.java    | 73 ++++++++++++++++++-
 .../runnable/RunnableInstrumentation.java     | 11 +++
 .../LambdaMetafactoryInstrumentationTest.java | 14 ++--
 16 files changed, 322 insertions(+), 39 deletions(-)
 create mode 100644 dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/LambdaMatchRecorder.java
 create mode 100644 dd-java-agent/agent-installer/src/test/java/datadog/trace/agent/tooling/CombiningMatcherTest.java

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
index daf9525a1dd..2bffc2423e6 100644
--- 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
@@ -6,7 +6,12 @@ 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
+   * @param interfaceClassName the functional interface implemented by the lambda
    * @return the transformed bytes, or {@code null}/the original bytes if unchanged
    */
-  byte[] transform(String slashClassName, Class targetClass, byte[] classBytes);
+  byte[] transform(
+      String slashClassName,
+      Class targetClass,
+      byte[] classBytes,
+      String interfaceClassName);
 }
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
index 5054e657589..79f70052e06 100644
--- 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
@@ -47,7 +47,9 @@ public static byte[] transform(
       }
       TRANSFORMING.set(Boolean.TRUE);
       try {
-        byte[] result = transformer.transform(lambdaClassName, targetClass, classBytes);
+        byte[] result =
+            transformer.transform(
+                lambdaClassName, targetClass, classBytes, interfaceClass.getName());
         if (result == null) {
           log.debug("Lambda {} not transformed", lambdaClassName);
           return classBytes;
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 501f97866c6..1dbbeafa0e1 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
@@ -299,8 +299,12 @@ private static LambdaTransformer newLambdaTransformer(
     // Avoid invoking the instrumented metafactory while installing its transformer.
     return new LambdaTransformer() {
       @Override
-      public byte[] transform(String slashClassName, Class targetClass, byte[] classBytes) {
-        TypePoolFacade.beginLambdaTransform();
+      public byte[] transform(
+          String slashClassName,
+          Class targetClass,
+          byte[] classBytes,
+          String interfaceClassName) {
+        TypePoolFacade.beginLambdaTransform(interfaceClassName);
         try {
           return classFileTransformer.transform(
               targetClass.getClassLoader(),
diff --git a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningMatcher.java b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningMatcher.java
index 5ac7ea2e67c..c6cf17d0098 100644
--- a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningMatcher.java
+++ b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningMatcher.java
@@ -2,9 +2,11 @@
 
 import static datadog.trace.api.config.TraceInstrumentationConfig.EXPERIMENTAL_DEFER_INTEGRATIONS_UNTIL;
 import static datadog.trace.util.AgentThreadFactory.AgentThread.RETRANSFORMER;
+import static java.util.Collections.unmodifiableMap;
 
 import datadog.trace.agent.tooling.bytebuddy.matcher.CustomExcludes;
 import datadog.trace.agent.tooling.bytebuddy.matcher.ProxyClassIgnores;
+import datadog.trace.agent.tooling.bytebuddy.outline.TypePoolFacade;
 import datadog.trace.api.InstrumenterConfig;
 import datadog.trace.api.time.TimeUtils;
 import datadog.trace.util.AgentTaskScheduler;
@@ -12,8 +14,10 @@
 import java.security.ProtectionDomain;
 import java.util.ArrayList;
 import java.util.BitSet;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import net.bytebuddy.agent.builder.AgentBuilder;
@@ -45,13 +49,22 @@ final class CombiningMatcher implements AgentBuilder.RawMatcher {
 
   private final BitSet knownTypesMask;
   private final MatchRecorder[] matchers;
+  private final Map lambdaMatchers;
 
   private volatile boolean deferring;
 
   CombiningMatcher(
-      Instrumentation instrumentation, BitSet knownTypesMask, List matchers) {
+      Instrumentation instrumentation,
+      BitSet knownTypesMask,
+      List matchers,
+      Map> lambdaMatchers) {
     this.knownTypesMask = knownTypesMask;
     this.matchers = matchers.toArray(new MatchRecorder[0]);
+    Map lambdaMatchersByInterface = new HashMap<>();
+    lambdaMatchers.forEach(
+        (name, recorders) ->
+            lambdaMatchersByInterface.put(name, recorders.toArray(new LambdaMatchRecorder[0])));
+    this.lambdaMatchers = unmodifiableMap(lambdaMatchersByInterface);
 
     if (DEFER_MATCHING) {
       scheduleResumeMatching(instrumentation, InstrumenterConfig.get().deferIntegrationsUntil());
@@ -75,6 +88,18 @@ public boolean matches(
     ids.clear();
 
     long fromTick = InstrumenterMetrics.tick();
+    String lambdaInterface = TypePoolFacade.lambdaInterface();
+    if (null != lambdaInterface) {
+      LambdaMatchRecorder[] recorders = lambdaMatchers.get(lambdaInterface);
+      if (null != recorders) {
+        for (LambdaMatchRecorder recorder : recorders) {
+          recorder.record(target, classLoader, ids);
+        }
+      }
+      InstrumenterMetrics.matchType(fromTick);
+      return !ids.isEmpty();
+    }
+
     knownTypesIndex.apply(target.getName(), knownTypesMask, ids);
     if (ids.isEmpty()) {
       InstrumenterMetrics.knownTypeMiss(fromTick);
diff --git a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java
index 5a7e4b9c3df..2738ef18e4b 100644
--- a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java
+++ b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java
@@ -59,6 +59,10 @@ public final class CombiningTransformerBuilder
   private final Map, ElementMatcher> contextStoreInjection =
       new HashMap<>();
 
+  private final Map> lambdaMatchers = new HashMap<>();
+  private final Map, List>
+      lambdaContextStoreInjection = new HashMap<>();
+
   private final AgentBuilder agentBuilder;
   private final InstrumenterIndex instrumenterIndex;
   private final int knownTransformationCount;
@@ -162,9 +166,37 @@ private void buildTypeInstrumentation(Instrumenter member) {
     }
 
     buildTypeMatcher(member, transformationId);
+    buildLambdaMatcher(member, transformationId);
     buildTypeAdvice(member, transformationId);
   }
 
+  private void buildLambdaMatcher(Instrumenter member, int transformationId) {
+    if (!(member instanceof Instrumenter.ForLambda)) {
+      return;
+    }
+
+    Instrumenter.ForLambda lambdaInstrumenter = (Instrumenter.ForLambda) member;
+    ElementMatcher typeMatcher = lambdaInstrumenter.lambdaMatcher();
+    if (member instanceof Instrumenter.WithTypeStructure) {
+      typeMatcher =
+          new ElementMatcher.Junction.Conjunction<>(
+              typeMatcher, ((Instrumenter.WithTypeStructure) member).structureMatcher());
+    }
+
+    LambdaMatchRecorder recorder =
+        new LambdaMatchRecorder(
+            transformationId, typeMatcher, requireBoth(classLoaderMatcher, muzzle));
+    lambdaMatchers
+        .computeIfAbsent(lambdaInstrumenter.lambdaInterface(), ignored -> new ArrayList<>())
+        .add(recorder);
+
+    for (Map.Entry store : contextStore.entrySet()) {
+      lambdaContextStoreInjection
+          .computeIfAbsent(store, ignored -> new ArrayList<>())
+          .add(recorder);
+    }
+  }
+
   private void buildTypeMatcher(Instrumenter member, int transformationId) {
 
     if (member instanceof Instrumenter.ForSingleType) {
@@ -291,7 +323,7 @@ public ClassFileTransformer installOn(Instrumentation instrumentation) {
     }
 
     return agentBuilder
-        .type(new CombiningMatcher(instrumentation, knownTypesMask, matchers))
+        .type(new CombiningMatcher(instrumentation, knownTypesMask, matchers, lambdaMatchers))
         .and(NOT_DECORATOR_MATCHER)
         .transform(defaultTransformers())
         .transform(new SplittingTransformer(transformers))
@@ -360,6 +392,15 @@ private void applyContextStoreInjection(
 
     matchers.add(new MatchRecorder.ForContextStore(transformationId, activation, contextMatcher));
     transformers[transformationId] = new AdviceStack(new VisitingTransformer(contextAdvice));
+
+    List lambdaRecorders = lambdaContextStoreInjection.get(contextStore);
+    if (null != lambdaRecorders) {
+      // Lambda transformation happens before definition, so its field injector can be selected
+      // directly along with the instrumentation that requested this context store.
+      for (LambdaMatchRecorder recorder : lambdaRecorders) {
+        recorder.addTransformation(transformationId);
+      }
+    }
   }
 
   static final class VisitingTransformer implements AgentBuilder.Transformer {
diff --git a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/LambdaMatchRecorder.java b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/LambdaMatchRecorder.java
new file mode 100644
index 00000000000..629913181d0
--- /dev/null
+++ b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/LambdaMatchRecorder.java
@@ -0,0 +1,31 @@
+package datadog.trace.agent.tooling;
+
+import java.util.BitSet;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+/** Selects the transformations explicitly registered for a generated lambda interface. */
+final class LambdaMatchRecorder {
+  private final ElementMatcher typeMatcher;
+  private final ElementMatcher classLoaderMatcher;
+  private final BitSet transformationIds = new BitSet();
+
+  LambdaMatchRecorder(
+      int transformationId,
+      ElementMatcher typeMatcher,
+      ElementMatcher classLoaderMatcher) {
+    this.typeMatcher = typeMatcher;
+    this.classLoaderMatcher = classLoaderMatcher;
+    transformationIds.set(transformationId);
+  }
+
+  void addTransformation(int transformationId) {
+    transformationIds.set(transformationId);
+  }
+
+  void record(TypeDescription type, ClassLoader classLoader, BitSet matches) {
+    if (classLoaderMatcher.matches(classLoader) && typeMatcher.matches(type)) {
+      matches.or(transformationIds);
+    }
+  }
+}
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
index 1ffa6640693..d385d385589 100644
--- 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
@@ -24,8 +24,9 @@ public DDJava9LambdaTransformer(ClassFileTransformer classFileTransformer) {
   }
 
   @Override
-  public byte[] transform(String slashClassName, Class targetClass, byte[] classBytes) {
-    TypePoolFacade.beginLambdaTransform();
+  public byte[] transform(
+      String slashClassName, Class targetClass, byte[] classBytes, String interfaceClassName) {
+    TypePoolFacade.beginLambdaTransform(interfaceClassName);
     try {
       return classFileTransformer.transform(
           targetClass.getModule(),
diff --git a/dd-java-agent/agent-installer/src/test/java/datadog/trace/agent/tooling/CombiningMatcherTest.java b/dd-java-agent/agent-installer/src/test/java/datadog/trace/agent/tooling/CombiningMatcherTest.java
new file mode 100644
index 00000000000..c36fe8c0b8e
--- /dev/null
+++ b/dd-java-agent/agent-installer/src/test/java/datadog/trace/agent/tooling/CombiningMatcherTest.java
@@ -0,0 +1,48 @@
+package datadog.trace.agent.tooling;
+
+import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static java.util.Collections.singletonMap;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import datadog.trace.agent.tooling.bytebuddy.outline.TypePoolFacade;
+import java.util.BitSet;
+import java.util.List;
+import java.util.Map;
+import net.bytebuddy.description.type.TypeDescription;
+import org.junit.jupiter.api.Test;
+
+class CombiningMatcherTest {
+
+  @Test
+  void selectsOnlyTransformationsRegisteredForLambdaInterface() {
+    TypeDescription target = new TypeDescription.ForLoadedType(getClass());
+    ClassLoader classLoader = getClass().getClassLoader();
+    LambdaMatchRecorder recorder =
+        new LambdaMatchRecorder(3, named(getClass().getName()), loader -> loader == classLoader);
+    recorder.addTransformation(7);
+
+    Map> lambdaMatchers =
+        singletonMap(Runnable.class.getName(), singletonList(recorder));
+    CombiningMatcher matcher =
+        new CombiningMatcher(null, new BitSet(), emptyList(), lambdaMatchers);
+
+    TypePoolFacade.beginLambdaTransform(Runnable.class.getName());
+    try {
+      assertTrue(matcher.matches(target, classLoader, null, null, null));
+      assertTrue(CombiningMatcher.recordedMatches.get().get(3));
+      assertTrue(CombiningMatcher.recordedMatches.get().get(7));
+    } finally {
+      TypePoolFacade.endLambdaTransform();
+    }
+
+    TypePoolFacade.beginLambdaTransform(java.util.function.Supplier.class.getName());
+    try {
+      assertFalse(matcher.matches(target, classLoader, null, null, null));
+    } finally {
+      TypePoolFacade.endLambdaTransform();
+    }
+  }
+}
diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Instrumenter.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Instrumenter.java
index b146121d273..c744f3516ea 100644
--- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Instrumenter.java
+++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Instrumenter.java
@@ -36,6 +36,15 @@ interface ForTypeHierarchy {
   /** Instrumentation that transforms types on the bootstrap class-path. */
   interface ForBootstrap {}
 
+  /** Instrumentation selected directly for an exact lambda interface allowed by the lambda trie. */
+  interface ForLambda {
+    /** Exact functional interface name reported by the lambda metafactory. */
+    String lambdaInterface();
+
+    /** Additional checks not already guaranteed by the lambda metafactory. */
+    ElementMatcher lambdaMatcher();
+  }
+
   /**
    * Instrumentation that matches a series of types configured at runtime. This is used for last
    * minute additions in the field such as testing a new JDBC driver that is not yet in the allowed
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 b29989fc259..e6454169f2d 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,9 +155,7 @@ 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)
+      if ((isCacheable(target) && noMatchFilter.contains(targetName))
           || "java.lang.Object".equals(targetName)
           || target.isPrimitive()) {
         return false;
@@ -168,7 +166,7 @@ protected boolean doMatch(TypeDescription target) {
   }
 
   static BitSet memoizeHierarchy(TypeDescription type, Map localMemos) {
-    if (noMatchFilter.contains(type.getName())) {
+    if (isCacheable(type) && noMatchFilter.contains(type.getName())) {
       return NO_MATCH;
     } else {
       return doMemoize(type, localMemos);
@@ -178,13 +176,14 @@ static BitSet memoizeHierarchy(TypeDescription type, Map localMe
   static BitSet doMemoize(TypeDescription type, Map localMemos) {
 
     String name = type.getName();
+    boolean cacheable = isCacheable(type);
     BitSet memo = localMemos.get(name);
     if (null != memo) {
       return memo; // short-circuit circular references
     }
 
     long fromTick = InstrumenterMetrics.tick();
-    SharedTypeInfo sharedMemo = memos.find(name);
+    SharedTypeInfo sharedMemo = cacheable ? memos.find(name) : null;
     if (null != sharedMemo) {
       if (namesAreUnique || name.startsWith("java.") || sameOrigin(type, sharedMemo)) {
         InstrumenterMetrics.reuseTypeMemo(fromTick);
@@ -235,16 +234,20 @@ static BitSet doMemoize(TypeDescription type, Map localMemos) {
 
     // update no-match filter if there's no interesting matches and result is complete
     if (memo.nextSetBit(INTERNAL_MATCHERS) < 0 && !memo.get(isPartial.matcherId)) {
-      noMatchFilter.add(name);
+      if (cacheable) {
+        noMatchFilter.add(name);
+      }
       return NO_MATCH;
     }
 
-    // otherwise share result for this location (other locations may have different results)
-    if (namesAreUnique || name.startsWith("java.") || !(type instanceof WithLocation)) {
-      memos.share(name, 0, null, memo);
-    } else {
-      WithLocation origin = (WithLocation) type;
-      memos.share(name, origin.getClassLoaderId(), origin.getClassFile(), memo);
+    if (cacheable) {
+      // otherwise share result for this location (other locations may have different results)
+      if (namesAreUnique || name.startsWith("java.") || !(type instanceof WithLocation)) {
+        memos.share(name, 0, null, memo);
+      } else {
+        WithLocation origin = (WithLocation) type;
+        memos.share(name, origin.getClassLoaderId(), origin.getClassFile(), memo);
+      }
     }
 
     return memo;
@@ -255,6 +258,10 @@ static boolean potentialMatch(String name) {
     return !noMatchFilter.contains(name);
   }
 
+  private static boolean isCacheable(TypeDescription type) {
+    return !(type instanceof WithLocation) || ((WithLocation) type).isCacheable();
+  }
+
   private static boolean sameOrigin(TypeDescription type, SharedTypeInfo sharedMemo) {
     return !(type instanceof WithLocation)
         || sharedMemo.sameClassLoader(((WithLocation) type).getClassLoaderId())
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 6f044d89c27..73c4d3eb40e 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,7 +100,7 @@ final class TypeFactory {
 
   boolean createOutlines = OUTLINING_ENABLED;
 
-  boolean transformingLambda;
+  String lambdaInterface;
 
   ClassLoader originalClassLoader;
 
@@ -161,12 +161,16 @@ void beginTransform(String name, byte[] bytecode) {
     }
   }
 
-  void beginLambdaTransform() {
-    transformingLambda = true;
+  void beginLambdaTransform(String interfaceClassName) {
+    lambdaInterface = interfaceClassName;
   }
 
   void endLambdaTransform() {
-    transformingLambda = false;
+    lambdaInterface = null;
+  }
+
+  String lambdaInterface() {
+    return lambdaInterface;
   }
 
   /** Once matching is complete we need full descriptions for the actual transformation. */
@@ -241,6 +245,10 @@ private TypeDescription deferTypeResolution(String name) {
     return deferredTypes.computeIfAbsent(name, deferType);
   }
 
+  private boolean isLambdaTarget(String name) {
+    return null != lambdaInterface && name.equals(targetName);
+  }
+
   /** Attempts to resolve the named type using the current context. */
   TypeDescription resolveType(LazyType request) {
     if (null != classFileLocator) {
@@ -267,10 +275,10 @@ private TypeDescription lookupType(
     int classLoaderId = request.getClassLoaderId();
     boolean isOutline = typeParser == outlineTypeParser;
     long fromTick = InstrumenterMetrics.tick();
+    // Hidden lambda names may later be reused by an ordinary class definition.
+    boolean cacheable = !isLambdaTarget(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);
+    SharedTypeInfo sharedType = cacheable ? types.find(name) : null;
     if (null != sharedType
         && (name.startsWith("java.") || sharedType.sameClassLoader(classLoaderId))) {
       InstrumenterMetrics.reuseTypeDescription(fromTick, isOutline);
@@ -297,14 +305,16 @@ private TypeDescription lookupType(
 
     InstrumenterMetrics.buildTypeDescription(fromTick, isOutline);
 
-    if (MEMOIZING_ENABLED && null != type) {
+    if (cacheable && MEMOIZING_ENABLED && null != type) {
       if (type.isPublic()) {
         isPublicFilter.add(name);
       }
     }
 
-    // share result, whether we found it or not
-    types.share(name, classLoaderId, classFile, type);
+    if (cacheable) {
+      // share result, whether we found it or not
+      types.share(name, classLoaderId, classFile, type);
+    }
 
     return type;
   }
@@ -379,6 +389,11 @@ public byte[] getBytecode() {
       return null;
     }
 
+    @Override
+    public boolean isCacheable() {
+      return !isLambdaTarget(name);
+    }
+
     private ClassFileLocator.Resolution locateClassFile() {
       if (name.equals(targetName)) {
         return new ClassFileLocator.Resolution.Explicit(targetBytecode);
@@ -407,7 +422,7 @@ public TypeList.Generic getInterfaces() {
 
     @Override
     public boolean isPublic() {
-      return isPublicFilter.contains(name) || super.isPublic();
+      return (isCacheable() && isPublicFilter.contains(name)) || super.isPublic();
     }
 
     private TypeDescription outline() {
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 f4c08b8cc1c..5e576849afa 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,14 +49,18 @@ public static void beginTransform(String name, byte[] bytecode) {
     typeFactory.get().beginTransform(name, bytecode);
   }
 
-  public static void beginLambdaTransform() {
-    typeFactory.get().beginLambdaTransform();
+  public static void beginLambdaTransform(String interfaceClassName) {
+    typeFactory.get().beginLambdaTransform(interfaceClassName);
   }
 
   public static void endLambdaTransform() {
     typeFactory.get().endLambdaTransform();
   }
 
+  public static String lambdaInterface() {
+    return typeFactory.get().lambdaInterface();
+  }
+
   /** 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/java/datadog/trace/agent/tooling/bytebuddy/outline/WithLocation.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/outline/WithLocation.java
index 55bde94f0ba..3e1aa6f329b 100644
--- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/outline/WithLocation.java
+++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/outline/WithLocation.java
@@ -9,4 +9,9 @@ public interface WithLocation {
   URL getClassFile();
 
   byte[] getBytecode();
+
+  /** Whether details for this type may be shared by symbolic name. */
+  default boolean isCacheable() {
+    return true;
+  }
 }
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
index e00f938ce3d..089e18be343 100644
--- 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
@@ -1,10 +1,17 @@
 package datadog.trace.agent.tooling.bytebuddy.outline;
 
+import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.isPublic;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import datadog.trace.agent.tooling.bytebuddy.memoize.MemoizedMatchers;
 import java.util.concurrent.Callable;
 import net.bytebuddy.ByteBuddy;
+import net.bytebuddy.description.modifier.Visibility;
 import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
 import org.junit.jupiter.api.Test;
 
 class TypeFactoryTest {
@@ -28,11 +35,52 @@ void rebuildsLambdaTransformationTargetFromSuppliedBytes() {
         Callable.class.getName(), resolveInterface(name, bytes(name, Callable.class), true));
   }
 
+  @Test
+  void doesNotCacheLambdaTransformationTarget() {
+    String name = getClass().getName() + "$UncachedLambdaTarget";
+
+    assertEquals(
+        Runnable.class.getName(), resolveInterface(name, bytes(name, Runnable.class), true));
+    assertEquals(
+        Callable.class.getName(), resolveInterface(name, bytes(name, Callable.class), false));
+  }
+
+  @Test
+  void doesNotMemoizeLambdaMatcherResults() {
+    ElementMatcher implementsRunnable =
+        new MemoizedMatchers().hasInterface(named(Runnable.class.getName()));
+
+    String lambdaFirstName = getClass().getName() + "$LambdaMatcherFirst";
+    assertTrue(
+        matches(lambdaFirstName, bytes(lambdaFirstName, Runnable.class), true, implementsRunnable));
+    assertFalse(
+        matches(
+            lambdaFirstName, bytes(lambdaFirstName, Callable.class), false, implementsRunnable));
+
+    String lambdaSecondName = getClass().getName() + "$LambdaMatcherSecond";
+    assertFalse(
+        matches(
+            lambdaSecondName, bytes(lambdaSecondName, Callable.class), false, implementsRunnable));
+    assertTrue(
+        matches(
+            lambdaSecondName, bytes(lambdaSecondName, Runnable.class), true, implementsRunnable));
+  }
+
+  @Test
+  void doesNotReuseCachedVisibilityForLambda() {
+    ElementMatcher isPublic = isPublic();
+    String name = getClass().getName() + "$LambdaVisibility";
+
+    assertTrue(matches(name, bytes(name, Runnable.class, Visibility.PUBLIC), false, isPublic));
+    assertFalse(
+        matches(name, bytes(name, Runnable.class, Visibility.PACKAGE_PRIVATE), true, isPublic));
+  }
+
   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.beginLambdaTransform(Runnable.class.getName());
     }
     typeFactory.beginTransform(name, bytecode);
     try {
@@ -46,11 +94,34 @@ private static String resolveInterface(String name, byte[] bytecode, boolean lam
     }
   }
 
+  private static boolean matches(
+      String name, byte[] bytecode, boolean lambda, ElementMatcher matcher) {
+    TypeFactory typeFactory = TypeFactory.typeFactory.get();
+    typeFactory.switchContext(TypeFactoryTest.class.getClassLoader());
+    if (lambda) {
+      typeFactory.beginLambdaTransform(Runnable.class.getName());
+    }
+    typeFactory.beginTransform(name, bytecode);
+    try {
+      return matcher.matches(TypeFactory.findType(name));
+    } finally {
+      typeFactory.endTransform();
+      if (lambda) {
+        typeFactory.endLambdaTransform();
+      }
+    }
+  }
+
   private static byte[] bytes(String name, Class implementedInterface) {
+    return bytes(name, implementedInterface, Visibility.PUBLIC);
+  }
+
+  private static byte[] bytes(String name, Class implementedInterface, Visibility visibility) {
     return new ByteBuddy()
         .subclass(Object.class)
         .name(name)
         .implement(implementedInterface)
+        .modifiers(visibility)
         .make()
         .getBytes();
   }
diff --git a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/main/java/datadog/trace/instrumentation/java/concurrent/runnable/RunnableInstrumentation.java b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/main/java/datadog/trace/instrumentation/java/concurrent/runnable/RunnableInstrumentation.java
index 32dcff9f561..64f7fe88522 100644
--- a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/main/java/datadog/trace/instrumentation/java/concurrent/runnable/RunnableInstrumentation.java
+++ b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/main/java/datadog/trace/instrumentation/java/concurrent/runnable/RunnableInstrumentation.java
@@ -29,6 +29,7 @@
 @AutoService(InstrumenterModule.class)
 public final class RunnableInstrumentation extends InstrumenterModule.ContextTracking
     implements Instrumenter.ForBootstrap,
+        Instrumenter.ForLambda,
         Instrumenter.ForTypeHierarchy,
         Instrumenter.HasMethodAdvice {
 
@@ -41,6 +42,16 @@ public String hierarchyMarkerType() {
     return null; // bootstrap type
   }
 
+  @Override
+  public String lambdaInterface() {
+    return Runnable.class.getName();
+  }
+
+  @Override
+  public ElementMatcher lambdaMatcher() {
+    return notExcludedByName(RUNNABLE);
+  }
+
   @Override
   public ElementMatcher hierarchyMatcher() {
     return notExcludedByName(RUNNABLE)
diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java
index 7b173e0578a..d7ea09d6c15 100644
--- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java
+++ b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java
@@ -20,6 +20,7 @@
 import datadog.trace.instrumentation.java.lang.invoke.LambdaMetafactoryInstrumentation.MetafactoryVisitorWrapper;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Supplier;
 import net.bytebuddy.description.type.TypeDescription;
 import net.bytebuddy.jar.asm.ClassReader;
@@ -233,7 +234,7 @@ void nonRunnableLambdaBypassesTransformer() {
     byte[] originalBytes = new byte[0];
     AtomicBoolean transformed = new AtomicBoolean();
     LambdaTransformerHolder.set(
-        (className, targetClass, classBytes) -> {
+        (className, targetClass, classBytes, interfaceClassName) -> {
           transformed.set(true);
           return classBytes;
         });
@@ -253,9 +254,11 @@ void nonRunnableLambdaBypassesTransformer() {
   void exactRunnableInterfaceUsesTransformer() {
     byte[] originalBytes = new byte[0];
     AtomicBoolean transformed = new AtomicBoolean();
+    AtomicReference transformedInterface = new AtomicReference<>();
     LambdaTransformerHolder.set(
-        (className, targetClass, classBytes) -> {
+        (className, targetClass, classBytes, interfaceClassName) -> {
           transformed.set(true);
+          transformedInterface.set(interfaceClassName);
           return classBytes;
         });
     try {
@@ -265,6 +268,7 @@ void exactRunnableInterfaceUsesTransformer() {
 
       assertSame(originalBytes, result);
       assertTrue(transformed.get());
+      assertEquals(Runnable.class.getName(), transformedInterface.get());
     } finally {
       LambdaTransformerHolder.set(null);
     }
@@ -275,7 +279,7 @@ void runnableSubinterfaceBypassesTransformer() {
     byte[] originalBytes = new byte[0];
     AtomicBoolean transformed = new AtomicBoolean();
     LambdaTransformerHolder.set(
-        (className, targetClass, classBytes) -> {
+        (className, targetClass, classBytes, interfaceClassName) -> {
           transformed.set(true);
           return classBytes;
         });
@@ -297,7 +301,7 @@ void transformerFailureFallsBackAndDoesNotPoisonNextLambda() {
     byte[] transformedBytes = new byte[1];
     AtomicInteger calls = new AtomicInteger();
     LambdaTransformerHolder.set(
-        (className, targetClass, classBytes) -> {
+        (className, targetClass, classBytes, interfaceClassName) -> {
           if (calls.getAndIncrement() == 0) {
             throw new IllegalStateException("expected test failure");
           }
@@ -324,7 +328,7 @@ void nullTransformFallsBackAndDoesNotPoisonNextLambda() {
     byte[] transformedBytes = new byte[1];
     AtomicInteger calls = new AtomicInteger();
     LambdaTransformerHolder.set(
-        (className, targetClass, classBytes) ->
+        (className, targetClass, classBytes, interfaceClassName) ->
             calls.getAndIncrement() == 0 ? null : transformedBytes);
     try {
       assertSame(

From b11c8c6c6e7370e459122b18748c69f5c3c5bf9d Mon Sep 17 00:00:00 2001
From: Andrea Marziali 
Date: Tue, 1 Sep 2026 10:46:17 +0200
Subject: [PATCH 20/23] spotless

---
 .../instrumentation/java/lang/invoke/LambdaTransformer.java  | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

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
index 2bffc2423e6..0c9bb065e88 100644
--- 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
@@ -10,8 +10,5 @@ public interface LambdaTransformer {
    * @return the transformed bytes, or {@code null}/the original bytes if unchanged
    */
   byte[] transform(
-      String slashClassName,
-      Class targetClass,
-      byte[] classBytes,
-      String interfaceClassName);
+      String slashClassName, Class targetClass, byte[] classBytes, String interfaceClassName);
 }

From 35be7265ab776fdb5537df6b8007a47d164e0597 Mon Sep 17 00:00:00 2001
From: Andrea Marziali 
Date: Tue, 1 Sep 2026 14:10:06 +0200
Subject: [PATCH 21/23] refactor

---
 .../java-lambda/java-lambda-1.8/build.gradle  | 20 -------------------
 .../java/java-lang/java-lang-1.8/build.gradle |  4 ++++
 .../java-lang/java-lang-1.8/gradle.lockfile   |  3 +++
 .../LambdaMetafactoryInstrumentation.java     |  0
 .../LambdaMetafactoryInstrumentationTest.java |  0
 .../lambda/ClojureAFnIntegrationTest.java     |  0
 .../LambdaMetafactoryDisabledForkedTest.java  |  0
 .../LambdaMetafactoryIntegrationTest.java     |  0
 settings.gradle.kts                           |  1 -
 9 files changed, 7 insertions(+), 21 deletions(-)
 delete mode 100644 dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/build.gradle
 rename dd-java-agent/instrumentation/java/{java-lambda/java-lambda-1.8 => java-lang/java-lang-1.8}/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java (100%)
 rename dd-java-agent/instrumentation/java/{java-lambda/java-lambda-1.8 => java-lang/java-lang-1.8}/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java (100%)
 rename dd-java-agent/instrumentation/java/{java-lambda/java-lambda-1.8 => java-lang/java-lang-1.8}/src/test/java/testdog/trace/instrumentation/lambda/ClojureAFnIntegrationTest.java (100%)
 rename dd-java-agent/instrumentation/java/{java-lambda/java-lambda-1.8 => java-lang/java-lang-1.8}/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryDisabledForkedTest.java (100%)
 rename dd-java-agent/instrumentation/java/{java-lambda/java-lambda-1.8 => java-lang/java-lang-1.8}/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java (100%)

diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/build.gradle b/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/build.gradle
deleted file mode 100644
index a47efe00d4a..00000000000
--- a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/build.gradle
+++ /dev/null
@@ -1,20 +0,0 @@
-plugins {
-  id 'dd-trace-java.module.instrumentation'
-}
-
-muzzle {
-  pass {
-    coreJdk()
-  }
-}
-
-tasks.named("compileJava") {
-  configureCompiler(it, 8)
-}
-
-dependencies {
-  testImplementation project(':dd-java-agent:instrumentation:datadog:tracing:trace-annotation')
-  testImplementation 'org.clojure:clojure:1.11.1'
-  // Runs the existing Runnable propagation advice on injected lambdas.
-  testRuntimeOnly project(':dd-java-agent:instrumentation:java:java-concurrent:java-concurrent-1.8')
-}
diff --git a/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/build.gradle b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/build.gradle
index ab9fd7f8aa8..727e1efed3d 100644
--- a/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/build.gradle
+++ b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/build.gradle
@@ -12,7 +12,11 @@ muzzle {
 addTestSuiteForDir('latestDepTest', 'test')
 
 dependencies {
+  testImplementation project(':dd-java-agent:instrumentation:datadog:tracing:trace-annotation')
+  testImplementation 'org.clojure:clojure:1.11.1'
   testRuntimeOnly project(':dd-java-agent:instrumentation:datadog:asm:iast-instrumenter')
+  // Runs Runnable propagation advice on injected lambdas.
+  testRuntimeOnly project(':dd-java-agent:instrumentation:java:java-concurrent:java-concurrent-1.8')
   testImplementation libs.bundles.mockito
   testImplementation group: 'org.mockito', name: 'mockito-inline', version: '4.11.0'
 }
diff --git a/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/gradle.lockfile b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/gradle.lockfile
index 16223f2ee15..288d9c5b0ab 100644
--- a/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/gradle.lockfile
+++ b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/gradle.lockfile
@@ -69,6 +69,9 @@ org.apache.logging.log4j:log4j-api:2.26.1=spotbugs
 org.apache.logging.log4j:log4j-core:2.26.1=spotbugs
 org.apiguardian:apiguardian-api:1.1.2=latestDepTestCompileClasspath,testCompileClasspath
 org.checkerframework:checker-qual:3.33.0=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor
+org.clojure:clojure:1.11.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
+org.clojure:core.specs.alpha:0.2.62=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
+org.clojure:spec.alpha:0.3.218=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
 org.codehaus.groovy:groovy-ant:3.0.23=codenarc
 org.codehaus.groovy:groovy-docgenerator:3.0.23=codenarc
 org.codehaus.groovy:groovy-groovydoc:3.0.23=codenarc
diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java
similarity index 100%
rename from dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java
rename to dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java
diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java
similarity index 100%
rename from dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java
rename to dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java
diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/ClojureAFnIntegrationTest.java b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/ClojureAFnIntegrationTest.java
similarity index 100%
rename from dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/ClojureAFnIntegrationTest.java
rename to dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/ClojureAFnIntegrationTest.java
diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryDisabledForkedTest.java b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryDisabledForkedTest.java
similarity index 100%
rename from dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryDisabledForkedTest.java
rename to dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryDisabledForkedTest.java
diff --git a/dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java
similarity index 100%
rename from dd-java-agent/instrumentation/java/java-lambda/java-lambda-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java
rename to dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 88316d62a6c..14e988af3fe 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -399,7 +399,6 @@ include(
   ":dd-java-agent:instrumentation:java:java-concurrent:java-concurrent-21.0",
   ":dd-java-agent:instrumentation:java:java-concurrent:java-concurrent-25.0",
   ":dd-java-agent:instrumentation:java:java-io-1.8",
-  ":dd-java-agent:instrumentation:java:java-lambda:java-lambda-1.8",
   ":dd-java-agent:instrumentation:java:java-lang:java-lang-1.8",
   ":dd-java-agent:instrumentation:java:java-lang:java-lang-11.0",
   ":dd-java-agent:instrumentation:java:java-lang:java-lang-15.0",

From 1e0c3ced01dc4ffafcf7326b5e4850e0c5a0969b Mon Sep 17 00:00:00 2001
From: Andrea Marziali 
Date: Tue, 1 Sep 2026 14:56:57 +0200
Subject: [PATCH 22/23] prefilter enabled lambda interfaces

---
 .../lang/invoke/LambdaTransformerHelper.java  |  9 ++++--
 .../trace/agent/tooling/AgentInstaller.java   | 25 ++++++++++++---
 .../tooling/CombiningTransformerBuilder.java  |  4 +++
 .../agent/tooling/LambdaTransformerTest.java  | 32 +++++++++++++++++++
 4 files changed, 62 insertions(+), 8 deletions(-)
 create mode 100644 dd-java-agent/agent-installer/src/test/java/datadog/trace/agent/tooling/LambdaTransformerTest.java

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
index 79f70052e06..c55de1677c1 100644
--- 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
@@ -23,7 +23,11 @@ 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) {
+      if (interfaceClass == null) {
+        return classBytes;
+      }
+      String interfaceName = interfaceClass.getName();
+      if (LambdaInterfaceNameTrie.apply(interfaceName) != 1) {
         return classBytes;
       }
       LambdaTransformer transformer = LambdaTransformerHolder.get();
@@ -48,8 +52,7 @@ public static byte[] transform(
       TRANSFORMING.set(Boolean.TRUE);
       try {
         byte[] result =
-            transformer.transform(
-                lambdaClassName, targetClass, classBytes, interfaceClass.getName());
+            transformer.transform(lambdaClassName, targetClass, classBytes, interfaceName);
         if (result == null) {
           log.debug("Lambda {} not transformed", lambdaClassName);
           return classBytes;
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 1dbbeafa0e1..16998c0f490 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
@@ -259,7 +259,7 @@ public void applied(Iterable instrumentationNames) {
     InstrumenterState.resetDefaultState();
     try {
       ClassFileTransformer classFileTransformer = transformerBuilder.installOn(inst);
-      registerLambdaTransformer(classFileTransformer);
+      registerLambdaTransformer(classFileTransformer, transformerBuilder.lambdaInterfaces());
       return classFileTransformer;
     } finally {
       SharedTypePools.endInstall();
@@ -267,11 +267,26 @@ public void applied(Iterable instrumentationNames) {
   }
 
   /** 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);
+  private static void registerLambdaTransformer(
+      final ClassFileTransformer classFileTransformer, final String[] lambdaInterfaces) {
+    LambdaTransformer transformer =
+        lambdaInterfaces.length == 0 ? null : newLambdaTransformer(classFileTransformer);
+    LambdaTransformerHolder.set(filterLambdaTransformer(transformer, lambdaInterfaces));
+  }
+
+  static LambdaTransformer filterLambdaTransformer(
+      final LambdaTransformer transformer, final String[] lambdaInterfaces) {
+    if (transformer == null) {
+      return null;
     }
+    return (className, targetClass, classBytes, interfaceName) -> {
+      for (String enabledInterface : lambdaInterfaces) {
+        if (enabledInterface.equals(interfaceName)) {
+          return transformer.transform(className, targetClass, classBytes, interfaceName);
+        }
+      }
+      return null;
+    };
   }
 
   /**
diff --git a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java
index 2738ef18e4b..ab8eb3708b0 100644
--- a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java
+++ b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java
@@ -197,6 +197,10 @@ private void buildLambdaMatcher(Instrumenter member, int transformationId) {
     }
   }
 
+  String[] lambdaInterfaces() {
+    return lambdaMatchers.keySet().toArray(new String[0]);
+  }
+
   private void buildTypeMatcher(Instrumenter member, int transformationId) {
 
     if (member instanceof Instrumenter.ForSingleType) {
diff --git a/dd-java-agent/agent-installer/src/test/java/datadog/trace/agent/tooling/LambdaTransformerTest.java b/dd-java-agent/agent-installer/src/test/java/datadog/trace/agent/tooling/LambdaTransformerTest.java
new file mode 100644
index 00000000000..9f7f3b3c3e3
--- /dev/null
+++ b/dd-java-agent/agent-installer/src/test/java/datadog/trace/agent/tooling/LambdaTransformerTest.java
@@ -0,0 +1,32 @@
+package datadog.trace.agent.tooling;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import datadog.trace.bootstrap.instrumentation.java.lang.invoke.LambdaTransformer;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.jupiter.api.Test;
+
+class LambdaTransformerTest {
+
+  @Test
+  void onlyTransformsEnabledInterfaces() {
+    byte[] transformedBytes = new byte[0];
+    AtomicBoolean transformed = new AtomicBoolean();
+    LambdaTransformer delegate =
+        (className, targetClass, classBytes, interfaceName) -> {
+          transformed.set(true);
+          return transformedBytes;
+        };
+    LambdaTransformer transformer =
+        AgentInstaller.filterLambdaTransformer(delegate, new String[] {Runnable.class.getName()});
+
+    assertSame(
+        transformedBytes,
+        transformer.transform("test/Lambda", Object.class, new byte[0], Runnable.class.getName()));
+    transformed.set(false);
+    assertNull(transformer.transform("test/Lambda", Object.class, new byte[0], "other.Interface"));
+    assertFalse(transformed.get());
+  }
+}

From 9c89b93cbaf8e18fabd65c4bdc6d47e9d8094483 Mon Sep 17 00:00:00 2001
From: Andrea Marziali 
Date: Wed, 2 Sep 2026 16:05:47 +0200
Subject: [PATCH 23/23] Remove runnable related lambda instrumentation and make
 it opt-in

---
 dd-java-agent/agent-bootstrap/build.gradle    |   5 -
 .../java/concurrent/RunnableWrapper.java      |  13 +-
 .../java/concurrent/TPEHelper.java            |  10 +-
 .../lang/invoke/LambdaTransformerHelper.java  |   6 +-
 .../lang/invoke/lambda_interface_name.trie    |  14 +-
 .../trace/agent/tooling/AgentInstaller.java   |  17 +-
 .../trace/agent/tooling/Instrumenter.java     |   8 +-
 .../bytebuddy/matcher/ignored_class_name.trie |   2 +-
 dd-java-agent/benchmark/build.gradle          |  10 +-
 .../lambdabench/LambdaExecutorBenchmark.java  | 161 ------------------
 .../runnable/RunnableInstrumentation.java     |  11 --
 .../java/java-lang/java-lang-1.8/build.gradle |   4 -
 .../java-lang/java-lang-1.8/gradle.lockfile   |   3 -
 .../LambdaMetafactoryInstrumentation.java     |  25 ++-
 .../LambdaMetafactoryInstrumentationTest.java |  47 +----
 .../lambda/ClojureAFnIntegrationTest.java     |  26 ---
 .../LambdaMetafactoryDisabledForkedTest.java  |  19 ++-
 .../LambdaMetafactoryIntegrationTest.java     |  65 +++----
 .../TestRunnableLambdaInstrumentation.java    |  80 +++++++++
 .../moduleapp/ModuleApplication.java          |   3 -
 .../java11/testdog/moduleapp/LambdaTask.java  |  46 -----
 metadata/supported-configurations.json        |   2 +-
 22 files changed, 175 insertions(+), 402 deletions(-)
 delete mode 100644 dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java
 delete mode 100644 dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/ClojureAFnIntegrationTest.java
 create mode 100644 dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/TestRunnableLambdaInstrumentation.java
 delete mode 100644 dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java

diff --git a/dd-java-agent/agent-bootstrap/build.gradle b/dd-java-agent/agent-bootstrap/build.gradle
index d626307ed64..7a59e3d7aca 100644
--- a/dd-java-agent/agent-bootstrap/build.gradle
+++ b/dd-java-agent/agent-bootstrap/build.gradle
@@ -5,17 +5,12 @@ 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 54570a00559..5d32250ae4c 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,10 +1,11 @@
 package datadog.trace.bootstrap.instrumentation.java.concurrent;
 
-import datadog.trace.bootstrap.FieldBackedContextAccessor;
 import datadog.trace.bootstrap.instrumentation.java.concurrent.ExcludeFilter.ExcludeType;
 
 /**
- * Wraps anonymous Runnable classes that were not field-injected.
+ * 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.
  *
  * 

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 @@ -24,11 +25,9 @@ public void run() { } public static Runnable wrapIfNeeded(final Runnable task) { - // 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 '/'. + 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. 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 9a81f06faeb..e95a6580849 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,7 +8,6 @@ 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; @@ -23,7 +22,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 never be propagated/wrapped + // A ThreadPoolExecutor with one of these types will newer 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; @@ -31,11 +30,10 @@ public final class TPEHelper { private static final ClassValue WRAP = GenericClassValue.of( input -> { - if (FieldBackedContextAccessor.class.isAssignableFrom(input)) { - return false; - } String className = input.getName(); - // Wrap anonymous lambda classes that were not field-injected. + // 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. return className.indexOf('/', className.lastIndexOf('.')) > 0; }); 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 index c55de1677c1..0f64da1b7b0 100644 --- 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 @@ -22,14 +22,10 @@ private LambdaTransformerHelper() {} public static byte[] transform( byte[] classBytes, String lambdaClassName, Class targetClass, Class interfaceClass) { try { - // Only exact allowlisted interfaces enter the transformer. if (interfaceClass == null) { return classBytes; } String interfaceName = interfaceClass.getName(); - if (LambdaInterfaceNameTrie.apply(interfaceName) != 1) { - return classBytes; - } LambdaTransformer transformer = LambdaTransformerHolder.get(); if (transformer == null) { log.debug("Lambda {} skipped: no transformer registered", lambdaClassName); @@ -59,7 +55,7 @@ public static byte[] transform( } return result; } finally { - TRANSFORMING.set(Boolean.FALSE); + TRANSFORMING.remove(); } } catch (Throwable e) { log.debug("Lambda {} skipped: {}", lambdaClassName, e.toString()); 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 index 93487386424..0539a77402f 100644 --- 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 @@ -1,7 +1,11 @@ -# Generates 'LambdaInterfaceNameTrie.java' +# Reserved lambda interface manifest (inactive) -# 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. +# Production lambda transformation is intentionally not enabled for any functional interface. +# This file is not read at build time or runtime. Runtime selection is driven exclusively by +# enabled Instrumenter.ForLambda implementations and also requires trace.lambda.enabled. -1 java.lang.Runnable +# If a validated manifest is introduced later, entries will use the ClassNameTrie format: +# +# 1 java.lang.Runnable + +# Tests register Runnable from TestRunnableLambdaInstrumentation; this file has no effect on them. 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 16998c0f490..2f5fdeed4f5 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 @@ -152,7 +152,7 @@ public static ClassFileTransformer installBytebuddyAgent( agentBuilder = agentBuilder .disableClassFormatChanges() - .assureReadEdgeTo(inst, FieldBackedContextAccessor.class, LambdaTransformerHelper.class) + .assureReadEdgeTo(inst, FieldBackedContextAccessor.class) .with(AgentStrategies.transformerDecorator()) .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) .with(AgentStrategies.rediscoveryStrategy()) @@ -168,6 +168,15 @@ public static ClassFileTransformer installBytebuddyAgent( // .with(AgentBuilder.LambdaInstrumentationStrategy.ENABLED) .ignore(globalIgnoresMatcher(skipAdditionalLibraryMatcher)); + boolean lambdaTransformationEnabled = + !Platform.isNativeImageBuilder() + && InstrumenterConfig.get() + .isIntegrationEnabled(Collections.singleton("lambda"), false); + if (lambdaTransformationEnabled) { + // The injected metafactory call needs java.base to read the bootstrap helper's module. + agentBuilder = agentBuilder.assureReadEdgeTo(inst, LambdaTransformerHelper.class); + } + if (DEBUG) { agentBuilder = agentBuilder @@ -259,7 +268,9 @@ public void applied(Iterable instrumentationNames) { InstrumenterState.resetDefaultState(); try { ClassFileTransformer classFileTransformer = transformerBuilder.installOn(inst); - registerLambdaTransformer(classFileTransformer, transformerBuilder.lambdaInterfaces()); + if (lambdaTransformationEnabled) { + registerLambdaTransformer(classFileTransformer, transformerBuilder.lambdaInterfaces()); + } return classFileTransformer; } finally { SharedTypePools.endInstall(); @@ -307,7 +318,7 @@ private static LambdaTransformer newLambdaTransformer( .get(null); return factory.apply(classFileTransformer); } catch (Throwable e) { - log.debug("Problem loading Java 9 lambda transformer, disabling lambda field-injection", e); + log.debug("Problem loading Java 9 lambda transformer, disabling lambda transformation", e); return null; } } diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Instrumenter.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Instrumenter.java index c744f3516ea..1c8174d6fa5 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Instrumenter.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Instrumenter.java @@ -36,7 +36,13 @@ interface ForTypeHierarchy { /** Instrumentation that transforms types on the bootstrap class-path. */ interface ForBootstrap {} - /** Instrumentation selected directly for an exact lambda interface allowed by the lambda trie. */ + /** + * Instrumentation selected directly for an exact lambda interface. + * + *

Lambda transformation also requires the opt-in {@code trace.lambda.enabled} setting. An + * enabled {@code ForLambda} instrumentation registers its interface but does not enable the + * lambda metafactory instrumentation by itself. + */ interface ForLambda { /** Exact functional interface name reported by the lambda metafactory. */ String lambdaInterface(); 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 dc90bdf2c60..939b8181417 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,7 +57,7 @@ 0 java.lang.Runtime # allow context tracking for VirtualThread 0 java.lang.VirtualThread -# allow instrumenting the lambda metafactory to field-inject generated lambda classes +# allow opt-in instrumentation of the lambda metafactory 0 java.lang.invoke.InnerClassLambdaMetafactory 0 java.net.http.* 0 java.net.HttpURLConnection diff --git a/dd-java-agent/benchmark/build.gradle b/dd-java-agent/benchmark/build.gradle index bc963b78d8e..178eefd2def 100644 --- a/dd-java-agent/benchmark/build.gradle +++ b/dd-java-agent/benchmark/build.gradle @@ -38,16 +38,8 @@ 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 agentJarForBenchmarks + dependsOn ':dd-java-agent:shadowJar' } /* diff --git a/dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java b/dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java deleted file mode 100644 index c1a59f85c77..00000000000 --- a/dd-java-agent/benchmark/src/jmh/java/lambdabench/LambdaExecutorBenchmark.java +++ /dev/null @@ -1,161 +0,0 @@ -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: - * - *

{@code
- * ./gradlew :dd-java-agent:benchmark:jmh \
- *   '-Pjmh.includes=LambdaExecutorBenchmark.*allocateCapturingRunnable' \
- *   -Pjmh.profilers=gc
- * }
- */ -public abstract class LambdaExecutorBenchmark { - - // Must remain outside datadog.* because the helper skips agent-owned lambdas. - // Relative to the JMH working directory, which Gradle sets to this project's directory. - private static final String AGENT = "-javaagent:build/agent/dd-java-agent.jar"; - - private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); - private static final MethodHandle RUNNABLE_TARGET; - private static final MethodHandle SUPPLIER_TARGET; - - static { - try { - RUNNABLE_TARGET = - LOOKUP.findStatic( - LambdaExecutorBenchmark.class, "runTarget", MethodType.methodType(void.class)); - SUPPLIER_TARGET = - LOOKUP.findStatic( - LambdaExecutorBenchmark.class, "supplyTarget", MethodType.methodType(Object.class)); - } catch (NoSuchMethodException | IllegalAccessException e) { - throw new ExceptionInInitializerError(e); - } - } - - @State(Scope.Benchmark) - public static class ExecutorState { - ExecutorService pool; - - @Setup - public void setup() { - pool = Executors.newSingleThreadExecutor(); - } - - @TearDown - public void tearDown() { - pool.shutdownNow(); - } - } - - @State(Scope.Thread) - public static class CapturingLambdaState { - public void run() {} - } - - @State(Scope.Thread) - public static class DirectRunState { - Runnable runnable; - int executions; - - @Setup - public void setup() { - runnable = () -> executions++; - } - } - - /** Allocates an untraced capturing Runnable. */ - @Benchmark - public Runnable allocateCapturingRunnable(CapturingLambdaState state) { - return state::run; - } - - /** Executes an already-created lambda Runnable without an active trace. */ - @Benchmark - public int runUntracedLambda(DirectRunState state) { - state.runnable.run(); - return state.executions; - } - - /** Submit a lambda Runnable to the executor under an active trace, and wait for it to run. */ - @Benchmark - public void submitLambda(ExecutorState state) throws InterruptedException { - runUnderTrace(state.pool); - } - - /** Measures cold linkage of an eligible {@link Runnable} lambda class. */ - @Benchmark - public CallSite linkRunnableLambda() throws LambdaConversionException { - return LambdaMetafactory.metafactory( - LOOKUP, - "run", - MethodType.methodType(Runnable.class), - MethodType.methodType(void.class), - RUNNABLE_TARGET, - MethodType.methodType(void.class)); - } - - /** Measures cold linkage of a non-task lambda, which should bypass the agent transformer. */ - @Benchmark - public CallSite linkSupplierLambda() throws LambdaConversionException { - return LambdaMetafactory.metafactory( - LOOKUP, - "get", - MethodType.methodType(Supplier.class), - MethodType.methodType(Object.class), - SUPPLIER_TARGET, - MethodType.methodType(Object.class)); - } - - @Trace(operationName = "parent") - private void runUnderTrace(ExecutorService pool) throws InterruptedException { - CountDownLatch latch = new CountDownLatch(1); - pool.execute(latch::countDown); - latch.await(); - } - - private static void runTarget() {} - - private static Object supplyTarget() { - return null; - } - - @Fork - public static class NoAgent extends LambdaExecutorBenchmark {} - - @Fork(jvmArgsAppend = {AGENT, "-Ddd.trace.lambda.enabled=false"}) - public static class AgentLambdaOff extends LambdaExecutorBenchmark {} - - @Fork(jvmArgsAppend = AGENT) - public static class AgentLambdaOn extends LambdaExecutorBenchmark {} -} diff --git a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/main/java/datadog/trace/instrumentation/java/concurrent/runnable/RunnableInstrumentation.java b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/main/java/datadog/trace/instrumentation/java/concurrent/runnable/RunnableInstrumentation.java index 64f7fe88522..32dcff9f561 100644 --- a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/main/java/datadog/trace/instrumentation/java/concurrent/runnable/RunnableInstrumentation.java +++ b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/main/java/datadog/trace/instrumentation/java/concurrent/runnable/RunnableInstrumentation.java @@ -29,7 +29,6 @@ @AutoService(InstrumenterModule.class) public final class RunnableInstrumentation extends InstrumenterModule.ContextTracking implements Instrumenter.ForBootstrap, - Instrumenter.ForLambda, Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice { @@ -42,16 +41,6 @@ public String hierarchyMarkerType() { return null; // bootstrap type } - @Override - public String lambdaInterface() { - return Runnable.class.getName(); - } - - @Override - public ElementMatcher lambdaMatcher() { - return notExcludedByName(RUNNABLE); - } - @Override public ElementMatcher hierarchyMatcher() { return notExcludedByName(RUNNABLE) diff --git a/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/build.gradle b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/build.gradle index 727e1efed3d..ab9fd7f8aa8 100644 --- a/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/build.gradle +++ b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/build.gradle @@ -12,11 +12,7 @@ muzzle { addTestSuiteForDir('latestDepTest', 'test') dependencies { - testImplementation project(':dd-java-agent:instrumentation:datadog:tracing:trace-annotation') - testImplementation 'org.clojure:clojure:1.11.1' testRuntimeOnly project(':dd-java-agent:instrumentation:datadog:asm:iast-instrumenter') - // Runs Runnable propagation advice on injected lambdas. - testRuntimeOnly project(':dd-java-agent:instrumentation:java:java-concurrent:java-concurrent-1.8') testImplementation libs.bundles.mockito testImplementation group: 'org.mockito', name: 'mockito-inline', version: '4.11.0' } diff --git a/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/gradle.lockfile b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/gradle.lockfile index 288d9c5b0ab..16223f2ee15 100644 --- a/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/gradle.lockfile +++ b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/gradle.lockfile @@ -69,9 +69,6 @@ org.apache.logging.log4j:log4j-api:2.26.1=spotbugs org.apache.logging.log4j:log4j-core:2.26.1=spotbugs org.apiguardian:apiguardian-api:1.1.2=latestDepTestCompileClasspath,testCompileClasspath org.checkerframework:checker-qual:3.33.0=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -org.clojure:clojure:1.11.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.clojure:core.specs.alpha:0.2.62=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.clojure:spec.alpha:0.3.218=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath org.codehaus.groovy:groovy-ant:3.0.23=codenarc org.codehaus.groovy:groovy-docgenerator:3.0.23=codenarc org.codehaus.groovy:groovy-groovydoc:3.0.23=codenarc diff --git a/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java index c63c9a98b07..ba468a4b7fd 100644 --- a/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java +++ b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/main/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentation.java @@ -3,11 +3,8 @@ import com.google.auto.service.AutoService; import datadog.trace.agent.tooling.Instrumenter; import datadog.trace.agent.tooling.InstrumenterModule; -import datadog.trace.agent.tooling.JavaModuleOpenProvider; import datadog.trace.api.Platform; import datadog.trace.bootstrap.instrumentation.java.lang.invoke.LambdaTransformerHelper; -import java.util.Collection; -import java.util.Collections; import net.bytebuddy.asm.AsmVisitorWrapper; import net.bytebuddy.description.field.FieldDescription; import net.bytebuddy.description.field.FieldList; @@ -28,18 +25,20 @@ /** * Routes generated lambda bytes through the agent transformer before definition, allowing - * allowlisted interfaces such as {@link Runnable} to receive field injection and advice. + * instrumentations registered for an exact functional interface to transform them. * *

An ASM visitor is required because the transform call must be inserted immediately after the * lambda bytes are generated, in the middle of the metafactory method. + * + *

The injected call executes inside {@code java.lang.invoke}, so it only requires a module read + * edge to the bootstrap helper; the package does not need to be opened reflectively. */ @AutoService(InstrumenterModule.class) public final class LambdaMetafactoryInstrumentation extends InstrumenterModule.ContextTracking implements Instrumenter.ForBootstrap, Instrumenter.ForSingleType, Instrumenter.HasTypeAdvice, - Instrumenter.WithTypeStructure, - JavaModuleOpenProvider { + Instrumenter.WithTypeStructure { private static final Logger log = LoggerFactory.getLogger(LambdaMetafactoryInstrumentation.class); @@ -54,6 +53,11 @@ public LambdaMetafactoryInstrumentation() { super("lambda"); } + @Override + protected boolean defaultEnabled() { + return false; + } + @Override public boolean isEnabled() { return super.isEnabled() && !Platform.isNativeImageBuilder(); @@ -64,11 +68,6 @@ public String instrumentedType() { return METAFACTORY; } - @Override - public Collection triggerClasses() { - return Collections.singleton(METAFACTORY); - } - /** Require every field read by the injected bytecode. */ @Override public ElementMatcher structureMatcher() { @@ -171,7 +170,7 @@ public void visitEnd() { super.visitEnd(); if (!injected) { log.debug( - "No injection site found in {}; lambda field-injection is inactive.", slashClassName); + "No injection site found in {}; lambda transformation is inactive.", slashClassName); } } } @@ -216,7 +215,7 @@ public void visitMethodInsn( super.visitFieldInsn( Opcodes.GETFIELD, slashClassName, TARGET_CLASS_FIELD, "Ljava/lang/Class;"); super.visitVarInsn(Opcodes.ALOAD, 0); - // Allows the helper to reject non-allowlisted interfaces before full matching. + // Allows the helper to reject interfaces without a registered ForLambda instrumentation. super.visitFieldInsn( Opcodes.GETFIELD, slashClassName, interfaceClassField, "Ljava/lang/Class;"); super.visitMethodInsn( diff --git a/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java index d7ea09d6c15..99a42059e30 100644 --- a/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java +++ b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/datadog/trace/instrumentation/java/lang/invoke/LambdaMetafactoryInstrumentationTest.java @@ -21,7 +21,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.jar.asm.ClassReader; import net.bytebuddy.jar.asm.ClassVisitor; @@ -230,28 +229,7 @@ private static final class LegacyMetafactoryFields { } @Test - void nonRunnableLambdaBypassesTransformer() { - byte[] originalBytes = new byte[0]; - AtomicBoolean transformed = new AtomicBoolean(); - LambdaTransformerHolder.set( - (className, targetClass, classBytes, interfaceClassName) -> { - transformed.set(true); - return classBytes; - }); - try { - byte[] result = - LambdaTransformerHelper.transform( - originalBytes, "test/Lambda", Object.class, Supplier.class); - - assertSame(originalBytes, result); - assertFalse(transformed.get()); - } finally { - LambdaTransformerHolder.set(null); - } - } - - @Test - void exactRunnableInterfaceUsesTransformer() { + void registeredTransformerReceivesFunctionalInterface() { byte[] originalBytes = new byte[0]; AtomicBoolean transformed = new AtomicBoolean(); AtomicReference transformedInterface = new AtomicReference<>(); @@ -274,27 +252,6 @@ void exactRunnableInterfaceUsesTransformer() { } } - @Test - void runnableSubinterfaceBypassesTransformer() { - byte[] originalBytes = new byte[0]; - AtomicBoolean transformed = new AtomicBoolean(); - LambdaTransformerHolder.set( - (className, targetClass, classBytes, interfaceClassName) -> { - transformed.set(true); - return classBytes; - }); - try { - byte[] result = - LambdaTransformerHelper.transform( - originalBytes, "test/Lambda", Object.class, RunnableSubtype.class); - - assertSame(originalBytes, result); - assertFalse(transformed.get()); - } finally { - LambdaTransformerHolder.set(null); - } - } - @Test void transformerFailureFallsBackAndDoesNotPoisonNextLambda() { byte[] originalBytes = new byte[0]; @@ -345,8 +302,6 @@ void nullTransformFallsBackAndDoesNotPoisonNextLambda() { } } - private interface RunnableSubtype extends Runnable {} - @FunctionalInterface private interface ClassBody { void write(MethodVisitor mv); diff --git a/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/ClojureAFnIntegrationTest.java b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/ClojureAFnIntegrationTest.java deleted file mode 100644 index a52ac12a473..00000000000 --- a/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/ClojureAFnIntegrationTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package testdog.trace.instrumentation.lambda; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import clojure.java.api.Clojure; -import clojure.lang.AFn; -import datadog.trace.agent.test.AbstractInstrumentationTest; -import datadog.trace.bootstrap.FieldBackedContextAccessor; -import datadog.trace.test.junit.utils.config.WithConfig; -import org.junit.jupiter.api.Test; - -@WithConfig(key = "trace.runnable.enabled", value = "false") -public class ClojureAFnIntegrationTest extends AbstractInstrumentationTest { - - @Test - void afnIsNotFieldInjected() { - // Runnable instrumentation can be disabled to avoid inflating every AFn; see - // https://github.com/DataDog/dd-trace-java/pull/2925. - Object function = Clojure.var("clojure.core", "eval").invoke(Clojure.read("(fn [] nil)")); - - assertTrue(function instanceof AFn); - assertTrue(function instanceof Runnable); - assertFalse(function instanceof FieldBackedContextAccessor); - } -} diff --git a/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryDisabledForkedTest.java b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryDisabledForkedTest.java index 7edd1873b90..543e16a3f84 100644 --- a/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryDisabledForkedTest.java +++ b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryDisabledForkedTest.java @@ -1,19 +1,32 @@ package testdog.trace.instrumentation.lambda; import static org.junit.jupiter.api.Assertions.assertFalse; +import static testdog.trace.instrumentation.lambda.TestRunnableLambdaInstrumentation.ADVICE_MARKER_FIELD; import datadog.trace.agent.test.AbstractInstrumentationTest; import datadog.trace.bootstrap.FieldBackedContextAccessor; -import datadog.trace.test.junit.utils.config.WithConfig; +import datadog.trace.bootstrap.instrumentation.java.module.JpmsHelper; import org.junit.jupiter.api.Test; -@WithConfig(key = "trace.lambda.enabled", value = "false") public class LambdaMetafactoryDisabledForkedTest extends AbstractInstrumentationTest { @Test - void runnableLambdaIsNotFieldInjected() { + void lambdaTransformationIsDisabledByDefault() { Runnable lambda = () -> {}; assertFalse(lambda instanceof FieldBackedContextAccessor); + assertFalse(hasAdviceMarker(lambda)); + assertFalse( + JpmsHelper.getAllTriggers().contains("java.lang.invoke.InnerClassLambdaMetafactory"), + "disabled lambda instrumentation should not register a JPMS clearance trigger"); + } + + private static boolean hasAdviceMarker(Object lambda) { + try { + lambda.getClass().getDeclaredField(ADVICE_MARKER_FIELD); + return true; + } catch (NoSuchFieldException ignored) { + return false; + } } } diff --git a/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java index eeb1f805042..83453c5cec9 100644 --- a/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java +++ b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/LambdaMetafactoryIntegrationTest.java @@ -1,41 +1,36 @@ package testdog.trace.instrumentation.lambda; -import static datadog.trace.agent.test.assertions.SpanMatcher.span; -import static datadog.trace.agent.test.assertions.TraceMatcher.SORT_BY_START_TIME; -import static datadog.trace.agent.test.assertions.TraceMatcher.trace; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import static testdog.trace.instrumentation.lambda.TestRunnableLambdaInstrumentation.ADVICE_MARKER_FIELD; import datadog.trace.agent.test.AbstractInstrumentationTest; -import datadog.trace.api.Trace; import datadog.trace.bootstrap.FieldBackedContextAccessor; -import datadog.trace.bootstrap.instrumentation.java.concurrent.RunnableWrapper; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; +import datadog.trace.test.junit.utils.config.WithConfig; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; import org.junit.jupiter.api.Test; /** Lambda integration tests outside the ignored {@code datadog.*} prefix. */ +@WithConfig(key = "trace.lambda.enabled", value = "true") public class LambdaMetafactoryIntegrationTest extends AbstractInstrumentationTest { @Test - void lambdaRunnableIsFieldInjectedNotWrapped() { + void registeredLambdaReceivesItsInstrumentationTransformations() { // Link after the agent is installed. Runnable lambda = () -> {}; assertTrue( lambda instanceof FieldBackedContextAccessor, - "lambda Runnable should be field-injected via the metafactory instrumentation"); - assertSame(lambda, RunnableWrapper.wrapIfNeeded(lambda)); + "test instrumentation should field-inject Runnable lambdas"); + assertTrue( + hasAdviceMarker(lambda), + "test instrumentation should apply its own type advice to Runnable lambdas"); } @Test - void sameOwnerRunnableCaptureShapesAreAllFieldInjected() { + void testInstrumentationHandlesRunnableCaptureShapes() { AtomicInteger counter = new AtomicInteger(); int delta = 7; Runnable[] lambdas = {() -> {}, counter::incrementAndGet, () -> counter.addAndGet(delta)}; @@ -44,48 +39,32 @@ void sameOwnerRunnableCaptureShapesAreAllFieldInjected() { assertTrue( lambda instanceof FieldBackedContextAccessor, "every Runnable lambda shape should be field-injected"); - assertSame(lambda, RunnableWrapper.wrapIfNeeded(lambda)); + assertTrue( + hasAdviceMarker(lambda), + "every Runnable lambda shape should receive the test instrumentation advice"); lambda.run(); } assertEquals(8, counter.get()); } @Test - void nonRunnableLambdaIsNotTransformed() { + void unregisteredLambdaInterfaceIsNotTransformed() { Supplier lambda = Object::new; assertFalse( lambda instanceof FieldBackedContextAccessor, - "non-Runnable lambda should bypass the agent transformer"); + "only interfaces registered by a lambda instrumentation should be transformed"); + assertFalse( + hasAdviceMarker(lambda), + "an unregistered lambda interface should not receive the test instrumentation advice"); } - @Test - void lambdaPropagatesContextAcrossExecutor() throws Exception { - ExecutorService pool = Executors.newSingleThreadExecutor(); + private static boolean hasAdviceMarker(Object lambda) { try { - CountDownLatch latch = new CountDownLatch(1); - submitUnderParent(pool, latch); - assertTrue(latch.await(10, TimeUnit.SECONDS), "child task did not run"); - - assertTraces( - trace( - SORT_BY_START_TIME, - span().root().operationName("parent"), - span().childOfPrevious().operationName("lambda-child"))); - } finally { - pool.shutdownNow(); + lambda.getClass().getDeclaredField(ADVICE_MARKER_FIELD); + return true; + } catch (NoSuchFieldException ignored) { + return false; } } - - @Trace(operationName = "parent") - void submitUnderParent(ExecutorService pool, CountDownLatch latch) { - pool.execute( - () -> { - child(); - latch.countDown(); - }); - } - - @Trace(operationName = "lambda-child") - void child() {} } diff --git a/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/TestRunnableLambdaInstrumentation.java b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/TestRunnableLambdaInstrumentation.java new file mode 100644 index 00000000000..9653216290e --- /dev/null +++ b/dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/java/testdog/trace/instrumentation/lambda/TestRunnableLambdaInstrumentation.java @@ -0,0 +1,80 @@ +package testdog.trace.instrumentation.lambda; + +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.any; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.bootstrap.instrumentation.java.concurrent.State; +import java.util.Map; +import net.bytebuddy.asm.AsmVisitorWrapper; +import net.bytebuddy.description.field.FieldDescription; +import net.bytebuddy.description.field.FieldList; +import net.bytebuddy.description.method.MethodList; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.Implementation; +import net.bytebuddy.jar.asm.ClassVisitor; +import net.bytebuddy.jar.asm.FieldVisitor; +import net.bytebuddy.jar.asm.Opcodes; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.pool.TypePool; +import net.bytebuddy.utility.OpenedClassReader; + +/** Test-only consumer of generated Runnable lambda transformation. */ +@AutoService(InstrumenterModule.class) +public final class TestRunnableLambdaInstrumentation extends InstrumenterModule.ContextTracking + implements Instrumenter.ForLambda, Instrumenter.HasTypeAdvice { + + static final String ADVICE_MARKER_FIELD = "__datadog_test_for_lambda"; + + public TestRunnableLambdaInstrumentation() { + super("test-runnable-lambda"); + } + + @Override + public String lambdaInterface() { + return Runnable.class.getName(); + } + + @Override + public ElementMatcher lambdaMatcher() { + return any(); + } + + @Override + public void typeAdvice(TypeTransformer transformer) { + transformer.applyAdvice(new AdviceMarkerVisitor()); + } + + @Override + public Map contextStore() { + return singletonMap(Runnable.class.getName(), State.class.getName()); + } + + private static final class AdviceMarkerVisitor extends AsmVisitorWrapper.AbstractBase { + @Override + public ClassVisitor wrap( + TypeDescription instrumentedType, + ClassVisitor classVisitor, + Implementation.Context implementationContext, + TypePool typePool, + FieldList fields, + MethodList methods, + int writerFlags, + int readerFlags) { + return new ClassVisitor(OpenedClassReader.ASM_API, classVisitor) { + @Override + public void visitEnd() { + FieldVisitor marker = + cv.visitField( + Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, ADVICE_MARKER_FIELD, "Z", null, null); + if (marker != null) { + marker.visitEnd(); + } + super.visitEnd(); + } + }; + } + } +} diff --git a/dd-smoke-tests/java9-modules/src/main/java11/datadog/smoketest/moduleapp/ModuleApplication.java b/dd-smoke-tests/java9-modules/src/main/java11/datadog/smoketest/moduleapp/ModuleApplication.java index 41808269b44..b7e484b2d7b 100644 --- a/dd-smoke-tests/java9-modules/src/main/java11/datadog/smoketest/moduleapp/ModuleApplication.java +++ b/dd-smoke-tests/java9-modules/src/main/java11/datadog/smoketest/moduleapp/ModuleApplication.java @@ -1,10 +1,7 @@ package datadog.smoketest.moduleapp; -import testdog.moduleapp.LambdaTask; - public class ModuleApplication { public static void main(final String[] args) throws InterruptedException { - LambdaTask.runOnExecutor(); Thread.sleep(600); } } diff --git a/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java b/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java deleted file mode 100644 index 9d8338bd1b8..00000000000 --- a/dd-smoke-tests/java9-modules/src/main/java11/testdog/moduleapp/LambdaTask.java +++ /dev/null @@ -1,46 +0,0 @@ -package testdog.moduleapp; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - -/** Links a Runnable lambda from a named module and outside the ignored {@code datadog.*} prefix. */ -public final class LambdaTask { - private static final String FIELD_BACKED_CONTEXT_ACCESSOR = - "datadog.trace.bootstrap.FieldBackedContextAccessor"; - - private LambdaTask() {} - - public static void runOnExecutor() throws InterruptedException { - final ExecutorService pool = Executors.newSingleThreadExecutor(); - try { - final CountDownLatch latch = new CountDownLatch(1); - final Runnable task = latch::countDown; - assertFieldInjection(task); - pool.execute(task); - if (!latch.await(10, TimeUnit.SECONDS)) { - throw new IllegalStateException("lambda task did not run"); - } - } finally { - pool.shutdownNow(); - } - } - - private static void assertFieldInjection(final Runnable task) { - final List interfaces = new ArrayList<>(); - for (final Class type : task.getClass().getInterfaces()) { - interfaces.add(type.getName()); - } - final boolean injected = interfaces.contains(FIELD_BACKED_CONTEXT_ACCESSOR); - if (!injected) { - throw new IllegalStateException( - "expected lambda field-injection; " - + task.getClass().getName() - + " implements " - + interfaces); - } - } -} diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index c5d035c44a4..6a97071521b 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -7976,7 +7976,7 @@ { "version": "A", "type": "boolean", - "default": "true", + "default": "false", "aliases": ["DD_TRACE_INTEGRATION_LAMBDA_ENABLED", "DD_INTEGRATION_LAMBDA_ENABLED"] } ],