Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2fe8842
wip
amarziali Jun 22, 2026
4a701c8
perf test
amarziali Jun 22, 2026
08618e6
Enforce type checks
amarziali Aug 26, 2026
001a20e
Correctly manage JPMS
amarziali Aug 26, 2026
0256de0
match the call owner
amarziali Aug 26, 2026
e7dc493
Improve smoke test
amarziali Aug 26, 2026
ad2f1ab
Better logs
amarziali Aug 26, 2026
60c55ff
Bind agent jar to jmh
amarziali Aug 26, 2026
821375a
Switch to default asm version
amarziali Aug 26, 2026
2bb80d9
skip the cache
amarziali Aug 26, 2026
18cbe22
add a guard on runnable
amarziali Aug 26, 2026
534a3f3
Better JMH
amarziali Aug 26, 2026
2ba327f
refinements
amarziali Aug 31, 2026
425f083
Enable by default
amarziali Aug 31, 2026
ff5b605
update integration golden file
amarziali Aug 31, 2026
54db172
Disable instrumenting lambdas during graal builds
amarziali Aug 31, 2026
fbc375d
Support java 8/11 factories
amarziali Aug 31, 2026
f67142f
Improve smoke test
amarziali Aug 31, 2026
8d5ecb0
Add a separate lambda matcher and restore cache correctness
amarziali Sep 1, 2026
44cd67e
spotless
amarziali Sep 1, 2026
b940ebb
refactor
amarziali Sep 1, 2026
9f34e41
prefilter enabled lambda interfaces
amarziali Sep 1, 2026
88ce845
Remove runnable related lambda instrumentation and make it opt-in
amarziali Sep 2, 2026
a678fae
address most comments
amarziali Sep 8, 2026
b5bdc17
Unblock test failures
amarziali Sep 9, 2026
ba1227e
Fix nested transformer cleanup for generated lambda classes
amarziali Sep 9, 2026
3fb27f2
use global ignore fast path for lambda
amarziali Sep 9, 2026
df813b9
cleaunup and docuemntation update
amarziali Sep 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package datadog.trace.bootstrap.instrumentation.java.lang.invoke;

