Add JMH benchmarks and optimizations to affected components.
See #1601 Original pull request: #1617
This commit is contained in:
95
pom.xml
95
pom.xml
@@ -47,6 +47,9 @@
|
||||
<!-- test utilities-->
|
||||
<awaitility.version>4.2.0</awaitility.version>
|
||||
<archunit.version>1.0.1</archunit.version>
|
||||
|
||||
<jmh.version>1.37</jmh.version>
|
||||
<mbr.version>0.4.0.BUILD-SNAPSHOT</mbr.version>
|
||||
</properties>
|
||||
|
||||
<inceptionYear>2017</inceptionYear>
|
||||
@@ -154,6 +157,98 @@
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>jmh</id>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.github.mp911de.microbenchmark-runner</groupId>
|
||||
<artifactId>microbenchmark-runner-junit5</artifactId>
|
||||
<version>${mbr.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjdk.jmh</groupId>
|
||||
<artifactId>jmh-core</artifactId>
|
||||
<version>${jmh.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjdk.jmh</groupId>
|
||||
<artifactId>jmh-generator-annprocess</artifactId>
|
||||
<version>${jmh.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>build-helper-maven-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>add-source</id>
|
||||
<phase>generate-sources</phase>
|
||||
<goals>
|
||||
<goal>add-test-source</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sources>
|
||||
<source>src/jmh/java</source>
|
||||
</sources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>run-benchmarks</id>
|
||||
<phase>pre-integration-test</phase>
|
||||
<goals>
|
||||
<goal>exec</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<classpathScope>test</classpathScope>
|
||||
<executable>java</executable>
|
||||
<arguments>
|
||||
<argument>-classpath</argument>
|
||||
<classpath/>
|
||||
<argument>org.openjdk.jmh.Main</argument>
|
||||
<argument>.*</argument>
|
||||
</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>jitpack.io</id>
|
||||
<url>https://jitpack.io</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -69,10 +69,7 @@ class AggregateReader<T> {
|
||||
this.aggregate = aggregate;
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.table = Table.create(aggregate.getQualifiedTableName());
|
||||
|
||||
this.sqlGenerator = new CachingSqlGenerator(
|
||||
new SingleQuerySqlGenerator(converter.getMappingContext(), aliasFactory, dialect, aggregate));
|
||||
|
||||
this.sqlGenerator = new SingleQuerySqlGenerator(converter.getMappingContext(), aliasFactory, dialect, aggregate);
|
||||
this.extractor = new RowDocumentResultSetExtractor(converter.getMappingContext(),
|
||||
createPathToColumnMapping(aliasFactory));
|
||||
}
|
||||
@@ -187,38 +184,4 @@ class AggregateReader<T> {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper for the {@link org.springframework.data.relational.core.sqlgeneration.SqlGenerator} that caches the
|
||||
* generated statements.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
* @since 3.2
|
||||
*/
|
||||
static class CachingSqlGenerator implements SqlGenerator {
|
||||
|
||||
private final SqlGenerator delegate;
|
||||
private final String findAll;
|
||||
|
||||
public CachingSqlGenerator(SqlGenerator delegate) {
|
||||
|
||||
this.delegate = delegate;
|
||||
this.findAll = delegate.findAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String findAll() {
|
||||
return findAll;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String findAll(@Nullable Condition condition) {
|
||||
return delegate.findAll(condition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AliasFactory getAliasFactory() {
|
||||
return delegate.getAliasFactory();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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.relational.core.sqlgeneration;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||
import org.openjdk.jmh.annotations.Fork;
|
||||
import org.openjdk.jmh.annotations.Measurement;
|
||||
import org.openjdk.jmh.annotations.Mode;
|
||||
import org.openjdk.jmh.annotations.OutputTimeUnit;
|
||||
import org.openjdk.jmh.annotations.Warmup;
|
||||
|
||||
/**
|
||||
* Global benchmark settings.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@Warmup(iterations = 5, time = 2000, timeUnit = TimeUnit.MILLISECONDS)
|
||||
@Measurement(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS)
|
||||
@Fork(value = 1, warmups = 0)
|
||||
@BenchmarkMode(Mode.Throughput)
|
||||
@OutputTimeUnit(TimeUnit.SECONDS)
|
||||
public abstract class BenchmarkSettings {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2023 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.relational.core.sqlgeneration;
|
||||
|
||||
import jmh.mbr.junit5.Microbenchmark;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.openjdk.jmh.annotations.Benchmark;
|
||||
import org.openjdk.jmh.annotations.Scope;
|
||||
import org.openjdk.jmh.annotations.Setup;
|
||||
import org.openjdk.jmh.annotations.State;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.relational.core.dialect.PostgresDialect;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
|
||||
/**
|
||||
* Benchmark for {@link SingleQuerySqlGenerator}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@Microbenchmark
|
||||
public class SingleQuerySqlGeneratorBenchmark extends BenchmarkSettings {
|
||||
|
||||
@Benchmark
|
||||
public String findAll(StateHolder state) {
|
||||
return new SingleQuerySqlGenerator(state.context, state.aliasFactory, PostgresDialect.INSTANCE,
|
||||
state.persistentEntity).findAll(null);
|
||||
}
|
||||
|
||||
@State(Scope.Benchmark)
|
||||
public static class StateHolder {
|
||||
|
||||
RelationalMappingContext context = new RelationalMappingContext();
|
||||
|
||||
RelationalPersistentEntity<?> persistentEntity;
|
||||
|
||||
AliasFactory aliasFactory = new AliasFactory();
|
||||
|
||||
@Setup
|
||||
public void setup() {
|
||||
persistentEntity = context.getRequiredPersistentEntity(SingleReferenceAggregate.class);
|
||||
}
|
||||
}
|
||||
|
||||
record TrivialAggregate(@Id Long id, String name) {
|
||||
}
|
||||
|
||||
record SingleReferenceAggregate(@Id Long id, String name, List<TrivialAggregate> trivials) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.data.mapping.PersistentPropertyPath;
|
||||
import org.springframework.data.util.Lazy;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -38,6 +39,10 @@ class DefaultAggregatePath implements AggregatePath {
|
||||
|
||||
private final @Nullable PersistentPropertyPath<RelationalPersistentProperty> path;
|
||||
|
||||
private final Lazy<TableInfo> tableInfo = Lazy.of(() -> TableInfo.of(this));
|
||||
|
||||
private final Lazy<ColumnInfo> columnInfo = Lazy.of(() -> ColumnInfo.of(this));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
DefaultAggregatePath(RelationalMappingContext context,
|
||||
PersistentPropertyPath<? extends RelationalPersistentProperty> path) {
|
||||
@@ -189,14 +194,24 @@ class DefaultAggregatePath implements AggregatePath {
|
||||
return AggregatePathTraversal.getTableOwningPath(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link Iterator} that iterates over the current path and all ancestors. It will start with the current
|
||||
* path, followed by its parent until ending with the root.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AggregatePath["
|
||||
+ (rootType == null ? path.getBaseProperty().getOwner().getType().getName() : rootType.getName()) + "]"
|
||||
+ ((isRoot()) ? "/" : path.toDotPath());
|
||||
public Iterator<AggregatePath> iterator() {
|
||||
return new AggregatePathIterator(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TableInfo getTableInfo() {
|
||||
return this.tableInfo.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ColumnInfo getColumnInfo() {
|
||||
return this.columnInfo.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
@@ -215,13 +230,12 @@ class DefaultAggregatePath implements AggregatePath {
|
||||
return Objects.hash(context, rootType, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link Iterator} that iterates over the current path and all ancestors. It will start with the current
|
||||
* path, followed by its parent until ending with the root.
|
||||
*/
|
||||
|
||||
@Override
|
||||
public Iterator<AggregatePath> iterator() {
|
||||
return new AggregatePathIterator(this);
|
||||
public String toString() {
|
||||
return "AggregatePath["
|
||||
+ (rootType == null ? path.getBaseProperty().getOwner().getType().getName() : rootType.getName()) + "]"
|
||||
+ ((isRoot()) ? "/" : path.toDotPath());
|
||||
}
|
||||
|
||||
private static class AggregatePathIterator implements Iterator<AggregatePath> {
|
||||
|
||||
@@ -19,6 +19,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -92,15 +93,17 @@ class DefaultSelect implements Select {
|
||||
|
||||
Assert.notNull(visitor, "Visitor must not be null");
|
||||
|
||||
Consumer<? super AbstractSegment> action = it -> it.visit(visitor);
|
||||
|
||||
visitor.enter(this);
|
||||
|
||||
selectList.visit(visitor);
|
||||
from.visit(visitor);
|
||||
joins.forEach(it -> it.visit(visitor));
|
||||
joins.forEach(action);
|
||||
|
||||
visitIfNotNull(where, visitor);
|
||||
|
||||
orderBy.forEach(it -> it.visit(visitor));
|
||||
orderBy.forEach(action);
|
||||
|
||||
visitor.leave(this);
|
||||
}
|
||||
|
||||
@@ -200,11 +200,14 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
|
||||
}
|
||||
|
||||
@Override
|
||||
public Select build() {
|
||||
public Select build(boolean validate) {
|
||||
|
||||
DefaultSelect select = new DefaultSelect(distinct, selectList, from, limit, offset, joins, where, orderBy,
|
||||
lockMode);
|
||||
SelectValidator.validate(select);
|
||||
|
||||
if (validate) {
|
||||
SelectValidator.validate(select);
|
||||
}
|
||||
return select;
|
||||
}
|
||||
|
||||
@@ -359,9 +362,9 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
|
||||
}
|
||||
|
||||
@Override
|
||||
public Select build() {
|
||||
public Select build(boolean validate) {
|
||||
selectBuilder.join(finishJoin());
|
||||
return selectBuilder.build();
|
||||
return selectBuilder.build(validate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,6 +297,7 @@ public interface SelectBuilder {
|
||||
* @param offset row offset, zero-based.
|
||||
* @return {@code this} builder.
|
||||
*/
|
||||
@Override
|
||||
SelectFromAndJoin limitOffset(long limit, long offset);
|
||||
|
||||
/**
|
||||
@@ -305,6 +306,7 @@ public interface SelectBuilder {
|
||||
* @param limit rows to read.
|
||||
* @return {@code this} builder.
|
||||
*/
|
||||
@Override
|
||||
SelectFromAndJoin limit(long limit);
|
||||
|
||||
/**
|
||||
@@ -313,6 +315,7 @@ public interface SelectBuilder {
|
||||
* @param offset start offset.
|
||||
* @return {@code this} builder.
|
||||
*/
|
||||
@Override
|
||||
SelectFromAndJoin offset(long offset);
|
||||
}
|
||||
|
||||
@@ -331,6 +334,7 @@ public interface SelectBuilder {
|
||||
* @param offset row offset, zero-based.
|
||||
* @return {@code this} builder.
|
||||
*/
|
||||
@Override
|
||||
SelectFromAndJoin limitOffset(long limit, long offset);
|
||||
|
||||
/**
|
||||
@@ -339,6 +343,7 @@ public interface SelectBuilder {
|
||||
* @param limit rows to read.
|
||||
* @return {@code this} builder.
|
||||
*/
|
||||
@Override
|
||||
SelectFromAndJoin limit(long limit);
|
||||
|
||||
/**
|
||||
@@ -347,6 +352,7 @@ public interface SelectBuilder {
|
||||
* @param offset start offset.
|
||||
* @return {@code this} builder.
|
||||
*/
|
||||
@Override
|
||||
SelectFromAndJoin offset(long offset);
|
||||
}
|
||||
|
||||
@@ -488,11 +494,11 @@ public interface SelectBuilder {
|
||||
SelectOn leftOuterJoin(TableLike table);
|
||||
|
||||
/**
|
||||
* Declar a join, where the join type ({@code INNER}, {@code LEFT OUTER}, {@code RIGHT OUTER}, {@code FULL OUTER})
|
||||
* Declare a join, where the join type ({@code INNER}, {@code LEFT OUTER}, {@code RIGHT OUTER}, {@code FULL OUTER})
|
||||
* is specified by an extra argument.
|
||||
*
|
||||
*
|
||||
* @param table the table to join. Must not be {@literal null}.
|
||||
* @param joinType the type of joi. Must not be {@literal null}.
|
||||
* @param joinType the type of join. Must not be {@literal null}.
|
||||
* @return {@code this} builder.
|
||||
*/
|
||||
SelectOn join(TableLike table, Join.JoinType joinType);
|
||||
@@ -577,8 +583,20 @@ public interface SelectBuilder {
|
||||
* Build the {@link Select} statement and verify basic relationship constraints such as all referenced columns have
|
||||
* a {@code FROM} or {@code JOIN} table import.
|
||||
*
|
||||
* @return the build and immutable {@link Select} statement.
|
||||
* @return the built and immutable {@link Select} statement.
|
||||
*/
|
||||
Select build();
|
||||
default Select build() {
|
||||
return build(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the {@link Select} statement.
|
||||
*
|
||||
* @param validate whether to validate the generated select by checking basic relationship constraints such as all
|
||||
* referenced columns have a {@code FROM} or {@code JOIN} table import.
|
||||
* @return the built and immutable {@link Select} statement.
|
||||
* @since 3.2
|
||||
*/
|
||||
Select build(boolean validate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.springframework.core.ResolvableType;
|
||||
import org.springframework.data.relational.core.sql.Visitable;
|
||||
import org.springframework.data.relational.core.sql.Visitor;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
|
||||
/**
|
||||
* Type-filtering {@link DelegatingVisitor visitor} applying a {@link Class type filter} derived from the generic type
|
||||
@@ -37,28 +38,35 @@ import org.springframework.lang.Nullable;
|
||||
* {@link Visitable}.</li>
|
||||
* </ol>
|
||||
* </p>
|
||||
*
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 1.1
|
||||
* @see FilteredSubtreeVisitor
|
||||
*/
|
||||
abstract class TypedSubtreeVisitor<T extends Visitable> extends DelegatingVisitor {
|
||||
|
||||
private static final ConcurrentReferenceHashMap<Class<?>, ResolvableType> refCache = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
private final ResolvableType type;
|
||||
private @Nullable Visitable currentSegment;
|
||||
|
||||
enum Assignable {
|
||||
YES, NO,
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link TypedSubtreeVisitor}.
|
||||
*/
|
||||
TypedSubtreeVisitor() {
|
||||
this.type = ResolvableType.forClass(getClass()).as(TypedSubtreeVisitor.class).getGeneric(0);
|
||||
this.type = refCache.computeIfAbsent(this.getClass(),
|
||||
key -> ResolvableType.forClass(key).as(TypedSubtreeVisitor.class).getGeneric(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link TypedSubtreeVisitor} with an explicitly provided type.
|
||||
*/
|
||||
TypedSubtreeVisitor(Class <T> type) {
|
||||
this.type = ResolvableType.forType(type);
|
||||
TypedSubtreeVisitor(Class<T> type) {
|
||||
this.type = refCache.computeIfAbsent(type, key -> ResolvableType.forClass(type));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,7 +125,7 @@ abstract class TypedSubtreeVisitor<T extends Visitable> extends DelegatingVisito
|
||||
|
||||
if (currentSegment == null) {
|
||||
|
||||
if (this.type.isInstance(segment)) {
|
||||
if (type.isInstance(segment)) {
|
||||
|
||||
currentSegment = segment;
|
||||
return enterMatched((T) segment);
|
||||
@@ -142,4 +150,5 @@ abstract class TypedSubtreeVisitor<T extends Visitable> extends DelegatingVisito
|
||||
return leaveNested(segment);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ public class SingleQuerySqlGenerator implements SqlGenerator {
|
||||
finalColumns.add(rootIdExpression);
|
||||
|
||||
Select fullQuery = StatementBuilder.select(finalColumns).from(inlineQuery).orderBy(rootIdExpression, just("rn"))
|
||||
.build();
|
||||
.build(false);
|
||||
|
||||
return SqlRenderer.create(new RenderContextFactory(dialect).createRenderContext()).render(fullQuery);
|
||||
}
|
||||
@@ -118,7 +118,7 @@ public class SingleQuerySqlGenerator implements SqlGenerator {
|
||||
select = applyJoins(rootPath, inlineQueries, select);
|
||||
|
||||
SelectBuilder.BuildSelect buildSelect = applyWhereCondition(rootPath, inlineQueries, select);
|
||||
Select mainSelect = buildSelect.build();
|
||||
Select mainSelect = buildSelect.build(false);
|
||||
|
||||
return InlineQuery.create(mainSelect, "main");
|
||||
}
|
||||
@@ -215,7 +215,7 @@ public class SingleQuerySqlGenerator implements SqlGenerator {
|
||||
|
||||
SelectBuilder.BuildSelect buildSelect = condition != null ? select.where(condition) : select;
|
||||
|
||||
InlineQuery inlineQuery = InlineQuery.create(buildSelect.build(),
|
||||
InlineQuery inlineQuery = InlineQuery.create(buildSelect.build(false),
|
||||
aliases.getTableAlias(context.getAggregatePath(entity)));
|
||||
return QueryMeta.of(basePath, inlineQuery, columnAliases, just(id), just(backReferenceAlias), just(keyAlias),
|
||||
just(rowNumberAlias), just(rowCountAlias));
|
||||
|
||||
Reference in New Issue
Block a user