From 8c7c18c949f698a88241783894681a42aa4a183e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 7 Aug 2018 10:09:47 +0200 Subject: [PATCH] #78 - Import JMH benchmarks. --- benchmark/mongodb/pom.xml | 31 ++ .../mongodb/ProjectionsBenchmark.java | 181 ++++++++++ .../convert/DbRefMappingBenchmark.java | 111 ++++++ .../MappingMongoConverterBenchmark.java | 186 ++++++++++ benchmark/pom.xml | 134 +++++++ benchmark/support/pom.xml | 26 ++ .../common/AbstractMicrobenchmark.java | 328 ++++++++++++++++++ .../common/HttpResultsWriter.java | 81 +++++ .../common/MongoResultsWriter.java | 131 +++++++ .../microbenchmark/common/ResultsWriter.java | 67 ++++ .../support/src/main/resources/logback.xml | 14 + 11 files changed, 1290 insertions(+) create mode 100644 benchmark/mongodb/pom.xml create mode 100644 benchmark/mongodb/src/main/java/org/springframework/data/microbenchmark/mongodb/ProjectionsBenchmark.java create mode 100644 benchmark/mongodb/src/main/java/org/springframework/data/microbenchmark/mongodb/convert/DbRefMappingBenchmark.java create mode 100644 benchmark/mongodb/src/main/java/org/springframework/data/microbenchmark/mongodb/convert/MappingMongoConverterBenchmark.java create mode 100644 benchmark/pom.xml create mode 100644 benchmark/support/pom.xml create mode 100644 benchmark/support/src/main/java/org/springframework/data/microbenchmark/common/AbstractMicrobenchmark.java create mode 100644 benchmark/support/src/main/java/org/springframework/data/microbenchmark/common/HttpResultsWriter.java create mode 100644 benchmark/support/src/main/java/org/springframework/data/microbenchmark/common/MongoResultsWriter.java create mode 100644 benchmark/support/src/main/java/org/springframework/data/microbenchmark/common/ResultsWriter.java create mode 100644 benchmark/support/src/main/resources/logback.xml diff --git a/benchmark/mongodb/pom.xml b/benchmark/mongodb/pom.xml new file mode 100644 index 0000000..04ee597 --- /dev/null +++ b/benchmark/mongodb/pom.xml @@ -0,0 +1,31 @@ + + + + 4.0.0 + + + org.springframework.data + spring-data-benchmark-parent + 2.1.0.BUILD-SNAPSHOT + + + spring-data-benchmark-mongodb + + Spring Data Benchmarks - MongoDB Microbenchmarks + + + + + ${project.groupId} + spring-data-benchmark-support + + + + ${project.groupId} + spring-data-mongodb + + + + + diff --git a/benchmark/mongodb/src/main/java/org/springframework/data/microbenchmark/mongodb/ProjectionsBenchmark.java b/benchmark/mongodb/src/main/java/org/springframework/data/microbenchmark/mongodb/ProjectionsBenchmark.java new file mode 100644 index 0000000..45089b1 --- /dev/null +++ b/benchmark/mongodb/src/main/java/org/springframework/data/microbenchmark/mongodb/ProjectionsBenchmark.java @@ -0,0 +1,181 @@ +/* + * Copyright 2018 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.microbenchmark.mongodb; + +import org.bson.Document; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.TearDown; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.annotation.Id; +import org.springframework.data.microbenchmark.common.AbstractMicrobenchmark; +import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; +import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.query.BasicQuery; + +import com.mongodb.MongoClient; +import com.mongodb.ServerAddress; +import com.mongodb.client.MongoCollection; + +/** + * @author Christoph Strobl + */ +public class ProjectionsBenchmark extends AbstractMicrobenchmark { + + private static final String DB_NAME = "projections-benchmark"; + private static final String COLLECTION_NAME = "projections"; + + private MongoTemplate template; + private MongoClient client; + private MongoCollection mongoCollection; + + private Person source; + + private FindWithQuery asPerson; + private FindWithQuery asDtoProjection; + private FindWithQuery asClosedProjection; + private FindWithQuery asOpenProjection; + + private TerminatingFind asPersonWithFieldsRestriction; + private Document fields = new Document("firstname", 1); + + @Setup + public void setUp() { + + client = new MongoClient(new ServerAddress()); + template = new MongoTemplate(client, DB_NAME); + + source = new Person(); + source.firstname = "luke"; + source.lastname = "skywalker"; + + source.address = new Address(); + source.address.street = "melenium falcon 1"; + source.address.city = "deathstar"; + + template.save(source, COLLECTION_NAME); + + asPerson = template.query(Person.class).inCollection(COLLECTION_NAME); + asDtoProjection = template.query(Person.class).inCollection(COLLECTION_NAME).as(DtoProjection.class); + asClosedProjection = template.query(Person.class).inCollection(COLLECTION_NAME).as(ClosedProjection.class); + asOpenProjection = template.query(Person.class).inCollection(COLLECTION_NAME).as(OpenProjection.class); + + asPersonWithFieldsRestriction = template.query(Person.class).inCollection(COLLECTION_NAME) + .matching(new BasicQuery(new Document(), fields)); + + mongoCollection = client.getDatabase(DB_NAME).getCollection(COLLECTION_NAME); + } + + @TearDown + public void tearDown() { + + client.dropDatabase(DB_NAME); + client.close(); + } + + /** + * Set the baseline for comparison by using the plain MongoDB java driver api without any additional fluff. + * + * @return + */ + @Benchmark // DATAMONGO-1733 + public Object baseline() { + return mongoCollection.find().first(); + } + + /** + * Read into the domain type including all fields. + * + * @return + */ + @Benchmark // DATAMONGO-1733 + public Object readIntoDomainType() { + return asPerson.all(); + } + + /** + * Read into the domain type but restrict query to only return one field. + * + * @return + */ + @Benchmark // DATAMONGO-1733 + public Object readIntoDomainTypeRestrictingToOneField() { + return asPersonWithFieldsRestriction.all(); + } + + /** + * Read into dto projection that only needs to map one field back. + * + * @return + */ + @Benchmark // DATAMONGO-1733 + public Object readIntoDtoProjectionWithOneField() { + return asDtoProjection.all(); + } + + /** + * Read into closed interface projection. + * + * @return + */ + @Benchmark // DATAMONGO-1733 + public Object readIntoClosedProjectionWithOneField() { + return asClosedProjection.all(); + } + + /** + * Read into an open projection backed by the mapped domain object. + * + * @return + */ + @Benchmark // DATAMONGO-1733 + public Object readIntoOpenProjection() { + return asOpenProjection.all(); + } + + static class Person { + + @Id String id; + String firstname; + String lastname; + Address address; + } + + static class Address { + + String city; + String street; + } + + static class DtoProjection { + + @Field("firstname") String name; + } + + static interface ClosedProjection { + + String getFirstname(); + } + + static interface OpenProjection { + + @Value("#{target.firstname}") + String name(); + } + +} diff --git a/benchmark/mongodb/src/main/java/org/springframework/data/microbenchmark/mongodb/convert/DbRefMappingBenchmark.java b/benchmark/mongodb/src/main/java/org/springframework/data/microbenchmark/mongodb/convert/DbRefMappingBenchmark.java new file mode 100644 index 0000000..ef674d6 --- /dev/null +++ b/benchmark/mongodb/src/main/java/org/springframework/data/microbenchmark/mongodb/convert/DbRefMappingBenchmark.java @@ -0,0 +1,111 @@ +/* + * Copyright 2018 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.microbenchmark.mongodb.convert; + +import static org.springframework.data.mongodb.core.query.Criteria.*; +import static org.springframework.data.mongodb.core.query.Query.*; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +import org.bson.types.ObjectId; +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.openjdk.jmh.annotations.TearDown; +import org.springframework.data.annotation.Id; +import org.springframework.data.microbenchmark.common.AbstractMicrobenchmark; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.mapping.DBRef; +import org.springframework.data.mongodb.core.query.Query; + +import com.mongodb.MongoClient; +import com.mongodb.ServerAddress; + +/** + * @author Christoph Strobl + */ +@State(Scope.Benchmark) +public class DbRefMappingBenchmark extends AbstractMicrobenchmark { + + private static final String DB_NAME = "dbref-loading-benchmark"; + + private MongoClient client; + private MongoTemplate template; + + private Query queryObjectWithDBRef; + private Query queryObjectWithDBRefList; + + @Setup + public void setUp() throws Exception { + + client = new MongoClient(new ServerAddress()); + template = new MongoTemplate(client, DB_NAME); + + List refObjects = new ArrayList<>(); + for (int i = 0; i < 1; i++) { + RefObject o = new RefObject(); + template.save(o); + refObjects.add(o); + } + + ObjectWithDBRef singleDBRef = new ObjectWithDBRef(); + singleDBRef.ref = refObjects.iterator().next(); + template.save(singleDBRef); + + ObjectWithDBRef multipleDBRefs = new ObjectWithDBRef(); + multipleDBRefs.refList = refObjects; + template.save(multipleDBRefs); + + queryObjectWithDBRef = query(where("id").is(singleDBRef.id)); + queryObjectWithDBRefList = query(where("id").is(multipleDBRefs.id)); + } + + @TearDown + public void tearDown() { + + client.dropDatabase(DB_NAME); + client.close(); + } + + @Benchmark // DATAMONGO-1720 + public ObjectWithDBRef readSingleDbRef() { + return template.findOne(queryObjectWithDBRef, ObjectWithDBRef.class); + } + + @Benchmark // DATAMONGO-1720 + public ObjectWithDBRef readMultipleDbRefs() { + return template.findOne(queryObjectWithDBRefList, ObjectWithDBRef.class); + } + + @Data + static class ObjectWithDBRef { + + private @Id ObjectId id; + private @DBRef RefObject ref; + private @DBRef List refList; + } + + @Data + static class RefObject { + + private @Id String id; + private String someValue; + } +} diff --git a/benchmark/mongodb/src/main/java/org/springframework/data/microbenchmark/mongodb/convert/MappingMongoConverterBenchmark.java b/benchmark/mongodb/src/main/java/org/springframework/data/microbenchmark/mongodb/convert/MappingMongoConverterBenchmark.java new file mode 100644 index 0000000..4ce2024 --- /dev/null +++ b/benchmark/mongodb/src/main/java/org/springframework/data/microbenchmark/mongodb/convert/MappingMongoConverterBenchmark.java @@ -0,0 +1,186 @@ +/* + * Copyright 2018 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.microbenchmark.mongodb.convert; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.bson.Document; +import org.bson.types.ObjectId; +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.openjdk.jmh.annotations.TearDown; +import org.springframework.data.annotation.Id; +import org.springframework.data.geo.Point; +import org.springframework.data.microbenchmark.common.AbstractMicrobenchmark; +import org.springframework.data.mongodb.core.SimpleMongoDbFactory; +import org.springframework.data.mongodb.core.convert.DbRefResolver; +import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; + +import com.mongodb.MongoClient; +import com.mongodb.ServerAddress; + +/** + * @author Christoph Strobl + */ +@State(Scope.Benchmark) +public class MappingMongoConverterBenchmark extends AbstractMicrobenchmark { + + private static final String DB_NAME = "mapping-mongo-converter-benchmark"; + + private MongoClient client; + private MongoMappingContext mappingContext; + private MappingMongoConverter converter; + private Document documentWith2Properties, documentWith2PropertiesAnd1Nested; + private Customer objectWith2PropertiesAnd1Nested; + + private Document documentWithFlatAndComplexPropertiesPlusListAndMap; + private SlightlyMoreComplexObject objectWithFlatAndComplexPropertiesPlusListAndMap; + + @Setup + public void setUp() throws Exception { + + client = new MongoClient(new ServerAddress()); + + this.mappingContext = new MongoMappingContext(); + this.mappingContext.setInitialEntitySet(Collections.singleton(Customer.class)); + this.mappingContext.afterPropertiesSet(); + + DbRefResolver dbRefResolver = new DefaultDbRefResolver(new SimpleMongoDbFactory(client, DB_NAME)); + + this.converter = new MappingMongoConverter(dbRefResolver, mappingContext); + this.converter.setCustomConversions(new MongoCustomConversions(Collections.emptyList())); + this.converter.afterPropertiesSet(); + + // just a flat document + this.documentWith2Properties = new Document("firstname", "Dave").append("lastname", "Matthews"); + + // document with a nested one + Document address = new Document("zipCode", "ABCDE").append("city", "Some Place"); + this.documentWith2PropertiesAnd1Nested = new Document("firstname", "Dave").// + append("lastname", "Matthews").// + append("address", address); + + // object equivalent of documentWith2PropertiesAnd1Nested + this.objectWith2PropertiesAnd1Nested = new Customer("Dave", "Matthews", new Address("zipCode", "City")); + + // a bit more challenging object with list & map conversion. + objectWithFlatAndComplexPropertiesPlusListAndMap = new SlightlyMoreComplexObject(); + objectWithFlatAndComplexPropertiesPlusListAndMap.id = UUID.randomUUID().toString(); + objectWithFlatAndComplexPropertiesPlusListAndMap.addressList = Arrays.asList(new Address("zip-1", "city-1"), + new Address("zip-2", "city-2")); + objectWithFlatAndComplexPropertiesPlusListAndMap.customer = objectWith2PropertiesAnd1Nested; + objectWithFlatAndComplexPropertiesPlusListAndMap.customerMap = new LinkedHashMap<>(); + objectWithFlatAndComplexPropertiesPlusListAndMap.customerMap.put("dave", objectWith2PropertiesAnd1Nested); + objectWithFlatAndComplexPropertiesPlusListAndMap.customerMap.put("deborah", + new Customer("Deborah Anne", "Dyer", new Address("?", "london"))); + objectWithFlatAndComplexPropertiesPlusListAndMap.customerMap.put("eddie", + new Customer("Eddie", "Vedder", new Address("??", "Seattle"))); + objectWithFlatAndComplexPropertiesPlusListAndMap.intOne = Integer.MIN_VALUE; + objectWithFlatAndComplexPropertiesPlusListAndMap.intTwo = Integer.MAX_VALUE; + objectWithFlatAndComplexPropertiesPlusListAndMap.location = new Point(-33.865143, 151.209900); + objectWithFlatAndComplexPropertiesPlusListAndMap.renamedField = "supercalifragilisticexpialidocious"; + objectWithFlatAndComplexPropertiesPlusListAndMap.stringOne = "¯\\_(ツ)_/¯"; + objectWithFlatAndComplexPropertiesPlusListAndMap.stringTwo = " (╯°□°)╯︵ ┻━┻"; + + // JSON equivalent of objectWithFlatAndComplexPropertiesPlusListAndMap + documentWithFlatAndComplexPropertiesPlusListAndMap = Document.parse( + "{ \"_id\" : \"517f6aee-e9e0-44f0-88ed-f3694a019f27\", \"intOne\" : -2147483648, \"intTwo\" : 2147483647, \"stringOne\" : \"¯\\\\_(ツ)_/¯\", \"stringTwo\" : \" (╯°□°)╯︵ ┻━┻\", \"explicit-field-name\" : \"supercalifragilisticexpialidocious\", \"location\" : { \"x\" : -33.865143, \"y\" : 151.2099 }, \"objectWith2PropertiesAnd1Nested\" : { \"firstname\" : \"Dave\", \"lastname\" : \"Matthews\", \"address\" : { \"zipCode\" : \"zipCode\", \"city\" : \"City\" } }, \"addressList\" : [{ \"zipCode\" : \"zip-1\", \"city\" : \"city-1\" }, { \"zipCode\" : \"zip-2\", \"city\" : \"city-2\" }], \"customerMap\" : { \"dave\" : { \"firstname\" : \"Dave\", \"lastname\" : \"Matthews\", \"address\" : { \"zipCode\" : \"zipCode\", \"city\" : \"City\" } }, \"deborah\" : { \"firstname\" : \"Deborah Anne\", \"lastname\" : \"Dyer\", \"address\" : { \"zipCode\" : \"?\", \"city\" : \"london\" } }, \"eddie\" : { \"firstname\" : \"Eddie\", \"lastname\" : \"Vedder\", \"address\" : { \"zipCode\" : \"??\", \"city\" : \"Seattle\" } } }, \"_class\" : \"org.springframework.data.mongodb.core.convert.MappingMongoConverterBenchmark$SlightlyMoreComplexObject\" }"); + + } + + @TearDown + public void tearDown() { + + client.dropDatabase(DB_NAME); + client.close(); + } + + @Benchmark // DATAMONGO-1720 + public Customer readObjectWith2Properties() { + return converter.read(Customer.class, documentWith2Properties); + } + + @Benchmark // DATAMONGO-1720 + public Customer readObjectWith2PropertiesAnd1NestedObject() { + return converter.read(Customer.class, documentWith2PropertiesAnd1Nested); + } + + @Benchmark // DATAMONGO-1720 + public Document writeObjectWith2PropertiesAnd1NestedObject() { + + Document sink = new Document(); + converter.write(objectWith2PropertiesAnd1Nested, sink); + return sink; + } + + @Benchmark // DATAMONGO-1720 + public Object readObjectWithListAndMapsOfComplexType() { + return converter.read(SlightlyMoreComplexObject.class, documentWithFlatAndComplexPropertiesPlusListAndMap); + } + + @Benchmark // DATAMONGO-1720 + public Object writeObjectWithListAndMapsOfComplexType() { + + Document sink = new Document(); + converter.write(objectWithFlatAndComplexPropertiesPlusListAndMap, sink); + return sink; + } + + @Getter + @RequiredArgsConstructor + public static class Customer { + + private @Id ObjectId id; + private final String firstname, lastname; + private final Address address; + } + + @Getter + @AllArgsConstructor + public static class Address { + private String zipCode, city; + } + + @Data + public static class SlightlyMoreComplexObject { + + @Id String id; + int intOne, intTwo; + String stringOne, stringTwo; + @Field("explicit-field-name") String renamedField; + Point location; + Customer customer; + List
addressList; + Map customerMap; + } + +} diff --git a/benchmark/pom.xml b/benchmark/pom.xml new file mode 100644 index 0000000..4c01d8f --- /dev/null +++ b/benchmark/pom.xml @@ -0,0 +1,134 @@ + + + + 4.0.0 + + org.springframework.data + spring-data-benchmark-parent + pom + + Spring Data Benchmarks + JMH Benchmarks for Spring Data + + + org.springframework.data.build + spring-data-parent + 2.1.0.BUILD-SNAPSHOT + + + + support + mongodb + + + + Lovelace-BUILD-SNAPSHOT + 1.19 + + + + + + + ${project.groupId} + spring-data-releasetrain + ${releasetrain.version} + pom + import + + + + ${project.groupId} + spring-data-benchmark-support + ${project.version} + + + + + + + + org.springframework + spring-core + + + + junit + junit + ${junit} + compile + + + + org.openjdk.jmh + jmh-core + ${jmh.version} + + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + provided + + + + + + + pl.project13.maven + git-commit-id-plugin + 2.2.2 + + + + revision + + + + + + maven-surefire-plugin + + ${project.build.sourceDirectory} + ${project.build.outputDirectory} + + **/AbstractMicrobenchmark.java + **/*$*.class + **/generated/*.class + + + **/*Benchmark* + + + ${project.build.directory}/reports/performance + ${project.version} + ${git.dirty} + ${git.commit.id} + ${git.branch} + + + + + + + + + spring-libs-snapshot + https://repo.spring.io/libs-snapshot + + + + + + spring-plugins-release + https://repo.spring.io/plugins-release + + + spring-libs-milestone + https://repo.spring.io/libs-milestone + + + + diff --git a/benchmark/support/pom.xml b/benchmark/support/pom.xml new file mode 100644 index 0000000..945ef80 --- /dev/null +++ b/benchmark/support/pom.xml @@ -0,0 +1,26 @@ + + + + 4.0.0 + + + org.springframework.data + spring-data-benchmark-parent + 2.1.0.BUILD-SNAPSHOT + + + spring-data-benchmark-support + + Spring Data Benchmarks - Support + + + + + org.springframework.data + spring-data-mongodb + + + + + diff --git a/benchmark/support/src/main/java/org/springframework/data/microbenchmark/common/AbstractMicrobenchmark.java b/benchmark/support/src/main/java/org/springframework/data/microbenchmark/common/AbstractMicrobenchmark.java new file mode 100644 index 0000000..157815f --- /dev/null +++ b/benchmark/support/src/main/java/org/springframework/data/microbenchmark/common/AbstractMicrobenchmark.java @@ -0,0 +1,328 @@ +/* + * Copyright 2018 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.microbenchmark.common; + +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Collection; +import java.util.Date; + +import org.junit.Test; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.results.RunResult; +import org.openjdk.jmh.results.format.ResultFormatType; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.ChainedOptionsBuilder; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.openjdk.jmh.runner.options.TimeValue; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; + +/** + * @author Christoph Strobl + */ +@Warmup(iterations = AbstractMicrobenchmark.WARMUP_ITERATIONS) +@Measurement(iterations = AbstractMicrobenchmark.MEASUREMENT_ITERATIONS) +@Fork(AbstractMicrobenchmark.FORKS) +@State(Scope.Thread) +public class AbstractMicrobenchmark { + + static final int WARMUP_ITERATIONS = 5; + static final int MEASUREMENT_ITERATIONS = 10; + static final int FORKS = 1; + static final String[] JVM_ARGS = { "-server", "-XX:+HeapDumpOnOutOfMemoryError", "-Xms1024m", "-Xmx1024m", + "-XX:MaxDirectMemorySize=1024m" }; + + private final StandardEnvironment environment = new StandardEnvironment(); + + /** + * Run matching {@link org.openjdk.jmh.annotations.Benchmark} methods with options collected from + * {@link org.springframework.core.env.Environment}. + * + * @throws Exception + * @see #options(String) + */ + @Test + public void run() throws Exception { + + String includes = includes(); + + if (!includes.contains(org.springframework.util.ClassUtils.getShortName(getClass()))) { + return; + } + + publishResults(new Runner(options(includes).build()).run()); + } + + /** + * Get the regex for all benchmarks to be included in the run. By default every benchmark within classes matching the + * current ones short name.
+ * The {@literal benchmark} command line argument allows overriding the defaults using {@code #} as class / method + * name separator. + * + * @return never {@literal null}. + * @see org.springframework.util.ClassUtils#getShortName(Class) + */ + protected String includes() { + + String tests = environment.getProperty("benchmark", String.class); + + if (!StringUtils.hasText(tests)) { + return ".*" + org.springframework.util.ClassUtils.getShortName(getClass()) + ".*"; + } + + if (!tests.contains("#")) { + return ".*" + tests + ".*"; + } + + String[] args = tests.split("#"); + return ".*" + args[0] + "." + args[1]; + } + + /** + * Collect all options for the {@link Runner}. + * + * @param includes regex for matching benchmarks to be included in the run. + * @return never {@literal null}. + * @throws Exception + */ + protected ChainedOptionsBuilder options(String includes) throws Exception { + + ChainedOptionsBuilder optionsBuilder = new OptionsBuilder().include(includes).jvmArgs(jvmArgs()); + + optionsBuilder = warmup(optionsBuilder); + optionsBuilder = measure(optionsBuilder); + optionsBuilder = forks(optionsBuilder); + optionsBuilder = report(optionsBuilder); + + return optionsBuilder; + } + + /** + * JVM args to apply to {@link Runner} via its {@link org.openjdk.jmh.runner.options.Options}. + * + * @return {@link #JVM_ARGS} by default. + */ + protected String[] jvmArgs() { + + String[] args = new String[JVM_ARGS.length]; + System.arraycopy(JVM_ARGS, 0, args, 0, JVM_ARGS.length); + return args; + } + + /** + * Read {@code warmupIterations} property from {@link org.springframework.core.env.Environment}. + * + * @return -1 if not set. + */ + protected int getWarmupIterations() { + return environment.getProperty("warmupIterations", Integer.class, -1); + } + + /** + * Read {@code measurementIterations} property from {@link org.springframework.core.env.Environment}. + * + * @return -1 if not set. + */ + protected int getMeasurementIterations() { + return environment.getProperty("measurementIterations", Integer.class, -1); + + } + + /** + * Read {@code forks} property from {@link org.springframework.core.env.Environment}. + * + * @return -1 if not set. + */ + protected int getForksCount() { + return environment.getProperty("forks", Integer.class, -1); + } + + /** + * Read {@code benchmarkReportDir} property from {@link org.springframework.core.env.Environment}. + * + * @return {@literal null} if not set. + */ + protected String getReportDirectory() { + return environment.getProperty("benchmarkReportDir"); + } + + /** + * Read {@code measurementTime} property from {@link org.springframework.core.env.Environment}. + * + * @return -1 if not set. + */ + protected long getMeasurementTime() { + return environment.getProperty("measurementTime", Long.class, -1L); + } + + /** + * Read {@code warmupTime} property from {@link org.springframework.core.env.Environment}. + * + * @return -1 if not set. + */ + protected long getWarmupTime() { + return environment.getProperty("warmupTime", Long.class, -1L); + } + + /** + * {@code project.version_yyyy-MM-dd_ClassName.json} eg. + * {@literal 1.11.0.BUILD-SNAPSHOT_2017-03-07_MappingMongoConverterBenchmark.json} + * + * @return + */ + protected String reportFilename() { + + StringBuilder sb = new StringBuilder(); + + if (environment.containsProperty("project.version")) { + + sb.append(environment.getProperty("project.version")); + sb.append("_"); + } + + sb.append(new SimpleDateFormat("yyyy-MM-dd").format(new Date())); + sb.append("_"); + sb.append(org.springframework.util.ClassUtils.getShortName(getClass())); + sb.append(".json"); + return sb.toString(); + } + + /** + * Apply measurement options to {@link ChainedOptionsBuilder}. + * + * @param optionsBuilder must not be {@literal null}. + * @return {@link ChainedOptionsBuilder} with options applied. + * @see #getMeasurementIterations() + * @see #getMeasurementTime() + */ + private ChainedOptionsBuilder measure(ChainedOptionsBuilder optionsBuilder) { + + int measurementIterations = getMeasurementIterations(); + long measurementTime = getMeasurementTime(); + + if (measurementIterations > 0) { + optionsBuilder = optionsBuilder.measurementIterations(measurementIterations); + } + + if (measurementTime > 0) { + optionsBuilder = optionsBuilder.measurementTime(TimeValue.seconds(measurementTime)); + } + + return optionsBuilder; + } + + /** + * Apply warmup options to {@link ChainedOptionsBuilder}. + * + * @param optionsBuilder must not be {@literal null}. + * @return {@link ChainedOptionsBuilder} with options applied. + * @see #getWarmupIterations() + * @see #getWarmupTime() + */ + private ChainedOptionsBuilder warmup(ChainedOptionsBuilder optionsBuilder) { + + int warmupIterations = getWarmupIterations(); + long warmupTime = getWarmupTime(); + + if (warmupIterations > 0) { + optionsBuilder = optionsBuilder.warmupIterations(warmupIterations); + } + + if (warmupTime > 0) { + optionsBuilder = optionsBuilder.warmupTime(TimeValue.seconds(warmupTime)); + } + + return optionsBuilder; + } + + /** + * Apply forks option to {@link ChainedOptionsBuilder}. + * + * @param optionsBuilder must not be {@literal null}. + * @return {@link ChainedOptionsBuilder} with options applied. + * @see #getForksCount() + */ + private ChainedOptionsBuilder forks(ChainedOptionsBuilder optionsBuilder) { + + int forks = getForksCount(); + + if (forks <= 0) { + return optionsBuilder; + } + + return optionsBuilder.forks(forks); + } + + /** + * Apply report option to {@link ChainedOptionsBuilder}. + * + * @param optionsBuilder must not be {@literal null}. + * @return {@link ChainedOptionsBuilder} with options applied. + * @throws IOException if report file cannot be created. + * @see #getReportDirectory() + */ + private ChainedOptionsBuilder report(ChainedOptionsBuilder optionsBuilder) throws IOException { + + String reportDir = getReportDirectory(); + + if (!StringUtils.hasText(reportDir)) { + return optionsBuilder; + } + + String reportFilePath = reportDir + (reportDir.endsWith(File.separator) ? "" : File.separator) + reportFilename(); + File file = ResourceUtils.getFile(reportFilePath); + + if (file.exists()) { + file.delete(); + } else { + + file.getParentFile().mkdirs(); + file.createNewFile(); + } + + optionsBuilder.resultFormat(ResultFormatType.JSON); + optionsBuilder.result(reportFilePath); + + return optionsBuilder; + } + + /** + * Publish results to an external system. + * + * @param results must not be {@literal null}. + */ + private void publishResults(Collection results) { + + if (CollectionUtils.isEmpty(results) || !environment.containsProperty("publishTo")) { + return; + } + + String uri = environment.getProperty("publishTo"); + try { + ResultsWriter.forUri(uri).write(results); + } catch (Exception e) { + System.err.println(String.format("Cannot save benchmark results to '%s'. Error was %s.", uri, e)); + } + } +} diff --git a/benchmark/support/src/main/java/org/springframework/data/microbenchmark/common/HttpResultsWriter.java b/benchmark/support/src/main/java/org/springframework/data/microbenchmark/common/HttpResultsWriter.java new file mode 100644 index 0000000..00de702 --- /dev/null +++ b/benchmark/support/src/main/java/org/springframework/data/microbenchmark/common/HttpResultsWriter.java @@ -0,0 +1,81 @@ +/* + * Copyright 2018 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.microbenchmark.common; + +import lombok.SneakyThrows; + +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Collection; + +import org.openjdk.jmh.results.RunResult; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.util.CollectionUtils; + +/** + * {@link ResultsWriter} implementation of {@link URLConnection}. + * + * @since 2.0 + */ +class HttpResultsWriter implements ResultsWriter { + + private final String url; + + HttpResultsWriter(String url) { + this.url = url; + } + + @Override + @SneakyThrows + public void write(Collection results) { + + if (CollectionUtils.isEmpty(results)) { + return; + } + + StandardEnvironment env = new StandardEnvironment(); + + String projectVersion = env.getProperty("project.version", "unknown"); + String gitBranch = env.getProperty("git.branch", "unknown"); + String gitDirty = env.getProperty("git.dirty", "no"); + String gitCommitId = env.getProperty("git.commit.id", "unknown"); + + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + connection.setConnectTimeout((int) Duration.ofSeconds(1).toMillis()); + connection.setReadTimeout((int) Duration.ofSeconds(1).toMillis()); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + + connection.setRequestProperty("Content-Type", "application/json"); + connection.addRequestProperty("X-Project-Version", projectVersion); + connection.addRequestProperty("X-Git-Branch", gitBranch); + connection.addRequestProperty("X-Git-Dirty", gitDirty); + connection.addRequestProperty("X-Git-Commit-Id", gitCommitId); + + try (OutputStream output = connection.getOutputStream()) { + output.write(ResultsWriter.jsonifyResults(results).getBytes(StandardCharsets.UTF_8)); + } + + if (connection.getResponseCode() >= 400) { + throw new IllegalStateException( + String.format("Status %d %s", connection.getResponseCode(), connection.getResponseMessage())); + } + } +} diff --git a/benchmark/support/src/main/java/org/springframework/data/microbenchmark/common/MongoResultsWriter.java b/benchmark/support/src/main/java/org/springframework/data/microbenchmark/common/MongoResultsWriter.java new file mode 100644 index 0000000..785a851 --- /dev/null +++ b/benchmark/support/src/main/java/org/springframework/data/microbenchmark/common/MongoResultsWriter.java @@ -0,0 +1,131 @@ +/* + * Copyright 2018 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.microbenchmark.common; + +import java.util.Collection; +import java.util.Date; +import java.util.List; + +import org.bson.Document; +import org.openjdk.jmh.results.RunResult; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +import com.mongodb.BasicDBObject; +import com.mongodb.MongoClient; +import com.mongodb.MongoClientURI; +import com.mongodb.client.MongoDatabase; +import com.mongodb.util.JSON; + +/** + * MongoDB specific {@link ResultsWriter} implementation. + * + * @author Christoph Strobl + * @since 2.0 + */ +class MongoResultsWriter implements ResultsWriter { + + private final String uri; + + MongoResultsWriter(String uri) { + this.uri = uri; + } + + @Override + public void write(Collection results) { + + Date now = new Date(); + StandardEnvironment env = new StandardEnvironment(); + + String projectVersion = env.getProperty("project.version", "unknown"); + String gitBranch = env.getProperty("git.branch", "unknown"); + String gitDirty = env.getProperty("git.dirty", "no"); + String gitCommitId = env.getProperty("git.commit.id", "unknown"); + + MongoClientURI uri = new MongoClientURI(this.uri); + MongoClient client = new MongoClient(uri); + + String dbName = StringUtils.hasText(uri.getDatabase()) ? uri.getDatabase() : "spring-data-mongodb-benchmarks"; + MongoDatabase db = client.getDatabase(dbName); + + for (BasicDBObject dbo : (List) JSON.parse(ResultsWriter.jsonifyResults(results))) { + + String collectionName = extractClass(dbo.get("benchmark").toString()); + + Document sink = new Document(); + sink.append("_version", projectVersion); + sink.append("_branch", gitBranch); + sink.append("_commit", gitCommitId); + sink.append("_dirty", gitDirty); + sink.append("_method", extractBenchmarkName(dbo.get("benchmark").toString())); + sink.append("_date", now); + sink.append("_snapshot", projectVersion.toLowerCase().contains("snapshot")); + + sink.putAll(dbo); + + db.getCollection(collectionName).insertOne(fixDocumentKeys(sink)); + } + + client.close(); + } + + /** + * Replace {@code .} by {@code ,}. + * + * @param doc + * @return + */ + private Document fixDocumentKeys(Document doc) { + + Document sanitized = new Document(); + + for (Object key : doc.keySet()) { + + Object value = doc.get(key); + if (value instanceof Document) { + value = fixDocumentKeys((Document) value); + } else if (value instanceof BasicDBObject) { + value = fixDocumentKeys(new Document((BasicDBObject) value)); + } + + if (key instanceof String) { + + String newKey = (String) key; + if (newKey.contains(".")) { + newKey = newKey.replace('.', ','); + } + + sanitized.put(newKey, value); + } else { + sanitized.put(ObjectUtils.nullSafeToString(key).replace('.', ','), value); + } + } + + return sanitized; + } + + private static String extractClass(String source) { + + String tmp = source.substring(0, source.lastIndexOf('.')); + return tmp.substring(tmp.lastIndexOf(".") + 1); + } + + private static String extractBenchmarkName(String source) { + return source.substring(source.lastIndexOf(".") + 1); + } + +} diff --git a/benchmark/support/src/main/java/org/springframework/data/microbenchmark/common/ResultsWriter.java b/benchmark/support/src/main/java/org/springframework/data/microbenchmark/common/ResultsWriter.java new file mode 100644 index 0000000..25a9375 --- /dev/null +++ b/benchmark/support/src/main/java/org/springframework/data/microbenchmark/common/ResultsWriter.java @@ -0,0 +1,67 @@ +/* + * Copyright 2018 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.microbenchmark.common; + +import lombok.SneakyThrows; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.Collection; + +import org.openjdk.jmh.results.RunResult; +import org.openjdk.jmh.results.format.ResultFormatFactory; +import org.openjdk.jmh.results.format.ResultFormatType; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +interface ResultsWriter { + + /** + * Write the {@link RunResult}s. + * + * @param results can be {@literal null}. + */ + void write(Collection results); + + /** + * Get the uri specific {@link ResultsWriter}. + * + * @param uri must not be {@literal null}. + * @return + */ + static ResultsWriter forUri(String uri) { + return uri.startsWith("mongodb:") ? new MongoResultsWriter(uri) : new HttpResultsWriter(uri); + } + + /** + * Convert {@link RunResult}s to JMH Json representation. + * + * @param results + * @return json string representation of results. + * @see org.openjdk.jmh.results.format.JSONResultFormat + */ + @SneakyThrows + static String jsonifyResults(Collection results) { + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ResultFormatFactory.getInstance(ResultFormatType.JSON, new PrintStream(baos, true, "UTF-8")).writeOut(results); + + return new String(baos.toByteArray(), StandardCharsets.UTF_8); + } +} diff --git a/benchmark/support/src/main/resources/logback.xml b/benchmark/support/src/main/resources/logback.xml new file mode 100644 index 0000000..d076abd --- /dev/null +++ b/benchmark/support/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + + %d %5p %40.40c:%4L - %m%n + + + + + + + +