Compare commits

...

4 Commits

Author SHA1 Message Date
Stephan Schroevers
b5f825b9ff Thoughts 2024-05-10 19:25:11 +02:00
Stephan Schroevers
d85f2dd6a8 X 2024-05-10 19:25:11 +02:00
Stephan Schroevers
2b7a555c05 WIP: Benchmark generation 2024-05-10 19:25:11 +02:00
Stephan Schroevers
ca85533b0d X 2024-05-10 19:25:11 +02:00
18 changed files with 843 additions and 0 deletions

View File

@@ -179,6 +179,11 @@
<artifactId>mongodb-driver-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-core</artifactId>
@@ -272,6 +277,11 @@
<artifactId>refaster-compiler</artifactId>
<version>${project.version}</version>
</path>
<path>
<groupId>${project.groupId}</groupId>
<artifactId>refaster-rule-benchmark-generator</artifactId>
<version>${project.version}</version>
</path>
<path>
<groupId>${project.groupId}</groupId>
<artifactId>refaster-support</artifactId>
@@ -280,6 +290,7 @@
</annotationProcessorPaths>
<compilerArgs combine.children="append">
<arg>-Xplugin:DocumentationGenerator -XoutputDirectory=${project.build.directory}/docs</arg>
<arg>-Xplugin:RefasterRuleBenchmarkGenerator -XoutputDirectory=${project.build.directory}/generated-test-sources/test-annotations</arg>
</compilerArgs>
</configuration>
</plugin>

View File

@@ -22,9 +22,12 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Mono;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
/**
@@ -211,6 +214,27 @@ final class AssortedRules {
}
}
/**
* Don't defer to subscription-time non-reactive operations that can efficiently be performed
* during assembly.
*/
// XXX: There are all kinds of variations on this theme. Generalize.
// XXX: Drop or move to `ReactorRules`.
static final class MonoFromSupplierOptionalMapOrElse<T, S> {
@BeforeTemplate
Mono<S> before(Optional<T> optional, Function<? super T, S> transformer, S value) {
return Refaster.anyOf(
Mono.justOrEmpty(optional).map(transformer),
Mono.justOrEmpty(optional).mapNotNull(transformer))
.defaultIfEmpty(value);
}
@AfterTemplate
Mono<? extends S> after(Optional<T> optional, Function<? super T, S> transformer, S value) {
return Mono.fromSupplier(() -> optional.map(transformer).orElse(value));
}
}
// /**
// * Don't unnecessarily pass a method reference to {@link Supplier#get()} or wrap this method
// * in a lambda expression.

View File

