Pārlūkot izejas kodu

Faster type matching (#8525)

Lauri Tulmin 1 gadu atpakaļ
vecāks
revīzija
81f6a3a0c2
14 mainītis faili ar 560 papildinājumiem un 33 dzēšanām
  1. 51 0
      instrumentation/internal/internal-lambda/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/lambda/InnerClassLambdaMetafactoryInstrumentation.java
  2. 6 0
      javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/DefineClassHelper.java
  3. 34 14
      javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/ClassLoaderHasClassesNamedMatcher.java
  4. 8 1
      javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/SafeErasureMatcher.java
  5. 8 1
      javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/SafeExtendsClassMatcher.java
  6. 8 1
      javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/SafeHasSuperTypeMatcher.java
  7. 20 0
      javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/internal/DelegatingMatcher.java
  8. 14 0
      javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/internal/DelegatingSuperTypeMatcher.java
  9. 2 0
      javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentInstaller.java
  10. 47 13
      javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/DefineClassHandler.java
  11. 8 1
      javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/bytebuddy/LoggingFailSafeMatcher.java
  12. 7 1
      javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/util/IgnoreFailedTypeMatcher.java
  13. 7 1
      javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/util/NamedMatcher.java
  14. 340 0
      javaagent-tooling/src/main/java/net/bytebuddy/agent/builder/AgentBuilderUtil.java

+ 51 - 0
instrumentation/internal/internal-lambda/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/lambda/InnerClassLambdaMetafactoryInstrumentation.java

@@ -8,8 +8,11 @@ package io.opentelemetry.javaagent.instrumentation.internal.lambda;
 import static net.bytebuddy.matcher.ElementMatchers.named;
 
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import io.opentelemetry.javaagent.bootstrap.DefineClassHelper;
+import io.opentelemetry.javaagent.bootstrap.DefineClassHelper.Handler.DefineClassContext;
 import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
 import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import net.bytebuddy.asm.Advice;
 import net.bytebuddy.asm.AsmVisitorWrapper;
 import net.bytebuddy.description.field.FieldDescription;
 import net.bytebuddy.description.field.FieldList;
@@ -62,6 +65,24 @@ public class InnerClassLambdaMetafactoryInstrumentation implements TypeInstrumen
                         classVisitor, instrumentedType.getInternalName());
                   }
                 }));
+
+    transformer.applyAdviceToMethod(
+        named("spinInnerClass"),
+        InnerClassLambdaMetafactoryInstrumentation.class.getName()
+            + (hasInterfaceClassField() ? "$LambdaJdk17Advice" : "$LambdaAdvice"));
+  }
+
+  @SuppressWarnings("ReturnValueIgnored")
+  private static boolean hasInterfaceClassField() {
+    try {
+      Class<?> clazz = Class.forName("java.lang.invoke.AbstractValidatingLambdaMetafactory");
+      clazz.getDeclaredField("interfaceClass");
+      return true;
+    } catch (NoSuchFieldException exception) {
+      return false;
+    } catch (ClassNotFoundException exception) {
+      throw new IllegalStateException(exception);
+    }
   }
 
   private static class MetaFactoryClassVisitor extends ClassVisitor {
@@ -117,4 +138,34 @@ public class InnerClassLambdaMetafactoryInstrumentation implements TypeInstrumen
       return mv;
     }
   }
+
+  @SuppressWarnings("unused")
+  public static class LambdaAdvice {
+
+    @Advice.OnMethodEnter
+    public static DefineClassContext onEnter(
+        @Advice.FieldValue("samBase") Class<?> lambdaInterface) {
+      return DefineClassHelper.beforeDefineLambdaClass(lambdaInterface);
+    }
+
+    @Advice.OnMethodExit(onThrowable = Throwable.class)
+    public static void onExit(@Advice.Enter DefineClassContext context) {
+      DefineClassHelper.afterDefineClass(context);
+    }
+  }
+
+  @SuppressWarnings("unused")
+  public static class LambdaJdk17Advice {
+
+    @Advice.OnMethodEnter
+    public static DefineClassContext onEnter(
+        @Advice.FieldValue("interfaceClass") Class<?> lambdaInterface) {
+      return DefineClassHelper.beforeDefineLambdaClass(lambdaInterface);
+    }
+
+    @Advice.OnMethodExit(onThrowable = Throwable.class)
+    public static void onExit(@Advice.Enter DefineClassContext context) {
+      DefineClassHelper.afterDefineClass(context);
+    }
+  }
 }

