From be4d8528b3ef63191142c2cc51780b056e025da3 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 4 Apr 2025 13:00:19 +0200 Subject: [PATCH] Add AOT repository benchmarks. See #3830 --- pom.xml | 17 ++ .../AotRepositoryQueryMethodBenchmarks.java | 260 ++++++++++++++++++ ...a => RepositoryQueryMethodBenchmarks.java} | 23 +- .../aot/JpaRepositoryContributor.java | 11 +- .../jpa/repository/aot/QueriesFactory.java | 12 +- .../aot/StubRepositoryInformation.java | 4 +- .../aot/TestJpaAotRepositoryContext.java | 2 +- .../src/test/resources/logback.xml | 2 +- 8 files changed, 314 insertions(+), 17 deletions(-) create mode 100644 spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/AotRepositoryQueryMethodBenchmarks.java rename spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/{RepositoryFinderBenchmarks.java => RepositoryQueryMethodBenchmarks.java} (93%) diff --git a/pom.xml b/pom.xml index b63358380..3a6241117 100755 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,23 @@ + + jmh + + + com.github.mp911de.microbenchmark-runner + microbenchmark-runner-junit5 + 0.4.0.RELEASE + test + + + + + jitpack.io + https://jitpack.io + + + hibernate-70-snapshots diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/AotRepositoryQueryMethodBenchmarks.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/AotRepositoryQueryMethodBenchmarks.java new file mode 100644 index 000000000..a8682d32c --- /dev/null +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/AotRepositoryQueryMethodBenchmarks.java @@ -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 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 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 derivedFinderMethod(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findAllByFirstname(PERSON_FIRSTNAME); + } + + /*@Benchmark + public List derivedFinderMethodWithInterfaceProjection(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findAllAndProjectToInterfaceByFirstname(PERSON_FIRSTNAME); + } */ + + @Benchmark + public List stringBasedQuery(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname(PERSON_FIRSTNAME); + } + + @Benchmark + public List stringBasedQueryDynamicSort(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname(PERSON_FIRSTNAME, + Sort.by(COLUMN_PERSON_FIRSTNAME)); + } + + @Benchmark + public List 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); + } +} diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryFinderBenchmarks.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryQueryMethodBenchmarks.java similarity index 93% rename from spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryFinderBenchmarks.java rename to spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryQueryMethodBenchmarks.java index 209dc5531..f49d658a0 100644 --- a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryFinderBenchmarks.java +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryQueryMethodBenchmarks.java @@ -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 derivedFinderMethodWithInterfaceProjection(BenchmarkParameters parameters) { return parameters.repositoryProxy.findAllAndProjectToInterfaceByFirstname(PERSON_FIRSTNAME); - } + } */ @Benchmark public List stringBasedQuery(BenchmarkParameters parameters) { @@ -185,7 +191,8 @@ public class RepositoryFinderBenchmarks { @Benchmark public List 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 diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java index 53ce0f8cc..b5ab48031 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java @@ -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) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java index 1188ec7d2..f31d437fc 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java @@ -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> namedQueries = emf.getNamedQueries(candidate); + Map> namedQueries = entityManagerFactory.getNamedQueries(candidate); if (namedQueries.containsKey(queryName)) { return namedQueries.get(queryName); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/StubRepositoryInformation.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/StubRepositoryInformation.java index 6e9b1d900..f9c4042d3 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/StubRepositoryInformation.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/StubRepositoryInformation.java @@ -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 getQueryMethods() { + public List getQueryMethods() { return null; } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java index 0aeaba364..aaf2e5218 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java @@ -37,7 +37,7 @@ import org.springframework.lang.Nullable; /** * @author Christoph Strobl */ -class TestJpaAotRepositoryContext implements AotRepositoryContext { +public class TestJpaAotRepositoryContext implements AotRepositoryContext { private final StubRepositoryInformation repositoryInformation; private final Class repositoryInterface; diff --git a/spring-data-jpa/src/test/resources/logback.xml b/spring-data-jpa/src/test/resources/logback.xml index 780ba5e8f..2df750b92 100644 --- a/spring-data-jpa/src/test/resources/logback.xml +++ b/spring-data-jpa/src/test/resources/logback.xml @@ -19,7 +19,7 @@ - +