Skip to content

Commit

Permalink
Speedup Hibernate ORM's enhancement of large models
Browse files Browse the repository at this point in the history
  • Loading branch information
Sanne committed May 16, 2024
1 parent 2d4b470 commit 2166451
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@

import java.util.function.BiFunction;

import org.hibernate.bytecode.enhance.spi.DefaultEnhancementContext;
import org.hibernate.bytecode.enhance.internal.bytebuddy.CoreTypePool;
import org.hibernate.bytecode.enhance.internal.bytebuddy.EnhancerClassLocator;
import org.hibernate.bytecode.enhance.internal.bytebuddy.ModelTypePool;
import org.hibernate.bytecode.enhance.spi.Enhancer;
import org.hibernate.bytecode.enhance.spi.UnloadedField;
import org.hibernate.bytecode.spi.BytecodeProvider;
import org.hibernate.bytecode.internal.bytebuddy.BytecodeProviderImpl;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;

import io.quarkus.deployment.QuarkusClassVisitor;
import io.quarkus.deployment.QuarkusClassWriter;
import io.quarkus.gizmo.Gizmo;
import io.quarkus.hibernate.orm.deployment.integration.QuarkusClassFileLocator;
import io.quarkus.hibernate.orm.deployment.integration.QuarkusEnhancementContext;
import net.bytebuddy.ClassFileVersion;