/** Transforms a generated lambda class before it is defined. */
public interface LambdaTransformer {
/**
* @param slashClassName internal (slash-separated) name of the generated lambda class
* @param targetClass the class declaring the lambda
* @param classBytes the freshly generated lambda class bytes
* @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, String interfaceClassName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package datadog.trace.bootstrap.instrumentation.java.lang.invoke;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Transforms eligible lambda bytes before definition, falling back to the original on failure. */
public final class LambdaTransformerHelper {
private static final Logger log = LoggerFactory.getLogger(LambdaTransformerHelper.class);

// Agent transformation may itself create lambdas.
private static final ThreadLocal<Boolean> TRANSFORMING = new ThreadLocal<>();

private LambdaTransformerHelper() {}

/**
* @param classBytes the generated lambda class bytes
* @param lambdaClassName internal (slash-separated) name of the generated lambda class
* @param targetClass the class declaring the lambda
* @param interfaceClass the functional interface implemented by the lambda
* @return possibly transformed bytes; the original bytes on any failure
*/
public static byte[] transform(
byte[] classBytes, String lambdaClassName, Class<?> targetClass, Class<?> interfaceClass) {
try {
if (interfaceClass == null) {
return classBytes;
}
String interfaceName = interfaceClass.getName();
LambdaTransformer transformer = LambdaTransformerHolder.get();
if (transformer == null) {
log.debug("Lambda {} skipped: no transformer registered", lambdaClassName);
return classBytes;
}
if (targetClass == null) {
log.debug("Lambda {} skipped: no target class", lambdaClassName);
return classBytes;
}
if (Boolean.TRUE.equals(TRANSFORMING.get())) {
log.debug("Lambda {} skipped: re-entrant transform", lambdaClassName);
Comment thread
amarziali marked this conversation as resolved.
return classBytes;
}
TRANSFORMING.set(Boolean.TRUE);
try {
byte[] result =
transformer.transform(lambdaClassName, targetClass, classBytes, interfaceName);
if (result == null) {
log.debug("Lambda {} not transformed", lambdaClassName);
return classBytes;
}
return result;
} finally {
TRANSFORMING.remove();
}
} catch (Throwable e) {
log.debug("Lambda {} skipped: {}", lambdaClassName, e.toString());
return classBytes;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import static datadog.trace.agent.tooling.ExtensionFinder.findExtensions;
import static datadog.trace.agent.tooling.ExtensionLoader.loadExtensions;
import static datadog.trace.agent.tooling.bytebuddy.matcher.GlobalIgnores.isIgnored;
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.instrument.fieldinject.GlobalObjectStore;
import datadog.trace.agent.tooling.bytebuddy.SharedTypePools;
Expand All @@ -20,6 +22,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;
Expand All @@ -36,6 +41,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;
Expand Down Expand Up @@ -166,6 +172,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
Expand Down Expand Up @@ -216,7 +231,8 @@ public static ClassFileTransformer installBytebuddyAgent(
}

CombiningTransformerBuilder transformerBuilder =
new CombiningTransformerBuilder(agentBuilder, instrumenterIndex, enabledSystems);
new CombiningTransformerBuilder(
agentBuilder, instrumenterIndex, enabledSystems, lambdaTransformationEnabled);

int installedCount = 0;
for (InstrumenterModule module : instrumenterModules) {
Expand Down Expand Up @@ -265,12 +281,96 @@ public void applied(Iterable<String> instrumentationNames) {

InstrumenterState.resetDefaultState();
try {
return transformerBuilder.installOn(inst);
ClassFileTransformer classFileTransformer = transformerBuilder.installOn(inst);
registerLambdaTransformer(
lambdaTransformationEnabled, classFileTransformer, transformerBuilder.lambdaInterfaces());
return classFileTransformer;
} finally {
SharedTypePools.endInstall();
}
}

/** Registers the installed class-file transformer for generated lambdas. */
static void registerLambdaTransformer(
final boolean enabled,
final ClassFileTransformer classFileTransformer,
final String[] lambdaInterfaces) {
if (!enabled) {
// Agent installation can be repeated in tests and embedded environments.
LambdaTransformerHolder.set(null);
return;
}
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) {
Comment thread
amarziali marked this conversation as resolved.
if (enabledInterface.equals(interfaceName)) {
// Apply the system-level name filter before entering the full transformer pipeline.
if (isIgnored(targetClass.getName(), true)) {
return null;
}
return transformer.transform(className, targetClass, classBytes, interfaceName);
}
}
return null;
};
}

/**
* Java 9+ requires the module-aware transformer for injected read edges. Failure must disable
* lambda transformation rather than fall back to the module-less overload.
*/
@SuppressWarnings("unchecked")
private static LambdaTransformer newLambdaTransformer(
final ClassFileTransformer classFileTransformer) {
if (JavaVirtualMachine.isJavaVersionAtLeast(9)) {
Comment thread
amarziali marked this conversation as resolved.
try {
Function<ClassFileTransformer, LambdaTransformer> factory =
(Function<ClassFileTransformer, LambdaTransformer>)
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 transformation", e);
return null;
}
}
// Avoid invoking the instrumented metafactory while installing its transformer.
return new LambdaTransformer() {
@Override
public byte[] transform(
String slashClassName,
Class<?> targetClass,
byte[] classBytes,
String interfaceClassName) {
TypePoolFacade.beginLambdaTransform(interfaceClassName);
try {
return classFileTransformer.transform(
targetClass.getClassLoader(),
Comment thread
amarziali marked this conversation as resolved.
slashClassName,
null,
targetClass.getProtectionDomain(),
classBytes);
} catch (Throwable ignored) {
return null;
} finally {
TypePoolFacade.endLambdaTransform();
}
}
};
}

/** Returns an iterable that combines the original sequence with any discovered extensions. */
private static Iterable<InstrumenterModule> withExtensions(Iterable<InstrumenterModule> initial) {
String extensionsPath = InstrumenterConfig.get().getTraceExtensionsPath();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@

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;
import java.lang.instrument.Instrumentation;
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;
Expand Down Expand Up @@ -45,13 +49,22 @@ final class CombiningMatcher implements AgentBuilder.RawMatcher {

private final BitSet knownTypesMask;
private final MatchRecorder[] matchers;
private final Map<String, LambdaMatchRecorder[]> lambdaMatchers;

private volatile boolean deferring;

CombiningMatcher(
Instrumentation instrumentation, BitSet knownTypesMask, List<MatchRecorder> matchers) {
Instrumentation instrumentation,
BitSet knownTypesMask,
List<MatchRecorder> matchers,
Map<String, List<LambdaMatchRecorder>> lambdaMatchers) {
this.knownTypesMask = knownTypesMask;
this.matchers = matchers.toArray(new MatchRecorder[0]);
Map<String, LambdaMatchRecorder[]> 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());
Expand All @@ -66,15 +79,43 @@ public boolean matches(
Class<?> classBeingRedefined,
ProtectionDomain pd) {

String lambdaInterface = lambdaMatchers.isEmpty() ? null : TypePoolFacade.lambdaInterface();

// check initial requests to see if we should defer matching until retransformation
if (DEFER_MATCHING && null == classBeingRedefined && deferring && isDeferred(classLoader)) {
// Generated lambda classes cannot be retransformed because they are not yet defined here.
if (null == lambdaInterface
&& DEFER_MATCHING
&& null == classBeingRedefined
&& deferring
&& isDeferred(classLoader)) {
return false;
}

BitSet ids = recordedMatches.get();
ids.clear();

long fromTick = InstrumenterMetrics.tick();
if (null != lambdaInterface) {
LambdaMatchRecorder[] recorders = lambdaMatchers.get(lambdaInterface);
if (null != recorders) {
for (LambdaMatchRecorder recorder : recorders) {
try {
recorder.record(target, classLoader, ids);
} catch (Throwable e) {
if (log.isDebugEnabled()) {
log.debug(
"Lambda instrumentation matcher unexpected exception - interface={} matcher={}",
lambdaInterface,
recorder.describe(),
e);
}
}
}
}
InstrumenterMetrics.matchType(fromTick);
return !ids.isEmpty();
}

knownTypesIndex.apply(target.getName(), knownTypesMask, ids);
if (ids.isEmpty()) {
InstrumenterMetrics.knownTypeMiss(fromTick);
Expand Down
Loading
Loading