diff --git a/rewrite-benchmarks/build.gradle.kts b/rewrite-benchmarks/build.gradle.kts index f9fde57678c..bc825fb7542 100644 --- a/rewrite-benchmarks/build.gradle.kts +++ b/rewrite-benchmarks/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { jmh("org.openjdk.jmh:jmh-core:latest.release") jmh("org.openjdk.jol:jol-core:latest.release") jmh("io.github.fastfilter:fastfilter:latest.release") + jmh("org.xerial.snappy:snappy-java:1.1.10.7") // Nebula doesn't like having jmhAnnotationProcessor without jmh so we just add it twice. jmh("org.openjdk.jmh:jmh-generator-annprocess:latest.release") diff --git a/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaCompilationUnitState.java b/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaCompilationUnitState.java index a8847d87163..bd4c0937c4d 100644 --- a/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaCompilationUnitState.java +++ b/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaCompilationUnitState.java @@ -24,10 +24,13 @@ import org.openrewrite.SourceFile; import org.openrewrite.internal.InMemoryLargeSourceSet; import org.openrewrite.java.JavaParser; -import org.openrewrite.java.internal.AdaptiveRadixJavaTypeCache; import org.openrewrite.java.internal.JavaTypeCache; +import org.xerial.snappy.Snappy; +import java.io.IOException; +import java.io.UncheckedIOException; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; @@ -36,17 +39,20 @@ import java.util.Map; import java.util.stream.Collectors; +@SuppressWarnings("NotNullFieldNotInitialized") @State(Scope.Benchmark) public class JavaCompilationUnitState { JavaParser.Builder javaParser; List sourceFiles; List inputs; - JavaTypeCache snappyTypeCache; - AdaptiveRadixJavaTypeCache radixMapTypeCache; + SnappyJavaTypeCache snappyTypeCache; + JavaTypeCache radixMapTypeCache; MapJavaTypeCache typeCache; public static void main(String[] args) throws URISyntaxException { - new JavaCompilationUnitState().setup(); + JavaCompilationUnitState javaCompiler = new JavaCompilationUnitState(); + javaCompiler.setup(); + javaCompiler.printMemory(); } @Setup(Level.Trial) @@ -57,6 +63,7 @@ public void setup() throws URISyntaxException { inputs = Arrays.asList( rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/internal/lang/Nullable.java"), rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/internal/lang/NullUtils.java"), + rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/internal/AdaptiveRadixTree.java"), rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/internal/MetricsHelper.java"), rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/internal/ListUtils.java"), rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/internal/PropertyPlaceholderHelper.java"), @@ -70,6 +77,7 @@ public void setup() throws URISyntaxException { rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/style/Style.java"), rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/config/DeclarativeNamedStyles.java"), rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/style/NamedStyles.java"), + rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/Incubating.java"), rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/Option.java"), rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/config/OptionDescriptor.java"), rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/config/RecipeDescriptor.java"), @@ -99,12 +107,12 @@ public void setup() throws URISyntaxException { .parse(inputs, null, new InMemoryExecutionContext()) .collect(Collectors.toList()); - radixMapTypeCache = new AdaptiveRadixJavaTypeCache(); + radixMapTypeCache = new JavaTypeCache(); for (Map.Entry entry : typeCache.map().entrySet()) { radixMapTypeCache.put(entry.getKey(), entry.getValue()); } - snappyTypeCache = new JavaTypeCache(); + snappyTypeCache = new SnappyJavaTypeCache(); for (Map.Entry entry : typeCache.map().entrySet()) { snappyTypeCache.put(entry.getKey(), entry.getValue()); } @@ -115,6 +123,8 @@ void printMemory() { System.out.printf("Retained AdaptiveRadixTree size: %10d bytes\n", retainedSize); retainedSize = GraphLayout.parseInstance(snappyTypeCache).totalSize(); System.out.printf("Retained Snappy size: %10d bytes\n", retainedSize); + retainedSize = GraphLayout.parseInstance(typeCache).totalSize(); + System.out.printf("Retained HashMap size: %10d bytes\n", retainedSize); } @TearDown(Level.Trial) @@ -155,13 +165,64 @@ public void clear() { } @Override - public int size() { - return typeCache.size(); + public MapJavaTypeCache clone() { + MapJavaTypeCache clone = (MapJavaTypeCache) super.clone(); + clone.typeCache = new HashMap<>(this.typeCache); + return clone; + } + } + + static class SnappyJavaTypeCache extends JavaTypeCache { + + // empirical value: below this size, the compressed key is larger or only slightly smaller + // although also note that a String object has a 24 bytes overhead vs. the 16 bytes of a BytesKey object + public static final int COMPRESSION_THRESHOLD = 50; + + @SuppressWarnings("ClassCanBeRecord") + private static class BytesKey { + private final byte[] data; + BytesKey(byte[] data) { + this.data = data; + } } + Map typeCache = new HashMap<>(); + @Override - public MapJavaTypeCache clone() { - MapJavaTypeCache clone = (MapJavaTypeCache) super.clone(); + public @Nullable T get(String signature) { + //noinspection unchecked + return (T) typeCache.get(key(signature)); + } + + @Override + public void put(String signature, Object o) { + typeCache.put(key(signature), o); + } + + private static boolean snappyUsable = true; + + private Object key(String signature) { + if (signature.length() > COMPRESSION_THRESHOLD && snappyUsable) { + try { + return new BytesKey(Snappy.compress(signature.getBytes(StandardCharsets.UTF_8))); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (NoClassDefFoundError e) { + // Some systems fail to load Snappy native components, so fall back to not compressing + snappyUsable = false; + } + } + return signature; + } + + @Override + public void clear() { + typeCache.clear(); + } + + @Override + public SnappyJavaTypeCache clone() { + SnappyJavaTypeCache clone = (SnappyJavaTypeCache) super.clone(); clone.typeCache = new HashMap<>(this.typeCache); return clone; } diff --git a/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaParserBenchmark.java b/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaParserBenchmark.java index 37d839100bb..6bb8ff040e2 100644 --- a/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaParserBenchmark.java +++ b/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaParserBenchmark.java @@ -24,7 +24,6 @@ import org.openjdk.jmh.runner.options.OptionsBuilder; import org.openrewrite.InMemoryExecutionContext; import org.openrewrite.java.JavaParser; -import org.openrewrite.java.internal.AdaptiveRadixJavaTypeCache; import org.openrewrite.java.internal.JavaTypeCache; import java.net.URISyntaxException; @@ -40,7 +39,7 @@ public class JavaParserBenchmark { @Benchmark public void snappy(JavaCompilationUnitState state, Blackhole bh) { - JavaTypeCache typeCache = new JavaTypeCache(); + JavaTypeCache typeCache = new JavaCompilationUnitState.SnappyJavaTypeCache(); JavaParser parser = state.javaParser.typeCache(typeCache).build(); parser .parse(state.inputs, null, new InMemoryExecutionContext()) @@ -49,7 +48,7 @@ public void snappy(JavaCompilationUnitState state, Blackhole bh) { @Benchmark public void adaptiveRadix(JavaCompilationUnitState state, Blackhole bh) { - AdaptiveRadixJavaTypeCache typeCache = new AdaptiveRadixJavaTypeCache(); + JavaTypeCache typeCache = new JavaTypeCache(); JavaParser parser = state.javaParser.typeCache(typeCache).build(); parser .parse(state.inputs, null, new InMemoryExecutionContext()) diff --git a/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaTypeCacheBenchmark.java b/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaTypeCacheBenchmark.java index 42a5da8d16e..f6f8e1a762d 100644 --- a/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaTypeCacheBenchmark.java +++ b/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaTypeCacheBenchmark.java @@ -22,16 +22,15 @@ import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; -import org.openrewrite.java.internal.AdaptiveRadixJavaTypeCache; import org.openrewrite.java.internal.JavaTypeCache; import java.net.URISyntaxException; import java.util.Map; import java.util.concurrent.TimeUnit; -@Fork(1) -@Measurement(iterations = 2) -@Warmup(iterations = 2) +@Fork(value = 1) +@Measurement(iterations = 3, time = 5) +@Warmup(iterations = 3, time = 5) @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) @Threads(4) @@ -39,15 +38,32 @@ public class JavaTypeCacheBenchmark { @Benchmark public void writeSnappy(JavaCompilationUnitState state, Blackhole bh) { - JavaTypeCache typeCache = new JavaTypeCache(); + JavaTypeCache typeCache = new JavaCompilationUnitState.SnappyJavaTypeCache(); for (Map.Entry entry : state.typeCache.map().entrySet()) { typeCache.put(entry.getKey(), entry.getValue()); } } +// @Benchmark + public void writeHash(JavaCompilationUnitState state, Blackhole bh) { + JavaTypeCache typeCache = new JavaCompilationUnitState.MapJavaTypeCache(); + for (Map.Entry entry : state.typeCache.map().entrySet()) { + typeCache.put(new String(entry.getKey()), entry.getValue()); + } + } + @Benchmark public void writeAdaptiveRadix(JavaCompilationUnitState state, Blackhole bh) { - AdaptiveRadixJavaTypeCache typeCache = new AdaptiveRadixJavaTypeCache(); + JavaTypeCache typeCache = new JavaTypeCache(); + for (Map.Entry entry : state.typeCache.map().entrySet()) { + typeCache.put(entry.getKey(), entry.getValue()); + } + } + + @Benchmark + @Fork(value = 1, jvmArgsAppend = {"--add-opens", "java.base/java.lang=ALL-UNNAMED"}) + public void writeAdaptiveRadixAddOpens(JavaCompilationUnitState state, Blackhole bh) { + JavaTypeCache typeCache = new JavaTypeCache(); for (Map.Entry entry : state.typeCache.map().entrySet()) { typeCache.put(entry.getKey(), entry.getValue()); } @@ -60,6 +76,13 @@ public void readSnappy(JavaCompilationUnitState state, Blackhole bh) { } } +// @Benchmark + public void readHash(JavaCompilationUnitState state, Blackhole bh) { + for (Map.Entry entry : state.typeCache.map().entrySet()) { + bh.consume(state.typeCache.get(entry.getKey())); + } + } + @Benchmark public void readAdaptiveRadix(JavaCompilationUnitState state, Blackhole bh) { for (Map.Entry entry : state.typeCache.map().entrySet()) { @@ -67,6 +90,14 @@ public void readAdaptiveRadix(JavaCompilationUnitState state, Blackhole bh) { } } + @Benchmark + @Fork(value = 1, jvmArgsAppend = {"--add-opens", "java.base/java.lang=ALL-UNNAMED"}) + public void readAdaptiveRadixAddOpens(JavaCompilationUnitState state, Blackhole bh) { + for (Map.Entry entry : state.typeCache.map().entrySet()) { + bh.consume(state.radixMapTypeCache.get(entry.getKey())); + } + } + public static void main(String[] args) throws RunnerException, URISyntaxException { Options opt = new OptionsBuilder() .include(JavaTypeCacheBenchmark.class.getSimpleName()) diff --git a/rewrite-core/src/main/java/org/openrewrite/internal/AdaptiveRadixTree.java b/rewrite-core/src/main/java/org/openrewrite/internal/AdaptiveRadixTree.java index 944666344f6..df9c2e67195 100644 --- a/rewrite-core/src/main/java/org/openrewrite/internal/AdaptiveRadixTree.java +++ b/rewrite-core/src/main/java/org/openrewrite/internal/AdaptiveRadixTree.java @@ -647,6 +647,7 @@ public AdaptiveRadixTree copy() { public void clear() { root = null; + keyTable.clear(); } private static class KeyTable { @@ -710,5 +711,10 @@ private void ensureCapacity(int additional) { public byte get(int offset) { return storage[offset]; } + + public void clear() { + storage = new byte[INITIAL_CAPACITY]; + size = 0; + } } } diff --git a/rewrite-java/build.gradle.kts b/rewrite-java/build.gradle.kts index 1b85c3ea59e..76f0b33b6a0 100644 --- a/rewrite-java/build.gradle.kts +++ b/rewrite-java/build.gradle.kts @@ -50,8 +50,6 @@ dependencies { implementation("org.apache.commons:commons-text:latest.release") implementation("io.github.classgraph:classgraph:latest.release") - implementation("org.xerial.snappy:snappy-java:1.1.10.+") - api("com.fasterxml.jackson.core:jackson-annotations") // these are required for now so that `ChangeType` and `ChangePackage` can use the `Reference` trait diff --git a/rewrite-java/src/main/java/org/openrewrite/java/internal/AdaptiveRadixJavaTypeCache.java b/rewrite-java/src/main/java/org/openrewrite/java/internal/AdaptiveRadixJavaTypeCache.java deleted file mode 100644 index 01a6a3eae40..00000000000 --- a/rewrite-java/src/main/java/org/openrewrite/java/internal/AdaptiveRadixJavaTypeCache.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2021 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 - *

- * https://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.openrewrite.java.internal; - -import org.jspecify.annotations.Nullable; -import org.openrewrite.Incubating; -import org.openrewrite.internal.AdaptiveRadixTree; - -import java.lang.reflect.Field; -import java.nio.charset.StandardCharsets; - -@Incubating(since = "8.38.0") -public class AdaptiveRadixJavaTypeCache extends JavaTypeCache { - - AdaptiveRadixTree typeCache = new AdaptiveRadixTree<>(); - - @Override - public @Nullable T get(String signature) { - //noinspection unchecked - return (T) typeCache.search(getKeyBytes(signature)); - } - - @Override - public void put(String signature, Object o) { - typeCache.insert(getKeyBytes(signature), o); - } - - @Override - public void clear() { - typeCache.clear(); - } - - @Override - public AdaptiveRadixJavaTypeCache clone() { - AdaptiveRadixJavaTypeCache clone = (AdaptiveRadixJavaTypeCache) super.clone(); - clone.typeCache = this.typeCache.copy(); - return clone; - } - - private static final @Nullable Field STRING_VALUE; - private static final @Nullable Field STRING_CODER; - private static final boolean USE_REFLECTION; - - static { - Field value; - Field coder; - boolean hasCompactStrings = false; - - try { - // requires: --add-opens java.base/java.lang=ALL-UNNAMED - value = String.class.getDeclaredField("value"); - value.setAccessible(true); - - try { - coder = String.class.getDeclaredField("coder"); - coder.setAccessible(true); - Field compactStrings = String.class.getDeclaredField("COMPACT_STRINGS"); - compactStrings.setAccessible(true); - hasCompactStrings = compactStrings.getBoolean(null); - } catch (NoSuchFieldException e) { - // Java 8 - field doesn't exist - coder = null; - } - } catch (Exception e) { - value = null; - coder = null; - } - - STRING_VALUE = value; - STRING_CODER = coder; - USE_REFLECTION = STRING_VALUE != null && STRING_CODER != null && hasCompactStrings; - } - - /** - * For ASCII and Latin-1 strings this operation is allocation-free. - */ - static byte[] getKeyBytes(String s) { - // Try to get internal representation first - if (USE_REFLECTION) { - try { - //noinspection DataFlowIssue - byte[] bytes = (byte[]) STRING_VALUE.get(s); - //noinspection DataFlowIssue - byte coder = (byte) STRING_CODER.get(s); - if (coder == 0) { - // Latin1, use directly - return bytes; - } else { - // UTF-8: append NUL byte to avoid collisions - byte[] prefixed = new byte[bytes.length + 1]; - System.arraycopy(bytes, 0, prefixed, 0, bytes.length); - prefixed[bytes.length] = 0; - return prefixed; - } - } catch (Exception ignored) { - } - } - - return s.getBytes(StandardCharsets.UTF_8); - } -} diff --git a/rewrite-java/src/main/java/org/openrewrite/java/internal/JavaTypeCache.java b/rewrite-java/src/main/java/org/openrewrite/java/internal/JavaTypeCache.java index 16bb1c6ab36..ce710b8bc03 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/internal/JavaTypeCache.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/internal/JavaTypeCache.java @@ -15,72 +15,99 @@ */ package org.openrewrite.java.internal; -import lombok.Value; import org.jspecify.annotations.Nullable; -import org.xerial.snappy.Snappy; +import org.openrewrite.internal.AdaptiveRadixTree; -import java.io.IOException; -import java.io.UncheckedIOException; +import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; public class JavaTypeCache implements Cloneable { - // empirical value: below this size, the compressed key is larger or only slightly smaller - // although also note that a String object has a 24 bytes overhead vs. the 16 bytes of a BytesKey object - public static final int COMPRESSION_THRESHOLD = 50; - - @SuppressWarnings("ClassCanBeRecord") - @Value - private static class BytesKey { - byte[] data; - } - - Map typeCache = new HashMap<>(); + AdaptiveRadixTree typeCache = new AdaptiveRadixTree<>(); public @Nullable T get(String signature) { //noinspection unchecked - return (T) typeCache.get(key(signature)); + return (T) typeCache.search(getKeyBytes(signature)); } public void put(String signature, Object o) { - typeCache.put(key(signature), o); - } - - @Nullable - private static boolean snappyUsable = true; - - private Object key(String signature) { - if (signature.length() > COMPRESSION_THRESHOLD && snappyUsable) { - try { - return new BytesKey(Snappy.compress(signature.getBytes(StandardCharsets.UTF_8))); - } catch (IOException e) { - throw new UncheckedIOException(e); - } catch (NoClassDefFoundError e) { - // Some systems fail to load Snappy native components, so fall back to not compressing - snappyUsable = false; - } - } - return signature; + typeCache.insert(getKeyBytes(signature), o); } public void clear() { typeCache.clear(); } - public int size() { - return typeCache.size(); - } - @Override public JavaTypeCache clone() { try { JavaTypeCache clone = (JavaTypeCache) super.clone(); - clone.typeCache = new HashMap<>(this.typeCache); + clone.typeCache = this.typeCache.copy(); return clone; } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } } + + private static final @Nullable Field STRING_VALUE; + private static final @Nullable Field STRING_CODER; + private static final boolean USE_REFLECTION; + + static { + Field value; + Field coder; + boolean hasCompactStrings = false; + + try { + // requires: --add-opens java.base/java.lang=ALL-UNNAMED + value = String.class.getDeclaredField("value"); + value.setAccessible(true); + + try { + coder = String.class.getDeclaredField("coder"); + coder.setAccessible(true); + Field compactStrings = String.class.getDeclaredField("COMPACT_STRINGS"); + compactStrings.setAccessible(true); + hasCompactStrings = compactStrings.getBoolean(null); + } catch (NoSuchFieldException e) { + // Java 8 - field doesn't exist + coder = null; + } + } catch (Exception e) { + value = null; + coder = null; + } + + STRING_VALUE = value; + STRING_CODER = coder; + USE_REFLECTION = STRING_VALUE != null && STRING_CODER != null && hasCompactStrings; + } + + /** + * For ASCII and Latin-1 strings this operation is allocation-free. + */ + static byte[] getKeyBytes(String s) { + // Try to get internal representation first + if (USE_REFLECTION) { + try { + //noinspection DataFlowIssue + byte[] bytes = (byte[]) STRING_VALUE.get(s); + //noinspection DataFlowIssue + byte coder = (byte) STRING_CODER.get(s); + if (coder == 0) { + // Latin1, use directly + return bytes; + } else { + // UTF-8: append NUL byte to avoid collisions + byte[] prefixed = new byte[bytes.length + 1]; + System.arraycopy(bytes, 0, prefixed, 0, bytes.length); + prefixed[bytes.length] = 0; + return prefixed; + } + } catch (Exception ignored) { + } + } + + return s.getBytes(StandardCharsets.UTF_8); + } }