Optimize bytebuddy type description performance (#637)

### Improve the performance of type description of byte-buddy
The goal is to get the original class description at re-transform, so as to generate consistent results when the Skywalking agent is enhanced again (including implementing the EnhancedInstance interface, auxiliary fields and methods, etc.)

The previous type description used the `AgentBuilder.DescriptionStrategy.Default.POOL_FIRST` policy to get origin type description, which slows down the application startup, due to heavy I/O operations and parsing bytecode. 

New way is to remove dynamic fields, methods and interfaces generated by SkyWalking Agent from `TypeDescription`, and **make it as origin type descripton**.

**Key feature** :  
* No need to cache `TypeDescription` objects, less memory used.
* It only applies to the re-transform class processing flow and does not affect the startup process.

**Process flow:**
1. Find `TypeDescription` from commonly used type cache, such as primitive class.
2. Delegate to `AgentBuilder.DescriptionStrategy.Default.HYBRID`
3. Wrap `TypeDescription` by `SWTypeDescriptionWrapper` , remove fields, methods, interface generated by SkyWalking. 

**Relative Issue:**  https://github.com/apache/skywalking/issues/11460
This commit is contained in:
Gong Dewei 2023-11-03 09:45:28 +08:00 committed by GitHub
parent 2721438820
commit db54c65d09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 495 additions and 21 deletions

View File

@ -19,7 +19,7 @@ Release Notes.
* Upgrade netty-codec-http2 to 4.1.100.Final
* Add a netty-http 4.1.x plugin to trace HTTP requests.
* Fix Impala Jdbc URL (including schema without properties) parsing exception.
* Optimize byte-buddy type description performance.
#### Documentation

View File

@ -36,6 +36,6 @@ public class Constants {
/**
* The name trait for auxiliary type names, field names and method names which generated by ByteBuddy.
*/
public static final String NAME_TRAIT = "sw$";
public static final String NAME_TRAIT = "$sw$";
}

View File

@ -23,9 +23,11 @@ import java.security.ProtectionDomain;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.agent.builder.SWAgentBuilderDefault;
import net.bytebuddy.agent.builder.SWDescriptionStrategy;
import net.bytebuddy.agent.builder.SWNativeMethodStrategy;
import net.bytebuddy.description.NamedElement;
import net.bytebuddy.description.type.TypeDescription;
@ -156,9 +158,8 @@ public class SkyWalkingAgent {
.with(new SWAuxiliaryTypeNamingStrategy(NAME_TRAIT))
.with(new SWImplementationContextFactory(NAME_TRAIT));
SWNativeMethodStrategy nativeMethodStrategy = new SWNativeMethodStrategy(NAME_TRAIT);
return new SWAgentBuilderDefault(byteBuddy, nativeMethodStrategy)
.with(AgentBuilder.DescriptionStrategy.Default.POOL_FIRST);
return new SWAgentBuilderDefault(byteBuddy, new SWNativeMethodStrategy(NAME_TRAIT))
.with(new SWDescriptionStrategy(NAME_TRAIT));
}
private static class Transformer implements AgentBuilder.Transformer {

View File

@ -0,0 +1,335 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.bytebuddy.agent.builder;
import net.bytebuddy.description.annotation.AnnotationList;
import net.bytebuddy.description.field.FieldDescription;
import net.bytebuddy.description.field.FieldList;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.method.MethodList;
import net.bytebuddy.description.type.*;
import net.bytebuddy.dynamic.TargetType;
import net.bytebuddy.implementation.bytecode.StackSize;
import net.bytebuddy.pool.TypePool;
import net.bytebuddy.utility.JavaModule;
import net.bytebuddy.utility.nullability.MaybeNull;
import org.apache.skywalking.apm.agent.core.plugin.AbstractClassEnhancePluginDefine;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* A DescriptionStrategy to get the original class description by removing dynamic field and method tokens
* generated by SkyWalking.
*/
public class SWDescriptionStrategy implements AgentBuilder.DescriptionStrategy {
/**
* A cache of type descriptions for commonly used types to avoid unnecessary allocations.
*/
private static final Map<Class<?>, TypeDescription> TYPE_CACHE;
/*
* Initializes the type cache.
*/
static {
TYPE_CACHE = new HashMap<Class<?>, TypeDescription>();
TYPE_CACHE.put(TargetType.class, new TypeDescription.ForLoadedType(TargetType.class));
TYPE_CACHE.put(Class.class, new TypeDescription.ForLoadedType(Class.class));
TYPE_CACHE.put(Throwable.class, new TypeDescription.ForLoadedType(Throwable.class));
TYPE_CACHE.put(Annotation.class, new TypeDescription.ForLoadedType(Annotation.class));
TYPE_CACHE.put(Object.class, new TypeDescription.ForLoadedType(Object.class));
TYPE_CACHE.put(String.class, new TypeDescription.ForLoadedType(String.class));
TYPE_CACHE.put(Boolean.class, new TypeDescription.ForLoadedType(Boolean.class));
TYPE_CACHE.put(Byte.class, new TypeDescription.ForLoadedType(Byte.class));
TYPE_CACHE.put(Short.class, new TypeDescription.ForLoadedType(Short.class));
TYPE_CACHE.put(Character.class, new TypeDescription.ForLoadedType(Character.class));
TYPE_CACHE.put(Integer.class, new TypeDescription.ForLoadedType(Integer.class));
TYPE_CACHE.put(Long.class, new TypeDescription.ForLoadedType(Long.class));
TYPE_CACHE.put(Float.class, new TypeDescription.ForLoadedType(Float.class));
TYPE_CACHE.put(Double.class, new TypeDescription.ForLoadedType(Double.class));
TYPE_CACHE.put(void.class, new TypeDescription.ForLoadedType(void.class));
TYPE_CACHE.put(boolean.class, new TypeDescription.ForLoadedType(boolean.class));
TYPE_CACHE.put(byte.class, new TypeDescription.ForLoadedType(byte.class));
TYPE_CACHE.put(short.class, new TypeDescription.ForLoadedType(short.class));
TYPE_CACHE.put(char.class, new TypeDescription.ForLoadedType(char.class));
TYPE_CACHE.put(int.class, new TypeDescription.ForLoadedType(int.class));
TYPE_CACHE.put(long.class, new TypeDescription.ForLoadedType(long.class));
TYPE_CACHE.put(float.class, new TypeDescription.ForLoadedType(float.class));
TYPE_CACHE.put(double.class, new TypeDescription.ForLoadedType(double.class));
}
private AgentBuilder.DescriptionStrategy delegate = Default.HYBRID;
private String nameTrait;
public SWDescriptionStrategy(String nameTrait) {
this.nameTrait = nameTrait;
}
@Override
public boolean isLoadedFirst() {
return true;
}
@Override
public TypeDescription apply(String name,
@MaybeNull Class<?> type,
TypePool typePool,
AgentBuilder.CircularityLock circularityLock,
@MaybeNull ClassLoader classLoader,
@MaybeNull JavaModule module) {
// find from type cache
if (type != null) {
TypeDescription typeDescription = TYPE_CACHE.get(type);
if (typeDescription != null) {
return typeDescription;
}
}
// wrap result
return new SWTypeDescriptionWrapper(delegate.apply(name, type, typePool, circularityLock, classLoader, module), nameTrait);
}
/**
* A TypeDescription wrapper to remove fields, methods, interface generated by SkyWalking.
*/
static class SWTypeDescriptionWrapper extends TypeDescription.AbstractBase implements Serializable {
/**
* The class's serial version UID.
*/
private static final long serialVersionUID = 1L;
private static final List<String> IGNORED_INTERFACES = Arrays.asList(EnhancedInstance.class.getName());
private static final List<String> IGNORED_FIELDS = Arrays.asList(AbstractClassEnhancePluginDefine.CONTEXT_ATTR_NAME);
private static final List<String> IGNORED_METHODS = Arrays.asList("getSkyWalkingDynamicField", "setSkyWalkingDynamicField");
private MethodList<MethodDescription.InDefinedShape> methods;
private FieldList<FieldDescription.InDefinedShape> fields;
private final String nameTrait;
private TypeList.Generic interfaces;
private TypeDescription delegate;
public SWTypeDescriptionWrapper(TypeDescription delegate, String nameTrait) {
this.delegate = delegate;
this.nameTrait = nameTrait;
}
@Override
public TypeList.Generic getInterfaces() {
if (this.interfaces == null) {
TypeList.Generic allInterfaces = delegate.getInterfaces();
if (allInterfaces.stream().anyMatch(s -> IGNORED_INTERFACES.contains(s.getTypeName()))) {
// remove interfaces added by SkyWalking
List<Generic> list = allInterfaces.stream()
.filter(s -> !IGNORED_INTERFACES.contains(s.getTypeName()))
.collect(Collectors.toList());
this.interfaces = new TypeList.Generic.Explicit(list);
} else {
this.interfaces = allInterfaces;
}
}
return this.interfaces;
}
@Override
public FieldList<FieldDescription.InDefinedShape> getDeclaredFields() {
if (this.fields == null) {
FieldList<FieldDescription.InDefinedShape> declaredFields = delegate.getDeclaredFields();
if (declaredFields.stream()
.anyMatch(f -> f.getName().contains(nameTrait) || IGNORED_FIELDS.contains(f.getName()))) {
// Remove dynamic field tokens generated by SkyWalking
fields = new FieldList.Explicit<>(declaredFields.stream()
.filter(f -> !f.getName().contains(nameTrait) && !IGNORED_FIELDS.contains(f.getName()))
.collect(Collectors.toList()));
} else {
fields = declaredFields;
}
}
return fields;
}
@Override
public MethodList<MethodDescription.InDefinedShape> getDeclaredMethods() {
if (this.methods == null) {
MethodList<MethodDescription.InDefinedShape> declaredMethods = delegate.getDeclaredMethods();
if (declaredMethods.stream()
.anyMatch(m -> m.getName().contains(nameTrait) || IGNORED_METHODS.contains(m.getName()))) {
// Remove dynamic method tokens generated by SkyWalking
methods = new MethodList.Explicit<>(declaredMethods.stream()
.filter(m -> !m.getName().contains(nameTrait) && !IGNORED_METHODS.contains(m.getName()))
.collect(Collectors.toList()));
} else {
methods = declaredMethods;
}
}
return methods;
}
@Override
public boolean isAssignableTo(Class<?> type) {
// ignore interface added by SkyWalking
if (IGNORED_INTERFACES.contains(type.getName())) {
return false;
}
return delegate.isAssignableTo(type);
}
@Override
public boolean isAccessibleTo(TypeDescription typeDescription) {
// ignore interface added by SkyWalking
if (IGNORED_INTERFACES.contains(typeDescription.getName())) {
return false;
}
return delegate.isAccessibleTo(typeDescription);
}
@Override
public RecordComponentList<RecordComponentDescription.InDefinedShape> getRecordComponents() {
return delegate.getRecordComponents();
}
@Override
public TypeDescription getComponentType() {
return delegate.getComponentType();
}
@Override
public TypeDescription getDeclaringType() {
return delegate.getDeclaringType();
}
@Override
public TypeList getDeclaredTypes() {
return delegate.getDeclaredTypes();
}
@Override
public MethodDescription.InDefinedShape getEnclosingMethod() {
return delegate.getEnclosingMethod();
}
@Override
public TypeDescription getEnclosingType() {
return delegate.getEnclosingType();
}
@Override
public String getSimpleName() {
return delegate.getSimpleName();
}
@Override
public String getCanonicalName() {
return delegate.getCanonicalName();
}
@Override
public boolean isAnonymousType() {
return delegate.isAnonymousType();
}
@Override
public boolean isLocalType() {
return delegate.isLocalType();
}
@Override
public PackageDescription getPackage() {
return delegate.getPackage();
}
@Override
public TypeDescription getNestHost() {
return delegate.getNestHost();
}
@Override
public TypeList getNestMembers() {
return delegate.getNestMembers();
}
@Override
public TypeList getPermittedSubtypes() {
return delegate.getPermittedSubtypes();
}
@Override
public String getDescriptor() {
return delegate.getDescriptor();
}
@Override
public String getName() {
return delegate.getName();
}
@Override
public TypeList.Generic getTypeVariables() {
return delegate.getTypeVariables();
}
@Override
public AnnotationList getDeclaredAnnotations() {
return delegate.getDeclaredAnnotations();
}
@Override
public Generic getSuperClass() {
return delegate.getSuperClass();
}
@Override
public StackSize getStackSize() {
return delegate.getStackSize();
}
@Override
public boolean isArray() {
return delegate.isArray();
}
@Override
public boolean isRecord() {
return delegate.isRecord();
}
@Override
public boolean isPrimitive() {
return delegate.isPrimitive();
}
@Override
public int getModifiers() {
return delegate.getModifiers();
}
}
}

View File

@ -35,7 +35,7 @@ import java.util.concurrent.TimeUnit;
*/
public class SWClassFileLocator implements ClassFileLocator {
private static final ILog LOGGER = LogManager.getLogger(SWClassFileLocator.class);
private static final String[] TYPE_NAME_TRAITS = {"auxiliary$", "ByteBuddy$", "sw$"};
private static final String[] TYPE_NAME_TRAITS = {"auxiliary$", "ByteBuddy$", "$sw$"};
private static final int DEFAULT_TIMEOUT_SECONDS = 2;
private final ForInstrumentation.ClassLoadingDelegate classLoadingDelegate;

View File

@ -18,7 +18,7 @@
package org.apache.skywalking.apm.agent.bytebuddy.biz;
public class BizFoo {
public class BizFoo implements BizInterface {
private String name;

View File

@ -0,0 +1,23 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.skywalking.apm.agent.bytebuddy.biz;
public interface BizInterface {
}

View File

@ -22,7 +22,9 @@ import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.agent.builder.SWAgentBuilderDefault;
import net.bytebuddy.agent.builder.SWDescriptionStrategy;
import net.bytebuddy.agent.builder.SWNativeMethodStrategy;
import net.bytebuddy.implementation.FieldAccessor;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.SWImplementationContextFactory;
import net.bytebuddy.implementation.SuperMethodCall;
@ -36,6 +38,7 @@ import org.apache.skywalking.apm.agent.bytebuddy.SWAsmVisitorWrapper;
import org.apache.skywalking.apm.agent.bytebuddy.SWAuxiliaryTypeNamingStrategy;
import org.apache.skywalking.apm.agent.bytebuddy.SWClassFileLocator;
import org.apache.skywalking.apm.agent.bytebuddy.biz.BizFoo;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Rule;
@ -52,6 +55,10 @@ import java.security.ProtectionDomain;
import java.util.Arrays;
import java.util.List;
import static net.bytebuddy.jar.asm.Opcodes.ACC_PRIVATE;
import static net.bytebuddy.jar.asm.Opcodes.ACC_VOLATILE;
import static org.apache.skywalking.apm.agent.core.plugin.AbstractClassEnhancePluginDefine.CONTEXT_ATTR_NAME;
public class AbstractInterceptTest {
public static final String BIZ_FOO_CLASS_NAME = "org.apache.skywalking.apm.agent.bytebuddy.biz.BizFoo";
public static final String PROJECT_SERVICE_CLASS_NAME = "org.apache.skywalking.apm.agent.bytebuddy.biz.ProjectService";
@ -60,7 +67,7 @@ public class AbstractInterceptTest {
public static final int BASE_INT_VALUE = 100;
public static final String CONSTRUCTOR_INTERCEPTOR_CLASS = "constructorInterceptorClass";
public static final String METHOD_INTERCEPTOR_CLASS = "methodInterceptorClass";
protected List<String> nameTraits = Arrays.asList("sw2023", "sw2024");
protected List<String> nameTraits = Arrays.asList("$sw$", "$sw$2");
protected boolean deleteDuplicatedFields = false;
@BeforeClass
@ -106,6 +113,11 @@ public class AbstractInterceptTest {
Log.info("Found interceptor: " + interceptorName);
}
protected static void checkInterface(Class testClass, Class interfaceCls) {
Assert.assertTrue("Check interface failure, the test class: " + testClass + " does not implement the expected interface: " + interfaceCls,
EnhancedInstance.class.isAssignableFrom(BizFoo.class));
}
protected static void checkErrors() {
Assert.assertEquals("Error occurred in transform", 0, EnhanceHelper.getErrors().size());
}
@ -142,7 +154,7 @@ public class AbstractInterceptTest {
protected void installConstructorInterceptorWithMethodDelegation(String className, int round) {
String interceptorClassName = CONSTRUCTOR_INTERCEPTOR_CLASS + "$" + className + "$" + round;
String nameTrait = getNameTrait(round);
String fieldName = nameTrait + "_delegate$constructor" + round;
String fieldName = nameTrait + "delegate$" + "constructor" + round;
AgentBuilder agentBuilder = newAgentBuilder(nameTrait);
agentBuilder.type(ElementMatchers.named(className))
@ -162,6 +174,36 @@ public class AbstractInterceptTest {
.installOn(ByteBuddyAgent.install());
}
protected void installInterface(String className) {
String nameTrait = getNameTrait(1);
newAgentBuilder(nameTrait).type(ElementMatchers.named(className))
.transform((builder, typeDescription, classLoader, module, protectionDomain) -> {
if (deleteDuplicatedFields) {
builder = builder.visit(new SWAsmVisitorWrapper());
}
/**
* Manipulate class source code.<br/>
*
* new class need:<br/>
* 1.Add field, name {@link #CONTEXT_ATTR_NAME}.
* 2.Add a field accessor for this field.
*
* And make sure the source codes manipulation only occurs once.
*
*/
if (!typeDescription.isAssignableTo(EnhancedInstance.class)) {
builder = builder.defineField(
CONTEXT_ATTR_NAME, Object.class, ACC_PRIVATE | ACC_VOLATILE)
.implement(EnhancedInstance.class)
.intercept(FieldAccessor.ofField(CONTEXT_ATTR_NAME));
}
return builder;
}
)
.with(getListener("EnhancedInstance"))
.installOn(ByteBuddyAgent.install());
}
protected String getNameTrait(int round) {
return nameTraits.get(round - 1);
}
@ -173,7 +215,7 @@ public class AbstractInterceptTest {
return new SWAgentBuilderDefault(byteBuddy, new SWNativeMethodStrategy(nameTrait))
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.with(AgentBuilder.DescriptionStrategy.Default.POOL_FIRST)
.with(new SWDescriptionStrategy(nameTrait))
.with(new SWClassFileLocator(ByteBuddyAgent.install(), getClassLoader()));
}

View File

@ -50,6 +50,19 @@ public class ReTransform1Test extends AbstractReTransformTest {
checkConstructorInterceptor(BIZ_FOO_CLASS_NAME, 1);
checkErrors();
// installTraceClassTransformer("Trace class: ");
// do retransform
reTransform(instrumentation, BizFoo.class);
// test again
callBizFoo(1);
// check interceptors
checkMethodInterceptor(SAY_HELLO_METHOD, 1);
checkConstructorInterceptor(BIZ_FOO_CLASS_NAME, 1);
// test ProjectService
ProjectService projectService = new ProjectService();
ProjectDO originProjectDO = ProjectDO.builder()
.name("test")
@ -59,19 +72,8 @@ public class ReTransform1Test extends AbstractReTransformTest {
ProjectDO projectDO = projectService.getById(1);
projectService.list();
// installTraceClassTransformer("Trace class: ");
// do retransform
reTransform(instrumentation, BizFoo.class);
reTransform(instrumentation, ProjectService.class);
// test again
callBizFoo(1);
// check interceptors
checkMethodInterceptor(SAY_HELLO_METHOD, 1);
checkConstructorInterceptor(BIZ_FOO_CLASS_NAME, 1);
projectDO = projectService.getById(1);
Assert.assertEquals(originProjectDO, projectDO);

View File

@ -56,6 +56,12 @@ public class ReTransform2Test extends AbstractReTransformTest {
// do retransform
reTransform(instrumentation, BizFoo.class);
// int retransformCount = 30;
// for (int i = 0; i < retransformCount; i++) {
// reTransform(instrumentation, BizFoo.class);
// // reTransform(instrumentation, ProjectService.class);
// }
// test again
callBizFoo(2);
// check interceptors

View File

@ -0,0 +1,65 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.skywalking.apm.agent.bytebuddy.cases;
import net.bytebuddy.agent.ByteBuddyAgent;
import org.apache.skywalking.apm.agent.bytebuddy.biz.BizFoo;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
import org.junit.Test;
import java.lang.instrument.Instrumentation;
public class ReTransform3Test extends AbstractReTransformTest {
@Test
public void testInterceptConstructor() throws Exception {
Instrumentation instrumentation = ByteBuddyAgent.install();
// install transformer
installMethodInterceptor(BIZ_FOO_CLASS_NAME, SAY_HELLO_METHOD, 1);
installConstructorInterceptor(BIZ_FOO_CLASS_NAME, 1);
// implement EnhancedInstance
installInterface(BIZ_FOO_CLASS_NAME);
// call target class
callBizFoo(1);
// check interceptors
checkInterface(BizFoo.class, EnhancedInstance.class);
checkMethodInterceptor(SAY_HELLO_METHOD, 1);
checkConstructorInterceptor(BIZ_FOO_CLASS_NAME, 1);
checkErrors();
// installTraceClassTransformer("Trace class: ");
// do retransform
reTransform(instrumentation, BizFoo.class);
// test again
callBizFoo(1);
// check interceptors
checkInterface(BizFoo.class, EnhancedInstance.class);
checkMethodInterceptor(SAY_HELLO_METHOD, 1);
checkConstructorInterceptor(BIZ_FOO_CLASS_NAME, 1);
checkErrors();
}
}