diff options
author | George Fraser <george@fivetran.com> | 2019-01-01 20:51:24 -0800 |
---|---|---|
committer | George Fraser <george@fivetran.com> | 2019-01-01 20:51:24 -0800 |
commit | b398d1ca3643a6e58aa83c7df98312e538c966c0 (patch) | |
tree | 021cdaab4c8ed9416904776b3ab11e450f5f7690 /src/test | |
parent | f3faf409445e82cfa12f9993c4075a5523000c0e (diff) | |
download | java-language-server-b398d1ca3643a6e58aa83c7df98312e538c966c0.zip |
Error-prone is too slow
Diffstat (limited to 'src/test')
4 files changed, 318 insertions, 24 deletions
diff --git a/src/test/java/org/javacs/ErrorProneBenchmark.java b/src/test/java/org/javacs/ErrorProneBenchmark.java new file mode 100644 index 0000000..6e0b7ba --- /dev/null +++ b/src/test/java/org/javacs/ErrorProneBenchmark.java @@ -0,0 +1,96 @@ +package org.javacs; + +import com.sun.source.util.JavacTask; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.StringJoiner; +import java.util.concurrent.TimeUnit; +import javax.tools.JavaCompiler; +import javax.tools.StandardJavaFileManager; +import org.openjdk.jmh.annotations.*; + +// TODO this is coloring badly +@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(1) +public class ErrorProneBenchmark { + private static Path file = Paths.get(FindResource.uri("/org/javacs/example/BenchmarkStringSearch.java")); + + @State(Scope.Thread) + public static class BenchmarkState { + JavaCompiler compiler = ServiceLoader.load(JavaCompiler.class).iterator().next(); + StandardJavaFileManager fileManager = + new FileManagerWrapper(compiler.getStandardFileManager(__ -> {}, null, Charset.defaultCharset())); + } + + @Benchmark + public void vanilla(BenchmarkState state) { + var options = JavaCompilerService.options(Set.of(), Set.of()); + + analyze(state, options); + } + + @Benchmark + public void withErrorProne(BenchmarkState state) { + var options = JavaCompilerService.options(Set.of(), Set.of()); + // Add error-prone + options.addAll(errorProneOptions()); + + analyze(state, options); + } + + private void analyze(BenchmarkState state, List<String> options) { + var sources = state.fileManager.getJavaFileObjectsFromPaths(List.of(file)); + // Create task + var task = + (JavacTask) + state.compiler.getTask( + null, state.fileManager, __ -> {}, options, Collections.emptyList(), sources); + // Print timing information for optimization + var profiler = new Profiler(); + try { + task.analyze(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static List<String> errorProneOptions() { + var options = new ArrayList<String>(); + + // https://errorprone.info/docs/installation "Command Line" + Collections.addAll(options, "-XDcompilePolicy=byfile"); + Collections.addAll(options, "-processorpath", Lib.ERROR_PRONE); + + // https://errorprone.info/bugpatterns + var bugPatterns = new StringJoiner(" "); + // bugPatterns.add("-Xep:EmptyIf"); + // bugPatterns.add("-Xep:NumericEquality"); + // bugPatterns.add("-Xep:ConstructorLeaksThis"); + // bugPatterns.add("-Xep:EqualsBrokenForNull"); + // bugPatterns.add("-Xep:InvalidThrows"); + // bugPatterns.add("-Xep:RedundantThrows"); + // bugPatterns.add("-Xep:StaticQualifiedUsingExpression"); + // bugPatterns.add("-Xep:StringEquality"); + // bugPatterns.add("-Xep:Unused"); + // bugPatterns.add("-Xep:UnusedException"); + // bugPatterns.add("-Xep:FieldCanBeFinal"); + // bugPatterns.add("-Xep:FieldMissingNullable"); + // bugPatterns.add("-Xep:MethodCanBeStatic"); + // bugPatterns.add("-Xep:PackageLocation"); + // bugPatterns.add("-Xep:PrivateConstructorForUtilityClass"); + // bugPatterns.add("-Xep:ReturnMissingNullable"); + + Collections.addAll( + options, "-Xplugin:ErrorProne -XepAllErrorsAsWarnings " + bugPatterns + " --illegal-access=warn"); + + return options; + } +} diff --git a/src/test/java/org/javacs/JavaCompilerServiceTest.java b/src/test/java/org/javacs/JavaCompilerServiceTest.java index e06e72a..0785acb 100644 --- a/src/test/java/org/javacs/JavaCompilerServiceTest.java +++ b/src/test/java/org/javacs/JavaCompilerServiceTest.java @@ -18,6 +18,7 @@ import java.util.stream.Collectors; import javax.lang.model.element.Element; import javax.tools.Diagnostic; import javax.tools.JavaFileObject; +import org.junit.Ignore; import org.junit.Test; public class JavaCompilerServiceTest { @@ -300,10 +301,10 @@ public class JavaCompilerServiceTest { return strings; } + // TODO get these back somehow @Test + @Ignore public void errorProne() { - // TODO verify that error-prone *only* runs when you call reportErrors(), - // by calling compileFile() and checking no diagnostic is reported var uri = resourceUri("ErrorProne.java"); var files = Collections.singleton(uri); var diags = compiler.reportErrors(files); @@ -311,7 +312,9 @@ public class JavaCompilerServiceTest { assertThat(strings, hasItem(containsString("ErrorProne.java(7): [CollectionIncompatibleType]"))); } + // TODO get these back somehow @Test + @Ignore public void unusedVar() { var uri = resourceUri("UnusedVar.java"); var files = Collections.singleton(uri); diff --git a/src/test/java/org/javacs/StringSearchBenchmark.java b/src/test/java/org/javacs/StringSearchBenchmark.java index 92abbbf..e044812 100644 --- a/src/test/java/org/javacs/StringSearchBenchmark.java +++ b/src/test/java/org/javacs/StringSearchBenchmark.java @@ -14,31 +14,31 @@ public class StringSearchBenchmark { smallFile = Paths.get(FindResource.uri("/org/javacs/example/Goto.java")); // "removeMethodBodies" appears late in the file, so stopping early will not be very effective private static final String query = "removeMethodBodies"; - /* - @Benchmark - public void containsWordMatchingSmall() { - var found = Parser.containsWordMatching(smallFile, query); - assert found; - } - @Benchmark - public void containsWordMatchingLarge() { - var found = Parser.containsWordMatching(largeFile, query); - assert found; - } + @Benchmark + public void containsWordMatchingSmall() { + var found = Parser.containsWordMatching(smallFile, query); + assert found; + } + + @Benchmark + public void containsWordMatchingLarge() { + var found = Parser.containsWordMatching(largeFile, query); + assert found; + } - @Benchmark - public void containsTextSmall() { - var found = Parser.containsText(smallFile, query); - assert found; - } + @Benchmark + public void containsTextSmall() { + var found = Parser.containsText(smallFile, query); + assert found; + } + + @Benchmark + public void containsTextLarge() { + var found = Parser.containsText(largeFile, query); + assert found; + } - @Benchmark - public void containsTextLarge() { - var found = Parser.containsText(largeFile, query); - assert found; - } - */ @Benchmark public void containsImportLarge() { var found = JavaCompilerService.containsImport("java.util.nopkg", "Logger", largeFile); diff --git a/src/test/test-project/workspace/src/org/javacs/example/BenchmarkStringSearch.java b/src/test/test-project/workspace/src/org/javacs/example/BenchmarkStringSearch.java new file mode 100644 index 0000000..218d766 --- /dev/null +++ b/src/test/test-project/workspace/src/org/javacs/example/BenchmarkStringSearch.java @@ -0,0 +1,195 @@ +package org.javacs.example; + +import java.nio.ByteBuffer; + +// Translated from https://golang.org/src/strings/search.go + +// Search efficiently finds strings in a source text. It's implemented +// using the Boyer-Moore string search algorithm: +// https://en.wikipedia.org/wiki/Boyer-Moore_string_search_algorithm +// https://www.cs.utexas.edu/~moore/publications/fstrpos.pdf (note: this aged +// document uses 1-based indexing) +class BenchmarkStringSearch { + // pattern is the string that we are searching for in the text. + private final byte[] pattern; + + // badCharSkip[b] contains the distance between the last byte of pattern + // and the rightmost occurrence of b in pattern. If b is not in pattern, + // badCharSkip[b] is len(pattern). + // + // Whenever a mismatch is found with byte b in the text, we can safely + // shift the matching frame at least badCharSkip[b] until the next time + // the matching char could be in alignment. + // TODO 256 is not coloring + private final int[] badCharSkip = new int[256]; + + // goodSuffixSkip[i] defines how far we can shift the matching frame given + // that the suffix pattern[i+1:] matches, but the byte pattern[i] does + // not. There are two cases to consider: + // + // 1. The matched suffix occurs elsewhere in pattern (with a different + // byte preceding it that we might possibly match). In this case, we can + // shift the matching frame to align with the next suffix chunk. For + // example, the pattern "mississi" has the suffix "issi" next occurring + // (in right-to-left order) at index 1, so goodSuffixSkip[3] == + // shift+len(suffix) == 3+4 == 7. + // + // 2. If the matched suffix does not occur elsewhere in pattern, then the + // matching frame may share part of its prefix with the end of the + // matching suffix. In this case, goodSuffixSkip[i] will contain how far + // to shift the frame to align this portion of the prefix to the + // suffix. For example, in the pattern "abcxxxabc", when the first + // mismatch from the back is found to be in position 3, the matching + // suffix "xxabc" is not found elsewhere in the pattern. However, its + // rightmost "abc" (at position 6) is a prefix of the whole pattern, so + // goodSuffixSkip[3] == shift+len(suffix) == 6+5 == 11. + private final int[] goodSuffixSkip; + + BenchmarkStringSearch(String pattern) { + this(pattern.getBytes()); + } + + BenchmarkStringSearch(byte[] pattern) { + this.pattern = pattern; + this.goodSuffixSkip = new int[pattern.length]; + + // last is the index of the last character in the pattern. + var last = pattern.length - 1; + + // Build bad character table. + // Bytes not in the pattern can skip one pattern's length. + for (var i = 0; i < badCharSkip.length; i++) { + badCharSkip[i] = pattern.length; + } + // The loop condition is < instead of <= so that the last byte does not + // have a zero distance to itself. Finding this byte out of place implies + // that it is not in the last position. + for (var i = 0; i < last; i++) { + badCharSkip[pattern[i] + 128] = last - i; + } + + // Build good suffix table. + // First pass: set each value to the next index which starts a prefix of + // pattern. + var lastPrefix = last; + for (var i = last; i >= 0; i--) { + if (hasPrefix(pattern, new Slice(pattern, i + 1))) lastPrefix = i + 1; + // lastPrefix is the shift, and (last-i) is len(suffix). + goodSuffixSkip[i] = lastPrefix + last - i; + } + // Second pass: find repeats of pattern's suffix starting from the front. + for (var i = 0; i < last; i++) { + var lenSuffix = longestCommonSuffix(pattern, new Slice(pattern, 1, i + 1)); + if (pattern[i - lenSuffix] != pattern[last - lenSuffix]) { + // (last-i) is the shift, and lenSuffix is len(suffix). + goodSuffixSkip[last - lenSuffix] = lenSuffix + last - i; + } + } + } + + int next(String text) { + return next(text.getBytes()); + } + + int next(byte[] text) { + return next(ByteBuffer.wrap(text)); + } + + int next(ByteBuffer text) { + return next(text, 0); + } + + int next(ByteBuffer text, int startingAfter) { + var i = startingAfter + pattern.length - 1; + while (i < text.limit()) { + // Compare backwards from the end until the first unmatching character. + var j = pattern.length - 1; + while (j >= 0 && text.get(i) == pattern[j]) { + i--; + j--; + } + if (j < 0) { + return i + 1; // match + } + i += Math.max(badCharSkip[text.get(i) + 128], goodSuffixSkip[j]); + } + return -1; + } + + boolean isWordChar(byte b) { + char c = (char) (b + 128); + return Character.isAlphabetic(c) || Character.isDigit(c) || c == '$' || c == '_'; + } + + boolean startsWord(ByteBuffer text, int offset) { + if (offset == 0) return true; + return !isWordChar(text.get(offset - 1)); + } + + boolean endsWord(ByteBuffer text, int offset) { + if (offset + 1 >= text.limit()) return true; + return !isWordChar(text.get(offset + 1)); + } + + boolean isWord(ByteBuffer text, int offset) { + return startsWord(text, offset) && endsWord(text, offset + pattern.length - 1); + } + + int nextWord(String text) { + return nextWord(text.getBytes()); + } + + int nextWord(byte[] text) { + return nextWord(ByteBuffer.wrap(text)); + } + + int nextWord(ByteBuffer text) { + var i = 0; + while (true) { + i = next(text, i); + if (i == -1) return -1; + if (isWord(text, i)) return i; + i++; + } + } + + private boolean hasPrefix(byte[] s, Slice prefix) { + for (int i = 0; i < prefix.length(); i++) { + if (s[i] != prefix.get(i)) return false; + } + return true; + } + + private int longestCommonSuffix(byte[] a, Slice b) { + int i = 0; + for (; i < a.length && i < b.length(); i++) { + if (a[a.length - 1 - i] != b.get(b.length() - 1 - i)) { + break; + } + } + return i; + } + + private static class Slice { + private final byte[] target; + private int from, until; + + int length() { + return until - from; + } + + byte get(int i) { + return target[from + i]; + } + + public Slice(byte[] target, int from) { + this(target, from, target.length); + } + + public Slice(byte[] target, int from, int until) { + this.target = target; + this.from = from; + this.until = until; + } + } +} |