@@ -127,6 +127,20 @@ final class OptionalRules {
}
}
/** Prefer {@link Optional#equals(Object)} over more contrived alternatives. */
static final class NotOptionalEqualsOptional<T, S> {
@BeforeTemplate
boolean before(Optional<T> optional, S value) {
return Refaster.anyOf(
optional.filter(value::equals).isEmpty(), optional.stream().noneMatch(value::equals));
}
@AfterTemplate
boolean after(Optional<T> optional, S value) {
return !optional.equals(Optional.of(value));
}
}
/**
* Don't use the ternary operator to extract the first element of a possibly-empty {@link
* Iterator} as an {@link Optional}.

View File

@@ -53,6 +53,7 @@ import tech.picnic.errorprone.refaster.annotation.Description;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
import tech.picnic.errorprone.refaster.matchers.IsEmpty;
import tech.picnic.errorprone.refaster.matchers.IsIdentityOperation;
import tech.picnic.errorprone.refaster.matchers.IsLikelyTrivialComputation;
import tech.picnic.errorprone.refaster.matchers.ThrowsCheckedException;
/** Refaster rules related to Reactor expressions and statements. */
@@ -172,6 +173,7 @@ final class ReactorRules {
*
* <p>In particular, avoid mixing of the {@link Optional} and {@link Mono} APIs.
*/
// XXX: Below we now have the opposite advice. Test.
static final class MonoFromOptionalSwitchIfEmpty<T> {
@BeforeTemplate
Mono<T> before(Optional<T> optional, Mono<T> mono) {
@@ -301,6 +303,38 @@ final class ReactorRules {
}
}
/**
* Don't eagerly instantiate {@link Throwable}s for {@link Mono#error(Throwable)}; let the
* framework do it on subscription.
*/
static final class MonoError<T> {
@BeforeTemplate
Mono<T> before(@NotMatches(IsLikelyTrivialComputation.class) Throwable throwable) {
return Mono.error(throwable);
}
@AfterTemplate
Mono<T> after(Throwable throwable) {
return Mono.error(() -> throwable);
}
}
/**
* Don't eagerly instantiate {@link Throwable}s for {@link Flux#error(Throwable)}; let the
* framework do it on subscription.
*/
static final class FluxError<T> {
@BeforeTemplate
Flux<T> before(@NotMatches(IsLikelyTrivialComputation.class) Throwable throwable) {
return Flux.error(throwable);
}
@AfterTemplate
Flux<T> after(Throwable throwable) {
return Flux.error(() -> throwable);
}
}
/** Don't unnecessarily defer {@link Mono#error(Throwable)}. */
static final class MonoDeferredError<T> {
@BeforeTemplate
@@ -1698,6 +1732,22 @@ final class ReactorRules {
}
}
/**
* Don't defer to subscription-time non-reactive operations that can efficiently be performed
* during assembly.
*/
static final class OptionalMapOrElse<T, S, M extends Mono<? extends S>> {
@BeforeTemplate
Mono<S> before(Optional<T> optional, Function<? super T, M> transformer, M mono) {
return Mono.justOrEmpty(optional).flatMap(transformer).switchIfEmpty(mono);
}
@AfterTemplate
Mono<? extends S> after(Optional<T> optional, Function<? super T, M> transformer, M mono) {
return optional.map(transformer).orElse(mono);
}
}
/** Prefer {@link reactor.util.context.Context#empty()}} over more verbose alternatives. */
// XXX: Introduce Refaster rules or a `BugChecker` that maps `(Immutable)Map.of(k, v)` to
// `Context.of(k, v)` and likewise for multi-pair overloads.

View File

@@ -0,0 +1,187 @@
package tech.picnic.errorprone.refasterrules;
import static org.openjdk.jmh.results.format.ResultFormatType.JSON;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.MayOptionallyUse;
import com.google.errorprone.refaster.annotation.Placeholder;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.profile.GCProfiler;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import tech.picnic.errorprone.refaster.annotation.Benchmarked;
// XXX: Fix warmup and measurements etc.
// XXX: Flag cases where the `before` code is faster, taking into account variation.
@SuppressWarnings("FieldCanBeFinal") // XXX: Triggers CompilesWithFix!!!
@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(
jvmArgs = {"-Xms256M", "-Xmx256M"},
value = 1)
@Warmup(iterations = 1)
@Measurement(iterations = 10, time = 1)
public class ReactorRulesBenchmarks {
public static void main(String[] args) throws RunnerException {
String testRegex = Pattern.quote(ReactorRulesBenchmarks.class.getCanonicalName());
new Runner(
new OptionsBuilder()
.include(testRegex)
.addProfiler(GCProfiler.class)
.resultFormat(JSON) // XXX: Review.
.build())
.run();
}
// XXX: What a benchmarked rule could look like.
@Benchmarked
static final class MonoFromOptionalSwitchIfEmpty<T> {
// XXX: These parameters could perhaps be inferred in the common case.
@Benchmarked.Param private final Optional<String> emptyOptional = Optional.empty();
@Benchmarked.Param private final Optional<String> nonEmptyOptional = Optional.of("foo");
@Benchmarked.Param private final Mono<String> emptyMono = Mono.<String>empty().hide();
@Benchmarked.Param private final Mono<String> nonEmptyMono = Mono.just("bar").hide();
// XXX: Dropped to hide this class from `RefasterRuleCompilerTaskListener`: @BeforeTemplate
Mono<T> before(Optional<T> optional, Mono<T> mono) {
return optional.map(Mono::just).orElse(mono);
}
@AfterTemplate
Mono<T> after(Optional<T> optional, Mono<T> mono) {
return Mono.justOrEmpty(optional).switchIfEmpty(mono);
}
// XXX: Methods such as this one could perhaps be inferred in the common case.
@Benchmarked.OnResult
T subscribe(Mono<T> mono) {
return mono.block();
}
}
// XXX: Variations like this would be generated.
public static class MonoFromOptionalSwitchIfEmptyBenchmark extends ReactorRulesBenchmarks {
private final MonoFromOptionalSwitchIfEmpty<String> rule = new MonoFromOptionalSwitchIfEmpty();
// private final Optional<String> optional = Optional.of("foo");
// private final Mono<String> mono = Mono.just("bar").hide();
private final Optional<String> optional = Optional.empty();
private final Mono<String> mono = Mono.<String>empty().hide();
private final Mono<String> before = rule.before(optional, mono);
private final Mono<String> after = rule.after(optional, mono);
@Benchmark
public Mono<String> before() {
return rule.before(optional, mono);
}
@Benchmark
public String beforeSubscribe1() {
return before.block();
}
@Benchmark
public Mono<String> after() {
return rule.after(optional, mono);
}
@Benchmark
public String afterSubscribe() {
return after.block();
}
}
/////////////////////////////
abstract static class MonoFlatMapToFlux<T, S> {
@Placeholder(allowsIdentity = true)
abstract Mono<S> transformation(@MayOptionallyUse T value);
// XXX: Dropped to hide this class from `RefasterRuleCompilerTaskListener`: @BeforeTemplate
Flux<S> before(Mono<T> mono) {
return mono.flatMapMany(v -> transformation(v));
}
@AfterTemplate
Flux<S> after(Mono<T> mono) {
return mono.flatMap(v -> transformation(v)).flux();
}
@Benchmarked.OnResult
Long subscribe(Flux<T> flux) {
return flux.count().block();
}
@Benchmarked.MinimalPlaceholder
<X> Mono<X> identity(X value) {
return Mono.just(value);
}
}
public static class MonoFlatMapToFluxBenchmarks extends ReactorRulesBenchmarks {
abstract static class Rule<T, S> {
abstract Mono<S> transformation(T value);
Flux<S> before(Mono<T> mono) {
return mono.flatMapMany(v -> transformation(v));
}
Flux<S> after(Mono<T> mono) {
return mono.flatMap(v -> transformation(v)).flux();
}
}
// XXX: For variantions, we could use `@Param(strings)` indirection, like in
// https://github.com/openjdk/jmh/blob/master/jmh-samples/src/main/java/org/openjdk/jmh/samples/JMHSample_35_Profilers.java
// XXX: Or we can make the benchmark abstract and have subclasses for each variant:
// https://github.com/openjdk/jmh/blob/master/jmh-samples/src/main/java/org/openjdk/jmh/samples/JMHSample_24_Inheritance.java
public static class MonoFlatMapToFluxStringStringBenchmark extends MonoFlatMapToFluxBenchmarks {
private final Rule<String, String> rule =
new Rule<>() {
@Override
Mono<String> transformation(String value) {
return Mono.just(value);
}
};
private final Mono<String> mono = Mono.just("foo");
private final Flux<String> before = rule.before(mono);
private final Flux<String> after = rule.after(mono);
@Benchmark
public Flux<String> before() {
return rule.before(mono);
}
@Benchmark
public Flux<String> after() {
return rule.after(mono);
}
@Benchmark
public Long onResultOfBefore() {
return before.count().block();
}
@Benchmark
public Long onResultOfAfter() {
return after.count().block();
}
}
}
}

View File

@@ -45,6 +45,12 @@ final class OptionalRulesTest implements RefasterRuleCollectionTestCase {
Optional.of("baz").stream().anyMatch("qux"::equals));
}
ImmutableSet<Boolean> testNotOptionalEqualsOptional() {
return ImmutableSet.of(
Optional.of("foo").filter("bar"::equals).isEmpty(),
Optional.of("baz").stream().noneMatch("qux"::equals));
}
ImmutableSet<Optional<String>> testOptionalFirstIteratorElement() {
return ImmutableSet.of(
ImmutableSet.of("foo").iterator().hasNext()

View File

@@ -45,6 +45,12 @@ final class OptionalRulesTest implements RefasterRuleCollectionTestCase {
Optional.of("baz").equals(Optional.of("qux")));
}
ImmutableSet<Boolean> testNotOptionalEqualsOptional() {
return ImmutableSet.of(
!Optional.of("foo").equals(Optional.of("bar")),
!Optional.of("baz").equals(Optional.of("qux")));
}
ImmutableSet<Optional<String>> testOptionalFirstIteratorElement() {
return ImmutableSet.of(
stream(ImmutableSet.of("foo").iterator()).findFirst(),

View File

@@ -118,6 +118,16 @@ final class ReactorRulesTest implements RefasterRuleCollectionTestCase {
return Flux.just("foo", "bar").zipWithIterable(ImmutableSet.of(1, 2), String::repeat);
}
ImmutableSet<?> testMonoError() {
IllegalStateException exception = new IllegalStateException();
return ImmutableSet.of(Mono.error(exception), Mono.error(new IllegalArgumentException()));
}
ImmutableSet<?> testFluxError() {
IllegalStateException exception = new IllegalStateException();
return ImmutableSet.of(Flux.error(exception), Flux.error(new IllegalArgumentException()));
}
Mono<Void> testMonoDeferredError() {
return Mono.defer(() -> Mono.error(new IllegalStateException()));
}
@@ -575,6 +585,10 @@ final class ReactorRulesTest implements RefasterRuleCollectionTestCase {
MathFlux.min(Flux.just(1), reverseOrder()), MathFlux.max(Flux.just(2), naturalOrder()));
}
Mono<String> testOptionalMapOrElse() {
return Mono.justOrEmpty(Optional.of("foo")).flatMap(Mono::just).switchIfEmpty(Mono.just("bar"));
}
ImmutableSet<Context> testContextEmpty() {
return ImmutableSet.of(Context.of(ImmutableMap.of()), Context.of(ImmutableMap.of(1, 2)));
}

View File

@@ -123,6 +123,16 @@ final class ReactorRulesTest implements RefasterRuleCollectionTestCase {
.map(function(String::repeat));
}
ImmutableSet<?> testMonoError() {
IllegalStateException exception = new IllegalStateException();
return ImmutableSet.of(Mono.error(exception), Mono.error(() -> new IllegalArgumentException()));
}
ImmutableSet<?> testFluxError() {
IllegalStateException exception = new IllegalStateException();
return ImmutableSet.of(Flux.error(exception), Flux.error(() -> new IllegalArgumentException()));
}
Mono<Void> testMonoDeferredError() {
return Mono.error(() -> new IllegalStateException());
}
@@ -564,6 +574,10 @@ final class ReactorRulesTest implements RefasterRuleCollectionTestCase {
return ImmutableSet.of(MathFlux.max(Flux.just(1)), MathFlux.max(Flux.just(2)));
}
Mono<String> testOptionalMapOrElse() {
return Optional.of("foo").map(Mono::just).orElse(Mono.just("bar"));
}
ImmutableSet<Context> testContextEmpty() {
return ImmutableSet.of(Context.empty(), Context.of(ImmutableMap.of(1, 2)));
}

View File

@@ -120,6 +120,7 @@
</annotationProcessorPaths>
<compilerArgs combine.children="append">
<arg>-Xplugin:DocumentationGenerator -XoutputDirectory=${project.build.directory}/docs</arg>
<!-- XXX: Enable `refaster-rule-benchmark-generator` here. -->
</compilerArgs>
</configuration>
</plugin>

73
pom.xml
View File

@@ -45,6 +45,7 @@
<module>error-prone-guidelines</module>
<module>error-prone-utils</module>
<module>refaster-compiler</module>
<module>refaster-rule-benchmark-generator</module>
<module>refaster-runner</module>
<module>refaster-support</module>
<module>refaster-test-support</module>
@@ -212,6 +213,7 @@
<version.error-prone-slf4j>0.1.24</version.error-prone-slf4j>
<version.guava-beta-checker>1.0</version.guava-beta-checker>
<version.jdk>17</version.jdk>
<version.jmh>1.37</version.jmh>
<version.maven>3.9.5</version.maven>
<version.mockito>5.11.0</version.mockito>
<version.nopen-checker>1.0.1</version.nopen-checker>
@@ -468,6 +470,11 @@
<artifactId>mongodb-driver-core</artifactId>
<version>5.1.0</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${version.jmh}</version>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-templating</artifactId>
@@ -932,6 +939,11 @@
<artifactId>auto-service</artifactId>
<version>${version.auto-service}</version>
</path>
<path>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${version.jmh}</version>
</path>
<path>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-templating</artifactId>
@@ -2054,6 +2066,67 @@
</pluginManagement>
</build>
</profile>
<profile>
<!-- Enables execution of a JMH benchmark. Given a benchmark class
`tech.picnic.MyBenchmark`, the following command (executed against
the (sub)module in which the benchmark resides) will compile and
execute said benchmark:
mvn process-test-classes -Dverification.skip \
-Djmh.run-benchmark=tech.picnic.MyBenchmark
-->
<id>run-jmh-benchmark</id>
<activation>
<property>
<name>jmh.run-benchmark</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>build-jmh-runtime-classpath</id>
<goals>
<goal>build-classpath</goal>
</goals>
<configuration>
<outputProperty>testClasspath</outputProperty>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<executions>
<execution>
<id>run-jmh-benchmark</id>
<goals>
<goal>java</goal>
</goals>
<phase>process-test-classes</phase>
<configuration>
<classpathScope>test</classpathScope>
<mainClass>${jmh.run-benchmark}</mainClass>
<systemProperties>
<!-- The runtime classpath is defined
in this way so that any JVMs forked by
JMH will have the desired classpath. -->
<systemProperty>
<key>java.class.path</key>
<value>${project.build.testOutputDirectory}${path.separator}${project.build.outputDirectory}${path.separator}${testClasspath}</value>
</systemProperty>
</systemProperties>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<!-- This profile is auto-activated when performing a release. -->
<id>release</id>

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>tech.picnic.error-prone-support</groupId>
<artifactId>error-prone-support</artifactId>
<version>0.16.2-SNAPSHOT</version>
</parent>
<artifactId>refaster-rule-benchmark-generator</artifactId>
<name>Picnic :: Error Prone Support :: Refaster Rule Benchmark Generator</name>
<!-- XXX: Review description: can this be an annotation processor? -->
<description>Compiler plugin that derives JMH benchmarks from Refaster Rules.</description>
<url>https://error-prone.picnic.tech</url>
<dependencies>
<!-- XXX: Prune this list. -->
<dependency>
<groupId>${groupId.error-prone}</groupId>
<artifactId>error_prone_annotation</artifactId>
</dependency>
<dependency>
<groupId>${groupId.error-prone}</groupId>
<artifactId>error_prone_annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${groupId.error-prone}</groupId>
<artifactId>error_prone_check_api</artifactId>
</dependency>
<dependency>
<groupId>${groupId.error-prone}</groupId>
<artifactId>error_prone_core</artifactId>
<!-- XXX: Review scope. -->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${groupId.error-prone}</groupId>
<artifactId>error_prone_test_helpers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>refaster-support</artifactId>
<!-- XXX: Review scope. -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-guava</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
</dependency>
<dependency>
<groupId>com.google.auto</groupId>
<artifactId>auto-common</artifactId>
</dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.auto.value</groupId>
<artifactId>auto-value-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<!-- XXX: Explicitly declared as a workaround for
https://github.com/pitest/pitest-junit5-plugin/issues/105. -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,55 @@
package tech.picnic.errorprone.refaster.benchmark;
import static com.google.common.base.Preconditions.checkArgument;
import com.google.auto.service.AutoService;
import com.google.common.annotations.VisibleForTesting;
import com.sun.source.util.JavacTask;
import com.sun.source.util.Plugin;
import com.sun.tools.javac.api.BasicJavacTask;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** A compiler {@link Plugin} that generates JMH benchmarks for Refaster rules. */
// XXX: Review whether this can be an annotation processor instead.
@AutoService(Plugin.class)
public final class RefasterRuleBenchmarkGenerator implements Plugin {
@VisibleForTesting static final String OUTPUT_DIRECTORY_FLAG = "-XoutputDirectory";
private static final Pattern OUTPUT_DIRECTORY_FLAG_PATTERN =
Pattern.compile(Pattern.quote(OUTPUT_DIRECTORY_FLAG) + "=(.*)");
/** Instantiates a new {@link RefasterRuleBenchmarkGenerator} instance. */
public RefasterRuleBenchmarkGenerator() {}
@Override
public String getName() {
return getClass().getSimpleName();
}
@Override
public void init(JavacTask javacTask, String... args) {
checkArgument(args.length == 1, "Precisely one path must be provided");
// XXX: Drop all path logic: instead generate in the same package as the Refaster rule
// collection. (But how do we then determine the base directory?)
javacTask.addTaskListener(
new RefasterRuleBenchmarkGeneratorTaskListener(
((BasicJavacTask) javacTask).getContext(), getOutputPath(args[0])));
}
@VisibleForTesting
static Path getOutputPath(String pathArg) {
Matcher matcher = OUTPUT_DIRECTORY_FLAG_PATTERN.matcher(pathArg);
checkArgument(
matcher.matches(), "'%s' must be of the form '%s=<value>'", pathArg, OUTPUT_DIRECTORY_FLAG);
String path = matcher.group(1);
try {
return Path.of(path);
} catch (InvalidPathException e) {
throw new IllegalArgumentException(String.format("Invalid path '%s'", path), e);
}
}
}

View File

@@ -0,0 +1,118 @@
package tech.picnic.errorprone.refaster.benchmark;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.hasAnnotation;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.VisitorState;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.Matchers;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskEvent.Kind;
import com.sun.source.util.TaskListener;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreePathScanner;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.util.Context;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.tools.JavaFileObject;
import org.jspecify.annotations.Nullable;
// XXX: Document.
final class RefasterRuleBenchmarkGeneratorTaskListener implements TaskListener {
private static final Matcher<Tree> IS_TEMPLATE =
anyOf(hasAnnotation(BeforeTemplate.class), hasAnnotation(AfterTemplate.class));
private static final Matcher<Tree> IS_BENCHMARKED =
Matchers.hasAnnotation("tech.picnic.errorprone.refaster.annotation.Benchmarked");
private final Context context;
private final Path outputPath;
RefasterRuleBenchmarkGeneratorTaskListener(Context context, Path outputPath) {
this.context = context;
this.outputPath = outputPath;
}
@Override
public void started(TaskEvent taskEvent) {
if (taskEvent.getKind() == Kind.ANALYZE) {
createOutputDirectory();
}
}
@Override
public void finished(TaskEvent taskEvent) {
if (taskEvent.getKind() != Kind.ANALYZE) {
return;
}
JavaFileObject sourceFile = taskEvent.getSourceFile();
CompilationUnitTree compilationUnit = taskEvent.getCompilationUnit();
ClassTree classTree = JavacTrees.instance(context).getTree(taskEvent.getTypeElement());
if (sourceFile == null || compilationUnit == null || classTree == null) {
return;
}
VisitorState state =
VisitorState.createForUtilityPurposes(context)
.withPath(new TreePath(new TreePath(compilationUnit), classTree));
new TreePathScanner<@Nullable Void, Boolean>() {
@Override
public @Nullable Void visitClass(ClassTree classTree, Boolean doBenchmark) {
// XXX: Validate that `@Benchmarked` is only placed in contexts with at least one Refaster
// rule.
boolean inspectClass = doBenchmark || IS_BENCHMARKED.matches(classTree, state);
if (inspectClass) {
System.out.println(handle(classTree, state));
// XXX: If this class has a `@BeforeTemplate` method, generate a benchmark for it.
}
return super.visitClass(classTree, inspectClass);
}
}.scan(compilationUnit, false);
}
// XXX: Name? Scope?
private static Rule handle(ClassTree classTree, VisitorState state) {
ImmutableList<Rule.Method> methods =
classTree.getMembers().stream()
.filter(m -> IS_TEMPLATE.matches(m, state))
.map(m -> process((MethodTree) m, state))
.collect(toImmutableList());
Rule rule = new Rule(classTree, methods);
return rule;
}
private static Rule.Method process(MethodTree methodTree, VisitorState state) {
// XXX: Initially, disallow `Refaster.x` usages.
// XXX: Initially, disallow references to `@Placeholder` methods.
// XXX: Disallow `void` methods. (Can't be black-holed.)
return new Rule.Method(methodTree);
}
// XXX: Move types down.
record Rule(ClassTree tree, ImmutableList<Rule.Method> methods) {
record Method(MethodTree tree) {}
}
private void createOutputDirectory() {
try {
Files.createDirectories(outputPath);
} catch (IOException e) {
throw new IllegalStateException(
String.format("Error while creating directory with path '%s'", outputPath), e);
}
}
}

View File

@@ -0,0 +1,72 @@
package tech.picnic.errorprone.refaster.benchmark;
import static org.assertj.core.api.Assertions.assertThat;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.FileManagers;
import com.google.errorprone.FileObjects;
import com.sun.tools.javac.api.JavacTaskImpl;
import com.sun.tools.javac.api.JavacTool;
import com.sun.tools.javac.file.JavacFileManager;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import javax.tools.Diagnostic;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
// XXX: This code is a near-duplicate of the identically named class in `documentation-support`.
public final class Compilation {
private Compilation() {}
public static void compileWithRefasterRuleBenchmarkGenerator(
Path outputDirectory, String path, String... lines) {
compileWithRefasterRuleBenchmarkGenerator(
outputDirectory.toAbsolutePath().toString(), path, lines);
}
public static void compileWithRefasterRuleBenchmarkGenerator(
String outputDirectory, String path, String... lines) {
/*
* The compiler options specified here largely match those used by Error Prone's
* `CompilationTestHelper`. A key difference is the stricter linting configuration. When
* compiling using JDK 21+, these lint options also require that certain JDK modules are
* explicitly exported.
*/
compile(
ImmutableList.of(
"--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
"--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
"--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
"-encoding",
"UTF-8",
"-parameters",
"-proc:none",
"-Werror",
"-Xlint:all,-serial",
"-Xplugin:RefasterRuleBenchmarkGenerator -XoutputDirectory=" + outputDirectory,
"-XDdev",
"-XDcompilePolicy=simple"),
FileObjects.forSourceLines(path, lines));
}
private static void compile(ImmutableList<String> options, JavaFileObject javaFileObject) {
JavacFileManager javacFileManager = FileManagers.testFileManager();
JavaCompiler compiler = JavacTool.create();
List<Diagnostic<?>> diagnostics = new ArrayList<>();
JavacTaskImpl task =
(JavacTaskImpl)
compiler.getTask(
null,
javacFileManager,
diagnostics::add,
options,
ImmutableList.of(),
ImmutableList.of(javaFileObject));
Boolean result = task.call();
assertThat(diagnostics).isEmpty();
assertThat(result).isTrue();
}
}

View File

@@ -0,0 +1,41 @@
package tech.picnic.errorprone.refaster.benchmark;
import static org.assertj.core.api.Assertions.assertThat;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
// XXX: Add relevant tests also present in `DocumentationGeneratorTaskListenerTest`.
// XXX: Test package placement.
final class RefasterRuleBenchmarkGeneratorTaskListenerTest {
// XXX: Rename!
@Test
void xxx(@TempDir Path outputDirectory) {
Compilation.compileWithRefasterRuleBenchmarkGenerator(
outputDirectory,
"TestCheckerWithoutAnnotation.java",
"""
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
import java.util.Optional;
import reactor.core.publisher.Mono;
import tech.picnic.errorprone.refaster.annotation.Benchmarked;
@Benchmarked
final class MonoFromOptionalSwitchIfEmpty<T> {
@BeforeTemplate
Mono<T> before(Optional<T> optional, Mono<T> mono) {
return optional.map(Mono::just).orElse(mono);
}
@AfterTemplate
Mono<T> after(Optional<T> optional, Mono<T> mono) {
return Mono.justOrEmpty(optional).switchIfEmpty(mono);
}
}
""");
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
}
}

View File

@@ -0,0 +1,5 @@
package tech.picnic.errorprone.refaster.benchmark;
final class RefasterRuleBenchmarkGeneratorTest {
// XXX: TBD. See `DocumentationGeneratorTest`.
}

View File

@@ -0,0 +1,35 @@
package tech.picnic.errorprone.refaster.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that benchmark(s) should be generated for the annotated Refaster rule or group of
* Refaster rules.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Benchmarked {
// XXX: Make configurable. E.g.
// - While not supported, don't complain about `Refaster.anyOf` or other `Refaster` utility method
// usages.
// - Specify warmup and measurement iterations.
// - Specify output time unit.
// - Value generation hints.f
// Once configuration is supported, annotations on nested classes should override the
// configuration specified by outer classes.
// XXX: Explain use. Allow restriction by name?
@Target(ElementType.FIELD)
@interface Param {}
// XXX: Explain use
@Target(ElementType.METHOD)
@interface OnResult {}
// XXX: Explain use
@Target(ElementType.METHOD)
@interface MinimalPlaceholder {}
}