From 633b90493fbdf72d110a80c55cd9763cb481e36e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 8 Feb 2017 17:06:05 +0100 Subject: [PATCH] DATACASS-399 - Create JMH benchmarks. Add benchmarks for plain reads and writes without mapping and using object mapping. --- pom.xml | 1 + spring-data-cassandra-benchmarks/pom.xml | 98 ++++++++ .../data/cassandra/benchmarks/Address.java | 32 +++ .../CassandraMappingContextBenchmark.java | 68 ++++++ .../data/cassandra/benchmarks/Customer.java | 33 +++ .../MappingCassandraConverterBenchmark.java | 231 ++++++++++++++++++ ...pingCassandraConverterOnlineBenchmark.java | 195 +++++++++++++++ 7 files changed, 658 insertions(+) create mode 100644 spring-data-cassandra-benchmarks/pom.xml create mode 100644 spring-data-cassandra-benchmarks/src/main/java/org/springframework/data/cassandra/benchmarks/Address.java create mode 100644 spring-data-cassandra-benchmarks/src/main/java/org/springframework/data/cassandra/benchmarks/CassandraMappingContextBenchmark.java create mode 100644 spring-data-cassandra-benchmarks/src/main/java/org/springframework/data/cassandra/benchmarks/Customer.java create mode 100644 spring-data-cassandra-benchmarks/src/main/java/org/springframework/data/cassandra/benchmarks/MappingCassandraConverterBenchmark.java create mode 100644 spring-data-cassandra-benchmarks/src/main/java/org/springframework/data/cassandra/benchmarks/MappingCassandraConverterOnlineBenchmark.java diff --git a/pom.xml b/pom.xml index ced9c50b8..e763fe1fd 100644 --- a/pom.xml +++ b/pom.xml @@ -82,6 +82,7 @@ spring-cql spring-data-cassandra spring-data-cassandra-distribution + spring-data-cassandra-benchmarks diff --git a/spring-data-cassandra-benchmarks/pom.xml b/spring-data-cassandra-benchmarks/pom.xml new file mode 100644 index 000000000..6cb3bdf66 --- /dev/null +++ b/spring-data-cassandra-benchmarks/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + + + org.springframework.data + spring-data-cassandra-parent + 2.0.0.DATACASS-389-SNAPSHOT + ../pom.xml + + + spring-data-cassandra-benchmarks + + Spring Data Cassandra - Benchmarks + + + + + + org.springframework.data + spring-data-cassandra + ${project.version} + + + + org.slf4j + slf4j-nop + 1.7.21 + + + + org.openjdk.jmh + jmh-core + ${jmh.version} + + + + org.projectlombok + lombok + 1.16.12 + provided + + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + provided + + + + + + 1.17.4 + benchmarks + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 2.2 + + + package + + shade + + + ${uberjar.name} + + + org.openjdk.jmh.Main + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + + \ No newline at end of file diff --git a/spring-data-cassandra-benchmarks/src/main/java/org/springframework/data/cassandra/benchmarks/Address.java b/spring-data-cassandra-benchmarks/src/main/java/org/springframework/data/cassandra/benchmarks/Address.java new file mode 100644 index 000000000..9811d5f8c --- /dev/null +++ b/spring-data-cassandra-benchmarks/src/main/java/org/springframework/data/cassandra/benchmarks/Address.java @@ -0,0 +1,32 @@ +/* + * Copyright 2017 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 + * + * 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.springframework.data.cassandra.benchmarks; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import org.springframework.data.cassandra.mapping.UserDefinedType; + +/** + * @author Mark Paluch + */ +@Getter +@RequiredArgsConstructor +@UserDefinedType +public class Address { + + final String city, zip; +} diff --git a/spring-data-cassandra-benchmarks/src/main/java/org/springframework/data/cassandra/benchmarks/CassandraMappingContextBenchmark.java b/spring-data-cassandra-benchmarks/src/main/java/org/springframework/data/cassandra/benchmarks/CassandraMappingContextBenchmark.java new file mode 100644 index 000000000..b193b4615 --- /dev/null +++ b/spring-data-cassandra-benchmarks/src/main/java/org/springframework/data/cassandra/benchmarks/CassandraMappingContextBenchmark.java @@ -0,0 +1,68 @@ +/* + * Copyright 2017 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 + * + * 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.springframework.data.cassandra.benchmarks; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; + +/** + * Benchmark for {@link CassandraMappingContext}. + * + * @author Mark Paluch + */ +@State(Scope.Benchmark) +public class CassandraMappingContextBenchmark { + + @Benchmark + public void measureGetPersistentEntity(Blackhole blackhole) { + + BasicCassandraMappingContext mappingContext = new BasicCassandraMappingContext(); + mappingContext.setUserTypeResolver(typeName -> null); + blackhole.consume(mappingContext.getPersistentEntity(Address.class)); + } + + @Benchmark + public void measureGetPersistentEntityWithUdtReference(Blackhole blackhole) { + + BasicCassandraMappingContext mappingContext = new BasicCassandraMappingContext(); + mappingContext.setUserTypeResolver(typeName -> null); + blackhole.consume(mappingContext.getRequiredPersistentEntity(Customer.class)); + } + + public static void main(String[] args) throws RunnerException { + + Options opt = new OptionsBuilder() // + .include(CassandraMappingContextBenchmark.class.getSimpleName()) // + .forks(1) // + .warmupIterations(5) // + .measurementIterations(10) // + .mode(Mode.AverageTime) // + .timeUnit(TimeUnit.NANOSECONDS) // + .build(); + + new Runner(opt).run(); + } +} diff --git a/spring-data-cassandra-benchmarks/src/main/java/org/springframework/data/cassandra/benchmarks/Customer.java b/spring-data-cassandra-benchmarks/src/main/java/org/springframework/data/cassandra/benchmarks/Customer.java new file mode 100644 index 000000000..eaeeca220 --- /dev/null +++ b/spring-data-cassandra-benchmarks/src/main/java/org/springframework/data/cassandra/benchmarks/Customer.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017 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 + * + * 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.springframework.data.cassandra.benchmarks; + +import lombok.Data; + +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.mapping.Table; + +/** + * @author Mark Paluch + */ +@Data +@Table +public class Customer { + + private @Id String id; + private String firstname, lastname; + private Address address; +} diff --git a/spring-data-cassandra-benchmarks/src/main/java/org/springframework/data/cassandra/benchmarks/MappingCassandraConverterBenchmark.java b/spring-data-cassandra-benchmarks/src/main/java/org/springframework/data/cassandra/benchmarks/MappingCassandraConverterBenchmark.java new file mode 100644 index 000000000..7376aeed8 --- /dev/null +++ b/spring-data-cassandra-benchmarks/src/main/java/org/springframework/data/cassandra/benchmarks/MappingCassandraConverterBenchmark.java @@ -0,0 +1,231 @@ +/* + * Copyright 2017 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 + * + * 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.springframework.data.cassandra.benchmarks; + +import static java.util.Arrays.*; +import static org.springframework.data.cassandra.benchmarks.MappingCassandraConverterBenchmark.BenchmarkDependencyFactory.*; + +import java.lang.reflect.Constructor; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.cassandra.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; +import org.springframework.data.cassandra.mapping.UserTypeResolver; +import org.springframework.util.ReflectionUtils; + +import com.datastax.driver.core.CodecRegistry; +import com.datastax.driver.core.ColumnDefinitions; +import com.datastax.driver.core.ColumnDefinitions.Definition; +import com.datastax.driver.core.DataType; +import com.datastax.driver.core.ProtocolVersion; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.TypeCodec; +import com.datastax.driver.core.UDTValue; +import com.datastax.driver.core.UserType; +import com.datastax.driver.core.UserType.Field; +import com.datastax.driver.core.querybuilder.QueryBuilder; + +/** + * Benchmark for {@link MappingCassandraConverter}. + * + * @author Mark Paluch + */ +@State(Scope.Benchmark) +public class MappingCassandraConverterBenchmark { + + private final BasicCassandraMappingContext mappingContext = new BasicCassandraMappingContext(); + private final MappingCassandraConverter converter = new MappingCassandraConverter(mappingContext); + + private ColumnDefinitions customerColumns; + private UserType addressType; + + private ByteBuffer id; + private ByteBuffer firstname; + private ByteBuffer lastname; + private ByteBuffer address; + + private Customer customer; + private Customer customerWithMappedUdt; + + @Setup + public void beforeBenchmark() throws ReflectiveOperationException { + + this.mappingContext.setUserTypeResolver(new BenchmarkUserTypeResolver()); + + this.mappingContext.getPersistentEntity(Customer.class); + this.mappingContext.getPersistentEntity(Address.class); + + this.addressType = createUserType("address", + asList(field("zip", DataType.varchar()), field("city", DataType.varchar()))); + + this.customerColumns = columnDefinitions(asList(definition("id", DataType.varchar()), // + definition("firstname", DataType.varchar()), // + definition("lastname", DataType.varchar()), // + definition("address", this.addressType) // + )); + + this.id = encode("my-id", DataType.varchar()); + this.firstname = encode("Walter", DataType.varchar()); + this.lastname = encode("White", DataType.varchar()); + + UDTValue udtValue = this.addressType.newValue(); + udtValue.setString("zip", "12345"); + udtValue.setString("city", "Albuquerque"); + + this.address = encode(udtValue, this.addressType); + + Address address = new Address("12345", "Albuquerque"); + + this.customer = createCustomer(); + + Customer customerWithMappedUdt = createCustomer(); + customerWithMappedUdt.setAddress(address); + + this.customerWithMappedUdt = customerWithMappedUdt; + } + + private Customer createCustomer() { + + Customer customer = new Customer(); + + customer.setId("my-id"); + customer.setFirstname("Walter"); + customer.setLastname("White"); + + return customer; + } + + // Benchmark + public void measureReadRow() throws ReflectiveOperationException { + + Row row = createRow(this.customerColumns, + asList(id.duplicate(), this.firstname.duplicate(), this.lastname.duplicate(), null)); + + this.converter.read(Customer.class, row); + } + + // @Benchmark + public void measureReadRowWithUdt() throws ReflectiveOperationException { + + Row row = createRow(this.customerColumns, + asList(this.id.duplicate(), this.firstname.duplicate(), this.lastname.duplicate(), this.address.duplicate())); + + converter.read(Customer.class, row); + } + + @Benchmark + public void measureWriteQuery() throws ReflectiveOperationException { + converter.write(this.customer, QueryBuilder.insertInto("table")); + } + + @Benchmark + public void measureWriteRowWithUdt() throws ReflectiveOperationException { + converter.write(this.customerWithMappedUdt, QueryBuilder.insertInto("table")); + } + + public static void main(String[] args) throws Exception { + + Options opt = new OptionsBuilder() // + .include(MappingCassandraConverterBenchmark.class.getSimpleName()) // + .forks(1) // + .warmupIterations(5) // + .measurementIterations(10) // + .mode(Mode.AverageTime) // + .timeUnit(TimeUnit.NANOSECONDS) // + .build(); + + new Runner(opt).run(); + } + + class BenchmarkUserTypeResolver implements UserTypeResolver { + + @Override + public UserType resolveType(CqlIdentifier typeName) { + return addressType; + } + } + + /** + * Factory to create dependencies required for the benchmark. + */ + static class BenchmarkDependencyFactory { + + static Row createRow(ColumnDefinitions definitions, List data) throws ReflectiveOperationException { + + Class rowClass = (Class) Class.forName("com.datastax.driver.core.ArrayBackedRow"); + Class tokenFactoryClass = Class.forName("com.datastax.driver.core.Token$Factory"); + + Constructor constructor = ReflectionUtils.accessibleConstructor(rowClass, ColumnDefinitions.class, + tokenFactoryClass, ProtocolVersion.class, List.class); + + return constructor.newInstance(definitions, null, ProtocolVersion.NEWEST_SUPPORTED, data); + } + + static Definition definition(String name, DataType type) throws ReflectiveOperationException { + + Constructor constructor = ReflectionUtils.accessibleConstructor(Definition.class, String.class, + String.class, String.class, DataType.class); + + return constructor.newInstance("keyspace", "table", name, type); + } + + static ColumnDefinitions columnDefinitions(Collection definitions) throws ReflectiveOperationException { + + Constructor constructor = ReflectionUtils.accessibleConstructor(ColumnDefinitions.class, + Definition[].class, CodecRegistry.class); + + return constructor.newInstance(definitions.toArray(new ColumnDefinitions.Definition[0]), + CodecRegistry.DEFAULT_INSTANCE); + } + + static Field field(String name, DataType type) throws ReflectiveOperationException { + + Class fieldClass = (Class) Class.forName("com.datastax.driver.core.UserType$Field"); + + Constructor constructor = ReflectionUtils.accessibleConstructor(fieldClass, String.class, DataType.class); + + return constructor.newInstance(name, type); + } + + static UserType createUserType(String name, Collection fields) throws ReflectiveOperationException { + + Constructor constructor = ReflectionUtils.accessibleConstructor(UserType.class, String.class, + String.class, Collection.class, ProtocolVersion.class, CodecRegistry.class); + + return constructor.newInstance("keyspace", name, fields, ProtocolVersion.NEWEST_SUPPORTED, + CodecRegistry.DEFAULT_INSTANCE); + } + + static ByteBuffer encode(Object data, DataType type) throws ReflectiveOperationException { + + TypeCodec objectTypeCodec = CodecRegistry.DEFAULT_INSTANCE.codecFor(type); + + return objectTypeCodec.serialize(data, ProtocolVersion.NEWEST_SUPPORTED); + } + } +} diff --git a/spring-data-cassandra-benchmarks/src/main/java/org/springframework/data/cassandra/benchmarks/MappingCassandraConverterOnlineBenchmark.java b/spring-data-cassandra-benchmarks/src/main/java/org/springframework/data/cassandra/benchmarks/MappingCassandraConverterOnlineBenchmark.java new file mode 100644 index 000000000..866fdce0f --- /dev/null +++ b/spring-data-cassandra-benchmarks/src/main/java/org/springframework/data/cassandra/benchmarks/MappingCassandraConverterOnlineBenchmark.java @@ -0,0 +1,195 @@ +/* + * Copyright 2017 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 + * + * 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.springframework.data.cassandra.benchmarks; + +import static com.datastax.driver.core.querybuilder.QueryBuilder.*; + +import io.netty.channel.EventLoopGroup; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Mode; +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.infra.Blackhole; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.cassandra.config.CassandraSessionFactoryBean; +import org.springframework.data.cassandra.config.SchemaAction; +import org.springframework.data.cassandra.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; +import org.springframework.data.cassandra.mapping.SimpleUserTypeResolver; +import org.springframework.data.cassandra.mapping.UserTypeResolver; + +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.NettyOptions; +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.Statement; +import com.datastax.driver.core.UDTValue; +import com.datastax.driver.core.querybuilder.QueryBuilder; +import com.datastax.driver.core.querybuilder.Update; + +/** + * Benchmark for {@link MappingCassandraConverter} requiring a running Apache Cassandra server on {@code localhost:9042} + * providing a keyspace named {@code example}. + * + * @author Mark Paluch + */ +@State(Scope.Benchmark) +public class MappingCassandraConverterOnlineBenchmark { + + private final BasicCassandraMappingContext mappingContext = new BasicCassandraMappingContext(); + private final MappingCassandraConverter converter = new MappingCassandraConverter(mappingContext); + + private Cluster cluster; + private Session session; + private UserTypeResolver userTypeResolver; + + private Customer customerWithMappedUdt; + + @Setup + public void beforeBenchmark() throws Exception { + + cluster = Cluster.builder().addContactPoint("localhost").withNettyOptions(new NettyOptions() { + + public void onClusterClose(EventLoopGroup eventLoopGroup) { + eventLoopGroup.shutdownGracefully(0, 0, TimeUnit.SECONDS).syncUninterruptibly(); + } + + }).build(); + + CassandraSessionFactoryBean sessionFactoryBean = new CassandraSessionFactoryBean(); + + sessionFactoryBean.setKeyspaceName("example"); + sessionFactoryBean.setSchemaAction(SchemaAction.RECREATE); + sessionFactoryBean.setCluster(cluster); + sessionFactoryBean.setConverter(converter); + + this.userTypeResolver = new SimpleUserTypeResolver(cluster, "example"); + this.mappingContext.setUserTypeResolver(userTypeResolver); + + this.mappingContext.getPersistentEntity(Customer.class); + this.mappingContext.getPersistentEntity(Address.class); + + sessionFactoryBean.afterPropertiesSet(); + + this.session = sessionFactoryBean.getObject(); + + UDTValue udtValue = this.userTypeResolver.resolveType(CqlIdentifier.cqlId("address")).newValue(); + udtValue.setString("zip", "12345"); + udtValue.setString("city", "Albuquerque"); + + this.session.execute(QueryBuilder.truncate("customer")); + this.session.execute(QueryBuilder.insertInto("customer").value("id", "my-id").value("firstname", "Walter") + .value("lastname", "White").value("address", udtValue)); + + Address address = new Address("12345", "Albuquerque"); + + Customer customerWithMappedUdt = createCustomer(); + customerWithMappedUdt.setAddress(address); + + this.customerWithMappedUdt = customerWithMappedUdt; + } + + private Customer createCustomer() { + + Customer customer = new Customer(); + + customer.setId("my-id"); + customer.setFirstname("Walter"); + customer.setLastname("White"); + + return customer; + } + + @TearDown + public void afterBenchmark() { + + this.session.close(); + this.cluster.close(); + } + + @Benchmark + public void measureReadRowPlain(Blackhole blackhole) { + + ResultSet rows = this.session.execute(QueryBuilder.select().from("customer").where(eq("id", "my-id"))); + + Row row = rows.one(); + blackhole.consume(row.getString("id")); + blackhole.consume(row.getString("firstname")); + blackhole.consume(row.getString("lastname")); + + UDTValue address = row.getUDTValue("address"); + blackhole.consume(address.getString("zip")); + blackhole.consume(address.getString("city")); + } + + @Benchmark + public void measureReadRowMapped(Blackhole blackhole) { + + ResultSet rows = this.session.execute(QueryBuilder.select().from("customer").where(eq("id", "my-id"))); + + Row row = rows.one(); + blackhole.consume(this.converter.read(Customer.class, row)); + } + + @Benchmark + public void measureWriteRowPlain() { + + UDTValue udtValue = this.userTypeResolver.resolveType(CqlIdentifier.cqlId("address")).newValue(); + udtValue.setString("zip", "12345"); + udtValue.setString("city", "Albuquerque"); + + Statement statement = QueryBuilder.update("customer") // + .where(eq("id", "my-id")) // + .with(QueryBuilder.set("firstname", "Walter")) // + .and(QueryBuilder.set("lastname", "White")) // + .and(QueryBuilder.set("address", udtValue)); + + this.session.execute(statement); + } + + @Benchmark + public void measureWriteRowMapped() { + + Update update = QueryBuilder.update("customer"); + + this.converter.write(customerWithMappedUdt, update); + this.session.execute(update); + } + + public static void main(String[] args) throws Exception { + + Options opt = new OptionsBuilder() // + .include(MappingCassandraConverterOnlineBenchmark.class.getSimpleName()) // + .forks(1) // + .warmupIterations(5) // + .measurementIterations(10) // + .mode(Mode.AverageTime) // + .timeUnit(TimeUnit.NANOSECONDS) // + .build(); + + new Runner(opt).run(); + + } +}