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
index 157815f..392aa99 100644
--- 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
@@ -15,314 +15,25 @@
*/
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.junit.runner.RunWith;
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;
/**
+ * Base class for microbenchmarks providing default JMH settings and allowing execution through JUnit.
+ *
* @author Christoph Strobl
+ * @author Mark Paluch
+ * @see Microbenchmark
*/
-@Warmup(iterations = AbstractMicrobenchmark.WARMUP_ITERATIONS)
-@Measurement(iterations = AbstractMicrobenchmark.MEASUREMENT_ITERATIONS)
-@Fork(AbstractMicrobenchmark.FORKS)
+@Warmup(iterations = JmhSupport.WARMUP_ITERATIONS)
+@Measurement(iterations = JmhSupport.MEASUREMENT_ITERATIONS)
+@Fork(JmhSupport.FORKS)
@State(Scope.Thread)
-public class AbstractMicrobenchmark {
+@RunWith(Microbenchmark.class)
+public abstract 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/JmhSupport.java b/benchmark/support/src/main/java/org/springframework/data/microbenchmark/common/JmhSupport.java
new file mode 100644
index 0000000..eaf265c
--- /dev/null
+++ b/benchmark/support/src/main/java/org/springframework/data/microbenchmark/common/JmhSupport.java
@@ -0,0 +1,306 @@
+/*
+ * 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.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.junit.runners.model.FrameworkMethod;
+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
+ * @author Mark Paluch
+ */
+class JmhSupport {
+
+ 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();
+
+ /**
+ * Get the regex for all benchmarks to be included in the run. By default every benchmark within classes matching the
+ * fqcn.
+ * The {@literal benchmark} command line argument allows overriding the defaults using {@code #} as class / method
+ * name separator.
+ *
+ * @return never {@literal null}.
+ * @param methods
+ */
+ protected List includes(Collection methods) {
+
+ String tests = environment.getProperty("benchmark", String.class);
+
+ if (!StringUtils.hasText(tests)) {
+
+ return methods.stream().map(it -> Pattern.quote(it.getDeclaringClass().getName()) + "\\." + it.getName())
+ .collect(Collectors.toList());
+ }
+
+ if (!tests.contains("#")) {
+ return Collections.singletonList(".*" + tests + ".*");
+ }
+
+ String[] args = tests.split("#");
+ return Collections.singletonList(".*" + args[0] + "." + args[1]);
+ }
+
+ /**
+ * Collect all options for the {@link Runner}.
+ *
+ * @return never {@literal null}.
+ * @throws Exception
+ */
+ protected ChainedOptionsBuilder options() throws Exception {
+
+ ChainedOptionsBuilder optionsBuilder = new OptionsBuilder().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}.
+ */
+ 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/Microbenchmark.java b/benchmark/support/src/main/java/org/springframework/data/microbenchmark/common/Microbenchmark.java
new file mode 100644
index 0000000..1df2a22
--- /dev/null
+++ b/benchmark/support/src/main/java/org/springframework/data/microbenchmark/common/Microbenchmark.java
@@ -0,0 +1,475 @@
+/*
+ * 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.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.junit.runner.Description;
+import org.junit.runner.manipulation.Filter;
+import org.junit.runner.manipulation.NoTestsRemainException;
+import org.junit.runner.manipulation.Sorter;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.Statement;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.infra.BenchmarkParams;
+import org.openjdk.jmh.infra.IterationParams;
+import org.openjdk.jmh.results.BenchmarkResult;
+import org.openjdk.jmh.results.IterationResult;
+import org.openjdk.jmh.results.RunResult;
+import org.openjdk.jmh.runner.Defaults;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.format.OutputFormat;
+import org.openjdk.jmh.runner.format.OutputFormatFactory;
+import org.openjdk.jmh.runner.options.ChainedOptionsBuilder;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.util.UnCloseablePrintStream;
+import org.openjdk.jmh.util.Utils;
+import org.springframework.util.StringUtils;
+
+/**
+ * JMH Microbenchmark runner that turns methods annotated with {@link Benchmark} into runnable methods allowing
+ * execution through JUnit.
+ *
+ * @author Mark Paluch
+ */
+public class Microbenchmark extends BlockJUnit4ClassRunner {
+
+ private final Object childrenLock = new Object();
+ private final JmhSupport jmhRunner = new JmhSupport();
+
+ private Collection filteredChildren;
+
+ /**
+ * Creates a {@link Microbenchmark} to run {@link Class test class}.
+ *
+ * @param testClass
+ * @throws InitializationError if the test class is malformed.
+ */
+ public Microbenchmark(Class> testClass) throws InitializationError {
+ super(testClass);
+ }
+
+ /**
+ * Ignore JUnit validation as we're using JMH here.
+ *
+ * @param errors
+ */
+ @Override
+ protected void collectInitializationErrors(List errors) {}
+
+ /**
+ * JMH has no means to exclude benchmark methods.
+ */
+ @Override
+ protected boolean isIgnored(FrameworkMethod child) {
+ return false;
+ }
+
+ /**
+ * Returns the methods that run tests. Default implementation returns all methods annotated with {@code @Test} on this
+ * class and superclasses that are not overridden.
+ */
+ protected List computeTestMethods() {
+
+ List annotatedMethods = new ArrayList<>(getTestClass().getAnnotatedMethods(Benchmark.class));
+
+ annotatedMethods.sort(Comparator.comparing(Microbenchmark::getBenchmarkName));
+
+ return Collections.unmodifiableList(annotatedMethods);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.junit.runners.ParentRunner#classBlock(org.junit.runner.notification.RunNotifier)
+ */
+ @Override
+ protected Statement classBlock(RunNotifier notifier) {
+ return childrenInvoker(notifier);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.junit.runners.ParentRunner#getDescription()
+ */
+ @Override
+ public Description getDescription() {
+
+ Description description = Description.createSuiteDescription(getName(), getRunnerAnnotations());
+
+ getFilteredChildren().stream().map(this::describeChild).forEach(description::addChild);
+
+ return description;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.junit.runners.ParentRunner#filter(org.junit.runner.manipulation.Filter)
+ */
+ public void filter(Filter filter) throws NoTestsRemainException {
+
+ synchronized (childrenLock) {
+
+ List children = new ArrayList<>(getFilteredChildren());
+ List filtered = children.stream().filter(it -> {
+
+ if (filter.shouldRun(describeChild(it))) {
+ try {
+ filter.apply(it);
+ } catch (NoTestsRemainException e) {
+ return false;
+ }
+ }
+ return false;
+ }).collect(Collectors.toList());
+
+ if (filtered.isEmpty()) {
+ throw new NoTestsRemainException();
+ }
+
+ filteredChildren = Collections.unmodifiableCollection(filtered);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.junit.runners.ParentRunner#sort(org.junit.runner.manipulation.Sorter)
+ */
+ public void sort(Sorter sorter) {
+
+ synchronized (childrenLock) {
+
+ getFilteredChildren().forEach(sorter::apply);
+
+ List sortedChildren = new ArrayList<>(getFilteredChildren());
+
+ sortedChildren.sort((o1, o2) -> sorter.compare(describeChild(o1), describeChild(o2)));
+
+ filteredChildren = Collections.unmodifiableCollection(sortedChildren);
+ }
+ }
+
+ /**
+ * Run matching {@link org.openjdk.jmh.annotations.Benchmark} methods with options collected from
+ * {@link org.springframework.core.env.Environment}.
+ */
+ @Override
+ protected Statement childrenInvoker(RunNotifier notifier) {
+
+ Collection methods = getFilteredChildren();
+ CacheFunction cache = new CacheFunction(methods, this::describeChild);
+
+ return new Statement() {
+
+ @Override
+ public void evaluate() throws Throwable {
+ doRun(notifier, methods, cache);
+ }
+ };
+ }
+
+ private void doRun(RunNotifier notifier, Collection methods, CacheFunction cache) throws Exception {
+
+ List includes = jmhRunner.includes(methods);
+
+ ChainedOptionsBuilder optionsBuilder = jmhRunner.options();
+
+ includes.forEach(optionsBuilder::include);
+
+ Options options = optionsBuilder.build();
+ NotifyingOutputFormat notifyingOutputFormat = new NotifyingOutputFormat(notifier, cache,
+ createOutputFormat(options));
+
+ jmhRunner.publishResults(new Runner(options, notifyingOutputFormat).run());
+ }
+
+ private Collection getFilteredChildren() {
+
+ if (filteredChildren == null) {
+ synchronized (childrenLock) {
+ if (filteredChildren == null) {
+ filteredChildren = Collections.unmodifiableCollection(getChildren());
+ }
+ }
+ }
+ return filteredChildren;
+ }
+
+ private static OutputFormat createOutputFormat(Options options) {
+
+ // sadly required here as the check cannot be made before calling this method in constructor
+ if (options == null) {
+ throw new IllegalArgumentException("Options not allowed to be null.");
+ }
+
+ PrintStream out;
+ if (options.getOutput().hasValue()) {
+ try {
+ out = new PrintStream(options.getOutput().get());
+ } catch (FileNotFoundException ex) {
+ throw new IllegalStateException(ex);
+ }
+ } else {
+ // Protect the System.out from accidental closing
+ try {
+ out = new UnCloseablePrintStream(System.out, Utils.guessConsoleEncoding());
+ } catch (UnsupportedEncodingException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ return OutputFormatFactory.createFormatInstance(out, options.verbosity().orElse(Defaults.VERBOSITY));
+ }
+
+ private static String getBenchmarkName(FrameworkMethod it) {
+ return it.getDeclaringClass().getName() + "." + it.getName();
+ }
+
+ /**
+ * {@link OutputFormat} that delegates to another {@link OutputFormat} and notifies {@link RunNotifier} about the
+ * progress.
+ */
+ static class NotifyingOutputFormat implements OutputFormat {
+
+ private final RunNotifier notifier;
+ private final Function descriptionResolver;
+ private final OutputFormat delegate;
+ private final List log = new CopyOnWriteArrayList<>();
+
+ private volatile String lastKnownBenchmark;
+ private volatile boolean recordOutput;
+
+ NotifyingOutputFormat(RunNotifier notifier, Function methods, OutputFormat delegate) {
+ this.notifier = notifier;
+ this.descriptionResolver = methods;
+ this.delegate = delegate;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.openjdk.jmh.runner.format.OutputFormat#iteration(org.openjdk.jmh.infra.BenchmarkParams, org.openjdk.jmh.infra.IterationParams, int)
+ */
+ @Override
+ public void iteration(BenchmarkParams benchParams, IterationParams params, int iteration) {
+ delegate.iteration(benchParams, params, iteration);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.openjdk.jmh.runner.format.OutputFormat#iterationResult(org.openjdk.jmh.infra.BenchmarkParams, org.openjdk.jmh.infra.IterationParams, int, org.openjdk.jmh.results.IterationResult)
+ */
+ @Override
+ public void iterationResult(BenchmarkParams benchParams, IterationParams params, int iteration,
+ IterationResult data) {
+ delegate.iterationResult(benchParams, params, iteration, data);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.openjdk.jmh.runner.format.OutputFormat#startBenchmark(org.openjdk.jmh.infra.BenchmarkParams)
+ */
+ @Override
+ public void startBenchmark(BenchmarkParams benchParams) {
+
+ log.clear();
+
+ lastKnownBenchmark = benchParams.getBenchmark();
+ notifier.fireTestStarted(descriptionResolver.apply(benchParams.getBenchmark()));
+
+ delegate.startBenchmark(benchParams);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.openjdk.jmh.runner.format.OutputFormat#endBenchmark(org.openjdk.jmh.results.BenchmarkResult)
+ */
+ @Override
+ public void endBenchmark(BenchmarkResult result) {
+
+ recordOutput = false;
+ String lastKnownBenchmark = this.lastKnownBenchmark;
+ if (result != null) {
+ notifier.fireTestFinished(descriptionResolver.apply(result.getParams().getBenchmark()));
+ } else if (lastKnownBenchmark != null) {
+
+ String output = StringUtils.collectionToDelimitedString(log, System.getProperty("line.separator"));
+ notifier.fireTestFailure(
+ new Failure(descriptionResolver.apply(lastKnownBenchmark), new JmhRunnerException(output)));
+ }
+
+ log.clear();
+ delegate.endBenchmark(result);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.openjdk.jmh.runner.format.OutputFormat#startRun()
+ */
+ @Override
+ public void startRun() {
+ delegate.startRun();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.openjdk.jmh.runner.format.OutputFormat#endRun(java.util.Collection)
+ */
+ @Override
+ public void endRun(Collection result) {
+ delegate.endRun(result);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.openjdk.jmh.runner.format.OutputFormat#print(java.lang.String)
+ */
+ @Override
+ public void print(String s) {
+ delegate.print(s);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.openjdk.jmh.runner.format.OutputFormat#println(java.lang.String)
+ */
+ @Override
+ public void println(String s) {
+
+ if (recordOutput && StringUtils.hasText(s)) {
+ log.add(s);
+ }
+
+ if (s.equals("")) {
+ recordOutput = true;
+ }
+
+ delegate.println(s);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.openjdk.jmh.runner.format.OutputFormat#flush()
+ */
+ @Override
+ public void flush() {
+ delegate.flush();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.openjdk.jmh.runner.format.OutputFormat#close()
+ */
+ @Override
+ public void close() {
+ delegate.close();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.openjdk.jmh.runner.format.OutputFormat#verbosePrintln(java.lang.String)
+ */
+ @Override
+ public void verbosePrintln(String s) {
+ delegate.verbosePrintln(s);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.openjdk.jmh.runner.format.OutputFormat#write(int)
+ */
+ @Override
+ public void write(int b) {
+ delegate.write(b);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.openjdk.jmh.runner.format.OutputFormat#write(byte[])
+ */
+ @Override
+ public void write(byte[] b) throws IOException {
+ delegate.write(b);
+ }
+ }
+
+ /**
+ * Exception proxy without stack trace.
+ */
+ static class JmhRunnerException extends RuntimeException {
+
+ private static final long serialVersionUID = -1385006784559013618L;
+
+ JmhRunnerException(String message) {
+ super(message);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Throwable#fillInStackTrace()
+ */
+ @Override
+ public synchronized Throwable fillInStackTrace() {
+ return null;
+ }
+ }
+
+ /**
+ * Cache {@link Function} for benchmark names to {@link Description}.
+ */
+ static class CacheFunction implements Function {
+
+ private final Map methodMap = new ConcurrentHashMap<>();
+ private final Collection methods;
+ private final Function describeFunction;
+
+ CacheFunction(Collection methods, Function describeFunction) {
+ this.methods = methods;
+ this.describeFunction = describeFunction;
+ }
+
+ /**
+ * Resolve a benchmark name (fqcn + "." + method name) to a {@link Description}.
+ *
+ * @param benchmarkName
+ * @return
+ */
+ public Description apply(String benchmarkName) {
+
+ FrameworkMethod frameworkMethod = methodMap.computeIfAbsent(benchmarkName, key -> {
+ return methods.stream().filter(method -> getBenchmarkName(method).equals(key)).findFirst().get();
+ });
+
+ return describeFunction.apply(frameworkMethod);
+ }
+ }
+
+}