Add AOT repository benchmarks.

See #3830
This commit is contained in:
Mark Paluch
2025-04-04 13:00:19 +02:00
parent d3cc7da3d9
commit be4d8528b3
8 changed files with 314 additions and 17 deletions

17
pom.xml
View File

@@ -54,6 +54,23 @@
<profiles>
<profile>
<id>jmh</id>
<dependencies>
<dependency>
<groupId>com.github.mp911de.microbenchmark-runner</groupId>
<artifactId>microbenchmark-runner-junit5</artifactId>
<version>0.4.0.RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
</profile>
<profile>
<id>hibernate-70-snapshots</id>
<properties>

View File

@@ -0,0 +1,260 @@
/*
* Copyright 2024-2025 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.data.jpa.benchmark;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Query;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.junit.platform.commons.annotation.Testable;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.TearDown;
import org.openjdk.jmh.annotations.Timeout;
import org.openjdk.jmh.annotations.Warmup;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.core.test.tools.TestCompiler;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.benchmark.model.Person;
import org.springframework.data.jpa.benchmark.model.Profile;
import org.springframework.data.jpa.benchmark.repository.PersonRepository;
import org.springframework.data.jpa.repository.aot.JpaRepositoryContributor;
import org.springframework.data.jpa.repository.aot.TestJpaAotRepositoryContext;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryComposition;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.util.ObjectUtils;
/**
* @author Christoph Strobl
* @author Mark Paluch
*/
@Testable
@Fork(1)
@Warmup(time = 1, iterations = 3)
@Measurement(time = 1, iterations = 3)
@Timeout(time = 2)
public class AotRepositoryQueryMethodBenchmarks {
private static final String PERSON_FIRSTNAME = "first";
private static final String COLUMN_PERSON_FIRSTNAME = "firstname";
@State(Scope.Benchmark)
public static class BenchmarkParameters {
public static Class<?> aot;
public static TestJpaAotRepositoryContext<PersonRepository> repositoryContext = new TestJpaAotRepositoryContext<>(
PersonRepository.class, null);
EntityManager entityManager;
RepositoryComposition.RepositoryFragments fragments;
PersonRepository repositoryProxy;
@Setup(Level.Iteration)
public void doSetup() {
createEntityManager();
if (!entityManager.getTransaction().isActive()) {
if (ObjectUtils.nullSafeEquals(
entityManager.createNativeQuery("SELECT COUNT(*) FROM person", Integer.class).getSingleResult(),
Integer.valueOf(0))) {
entityManager.getTransaction().begin();
Profile generalProfile = new Profile("general");
Profile sdUserProfile = new Profile("sd-user");
entityManager.persist(generalProfile);
entityManager.persist(sdUserProfile);
Person person = new Person(PERSON_FIRSTNAME, "last");
person.setProfiles(Set.of(generalProfile, sdUserProfile));
entityManager.persist(person);
entityManager.getTransaction().commit();
}
}
if (this.aot == null) {
TestGenerationContext generationContext = new TestGenerationContext(PersonRepository.class);
new JpaRepositoryContributor(repositoryContext, entityManager.getEntityManagerFactory())
.contribute(generationContext);
TestCompiler.forSystem().withCompilerOptions("-parameters").with(generationContext).compile(compiled -> {
try {
this.aot = compiled.getClassLoader().loadClass(PersonRepository.class.getName() + "Impl__Aot");
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
try {
RepositoryFactoryBeanSupport.FragmentCreationContext creationContext = getCreationContext(repositoryContext);
fragments = RepositoryComposition.RepositoryFragments
.just(aot.getConstructor(EntityManager.class, RepositoryFactoryBeanSupport.FragmentCreationContext.class)
.newInstance(entityManager, creationContext));
this.repositoryProxy = createRepository(fragments);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private RepositoryFactoryBeanSupport.FragmentCreationContext getCreationContext(
TestJpaAotRepositoryContext<?> repositoryContext) {
RepositoryFactoryBeanSupport.FragmentCreationContext creationContext = new RepositoryFactoryBeanSupport.FragmentCreationContext() {
@Override
public RepositoryMetadata getRepositoryMetadata() {
return repositoryContext.getRepositoryInformation();
}
@Override
public ValueExpressionDelegate getValueExpressionDelegate() {
return ValueExpressionDelegate.create();
}
@Override
public ProjectionFactory getProjectionFactory() {
return new SpelAwareProxyProjectionFactory();
}
};
return creationContext;
}
@TearDown(Level.Iteration)
public void doTearDown() {
entityManager.close();
}
private void createEntityManager() {
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
factoryBean.setPersistenceUnitName("benchmark");
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
factoryBean.setPersistenceProviderClass(HibernatePersistenceProvider.class);
factoryBean.setPersistenceXmlLocation("classpath*:META-INF/persistence-jmh.xml");
factoryBean.setMappingResources("classpath*:META-INF/orm-jmh.xml");
Properties properties = new Properties();
properties.put("jakarta.persistence.jdbc.url", "jdbc:h2:mem:test");
properties.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
properties.put("hibernate.hbm2ddl.auto", "update");
properties.put("hibernate.xml_mapping_enabled", "false");
factoryBean.setJpaProperties(properties);
factoryBean.afterPropertiesSet();
EntityManagerFactory entityManagerFactory = factoryBean.getObject();
entityManager = entityManagerFactory.createEntityManager();
}
public PersonRepository createRepository(RepositoryComposition.RepositoryFragments fragments) {
JpaRepositoryFactory repositoryFactory = new JpaRepositoryFactory(entityManager);
return repositoryFactory.getRepository(PersonRepository.class, fragments);
}
}
protected PersonRepository doCreateRepository(EntityManager entityManager) {
JpaRepositoryFactory repositoryFactory = new JpaRepositoryFactory(entityManager);
return repositoryFactory.getRepository(PersonRepository.class);
}
@Benchmark
public PersonRepository repositoryBootstrap(BenchmarkParameters parameters) {
return parameters.createRepository(parameters.fragments);
}
@Benchmark
public List<Person> baselineEntityManagerHQLQuery(BenchmarkParameters parameters) {
Query query = parameters.entityManager
.createQuery("SELECT p FROM org.springframework.data.jpa.benchmark.model.Person p WHERE p.firstname = ?1");
query.setParameter(1, PERSON_FIRSTNAME);
return query.getResultList();
}
@Benchmark
public Long baselineEntityManagerCount(BenchmarkParameters parameters) {
Query query = parameters.entityManager.createQuery(
"SELECT COUNT(*) FROM org.springframework.data.jpa.benchmark.model.Person p WHERE p.firstname = ?1");
query.setParameter(1, PERSON_FIRSTNAME);
return (Long) query.getSingleResult();
}
@Benchmark
public List<Person> derivedFinderMethod(BenchmarkParameters parameters) {
return parameters.repositoryProxy.findAllByFirstname(PERSON_FIRSTNAME);
}
/*@Benchmark
public List<IPersonProjection> derivedFinderMethodWithInterfaceProjection(BenchmarkParameters parameters) {
return parameters.repositoryProxy.findAllAndProjectToInterfaceByFirstname(PERSON_FIRSTNAME);
} */
@Benchmark
public List<Person> stringBasedQuery(BenchmarkParameters parameters) {
return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname(PERSON_FIRSTNAME);
}
@Benchmark
public List<Person> stringBasedQueryDynamicSort(BenchmarkParameters parameters) {
return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname(PERSON_FIRSTNAME,
Sort.by(COLUMN_PERSON_FIRSTNAME));
}
@Benchmark
public List<Person> stringBasedNativeQuery(BenchmarkParameters parameters) {
return parameters.repositoryProxy.findAllWithNativeQueryByFirstname(PERSON_FIRSTNAME);
}
@Benchmark
public Long derivedCount(BenchmarkParameters parameters) {
return parameters.repositoryProxy.countByFirstname(PERSON_FIRSTNAME);
}
@Benchmark
public Long stringBasedCount(BenchmarkParameters parameters) {
return parameters.repositoryProxy.countWithAnnotatedQueryByFirstname(PERSON_FIRSTNAME);
}
}

View File

@@ -41,7 +41,6 @@ import org.openjdk.jmh.annotations.Timeout;
import org.openjdk.jmh.annotations.Warmup;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.benchmark.model.IPersonProjection;
import org.springframework.data.jpa.benchmark.model.Person;
import org.springframework.data.jpa.benchmark.model.Profile;
import org.springframework.data.jpa.benchmark.repository.PersonRepository;
@@ -52,13 +51,14 @@ import org.springframework.util.ObjectUtils;
/**
* @author Christoph Strobl
* @author Mark Paluch
*/
@Testable
@Fork(1)
@Warmup(time = 2, iterations = 3)
@Measurement(time = 2)
@Warmup(time = 1, iterations = 3)
@Measurement(time = 1, iterations = 3)
@Timeout(time = 2)
public class RepositoryFinderBenchmarks {
public class RepositoryQueryMethodBenchmarks {
private static final String PERSON_FIRSTNAME = "first";
private static final String COLUMN_PERSON_FIRSTNAME = "firstname";
@@ -125,10 +125,16 @@ public class RepositoryFinderBenchmarks {
entityManager = entityManagerFactory.createEntityManager();
}
PersonRepository createRepository() {
public PersonRepository createRepository() {
JpaRepositoryFactory repositoryFactory = new JpaRepositoryFactory(entityManager);
return repositoryFactory.getRepository(PersonRepository.class);
}
}
protected PersonRepository doCreateRepository(EntityManager entityManager) {
JpaRepositoryFactory repositoryFactory = new JpaRepositoryFactory(entityManager);
return repositoryFactory.getRepository(PersonRepository.class);
}
@Benchmark
@@ -173,10 +179,10 @@ public class RepositoryFinderBenchmarks {
return parameters.repositoryProxy.findAllByFirstname(PERSON_FIRSTNAME);
}
@Benchmark
/*@Benchmark
public List<IPersonProjection> derivedFinderMethodWithInterfaceProjection(BenchmarkParameters parameters) {
return parameters.repositoryProxy.findAllAndProjectToInterfaceByFirstname(PERSON_FIRSTNAME);
}
} */
@Benchmark
public List<Person> stringBasedQuery(BenchmarkParameters parameters) {
@@ -185,7 +191,8 @@ public class RepositoryFinderBenchmarks {
@Benchmark
public List<Person> stringBasedQueryDynamicSort(BenchmarkParameters parameters) {
return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname(PERSON_FIRSTNAME, Sort.by(COLUMN_PERSON_FIRSTNAME));
return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname(PERSON_FIRSTNAME,
Sort.by(COLUMN_PERSON_FIRSTNAME));
}
@Benchmark

View File

@@ -16,6 +16,7 @@
package org.springframework.data.jpa.repository.aot;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import java.lang.reflect.Method;
@@ -68,10 +69,18 @@ public class JpaRepositoryContributor extends RepositoryContributor {
AotMetamodel amm = new AotMetamodel(repositoryContext.getResolvedTypes());
this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(amm.getEntityManagerFactory());
this.queriesFactory = new QueriesFactory(amm, amm.getEntityManagerFactory());
this.queriesFactory = new QueriesFactory(amm.getEntityManagerFactory(), amm);
this.entityGraphLookup = new EntityGraphLookup(amm.getEntityManagerFactory());
}
public JpaRepositoryContributor(AotRepositoryContext repositoryContext, EntityManagerFactory entityManagerFactory) {
super(repositoryContext);
this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(entityManagerFactory);
this.queriesFactory = new QueriesFactory(entityManagerFactory);
this.entityGraphLookup = new EntityGraphLookup(entityManagerFactory);
}
@Override
protected void customizeClass(RepositoryInformation information, AotRepositoryFragmentMetadata metadata,
TypeSpec.Builder builder) {

View File

@@ -51,12 +51,16 @@ import org.springframework.util.StringUtils;
*/
class QueriesFactory {
private final EntityManagerFactory entityManagerFactory;
private final Metamodel metamodel;
private final EntityManagerFactory emf;
public QueriesFactory(AotMetamodel metamodel, EntityManagerFactory emf) {
public QueriesFactory(EntityManagerFactory entityManagerFactory) {
this(entityManagerFactory, entityManagerFactory.getMetamodel());
}
public QueriesFactory(EntityManagerFactory entityManagerFactory, Metamodel metamodel) {
this.metamodel = metamodel;
this.emf = emf;
this.entityManagerFactory = entityManagerFactory;
}
/**
@@ -183,7 +187,7 @@ class QueriesFactory {
for (Class<?> candidate : candidates) {
Map<String, ? extends TypedQueryReference<?>> namedQueries = emf.getNamedQueries(candidate);
Map<String, ? extends TypedQueryReference<?>> namedQueries = entityManagerFactory.getNamedQueries(candidate);
if (namedQueries.containsKey(queryName)) {
return namedQueries.get(queryName);

View File

@@ -16,6 +16,7 @@
package org.springframework.data.jpa.repository.aot;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;
import org.jspecify.annotations.Nullable;
@@ -27,7 +28,6 @@ import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryComposition;
import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.util.Streamable;
import org.springframework.data.util.TypeInformation;
/**
@@ -111,7 +111,7 @@ class StubRepositoryInformation implements RepositoryInformation {
}
@Override
public Streamable<Method> getQueryMethods() {
public List<Method> getQueryMethods() {
return null;
}

View File

@@ -37,7 +37,7 @@ import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
*/
class TestJpaAotRepositoryContext<T> implements AotRepositoryContext {
public class TestJpaAotRepositoryContext<T> implements AotRepositoryContext {
private final StubRepositoryInformation repositoryInformation;
private final Class<T> repositoryInterface;

View File

@@ -19,7 +19,7 @@
<!-- <logger name="org.testcontainers" level="debug" />-->
<logger name="org.springframework.data.repository.aot.generate.RepositoryContributor" level="trace" />
<!-- <logger name="org.springframework.data.repository.aot.generate.RepositoryContributor" level="trace" /> -->
<root level="error">
<appender-ref ref="console"/>