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