/**
Expand All @@ -29,44 +32,37 @@
*/
public final class HibernateEntityEnhancer implements BiFunction<String, ClassVisitor, ClassVisitor> {

private static final BytecodeProvider PROVIDER = new org.hibernate.bytecode.internal.bytebuddy.BytecodeProviderImpl(
private static final BytecodeProviderImpl PROVIDER = new org.hibernate.bytecode.internal.bytebuddy.BytecodeProviderImpl(
ClassFileVersion.JAVA_V11);

//Choose this set to include Jakarta annotations, basic Java types such as String and Map, Hibernate annotations, and Panache supertypes:
private static final CoreTypePool CORE_POOL = new CoreTypePool("jakarta.persistence.", "java.",
"org.hibernate.annotations.",
"io.quarkus.hibernate.reactive.panache.", "io.quarkus.hibernate.orm.panache.",
"org.hibernate.search.mapper.pojo.mapping.definition.annotation.",
"jakarta.validation.constraints.");

private final EnhancerHolder enhancerHolder = new EnhancerHolder();

@Override
public ClassVisitor apply(String className, ClassVisitor outputClassVisitor) {
return new HibernateEnhancingClassVisitor(className, outputClassVisitor);
return new HibernateEnhancingClassVisitor(className, outputClassVisitor, enhancerHolder);
}

private static class HibernateEnhancingClassVisitor extends QuarkusClassVisitor {

private final String className;
private final ClassVisitor outputClassVisitor;
private final Enhancer enhancer;
private final EnhancerHolder enhancerHolder;

public HibernateEnhancingClassVisitor(String className, ClassVisitor outputClassVisitor) {
public HibernateEnhancingClassVisitor(String className, ClassVisitor outputClassVisitor,
EnhancerHolder enhancerHolder) {
//Careful: the ASM API version needs to match the ASM version of Gizmo, not the one from Byte Buddy.
//Most often these match - but occasionally they will diverge which is acceptable as Byte Buddy is shading ASM.
super(Gizmo.ASM_API_VERSION, new QuarkusClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS));
this.className = className;
this.outputClassVisitor = outputClassVisitor;
//note that as getLoadingClassLoader is resolved immediately this can't be created until transform time

DefaultEnhancementContext enhancementContext = new DefaultEnhancementContext() {

@Override
public boolean doBiDirectionalAssociationManagement(final UnloadedField field) {
//Don't enable automatic association management as it's often too surprising.
//Also, there's several cases in which its semantics are of unspecified,
//such as what should happen when dealing with ordered collections.
return false;
}

@Override
public ClassLoader getLoadingClassLoader() {
return Thread.currentThread().getContextClassLoader();
}
};
this.enhancer = PROVIDER.getEnhancer(enhancementContext);
this.enhancerHolder = enhancerHolder;
}

@Override
Expand All @@ -83,21 +79,36 @@ public void visitEnd() {
}

private byte[] hibernateEnhancement(final String className, final byte[] originalBytes) {
final byte[] enhanced = enhancer.enhance(className, originalBytes);
final byte[] enhanced = enhancerHolder.getEnhancer().enhance(className, originalBytes);
return enhanced == null ? originalBytes : enhanced;
}

}

public byte[] enhance(String className, byte[] bytes) {
DefaultEnhancementContext enhancementContext = new DefaultEnhancementContext() {
@Override
public ClassLoader getLoadingClassLoader() {
return Thread.currentThread().getContextClassLoader();
}
return enhancerHolder.getEnhancer().enhance(className, bytes);
}

};
Enhancer enhancer = PROVIDER.getEnhancer(enhancementContext);
return enhancer.enhance(className, bytes);
private static class EnhancerHolder {

private volatile Enhancer actualEnhancer;

public Enhancer getEnhancer() {
//Lazily initialized for multiple reasons:
//1)it's expensive: try to skip it if we can; this is actually not unlikely to happen as the transformation is cacheable.
//2)We want the Advice loaders of the Hibernate ORM implementation to be initialized within the scope in which we
//have the transformation classloader installed in the thread's context.
if (actualEnhancer == null) {
synchronized (this) {
if (actualEnhancer == null) {
EnhancerClassLocator modelPool = ModelTypePool.buildModelTypePool(QuarkusClassFileLocator.INSTANCE,
CORE_POOL);
actualEnhancer = PROVIDER.getEnhancer(QuarkusEnhancementContext.INSTANCE, modelPool);
}
}
}
return actualEnhancer;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.quarkus.hibernate.orm.deployment.integration;

import java.io.IOException;

import io.quarkus.deployment.util.IoUtil;
import net.bytebuddy.dynamic.ClassFileLocator;

/**
* Custom implementation of a ClassFileLocator which will load resources
* from the context classloader which is set at the time of the locate()
* operation is being performed.
* Using a regular ForClassLoader implementation would capture the currently
* set ClassLoader and keep a reference to it, while we need it to look
* for a fresh copy during the enhancement.
* Additionally, we might be able to optimize how the resource is actually
* being loaded as we control the ClassLoader implementations
* (Such further optimisations are not implemented yet).
*/
public final class QuarkusClassFileLocator implements ClassFileLocator {

public static final QuarkusClassFileLocator INSTANCE = new QuarkusClassFileLocator();

private QuarkusClassFileLocator() {
//do not invoke, use INSTANCE
}

@Override
public Resolution locate(final String name) throws IOException {
final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
final byte[] bytes = IoUtil.readClassAsBytes(classLoader, name);
if (bytes != null) {
return new Resolution.Explicit(bytes);
} else {
return new Resolution.Illegal(name);
}
}

@Override
public void close() {
//nothing to do
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.quarkus.hibernate.orm.deployment.integration;

import org.hibernate.bytecode.enhance.spi.DefaultEnhancementContext;
import org.hibernate.bytecode.enhance.spi.UnloadedField;

public final class QuarkusEnhancementContext extends DefaultEnhancementContext {

public static final QuarkusEnhancementContext INSTANCE = new QuarkusEnhancementContext();

private QuarkusEnhancementContext() {
//do not invoke, use INSTANCE
}

@Override
public boolean doBiDirectionalAssociationManagement(final UnloadedField field) {
//Don't enable automatic association management as it's often too surprising.
//Also, there's several cases in which its semantics are of unspecified,
//such as what should happen when dealing with ordered collections.
return false;
}

@Override
public ClassLoader getLoadingClassLoader() {
//This shouldn't matter as we delegate resource location to QuarkusClassFileLocator;
//make sure of this:
throw new IllegalStateException("The Classloader of the EnhancementContext should not be used");
}

}

0 comments on commit 2166451

Please sign in to comment.