+ 6 - 0
javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/DefineClassHelper.java

@@ -14,6 +14,8 @@ public class DefineClassHelper {
     DefineClassContext beforeDefineClass(
         ClassLoader classLoader, String className, byte[] classBytes, int offset, int length);
 
+    DefineClassContext beforeDefineLambdaClass(Class<?> lambdaInterface);
+
     void afterDefineClass(DefineClassContext context);
 
     /** Context returned from {@code beforeDefineClass} and passed to {@code afterDefineClass}. */
@@ -48,6 +50,10 @@ public class DefineClassHelper {
     }
   }
 
+  public static Handler.DefineClassContext beforeDefineLambdaClass(Class<?> lambdaInterface) {
+    return handler.beforeDefineLambdaClass(lambdaInterface);
+  }
+
   public static void afterDefineClass(Handler.DefineClassContext context) {
     handler.afterDefineClass(context);
   }

+ 34 - 14
javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/ClassLoaderHasClassesNamedMatcher.java

@@ -12,6 +12,8 @@ import java.util.BitSet;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
 import net.bytebuddy.matcher.ElementMatcher;
 
 class ClassLoaderHasClassesNamedMatcher extends ElementMatcher.Junction.AbstractBase<ClassLoader> {
@@ -62,12 +64,14 @@ class ClassLoaderHasClassesNamedMatcher extends ElementMatcher.Junction.Abstract
   }
 
   private static class Manager {
-    private static final BitSet EMPTY = new BitSet(0);
     static final Manager INSTANCE = new Manager();
     private final List<ClassLoaderHasClassesNamedMatcher> matchers = new CopyOnWriteArrayList<>();
-    // each matcher gets a bit in BitSet, that bit indicates whether current matcher matched or not
-    // for given class loader
+    // each matcher gets a two bits in BitSet, that first bit indicates whether current matcher has
+    // been run for given class loader and the second whether it matched or not
     private final Cache<ClassLoader, BitSet> enabled = Cache.weak();
+    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+    private final Lock readLock = lock.readLock();
+    private final Lock writeLock = lock.writeLock();
     private volatile boolean matchCalled = false;
 
     Manager() {
@@ -83,20 +87,36 @@ class ClassLoaderHasClassesNamedMatcher extends ElementMatcher.Junction.Abstract
 
     boolean match(ClassLoaderHasClassesNamedMatcher matcher, ClassLoader cl) {
       matchCalled = true;
-      BitSet set = enabled.get(cl);
-      if (set == null) {
-        set = new BitSet(counter.get());
-        for (ClassLoaderHasClassesNamedMatcher m : matchers) {
-          if (hasResources(cl, m.resources)) {
-            // set the bit corresponding to the matcher when it matched
-            set.set(m.index);
+      BitSet set = enabled.computeIfAbsent(cl, (unused) -> new BitSet(counter.get() * 2));
+      int matcherRunBit = 2 * matcher.index;
+      int matchedBit = matcherRunBit + 1;
+      readLock.lock();
+      try {
+        if (!set.get(matcherRunBit)) {
+          // read lock needs to be released before upgrading to write lock
+          readLock.unlock();
+          // we do the resource presence check outside the lock to keep the time we need to hold
+          // the write lock minimal
+          boolean matches = hasResources(cl, matcher.resources);
+          writeLock.lock();
+          try {
+            if (!set.get(matcherRunBit)) {
+              if (matches) {
+                set.set(matchedBit);
+              }
+              set.set(matcherRunBit);
+            }
+          } finally {
+            // downgrading the write lock to the read lock
+            readLock.lock();
+            writeLock.unlock();
           }
         }
-        enabled.put(cl, set.isEmpty() ? EMPTY : set);
-      } else if (set.isEmpty()) {
-        return false;
+
+        return set.get(matchedBit);
+      } finally {
+        readLock.unlock();
       }
-      return set.get(matcher.index);
     }
   }
 }

+ 8 - 1
javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/SafeErasureMatcher.java

@@ -8,6 +8,7 @@ package io.opentelemetry.javaagent.extension.matcher;
 import static io.opentelemetry.javaagent.extension.matcher.Utils.safeTypeDefinitionName;
 import static java.util.logging.Level.FINE;
 
+import io.opentelemetry.javaagent.extension.matcher.internal.DelegatingMatcher;
 import java.util.logging.Logger;
 import javax.annotation.Nullable;
 import net.bytebuddy.description.type.TypeDefinition;
@@ -24,7 +25,8 @@ import net.bytebuddy.matcher.ElementMatcher;
  * @param <T> The type of the matched entity.
  * @see net.bytebuddy.matcher.ErasureMatcher
  */
-class SafeErasureMatcher<T extends TypeDefinition> extends ElementMatcher.Junction.AbstractBase<T> {
+class SafeErasureMatcher<T extends TypeDefinition> extends ElementMatcher.Junction.AbstractBase<T>
+    implements DelegatingMatcher {
 
   private static final Logger logger = Logger.getLogger(SafeErasureMatcher.class.getName());
 
@@ -88,4 +90,9 @@ class SafeErasureMatcher<T extends TypeDefinition> extends ElementMatcher.Juncti
   public int hashCode() {
     return matcher.hashCode();
   }
+
+  @Override
+  public ElementMatcher<?> getDelegate() {
+    return matcher;
+  }
 }

+ 8 - 1
javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/SafeExtendsClassMatcher.java

@@ -7,12 +7,14 @@ package io.opentelemetry.javaagent.extension.matcher;
 
 import static io.opentelemetry.javaagent.extension.matcher.SafeHasSuperTypeMatcher.safeGetSuperClass;
 
+import io.opentelemetry.javaagent.extension.matcher.internal.DelegatingSuperTypeMatcher;
 import javax.annotation.Nullable;
 import net.bytebuddy.description.type.TypeDefinition;
 import net.bytebuddy.description.type.TypeDescription;
 import net.bytebuddy.matcher.ElementMatcher;
 
-class SafeExtendsClassMatcher extends ElementMatcher.Junction.AbstractBase<TypeDescription> {
+class SafeExtendsClassMatcher extends ElementMatcher.Junction.AbstractBase<TypeDescription>
+    implements DelegatingSuperTypeMatcher {
 
   private final ElementMatcher<TypeDescription.Generic> matcher;
 
@@ -55,4 +57,9 @@ class SafeExtendsClassMatcher extends ElementMatcher.Junction.AbstractBase<TypeD
   public int hashCode() {
     return matcher.hashCode();
   }
+
+  @Override
+  public ElementMatcher<?> getDelegate() {
+    return matcher;
+  }
 }

+ 8 - 1
javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/SafeHasSuperTypeMatcher.java

@@ -9,6 +9,7 @@ import static io.opentelemetry.javaagent.extension.matcher.SafeErasureMatcher.sa
 import static io.opentelemetry.javaagent.extension.matcher.Utils.safeTypeDefinitionName;
 import static java.util.logging.Level.FINE;
 
+import io.opentelemetry.javaagent.extension.matcher.internal.DelegatingSuperTypeMatcher;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Set;
@@ -35,7 +36,8 @@ import net.bytebuddy.matcher.ElementMatcher;
  *
  * @see net.bytebuddy.matcher.HasSuperTypeMatcher
  */
-class SafeHasSuperTypeMatcher extends ElementMatcher.Junction.AbstractBase<TypeDescription> {
+class SafeHasSuperTypeMatcher extends ElementMatcher.Junction.AbstractBase<TypeDescription>
+    implements DelegatingSuperTypeMatcher {
 
   private static final Logger logger = Logger.getLogger(SafeHasSuperTypeMatcher.class.getName());
 
@@ -136,6 +138,11 @@ class SafeHasSuperTypeMatcher extends ElementMatcher.Junction.AbstractBase<TypeD
     return matcher.hashCode();
   }
 
+  @Override
+  public ElementMatcher<?> getDelegate() {
+    return matcher;
+  }
+
   /**
    * TypeDefinition#getInterfaces() produces an iterator which may throw an exception during
    * iteration if an interface is absent from the classpath.

+ 20 - 0
javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/internal/DelegatingMatcher.java

@@ -0,0 +1,20 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.extension.matcher.internal;
+
+import net.bytebuddy.matcher.ElementMatcher;
+
+/**
+ * Interface for extracting the matcher that the given matcher delegates to.
+ *
+ * <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
+ * at any time.
+ */
+public interface DelegatingMatcher {
+
+  /** Returns the matcher that the current matcher delegates to. */
+  ElementMatcher<?> getDelegate();
+}

+ 14 - 0
javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/internal/DelegatingSuperTypeMatcher.java

@@ -0,0 +1,14 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.extension.matcher.internal;
+
+/**
+ * Marker interface for delegating matchers that match based on the type hierarchy.
+ *
+ * <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
+ * at any time.
+ */
+public interface DelegatingSuperTypeMatcher extends DelegatingMatcher {}

+ 2 - 0
javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentInstaller.java

@@ -54,6 +54,7 @@ import java.util.stream.StreamSupport;
 import javax.annotation.Nullable;
 import net.bytebuddy.ByteBuddy;
 import net.bytebuddy.agent.builder.AgentBuilder;
+import net.bytebuddy.agent.builder.AgentBuilderUtil;
 import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
 import net.bytebuddy.description.type.TypeDefinition;
 import net.bytebuddy.description.type.TypeDescription;
@@ -184,6 +185,7 @@ public class AgentInstaller {
     }
     logger.log(FINE, "Installed {0} extension(s)", numberOfLoadedExtensions);
 
+    agentBuilder = AgentBuilderUtil.optimize(agentBuilder);
     ResettableClassFileTransformer resettableClassFileTransformer = agentBuilder.installOn(inst);
     ClassFileTransformerHolder.setClassFileTransformer(resettableClassFileTransformer);
 

+ 47 - 13
javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/DefineClassHandler.java

@@ -7,6 +7,9 @@ package io.opentelemetry.javaagent.tooling;
 
 import io.opentelemetry.javaagent.bootstrap.DefineClassHelper.Handler;
 import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
 import org.objectweb.asm.ClassReader;
 
 public class DefineClassHandler implements Handler {
@@ -27,7 +30,8 @@ public class DefineClassHandler implements Handler {
       return null;
     }
 
-    DefineClassContextImpl context = null;
+    Set<String> superNames = new HashSet<>();
+    DefineClassContextImpl context = DefineClassContextImpl.enter();
     // attempt to load super types of currently loaded class
     // for a class to be loaded all of its super types must be loaded, here we just change the order
     // of operations and load super types before transforming the bytes for current class so that
@@ -37,22 +41,48 @@ public class DefineClassHandler implements Handler {
       ClassReader cr = new ClassReader(classBytes, offset, length);
       String superName = cr.getSuperName();
       if (superName != null) {
-        Class.forName(superName.replace('/', '.'), false, classLoader);
+        String superDotName = superName.replace('/', '.');
+        Class<?> clazz = Class.forName(superDotName, false, classLoader);
+        addSuperNames(superNames, clazz);
       }
       String[] interfaces = cr.getInterfaces();
       for (String interfaceName : interfaces) {
-        Class.forName(interfaceName.replace('/', '.'), false, classLoader);
+        String interfaceDotName = interfaceName.replace('/', '.');
+        Class<?> clazz = Class.forName(interfaceDotName, false, classLoader);
+        addSuperNames(superNames, clazz);
       }
+      context.superDotNames = superNames;
     } catch (Throwable throwable) {
       // loading of super class or interface failed
       // mark current class as failed to skip matching and transforming it
       // we'll let defining the class proceed as usual so that it would throw the same exception as
       // it does when running without the agent
-      context = DefineClassContextImpl.enter(className);
+      context.failedClassDotName = className;
     }
+
+    return context;
+  }
+
+  @Override
+  public DefineClassContext beforeDefineLambdaClass(Class<?> lambdaInterface) {
+    DefineClassContextImpl context = DefineClassContextImpl.enter();
+    Set<String> superNames = new HashSet<>();
+    addSuperNames(superNames, lambdaInterface);
+    context.superDotNames = superNames;
+
     return context;
   }
 
+  private static void addSuperNames(Set<String> superNames, Class<?> clazz) {
+    if (clazz == null || !superNames.add(clazz.getName())) {
+      return;
+    }
+    addSuperNames(superNames, clazz.getSuperclass());
+    for (Class<?> interfaceClass : clazz.getInterfaces()) {
+      addSuperNames(superNames, interfaceClass);
+    }
+  }
+
   @Override
   public void afterDefineClass(DefineClassContext context) {
     if (context != null) {
@@ -63,33 +93,37 @@ public class DefineClassHandler implements Handler {
   /**
    * Detect whether loading the specified class is known to fail.
    *
-   * @param className class being loaded
+   * @param dotClassName class being loaded
    * @return true if it is known that loading class with given name will fail
    */
-  public static boolean isFailedClass(String className) {
+  public static boolean isFailedClass(String dotClassName) {
     DefineClassContextImpl context = defineClassContext.get();
-    return context.failedClassName != null && context.failedClassName.equals(className);
+    return context.failedClassDotName != null && context.failedClassDotName.equals(dotClassName);
+  }
+
+  public static Set<String> getSuperTypes() {
+    Set<String> superNames = defineClassContext.get().superDotNames;
+    return superNames == null ? Collections.emptySet() : superNames;
   }
 
   private static class DefineClassContextImpl implements DefineClassContext {
     private static final DefineClassContextImpl NOP = new DefineClassContextImpl();
 
     private final DefineClassContextImpl previous;
-    private final String failedClassName;
+    String failedClassDotName;
+    Set<String> superDotNames;
 
     private DefineClassContextImpl() {
       previous = null;
-      failedClassName = null;
     }
 
-    private DefineClassContextImpl(DefineClassContextImpl previous, String failedClassName) {
+    private DefineClassContextImpl(DefineClassContextImpl previous) {
       this.previous = previous;
-      this.failedClassName = failedClassName;
     }
 
-    static DefineClassContextImpl enter(String failedClassName) {
+    static DefineClassContextImpl enter() {
       DefineClassContextImpl previous = defineClassContext.get();
-      DefineClassContextImpl context = new DefineClassContextImpl(previous, failedClassName);
+      DefineClassContextImpl context = new DefineClassContextImpl(previous);
       defineClassContext.set(context);
       return context;
     }

+ 8 - 1
javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/bytebuddy/LoggingFailSafeMatcher.java

@@ -5,6 +5,7 @@
 
 package io.opentelemetry.javaagent.tooling.bytebuddy;
 
+import io.opentelemetry.javaagent.extension.matcher.internal.DelegatingMatcher;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import javax.annotation.Nullable;
@@ -19,7 +20,8 @@ import net.bytebuddy.matcher.ElementMatcher;
  * @param <T> The type of the matched entity.
  * @see net.bytebuddy.matcher.FailSafeMatcher
  */
-public class LoggingFailSafeMatcher<T> extends ElementMatcher.Junction.AbstractBase<T> {
+public class LoggingFailSafeMatcher<T> extends ElementMatcher.Junction.AbstractBase<T>
+    implements DelegatingMatcher {
 
   private static final Logger logger = Logger.getLogger(LoggingFailSafeMatcher.class.getName());
 
@@ -71,4 +73,9 @@ public class LoggingFailSafeMatcher<T> extends ElementMatcher.Junction.AbstractB
   public int hashCode() {
     return matcher.hashCode();
   }
+
+  @Override
+  public ElementMatcher<?> getDelegate() {
+    return matcher;
+  }
 }

+ 7 - 1
javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/util/IgnoreFailedTypeMatcher.java

@@ -5,6 +5,7 @@
 
 package io.opentelemetry.javaagent.tooling.util;
 
+import io.opentelemetry.javaagent.extension.matcher.internal.DelegatingMatcher;
 import io.opentelemetry.javaagent.tooling.DefineClassHandler;
 import net.bytebuddy.description.type.TypeDescription;
 import net.bytebuddy.matcher.ElementMatcher;
@@ -14,7 +15,7 @@ import net.bytebuddy.matcher.ElementMatcher;
  * matched type will fail. If we know that the class that is currently loading can't be loaded
  * successfully we can skip transforming it.
  */
-public class IgnoreFailedTypeMatcher implements ElementMatcher<TypeDescription> {
+public class IgnoreFailedTypeMatcher implements ElementMatcher<TypeDescription>, DelegatingMatcher {
   private final ElementMatcher<TypeDescription> delegate;
 
   public IgnoreFailedTypeMatcher(ElementMatcher<TypeDescription> delegate) {
@@ -30,4 +31,9 @@ public class IgnoreFailedTypeMatcher implements ElementMatcher<TypeDescription>
   public String toString() {
     return delegate.toString();
   }
+
+  @Override
+  public ElementMatcher<?> getDelegate() {
+    return delegate;
+  }
 }

+ 7 - 1
javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/util/NamedMatcher.java

@@ -5,13 +5,14 @@
 
 package io.opentelemetry.javaagent.tooling.util;
 
+import io.opentelemetry.javaagent.extension.matcher.internal.DelegatingMatcher;
 import net.bytebuddy.matcher.ElementMatcher;
 
 /**
  * A matcher wrapper that adds specified name to the output of {@code toString} to allow easy
  * identification of where the given matcher originates from.
  */
-public class NamedMatcher<T> implements ElementMatcher<T> {
+public class NamedMatcher<T> implements ElementMatcher<T>, DelegatingMatcher {
   private final String name;
   private final ElementMatcher<T> delegate;
 
@@ -29,4 +30,9 @@ public class NamedMatcher<T> implements ElementMatcher<T> {
   public String toString() {
     return name + "(" + delegate.toString() + ")";
   }
+
+  @Override
+  public ElementMatcher<?> getDelegate() {
+    return delegate;
+  }
 }

+ 340 - 0
javaagent-tooling/src/main/java/net/bytebuddy/agent/builder/AgentBuilderUtil.java

@@ -0,0 +1,340 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package net.bytebuddy.agent.builder;
+
+import static java.util.logging.Level.FINE;
+
+import io.opentelemetry.javaagent.extension.matcher.internal.DelegatingMatcher;
+import io.opentelemetry.javaagent.extension.matcher.internal.DelegatingSuperTypeMatcher;
+import io.opentelemetry.javaagent.tooling.DefineClassHandler;
+import java.lang.reflect.Field;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+import net.bytebuddy.agent.builder.AgentBuilder.Default.Transformation;
+import net.bytebuddy.matcher.ElementMatcher;
+import net.bytebuddy.matcher.ErasureMatcher;
+import net.bytebuddy.matcher.HasSuperClassMatcher;
+import net.bytebuddy.matcher.NameMatcher;
+import net.bytebuddy.matcher.StringMatcher;
+import net.bytebuddy.matcher.StringSetMatcher;
+import net.bytebuddy.utility.JavaModule;
+
+/** This class is in byte buddy package to get access to package private members and types. */
+public class AgentBuilderUtil {
+  private static final Logger logger = Logger.getLogger(AgentBuilderUtil.class.getName());
+
+  private static final Field agentBuilderTransformationsField =
+      getField(AgentBuilder.Default.class, "transformations");
+  private static final Field rawConjunctionMatchersField =
+      getField(AgentBuilder.RawMatcher.Conjunction.class, "matchers");
+  private static final Field forElementMatcherField =
+      getField(AgentBuilder.RawMatcher.ForElementMatchers.class, "typeMatcher");
+  private static final Field nameMatcherField = getField(NameMatcher.class, "matcher");
+  private static final Field hasSuperClassMatcherField =
+      getField(HasSuperClassMatcher.class, "matcher");
+  private static final Field erasureMatcherField = getField(ErasureMatcher.class, "matcher");
+  private static final Field conjunctionMatchersField =
+      getField(ElementMatcher.Junction.Conjunction.class, "matchers");
+  private static final Field stringMatcherValueField = getField(StringMatcher.class, "value");
+  private static final Field stringMatcherModeField = getField(StringMatcher.class, "mode");
+  private static final Field stringSetMatcherValuesField =
+      getField(StringSetMatcher.class, "values");
+
+  private AgentBuilderUtil() {}
+
+  /**
+   * Replaces byte buddy transformer list with a proxy that does not return the transformers that we
+   * know are not going to match for currently transformed class.
+   */
+  public static AgentBuilder optimize(AgentBuilder agentBuilder) {
+    try {
+      agentBuilder = agentBuilder.with(new TransformContext());
+
+      optimize((AgentBuilder.Default) agentBuilder);
+    } catch (Exception exception) {
+      throw new IllegalStateException("Failed to optimize transformations", exception);
+    }
+    return agentBuilder;
+  }
+
+  private static void optimize(AgentBuilder.Default agentBuilder) throws Exception {
+    // class names that have a matcher that matches by name
+    Set<String> classNames = new HashSet<>();
+    // class names that have a matcher that matches subtypes
+    Set<String> superTypeNames = new HashSet<>();
+    List<Transformation> unoptimizedTransformations = new ArrayList<>();
+    List<Transformation> transformations = agentBuilder.transformations;
+    for (Transformation transformation : transformations) {
+      AgentBuilder.RawMatcher matcher = transformation.getMatcher();
+      // attempt to decompose the matcher and find if it applies to a named class or a subclass
+      Result result = inspect(matcher);
+      if (result == null) {
+        // we were not able to decompose the matcher
+        unoptimizedTransformations.add(transformation);
+      } else if (result.subtype) {
+        superTypeNames.addAll(result.names);
+      } else {
+        classNames.addAll(result.names);
+      }
+    }
+
+    List<?> list =
+        (List<?>)
+            Proxy.newProxyInstance(
+                AgentBuilderUtil.class.getClassLoader(),
+                new Class<?>[] {List.class},
+                (proxy, method, args) -> {
+                  String name = TransformContext.getTransformedClassName();
+                  // iterator() is the only method we expect to be called on this List
+                  if (name != null && "iterator".equals(method.getName())) {
+                    // we know that this class is going to be transformed
+                    if (classNames.contains(name) || superTypeNames.contains(name)) {
+                      return transformations.iterator();
+                    }
+                    // we already know that loading this class is going to fail, no need to
+                    // transform it
+                    if (DefineClassHandler.isFailedClass(name)) {
+                      return Collections.emptyIterator();
+                    }
+                    Set<String> loadingSuperTypes = DefineClassHandler.getSuperTypes();
+                    // super types set should contain at least java.lang.Object if this set is
+                    // empty something unexpected has happened, run all transformations
+                    if (loadingSuperTypes.isEmpty()) {
+                      return transformations.iterator();
+                    }
+                    for (String className : loadingSuperTypes) {
+                      // we know that this class is going to be transformed
+                      if (superTypeNames.contains(className)) {
+                        return transformations.iterator();
+                      }
+                    }
+
+                    // apply only the transformations that we can't decompose
+                    return unoptimizedTransformations.iterator();
+                  }
+
+                  return method.invoke(transformations, args);
+                });
+
+    agentBuilderTransformationsField.set(agentBuilder, list);
+  }
+
+  @Nullable
+  private static Result inspect(AgentBuilder.RawMatcher matcher) throws Exception {
+    if (matcher instanceof AgentBuilder.RawMatcher.Conjunction) {
+      List<AgentBuilder.RawMatcher> matchers = getDelegateMatchers(matcher);
+      if (!matchers.isEmpty()) {
+        // with our current matchers we only need to inspect the first element of the conjunction
+        return inspect(matchers.get(0));
+      }
+    } else if (matcher instanceof AgentBuilder.RawMatcher.ForElementMatchers) {
+      ElementMatcher<?> elementMatcher =
+          getDelegateMatcher((AgentBuilder.RawMatcher.ForElementMatchers) matcher);
+      Result result = inspect(elementMatcher);
+      if (result == null && logger.isLoggable(FINE)) {
+        logger.log(Level.FINE, "Could not decompose matcher {0}", elementMatcher);
+      }
+      return result;
+    }
+
+    return null;
+  }
+
+  @Nullable
+  private static Result inspect(ElementMatcher<?> matcher) throws Exception {
+    if (matcher instanceof DelegatingMatcher) {
+      Result result = inspect(((DelegatingMatcher) matcher).getDelegate());
+      if (matcher instanceof DelegatingSuperTypeMatcher) {
+        return Result.subtype(result);
+      }
+      return result;
+    } else if (matcher instanceof HasSuperClassMatcher) {
+      return Result.subtype(inspect(getDelegateMatcher((HasSuperClassMatcher<?>) matcher)));
+    } else if (matcher instanceof ErasureMatcher) {
+      return inspect(getDelegateMatcher((ErasureMatcher<?>) matcher));
+    } else if (matcher instanceof NameMatcher) {
+      return inspectNameMatcher((NameMatcher<?>) matcher);
+    } else if (matcher instanceof ElementMatcher.Junction.Conjunction) {
+      List<ElementMatcher<?>> matchers =
+          getDelegateMatchers((ElementMatcher.Junction.Conjunction<?>) matcher);
+      for (ElementMatcher<?> elementMatcher : matchers) {
+        Result result = inspect(elementMatcher);
+        if (result != null) {
+          return result;
+        }
+      }
+    }
+
+    return null;
+  }
+
+  @Nullable
+  private static Result inspectNameMatcher(NameMatcher<?> nameMatcher) throws Exception {
+    ElementMatcher<?> matcher = getDelegateMatcher(nameMatcher);
+    if (matcher instanceof StringMatcher) {
+      String value = getStringMatcherValue((StringMatcher) matcher);
+      return Result.named(value);
+    } else if (matcher instanceof StringSetMatcher) {
+      Set<String> value = getStringSetMatcherValue((StringSetMatcher) matcher);
+      return Result.named(value);
+    }
+
+    return null;
+  }
+
+  private static class Result {
+    final Set<String> names = new HashSet<>();
+    // true if matcher matches based on type hierarchy
+    // false if matcher matches based on type name
+    final boolean subtype;
+
+    private Result(boolean subtype) {
+      this.subtype = subtype;
+    }
+
+    private Result() {
+      this(false);
+    }
+
+    @Nullable
+    static Result subtype(@Nullable Result value) {
+      if (value == null) {
+        return null;
+      }
+
+      Result result = new Result(true);
+      result.names.addAll(value.names);
+      return result;
+    }
+
+    @Nullable
+    static Result named(@Nullable String value) {
+      if (value == null) {
+        return null;
+      }
+      Result result = new Result();
+      result.names.add(value);
+      return result;
+    }
+
+    @Nullable
+    static Result named(@Nullable Set<String> value) {
+      if (value == null || value.isEmpty()) {
+        return null;
+      }
+      Result result = new Result();
+      result.names.addAll(value);
+      return result;
+    }
+
+    @Override
+    public String toString() {
+      return (subtype ? "subtype of " : "named ") + names;
+    }
+  }
+
+  private static ElementMatcher<?> getDelegateMatcher(
+      AgentBuilder.RawMatcher.ForElementMatchers matcher) throws Exception {
+    return (ElementMatcher<?>) forElementMatcherField.get(matcher);
+  }
+
+  private static ElementMatcher<?> getDelegateMatcher(NameMatcher<?> matcher) throws Exception {
+    return (ElementMatcher<?>) nameMatcherField.get(matcher);
+  }
+
+  private static ElementMatcher<?> getDelegateMatcher(HasSuperClassMatcher<?> matcher)
+      throws Exception {
+    return (ElementMatcher<?>) hasSuperClassMatcherField.get(matcher);
+  }
+
+  private static ElementMatcher<?> getDelegateMatcher(ErasureMatcher<?> matcher) throws Exception {
+    return (ElementMatcher<?>) erasureMatcherField.get(matcher);
+  }
+
+  @SuppressWarnings("unchecked")
+  private static List<AgentBuilder.RawMatcher> getDelegateMatchers(AgentBuilder.RawMatcher matcher)
+      throws Exception {
+    return (List<AgentBuilder.RawMatcher>) rawConjunctionMatchersField.get(matcher);
+  }
+
+  @SuppressWarnings("unchecked")
+  private static List<ElementMatcher<?>> getDelegateMatchers(
+      ElementMatcher.Junction.Conjunction<?> matcher) throws Exception {
+    return (List<ElementMatcher<?>>) conjunctionMatchersField.get(matcher);
+  }
+
+  /**
+   * @return the value given string matcher matches when matcher mode is
+   *     StringMatcher.Mode.EQUALS_FULLY, null otherwise
+   */
+  @Nullable
+  private static String getStringMatcherValue(StringMatcher matcher) throws Exception {
+    String value = (String) stringMatcherValueField.get(matcher);
+    StringMatcher.Mode mode = (StringMatcher.Mode) stringMatcherModeField.get(matcher);
+    return mode == StringMatcher.Mode.EQUALS_FULLY ? value : null;
+  }
+
+  @SuppressWarnings("unchecked")
+  private static Set<String> getStringSetMatcherValue(StringSetMatcher matcher) throws Exception {
+    return (Set<String>) stringSetMatcherValuesField.get(matcher);
+  }
+
+  private static Field getField(Class<?> clazz, String name) {
+    try {
+      Field field = clazz.getDeclaredField(name);
+      field.setAccessible(true);
+      return field;
+    } catch (NoSuchFieldException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  private static class TransformContext extends AgentBuilder.Listener.Adapter {
+    private static final ThreadLocal<String> transformedName = new ThreadLocal<>();
+
+    @Nullable
+    static String getTransformedClassName() {
+      return transformedName.get();
+    }
+
+    @Override
+    public void onDiscovery(
+        String typeName,
+        @Nullable ClassLoader classLoader,
+        @Nullable JavaModule module,
+        boolean loaded) {
+      if (classLoader != null) {
+        transformedName.set(typeName);
+      }
+    }
+
+    @Override
+    public void onError(
+        String typeName,
+        @Nullable ClassLoader classLoader,
+        @Nullable JavaModule module,
+        boolean loaded,
+        Throwable throwable) {
+      transformedName.remove();
+    }
+
+    @Override
+    public void onComplete(
+        String typeName,
+        @Nullable ClassLoader classLoader,
+        @Nullable JavaModule module,
+        boolean loaded) {
+      transformedName.remove();
+    }
+  }
+}