diff --git a/pom.xml b/pom.xml index e10290896..58c4a9843 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,7 @@ spring-data-mongodb-cross-store spring-data-mongodb-log4j spring-data-mongodb-distribution + spring-data-mongodb-benchmarks @@ -31,6 +32,7 @@ 1.13.5.BUILD-SNAPSHOT 2.14.3 2.13.0 + 1.19 diff --git a/spring-data-mongodb-benchmarks/README.md b/spring-data-mongodb-benchmarks/README.md new file mode 100644 index 000000000..e11925b7f --- /dev/null +++ b/spring-data-mongodb-benchmarks/README.md @@ -0,0 +1,76 @@ +# Benchmarks + +Benchmarks are based on [JMH](http://openjdk.java.net/projects/code-tools/jmh/). + +# Running Benchmarks + +Running benchmarks is disabled by default and can be activated via the `benchmarks` profile. +To run the benchmarks with default settings use. + +```bash +mvn -P benchmarks clean test +``` + +A basic report will be printed to the CLI. + +```bash +# Run complete. Total time: 00:00:15 + +Benchmark Mode Cnt Score Error Units +MappingMongoConverterBenchmark.readObject thrpt 10 1920157,631 ± 64310,809 ops/s +MappingMongoConverterBenchmark.writeObject thrpt 10 782732,857 ± 53804,130 ops/s +``` + +## Running all Benchmarks of a specific class + +To run all Benchmarks of a specific class, just provide its simple class name via the `benchmark` command line argument. + +```bash +mvn -P benchmarks clean test -D benchmark=MappingMongoConverterBenchmark +``` + +## Running a single Benchmark + +To run a single Benchmark provide its containing class simple name followed by `#` and the method name via the `benchmark` command line argument. + +```bash +mvn -P benchmarks clean test -D benchmark=MappingMongoConverterBenchmark#readObjectWith2Properties +``` + +# Saving Benchmark Results + +A detailed benchmark report is stored in JSON format in the `/target/reports/performance` directory. +To store the report in a different location use the `benchmarkReportDir` command line argument. + +## MongoDB + +Results can be directly piped to MongoDB by providing a valid [Connection String](https://docs.mongodb.com/manual/reference/connection-string/) via the `publishTo` command line argument. + +```bash +mvn -P benchmarks clean test -D publishTo=mongodb://127.0.0.1:27017 +``` + +NOTE: If the uri does not explicitly define a database the default `spring-data-mongodb-benchmarks` is used. + +## HTTP Endpoint + +The benchmark report can also be posted as `application/json` to an HTTP Endpoint by providing a valid URl via the `publishTo` command line argument. + +```bash +mvn -P benchmarks clean test -D publishTo=http://127.0.0.1:8080/capture-benchmarks +``` + +# Customizing Benchmarks + +Following options can be set via command line. + +Option | Default Value +--- | --- +warmupIterations | 10 +warmupTime | 1 (seconds) +measurementIterations | 10 +measurementTime | 1 (seconds) +forks | 1 +benchmarkReportDir | /target/reports/performance (always relative to project root dir) +benchmark | .* (single benchmark via `classname#benchmark`) +publishTo | \[not set\] (mongodb-uri or http-endpoint) \ No newline at end of file diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml new file mode 100644 index 000000000..21d537eda --- /dev/null +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -0,0 +1,91 @@ + + + + 4.0.0 + + + org.springframework.data + spring-data-mongodb-parent + 1.10.5.BUILD-SNAPSHOT + ../pom.xml + + + spring-data-mongodb-benchmarks + jar + + Spring Data MongoDB - Microbenchmarks + + + true + false + + + + + ${project.groupId} + spring-data-mongodb + ${project.version} + + + junit + junit + ${junit} + compile + + + org.openjdk.jmh + jmh-core + ${jmh.version} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + provided + + + + + + + benchmarks + + false + + + + + + + + maven-jar-plugin + + + default-jar + never + + + + + maven-surefire-plugin + + ${project.build.sourceDirectory} + ${project.build.outputDirectory} + + **/AbstractMicrobenchmark.java + **/*$*.class + **/generated/*.class + + + **/*Benchmark* + + + ${project.build.directory}/reports/performance + ${project.version} + + + + + + diff --git a/spring-data-mongodb-benchmarks/src/main/java/org/springframework/data/mongodb/core/convert/DbRefMappingBenchmark.java b/spring-data-mongodb-benchmarks/src/main/java/org/springframework/data/mongodb/core/convert/DbRefMappingBenchmark.java new file mode 100644 index 000000000..2138dcee4 --- /dev/null +++ b/spring-data-mongodb-benchmarks/src/main/java/org/springframework/data/mongodb/core/convert/DbRefMappingBenchmark.java @@ -0,0 +1,111 @@ +/* + * 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.mongodb.core.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.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.mapping.DBRef; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.microbenchmark.AbstractMicrobenchmark; + +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/spring-data-mongodb-benchmarks/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterBenchmark.java b/spring-data-mongodb-benchmarks/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterBenchmark.java new file mode 100644 index 000000000..64f9e1bd5 --- /dev/null +++ b/spring-data-mongodb-benchmarks/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterBenchmark.java @@ -0,0 +1,182 @@ +/* + * 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.mongodb.core.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.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.mongodb.core.SimpleMongoDbFactory; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.microbenchmark.AbstractMicrobenchmark; + +import com.mongodb.BasicDBObject; +import com.mongodb.MongoClient; +import com.mongodb.ServerAddress; +import com.mongodb.util.JSON; + +/** + * @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 BasicDBObject documentWith2Properties, documentWith2PropertiesAnd1Nested; + private Customer objectWith2PropertiesAnd1Nested; + + private BasicDBObject 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 CustomConversions(Collections.emptyList())); + this.converter.afterPropertiesSet(); + + // just a flat document + this.documentWith2Properties = new BasicDBObject("firstname", "Dave").append("lastname", "Matthews"); + + // document with a nested one + BasicDBObject address = new BasicDBObject("zipCode", "ABCDE").append("city", "Some Place"); + this.documentWith2PropertiesAnd1Nested = new BasicDBObject("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 = (BasicDBObject) JSON.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 readObjectWith2tPropertiesAnd1NestedObject() { + return converter.read(Customer.class, documentWith2PropertiesAnd1Nested); + } + + @Benchmark // DATAMONGO-1720 + public BasicDBObject writeObjectWith2PropertiesAnd1NestedObject() { + + BasicDBObject sink = new BasicDBObject(); + converter.write(objectWith2PropertiesAnd1Nested, sink); + return sink; + } + + @Benchmark // DATAMONGO-1720 + public Object readObjectWithListAndMapsOfComplexType() { + return converter.read(SlightlyMoreComplexObject.class, documentWithFlatAndComplexPropertiesPlusListAndMap); + } + + @Benchmark // DATAMONGO-1720 + public Object writeObjectWithListAndMapsOfComplexType() { + + BasicDBObject sink = new BasicDBObject(); + converter.write(objectWithFlatAndComplexPropertiesPlusListAndMap, sink); + return sink; + } + + @Getter + @RequiredArgsConstructor + static class Customer { + + private @Id ObjectId id; + private final String firstname, lastname; + private final Address address; + } + + @Getter + @AllArgsConstructor + static class Address { + private String zipCode, city; + } + + @Data + 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/spring-data-mongodb-benchmarks/src/main/java/org/springframework/data/mongodb/microbenchmark/AbstractMicrobenchmark.java b/spring-data-mongodb-benchmarks/src/main/java/org/springframework/data/mongodb/microbenchmark/AbstractMicrobenchmark.java new file mode 100644 index 000000000..c38b4e4ef --- /dev/null +++ b/spring-data-mongodb-benchmarks/src/main/java/org/springframework/data/mongodb/microbenchmark/AbstractMicrobenchmark.java @@ -0,0 +1,329 @@ +/* + * 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.mongodb.microbenchmark; + +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.data.mongodb.microbenchmark.ResultsWriter.Utils; +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 { + Utils.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/spring-data-mongodb-benchmarks/src/main/java/org/springframework/data/mongodb/microbenchmark/HttpResultsWriter.java b/spring-data-mongodb-benchmarks/src/main/java/org/springframework/data/mongodb/microbenchmark/HttpResultsWriter.java new file mode 100644 index 000000000..9df33ba77 --- /dev/null +++ b/spring-data-mongodb-benchmarks/src/main/java/org/springframework/data/mongodb/microbenchmark/HttpResultsWriter.java @@ -0,0 +1,67 @@ +/* + * 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.mongodb.microbenchmark; + +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.Charset; +import java.util.Collection; + +import org.openjdk.jmh.results.RunResult; +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 + public void write(Collection results) { + + if (CollectionUtils.isEmpty(results)) { + return; + } + + try { + + URLConnection connection = new URL(url).openConnection(); + connection.setConnectTimeout(1000); + connection.setDoOutput(true); + connection.setRequestProperty("Content-Type", "application/json"); + + OutputStream output = null; + try { + output = connection.getOutputStream(); + output.write(ResultsWriter.Utils.jsonifyResults(results).getBytes(Charset.forName("UTF-8"))); + } finally { + if (output != null) { + output.close(); + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/spring-data-mongodb-benchmarks/src/main/java/org/springframework/data/mongodb/microbenchmark/MongoResultsWriter.java b/spring-data-mongodb-benchmarks/src/main/java/org/springframework/data/mongodb/microbenchmark/MongoResultsWriter.java new file mode 100644 index 000000000..3f56c5f19 --- /dev/null +++ b/spring-data-mongodb-benchmarks/src/main/java/org/springframework/data/mongodb/microbenchmark/MongoResultsWriter.java @@ -0,0 +1,130 @@ +/* + * 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.mongodb.microbenchmark; + +import java.net.UnknownHostException; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +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.DB; +import com.mongodb.MongoClient; +import com.mongodb.MongoClientURI; +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"); + + MongoClientURI uri = new MongoClientURI(this.uri); + MongoClient client = null; + + try { + client = new MongoClient(uri); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + + String dbName = StringUtils.hasText(uri.getDatabase()) ? uri.getDatabase() : "spring-data-mongodb-benchmarks"; + DB db = client.getDB(dbName); + + for (BasicDBObject dbo : (List) JSON.parse(Utils.jsonifyResults(results))) { + + String collectionName = extractClass(dbo.get("benchmark").toString()); + + BasicDBObject sink = new BasicDBObject(); + sink.append("_version", projectVersion); + sink.append("_method", extractBenchmarkName(dbo.get("benchmark").toString())); + sink.append("_date", now); + sink.append("_snapshot", projectVersion.toLowerCase().contains("snapshot")); + + sink.putAll(dbo.toMap()); + + db.getCollection(collectionName).insert(fixDocumentKeys(sink)); + } + + client.close(); + + } + + /** + * Replace {@code .} by {@code ,}. + * + * @param doc + * @return + */ + private BasicDBObject fixDocumentKeys(BasicDBObject doc) { + + BasicDBObject sanitized = new BasicDBObject(); + + for (Object key : doc.keySet()) { + + Object value = doc.get(key); + if (value instanceof BasicDBObject) { + value = fixDocumentKeys((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 String extractClass(String source) { + + String tmp = source.substring(0, source.lastIndexOf('.')); + return tmp.substring(tmp.lastIndexOf(".") + 1); + } + + private String extractBenchmarkName(String source) { + return source.substring(source.lastIndexOf(".") + 1); + } + +} diff --git a/spring-data-mongodb-benchmarks/src/main/java/org/springframework/data/mongodb/microbenchmark/ResultsWriter.java b/spring-data-mongodb-benchmarks/src/main/java/org/springframework/data/mongodb/microbenchmark/ResultsWriter.java new file mode 100644 index 000000000..10c2326a4 --- /dev/null +++ b/spring-data-mongodb-benchmarks/src/main/java/org/springframework/data/mongodb/microbenchmark/ResultsWriter.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.mongodb.microbenchmark; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.Charset; +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); + + /* non Java8 hack */ + class Utils { + + /** + * 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 + */ + static String jsonifyResults(Collection results) { + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ResultFormatFactory.getInstance(ResultFormatType.JSON, new PrintStream(baos)).writeOut(results); + return new String(baos.toByteArray(), Charset.forName("UTF-8")); + } + } + +} diff --git a/spring-data-mongodb-benchmarks/src/main/resources/logback.xml b/spring-data-mongodb-benchmarks/src/main/resources/logback.xml new file mode 100644 index 000000000..bccb2dc4f --- /dev/null +++ b/spring-data-mongodb-benchmarks/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + + %d %5p %40.40c:%4L - %m%n + + + + + + + + \ No newline at end of file