diff --git a/rewrite-benchmarks/build.gradle.kts b/rewrite-benchmarks/build.gradle.kts index 5070cfcc10a..4374bbde2d4 100644 --- a/rewrite-benchmarks/build.gradle.kts +++ b/rewrite-benchmarks/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { jmh(project(":rewrite-maven")) jmh("org.rocksdb:rocksdbjni:latest.release") jmh("org.openjdk.jmh:jmh-core:latest.release") + jmh("org.openjdk.jol:jol-core:latest.release") jmh("io.github.fastfilter:fastfilter:latest.release") // Nebula doesn't like having jmhAnnotationProcessor without jmh so we just add it twice. 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 99ac500afe3..a8847d87163 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 @@ -15,38 +15,53 @@ */ package org.openrewrite.benchmarks.java; +import org.jspecify.annotations.Nullable; import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jol.info.GraphLayout; import org.openrewrite.InMemoryExecutionContext; import org.openrewrite.LargeSourceSet; 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 java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @State(Scope.Benchmark) public class JavaCompilationUnitState { + JavaParser.Builder javaParser; List sourceFiles; + List inputs; + JavaTypeCache snappyTypeCache; + AdaptiveRadixJavaTypeCache radixMapTypeCache; + MapJavaTypeCache typeCache; + + public static void main(String[] args) throws URISyntaxException { + new JavaCompilationUnitState().setup(); + } @Setup(Level.Trial) public void setup() throws URISyntaxException { Path rewriteRoot = Paths.get(ChangeTypeBenchmark.class.getResource("./") .toURI()).resolve("../../../../../../../../").normalize(); - List inputs = Arrays.asList( + 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/MetricsHelper.java"), rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/internal/ListUtils.java"), - rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/internal/StringUtils.java"), rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/internal/PropertyPlaceholderHelper.java"), rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/internal/RecipeIntrospectionUtils.java"), + rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/internal/StringUtils.java"), rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/Tree.java"), rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/ExecutionContext.java"), rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/InMemoryExecutionContext.java"), @@ -61,7 +76,11 @@ public void setup() throws URISyntaxException { rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/Result.java"), rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/SourceFile.java"), rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/Recipe.java"), + rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/ScanningRecipe.java"), rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/Validated.java"), + rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/ValidationException.java"), + rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/TreeVisitor.java"), + rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/TreeObserver.java"), rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/config/DeclarativeRecipe.java"), rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/config/ResourceLoader.java"), rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/config/YamlResourceLoader.java"), @@ -69,13 +88,33 @@ public void setup() throws URISyntaxException { rewriteRoot.resolve("rewrite-core/src/main/java/org/openrewrite/config/RecipeIntrospectionException.java") ); - sourceFiles = JavaParser.fromJavaVersion() - .classpath("jsr305", "classgraph", "jackson-annotations", "micrometer-core", "slf4j-api", - "org.openrewrite.jgit") + javaParser = JavaParser.fromJavaVersion() + .classpath("jsr305", "classgraph", "jackson-annotations", "micrometer-core", + "jgit", "jspecify", "lombok", "annotations"); // .logCompilationWarningsAndErrors(true) - .build() - .parse(inputs, null, new InMemoryExecutionContext(Throwable::printStackTrace)) + + typeCache = new MapJavaTypeCache(); + JavaParser parser = javaParser.typeCache(typeCache).build(); + sourceFiles = parser + .parse(inputs, null, new InMemoryExecutionContext()) .collect(Collectors.toList()); + + radixMapTypeCache = new AdaptiveRadixJavaTypeCache(); + for (Map.Entry entry : typeCache.map().entrySet()) { + radixMapTypeCache.put(entry.getKey(), entry.getValue()); + } + + snappyTypeCache = new JavaTypeCache(); + for (Map.Entry entry : typeCache.map().entrySet()) { + snappyTypeCache.put(entry.getKey(), entry.getValue()); + } + } + + void printMemory() { + long retainedSize = GraphLayout.parseInstance(radixMapTypeCache).totalSize(); + 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); } @TearDown(Level.Trial) @@ -90,4 +129,41 @@ public LargeSourceSet getSourceSet() { public List getSourceFiles() { return sourceFiles; } + + static class MapJavaTypeCache extends JavaTypeCache { + + Map typeCache = new HashMap<>(); + + @Override + public @Nullable T get(String signature) { + //noinspection unchecked + return (T) typeCache.get(signature); + } + + @Override + public void put(String signature, Object o) { + typeCache.put(signature, o); + } + + public Map map() { + return typeCache; + } + + @Override + public void clear() { + typeCache.clear(); + } + + @Override + public int size() { + return typeCache.size(); + } + + @Override + public MapJavaTypeCache clone() { + MapJavaTypeCache clone = (MapJavaTypeCache) 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 new file mode 100644 index 00000000000..37d839100bb --- /dev/null +++ b/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaParserBenchmark.java @@ -0,0 +1,70 @@ +/* + * Copyright 2022 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.benchmarks.java; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.profile.GCProfiler; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +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; +import java.util.concurrent.TimeUnit; + +@Fork(1) +@Measurement(iterations = 2) +@Warmup(iterations = 2) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@Threads(4) +public class JavaParserBenchmark { + + @Benchmark + public void snappy(JavaCompilationUnitState state, Blackhole bh) { + JavaTypeCache typeCache = new JavaTypeCache(); + JavaParser parser = state.javaParser.typeCache(typeCache).build(); + parser + .parse(state.inputs, null, new InMemoryExecutionContext()) + .forEach(bh::consume); + } + + @Benchmark + public void adaptiveRadix(JavaCompilationUnitState state, Blackhole bh) { + AdaptiveRadixJavaTypeCache typeCache = new AdaptiveRadixJavaTypeCache(); + JavaParser parser = state.javaParser.typeCache(typeCache).build(); + parser + .parse(state.inputs, null, new InMemoryExecutionContext()) + .forEach(bh::consume); + } + + public static void main(String[] args) throws RunnerException, URISyntaxException { + Options opt = new OptionsBuilder() + .include(JavaParserBenchmark.class.getSimpleName()) + .addProfiler(GCProfiler.class) + .shouldFailOnError(true) + .build(); + new Runner(opt).run(); + JavaCompilationUnitState state = new JavaCompilationUnitState(); + state.setup(); + state.printMemory(); + } +} 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 new file mode 100644 index 00000000000..a79b9aaead9 --- /dev/null +++ b/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaTypeCacheBenchmark.java @@ -0,0 +1,81 @@ +/* + * Copyright 2022 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.benchmarks.java; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.profile.GCProfiler; +import org.openjdk.jmh.runner.Runner; +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.JavaTypeCache; +import org.openrewrite.java.internal.AdaptiveRadixJavaTypeCache; + +import java.net.URISyntaxException; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Fork(1) +@Measurement(iterations = 2) +@Warmup(iterations = 2) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@Threads(4) +public class JavaTypeCacheBenchmark { + + @Benchmark + public void writeSnappy(JavaCompilationUnitState state, Blackhole bh) { + JavaTypeCache typeCache = new JavaTypeCache(); + for (Map.Entry entry : state.typeCache.map().entrySet()) { + typeCache.put(entry.getKey(), entry.getValue()); + } + } + + @Benchmark + public void writeAdaptiveRadix(JavaCompilationUnitState state, Blackhole bh) { + AdaptiveRadixJavaTypeCache typeCache = new AdaptiveRadixJavaTypeCache(); + for (Map.Entry entry : state.typeCache.map().entrySet()) { + typeCache.put(entry.getKey(), entry.getValue()); + } + } + + @Benchmark + public void readSnappy(JavaCompilationUnitState state, Blackhole bh) { + for (Map.Entry entry : state.typeCache.map().entrySet()) { + bh.consume(state.snappyTypeCache.get(entry.getKey())); + } + } + + @Benchmark + public void readAdaptiveRadix(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()) + .addProfiler(GCProfiler.class) + .shouldFailOnError(true) + .build(); + new Runner(opt).run(); + JavaCompilationUnitState state = new JavaCompilationUnitState(); + state.setup(); + state.printMemory(); + } +} diff --git a/rewrite-core/src/main/java/org/openrewrite/internal/AdaptiveRadixTree.java b/rewrite-core/src/main/java/org/openrewrite/internal/AdaptiveRadixTree.java new file mode 100644 index 00000000000..3981c963322 --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/internal/AdaptiveRadixTree.java @@ -0,0 +1,844 @@ +/* + * Copyright 2024 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.internal; + +import org.jspecify.annotations.Nullable; +import org.openrewrite.Incubating; + +import java.util.Arrays; + +@Incubating(since = "8.38.0") +public class AdaptiveRadixTree { + private transient int size = 0; + + @Nullable + private Node root; + + public AdaptiveRadixTree() { + } + + private AdaptiveRadixTree(AdaptiveRadixTree from) { + this.root = from.root == null ? null : from.root.copy(); + this.size = from.size; + } + + public AdaptiveRadixTree copy() { + return new AdaptiveRadixTree<>(this); + } + + public void insert(String key, V value) { + byte[] bytes = key.getBytes(); + if (root == null) { + // create leaf node and set root to that + root = new Node.LeafNode<>(bytes, value); + size = 1; + return; + } + put(bytes, value); + } + + public void clear() { + size = 0; + root = null; + } + + public @Nullable V search(String key) { + Node.LeafNode entry = getEntry(key); + return (entry == null ? null : entry.getValue()); + } + + private Node.@Nullable LeafNode getEntry(String key) { + if (root == null) { // empty tree + return null; + } + byte[] bytes = key.getBytes(); + return getEntry(root, bytes); + } + + // Arrays.equals() only available in Java 9+ + private static boolean equals(byte @Nullable [] a, int aFromIndex, int aToIndex, + byte @Nullable [] b, int bFromIndex, int bToIndex) { + // Check for null arrays + if (a == null || b == null) { + return a == b; // Both are null or one is null + } + + // Check for invalid index ranges + if ((aToIndex - aFromIndex) != (bToIndex - bFromIndex)) { + return false; + } + + // Compare the specified ranges + for (int i = 0; i < (aToIndex - aFromIndex); i++) { + if (a[aFromIndex + i] != b[bFromIndex + i]) { + return false; + } + } + + return true; + } + + private Node.@Nullable LeafNode getEntry(Node node, byte[] key) { + int depth = 0; + boolean skippedPrefix = false; + while (true) { + if (node instanceof Node.LeafNode) { + @SuppressWarnings("unchecked") Node.LeafNode leaf = (Node.LeafNode) node; + byte[] leafBytes = leaf.getKeyBytes(); + int startFrom = skippedPrefix ? 0 : depth; + if (equals(leafBytes, startFrom, leafBytes.length, key, startFrom, key.length)) { + return leaf; + } + return null; + } + + Node.InnerNode innerNode = (Node.InnerNode) node; + + if (key.length < depth + innerNode.prefixLen) { + return null; + } + + if (innerNode.prefixLen <= Node.InnerNode.PESSIMISTIC_PATH_COMPRESSION_LIMIT) { + // match pessimistic compressed path completely + for (int i = 0; i < innerNode.prefixLen; i++) { + if (innerNode.prefixKeys[i] != key[depth + i]) + return null; + } + } else { + // else take optimistic jump + skippedPrefix = true; + } + + // took pessimistic match or optimistic jump, continue search + depth = depth + innerNode.prefixLen; + Node nextNode; + if (depth == key.length) { + nextNode = innerNode.getLeaf(); + if (!skippedPrefix) { + //noinspection unchecked + return (Node.LeafNode) nextNode; + } + } else { + nextNode = innerNode.findChild(key[depth]); + depth++; + } + if (nextNode == null) { + return null; + } + // set fields for next iteration + node = nextNode; + } + } + + void replace(int depth, byte[] key, Node.@Nullable InnerNode prevDepth, Node replaceWith) { + if (prevDepth == null) { + root = replaceWith; + } else { + prevDepth.replace(key[depth - 1], replaceWith); + } + } + + private void put(byte[] keyBytes, V value) { + int depth = 0; + Node.InnerNode prevDepth = null; + Node node = root; + while (true) { + if (node instanceof Node.LeafNode) { + @SuppressWarnings("unchecked") + Node.LeafNode leaf = (Node.LeafNode) node; + Node pathCompressedNode = lazyExpansion(leaf, keyBytes, value, depth); + if (pathCompressedNode == node) { + // key already exists + leaf.setValue(value); + return; + } + // we gotta replace the prevDepth's child pointer to this new node + replace(depth, keyBytes, prevDepth, pathCompressedNode); + size++; + return; + } + // compare with compressed path + Node.InnerNode innerNode = (Node.InnerNode) node; + int newDepth = matchCompressedPath(innerNode, keyBytes, value, depth, prevDepth); + if (newDepth == -1) { // matchCompressedPath already inserted the leaf node for us + size++; + return; + } + + if (keyBytes.length == newDepth) { + @SuppressWarnings("unchecked") Node.LeafNode leaf = (Node.LeafNode) innerNode.getLeaf(); + leaf.setValue(value); + return; + } + + // we're now at line 26 in paper + byte partialKey = keyBytes[newDepth]; + Node child = innerNode.findChild(partialKey); + if (child != null) { + // set fields for next iteration + prevDepth = innerNode; + depth = newDepth + 1; + node = child; + continue; + } + + // add this key as child + Node leaf = new Node.LeafNode<>(keyBytes, value); + if (innerNode.isFull()) { + innerNode = innerNode.grow(); + replace(depth, keyBytes, prevDepth, innerNode); + } + innerNode.addChild(partialKey, leaf); + size++; + return; + } + } + + private static Node lazyExpansion(Node.LeafNode leaf, byte[] keyBytes, V value, int depth) { + + // find LCP + int lcp = 0; + byte[] leafKey = leaf.getKeyBytes(); // loadKey in paper + int end = Math.min(leafKey.length, keyBytes.length); + for (; depth < end && leafKey[depth] == keyBytes[depth]; depth++, lcp++) ; + if (depth == keyBytes.length && depth == leafKey.length) { + // we're referring to a key that already exists, replace value and return current + return leaf; + } + + // create new node with LCP + Node.Node4 pathCompressedNode = new Node.Node4(); + pathCompressedNode.prefixLen = lcp; + int pessimisticLcp = Math.min(lcp, Node.InnerNode.PESSIMISTIC_PATH_COMPRESSION_LIMIT); + System.arraycopy(keyBytes, depth - lcp, pathCompressedNode.prefixKeys, 0, pessimisticLcp); + + // add new key and old leaf as children + Node.LeafNode newLeaf = new Node.LeafNode<>(keyBytes, value); + if (depth == keyBytes.length) { + // barca to be inserted, barcalona already exists + // set barca's parent to be this path compressed node + // setup uplink whenever we set downlink + pathCompressedNode.setLeaf(newLeaf); + pathCompressedNode.addChild(leafKey[depth], leaf); // l + } else if (depth == leafKey.length) { + // barcalona to be inserted, barca already exists + pathCompressedNode.setLeaf(leaf); + pathCompressedNode.addChild(keyBytes[depth], newLeaf); // l + } else { + pathCompressedNode.addChild(leafKey[depth], leaf); + pathCompressedNode.addChild(keyBytes[depth], newLeaf); + } + + return pathCompressedNode; + } + + static void removeOptimisticLCPFromCompressedPath(Node.InnerNode node, int depth, int lcp, byte[] leafBytes) { + // since there's more compressed path left + // we need to "bring up" more of it what we can take + node.prefixLen = node.prefixLen - lcp - 1; + int end = Math.min(Node.InnerNode.PESSIMISTIC_PATH_COMPRESSION_LIMIT, node.prefixLen); + System.arraycopy(leafBytes, depth + 1, node.prefixKeys, 0, end); + } + + static void removePessimisticLCPFromCompressedPath(Node.InnerNode node, int depth, int lcp) { + if (node.prefixLen <= Node.InnerNode.PESSIMISTIC_PATH_COMPRESSION_LIMIT) { + node.prefixLen = node.prefixLen - lcp - 1; + System.arraycopy(node.prefixKeys, lcp + 1, node.prefixKeys, 0, node.prefixLen); + } else { + // since there's more compressed path left + // we need to "bring up" more of it what we can take + node.prefixLen = node.prefixLen - lcp - 1; + byte[] leafBytes = getFirstEntry(node).getKeyBytes(); + int end = Math.min(Node.InnerNode.PESSIMISTIC_PATH_COMPRESSION_LIMIT, node.prefixLen); + System.arraycopy(leafBytes, depth + 1, node.prefixKeys, 0, end); + } + } + + private int matchCompressedPath(Node.InnerNode node, byte[] keyBytes, V value, int depth, Node.@Nullable InnerNode prevDepth) { + int lcp = 0; + int end = Math.min(keyBytes.length - depth, Math.min(node.prefixLen, Node.InnerNode.PESSIMISTIC_PATH_COMPRESSION_LIMIT)); + // match pessimistic compressed path + for (; lcp < end && keyBytes[depth] == node.prefixKeys[lcp]; lcp++, depth++) ; + + if (lcp == node.prefixLen) { + if (depth == keyBytes.length && !node.hasLeaf()) { // key ended, it means it is a prefix + Node.LeafNode leafNode = new Node.LeafNode<>(keyBytes, value); + node.setLeaf(leafNode); + return -1; + } else { + return depth; + } + } + + Node.InnerNode newNode; + if (lcp == Node.InnerNode.PESSIMISTIC_PATH_COMPRESSION_LIMIT) { + // match remaining optimistic path + byte[] leafBytes = getFirstEntry(node).getKeyBytes(); + int leftToMatch = node.prefixLen - Node.InnerNode.PESSIMISTIC_PATH_COMPRESSION_LIMIT; + end = Math.min(keyBytes.length, depth + leftToMatch); + /* + match remaining optimistic path + if we match entirely we return with new depth and caller can proceed with findChild (depth + lcp + 1) + if we don't match entirely, then we split + */ + for (; depth < end && keyBytes[depth] == leafBytes[depth]; depth++, lcp++) ; + if (lcp == node.prefixLen) { + if (depth == keyBytes.length && !node.hasLeaf()) { // key ended, it means it is a prefix + Node.LeafNode leafNode = new Node.LeafNode<>(keyBytes, value); + node.setLeaf(leafNode); + return -1; + } else { + // matched entirely, but key is left + return depth; + } + } else { + newNode = branchOutOptimistic(node, keyBytes, value, lcp, depth, leafBytes); + } + } else { + newNode = branchOutPessimistic(node, keyBytes, value, lcp, depth); + } + // replace "this" node with newNode + // initialDepth can be zero even if prefixLen is not zero. + // the root node could have a prefix too, for example after insertions of + // BAR, BAZ? prefix would be BA kept in the root node itself + replace(depth - lcp, keyBytes, prevDepth, newNode); + return -1; // we've already inserted the leaf node, caller needs to do nothing more + } + + // called when lcp has become more than InnerNode.PESSIMISTIC_PATH_COMPRESSION_LIMIT + static Node.InnerNode branchOutOptimistic(Node.InnerNode node, byte[] keyBytes, V value, int lcp, int depth, + byte[] leafBytes) { + int initialDepth = depth - lcp; + Node.LeafNode leafNode = new Node.LeafNode<>(keyBytes, value); + + // new node with updated prefix len, compressed path + Node.Node4 branchOut = new Node.Node4(); + branchOut.prefixLen = lcp; + // note: depth is the updated depth (initialDepth = depth - lcp) + System.arraycopy(keyBytes, initialDepth, branchOut.prefixKeys, 0, Node.InnerNode.PESSIMISTIC_PATH_COMPRESSION_LIMIT); + if (depth == keyBytes.length) { + branchOut.setLeaf(leafNode); + } else { + branchOut.addChild(keyBytes[depth], leafNode); + } + branchOut.addChild(leafBytes[depth], node); // reusing "this" node + + // remove lcp common prefix key from "this" node + removeOptimisticLCPFromCompressedPath(node, depth, lcp, leafBytes); + return branchOut; + } + + static Node.InnerNode branchOutPessimistic(Node.InnerNode node, byte[] keyBytes, V value, int lcp, int depth) { + int initialDepth = depth - lcp; + + // create new lazy leaf node for unmatched key? + Node.LeafNode leafNode = new Node.LeafNode<>(keyBytes, value); + + // new node with updated prefix len, compressed path + Node.Node4 branchOut = new Node.Node4(); + branchOut.prefixLen = lcp; + // note: depth is the updated depth (initialDepth = depth - lcp) + System.arraycopy(keyBytes, initialDepth, branchOut.prefixKeys, 0, lcp); + if (depth == keyBytes.length) { // key ended it means it is a prefix + branchOut.setLeaf(leafNode); + } else { + branchOut.addChild(keyBytes[depth], leafNode); + } + branchOut.addChild(node.prefixKeys[lcp], node); // reusing "this" node + + // remove lcp common prefix key from "this" node + removePessimisticLCPFromCompressedPath(node, depth, lcp); + return branchOut; + } + + @SuppressWarnings("unchecked") + private static Node.LeafNode getFirstEntry(Node startFrom) { + Node node = startFrom; + Node next = node.firstOrLeaf(); + while (next != null) { + node = next; + next = node.firstOrLeaf(); + } + return (Node.LeafNode) node; + } + + public int size() { + return size; + } + + abstract static class Node { + // 2^7 = 128 + static final int BYTE_SHIFT = 1 << Byte.SIZE - 1; + + /** + * For Node4, Node16 to interpret every byte as unsigned when storing partial keys. + * Node 48, Node256 simply use {@link Byte#toUnsignedInt(byte)} + * to index into their key arrays. + */ + static byte unsigned(byte b) { + return (byte) (b ^ BYTE_SHIFT); + } + + // passed b must have been interpreted as unsigned already + // this is the reverse of unsigned + static byte signed(byte b) { + return unsigned(b); + } + + abstract @Nullable Node first(); + + abstract @Nullable Node firstOrLeaf(); + + abstract Node copy(); + + static abstract class InnerNode extends Node { + static final int PESSIMISTIC_PATH_COMPRESSION_LIMIT = 8; + final byte[] prefixKeys = new byte[PESSIMISTIC_PATH_COMPRESSION_LIMIT]; + int prefixLen; + short noOfChildren; + final @Nullable Node[] child; + + InnerNode(int size) { + child = new Node[size + 1]; + } + + InnerNode(InnerNode node, int size) { + super(); + child = new Node[size + 1]; + this.noOfChildren = node.noOfChildren; + this.prefixLen = node.prefixLen; + System.arraycopy(node.prefixKeys, 0, this.prefixKeys, 0, PESSIMISTIC_PATH_COMPRESSION_LIMIT); + LeafNode leaf = node.getLeaf(); + if (leaf != null) { + setLeaf(leaf); + } + } + + public InnerNode(InnerNode from) { + System.arraycopy(from.prefixKeys, 0, this.prefixKeys, 0, PESSIMISTIC_PATH_COMPRESSION_LIMIT); + this.prefixLen = from.prefixLen; + this.noOfChildren = from.noOfChildren; + this.child = new Node[from.child.length]; + for (int i = 0; i < child.length; i++) { + Node fromChild = from.child[i]; + this.child[i] = fromChild == null ? null : fromChild.copy(); + } + } + + public void setLeaf(LeafNode leaf) { + child[child.length - 1] = leaf; + } + + public boolean hasLeaf() { + return child[child.length - 1] != null; + } + + public @Nullable LeafNode getLeaf() { + return (LeafNode) child[child.length - 1]; + } + + @Override + public @Nullable Node firstOrLeaf() { + return hasLeaf() ? getLeaf() : first(); + } + + Node[] getChild() { + //noinspection NullableProblems + return child; + } + + public short size() { + return noOfChildren; + } + + abstract @Nullable Node findChild(byte partialKey); + + abstract void addChild(byte partialKey, Node child); + + abstract void replace(byte partialKey, Node newChild); + + abstract InnerNode grow(); + + abstract boolean isFull(); + } + + static class LeafNode extends Node { + private V value; + + // we have to save the keyBytes, because leaves are lazy expanded at times + private final byte[] keyBytes; + + LeafNode(byte[] keyBytes, V value) { + this.value = value; + // defensive copy + this.keyBytes = Arrays.copyOf(keyBytes, keyBytes.length); + } + + @Override + Node copy() { + return new LeafNode<>(keyBytes, value); + } + + public void setValue(V value) { + this.value = value; + } + + public V getValue() { + return value; + } + + byte[] getKeyBytes() { + return keyBytes; + } + + @Override + public @Nullable Node first() { + return null; + } + + @Override + public @Nullable Node firstOrLeaf() { + return null; + } + + @Override + public String toString() { + return Arrays.toString(keyBytes) + "=" + value; + } + } + + static class Node4 extends InnerNode { + + static final int NODE_SIZE = 4; + + // each array element would contain the partial byte key to match + // if key matches then take up the same index from the child pointer array + private final byte[] keys = new byte[NODE_SIZE]; + + Node4() { + super(NODE_SIZE); + } + + private Node4(Node4 node4) { + super(node4); + } + + @Override + Node copy() { + return new Node4(this); + } + + @Override + public @Nullable Node findChild(byte partialKey) { + partialKey = unsigned(partialKey); + // paper does simple loop over because it's a tiny array of size 4 + for (int i = 0; i < noOfChildren; i++) { + if (keys[i] == partialKey) { + return child[i]; + } + } + return null; + } + + @Override + public void addChild(byte partialKey, Node child) { + byte unsignedPartialKey = unsigned(partialKey); + // shift elements from this point to right by one place + // noOfChildren here would never be == Node_SIZE (since we have isFull() check) + int i = noOfChildren; + for (; i > 0 && unsignedPartialKey < keys[i - 1]; i--) { + keys[i] = keys[i - 1]; + this.child[i] = this.child[i - 1]; + } + keys[i] = unsignedPartialKey; + this.child[i] = child; + noOfChildren++; + } + + @Override + public void replace(byte partialKey, Node newChild) { + byte unsignedPartialKey = unsigned(partialKey); + + int index = 0; + for (; index < noOfChildren; index++) { + if (keys[index] == unsignedPartialKey) { + break; + } + } + // replace will be called from in a state where you know partialKey entry surely exists + child[index] = newChild; + } + + @Override + public InnerNode grow() { + // grow from Node4 to Node16 + return new Node16(this); + } + + @Override + public @Nullable Node first() { + return child[0]; + } + + @Override + public boolean isFull() { + return noOfChildren == NODE_SIZE; + } + + byte[] getKeys() { + return keys; + } + } + + static class Node16 extends InnerNode { + static final int NODE_SIZE = 16; + private final byte[] keys = new byte[NODE_SIZE]; + + Node16(Node4 node) { + super(node, NODE_SIZE); + byte[] keys = node.getKeys(); + Node[] child = node.getChild(); + System.arraycopy(keys, 0, this.keys, 0, node.noOfChildren); + System.arraycopy(child, 0, this.child, 0, node.noOfChildren); + } + + private Node16(Node16 from) { + super(from); + System.arraycopy(from.keys, 0, this.keys, 0, NODE_SIZE); + } + + @Override + Node copy() { + return new Node16(this); + } + + @Override + public @Nullable Node findChild(byte partialKey) { + // TODO: use simple loop to see if -XX:+SuperWord applies SIMD JVM instrinsics + partialKey = unsigned(partialKey); + for (int i = 0; i < noOfChildren; i++) { + if (keys[i] == partialKey) { + return child[i]; + } + } + return null; + } + + @Override + public void addChild(byte partialKey, Node child) { + byte unsignedPartialKey = unsigned(partialKey); + + int index = Arrays.binarySearch(keys, 0, noOfChildren, unsignedPartialKey); + // the partialKey should not exist + int insertionPoint = -(index + 1); + // shift elements from this point to right by one place + for (int i = noOfChildren; i > insertionPoint; i--) { + keys[i] = keys[i - 1]; + this.child[i] = this.child[i - 1]; + } + keys[insertionPoint] = unsignedPartialKey; + this.child[insertionPoint] = child; + noOfChildren++; + } + + @Override + public void replace(byte partialKey, Node newChild) { + byte unsignedPartialKey = unsigned(partialKey); + int index = Arrays.binarySearch(keys, 0, noOfChildren, unsignedPartialKey); + child[index] = newChild; + } + + @Override + public InnerNode grow() { + return new Node48(this); + } + + @Override + public @Nullable Node first() { + return child[0]; + } + + @Override + public boolean isFull() { + return noOfChildren == NODE_SIZE; + } + + byte[] getKeys() { + return keys; + } + } + + static class Node48 extends InnerNode { + static final int NODE_SIZE = 48; + static final int KEY_INDEX_SIZE = 256; + + // for partial keys of one byte size, you index directly into this array to find the + // array index of the child pointer array + // the index value can only be between 0 to 47 (to index into the child pointer array) + private final byte[] keyIndex = new byte[KEY_INDEX_SIZE]; + + // so that when you use the partial key to index into keyIndex + // and you see a -1, you know there's no mapping for this key + static final byte ABSENT = -1; + + Node48(Node16 node) { + super(node, NODE_SIZE); + + Arrays.fill(keyIndex, ABSENT); + + byte[] keys = node.getKeys(); + Node[] child = node.getChild(); + + for (int i = 0; i < Node16.NODE_SIZE; i++) { + byte key = signed(keys[i]); + int index = Byte.toUnsignedInt(key); + keyIndex[index] = (byte) i; + this.child[i] = child[i]; + } + } + + private Node48(Node48 from) { + super(from); + System.arraycopy(from.keyIndex, 0, this.keyIndex, 0, KEY_INDEX_SIZE); + } + + @Override + Node copy() { + return new Node48(this); + } + + @Override + public @Nullable Node findChild(byte partialKey) { + byte index = keyIndex[Byte.toUnsignedInt(partialKey)]; + if (index == ABSENT) { + return null; + } + + return child[index]; + } + + @Override + public void addChild(byte partialKey, Node child) { + int index = Byte.toUnsignedInt(partialKey); + // find a null place, left fragmented by a removeChild or has always been null + byte insertPosition = 0; + for (; this.child[insertPosition] != null && insertPosition < NODE_SIZE; insertPosition++) ; + + this.child[insertPosition] = child; + keyIndex[index] = insertPosition; + noOfChildren++; + } + + @Override + public void replace(byte partialKey, Node newChild) { + byte index = keyIndex[Byte.toUnsignedInt(partialKey)]; + child[index] = newChild; + } + + @Override + public InnerNode grow() { + return new Node256(this); + } + + @Override + public @Nullable Node first() { + int i = 0; + while (keyIndex[i] == ABSENT) i++; + return child[keyIndex[i]]; + } + + @Override + public boolean isFull() { + return noOfChildren == NODE_SIZE; + } + + + byte[] getKeyIndex() { + return keyIndex; + } + } + + static class Node256 extends InnerNode { + static final int NODE_SIZE = 256; + + Node256(Node48 node) { + super(node, NODE_SIZE); + + byte[] keyIndex = node.getKeyIndex(); + Node[] child = node.getChild(); + + for (int i = 0; i < Node48.KEY_INDEX_SIZE; i++) { + byte index = keyIndex[i]; + if (index == Node48.ABSENT) { + continue; + } + // index is byte, but gets type promoted + // https://docs.oracle.com/javase/specs/jls/se7/html/jls-10.html#jls-10.4-120 + this.child[i] = child[index]; + } + } + + private Node256(Node256 from) { + super(from); + } + + @Override + Node copy() { + return new Node256(this); + } + + @Override + public @Nullable Node findChild(byte partialKey) { + // We treat the 8 bits as unsigned int since we've got 256 slots + int index = Byte.toUnsignedInt(partialKey); + return child[index]; + } + + @Override + public void addChild(byte partialKey, Node child) { + // addChild would never be called on a full Node256 + // since the corresponding findChild for any byte key + // would always find the byte since the Node is full. + int index = Byte.toUnsignedInt(partialKey); + this.child[index] = child; + noOfChildren++; + } + + @Override + public void replace(byte partialKey, Node newChild) { + int index = Byte.toUnsignedInt(partialKey); + child[index] = newChild; + } + + @Override + public InnerNode grow() { + throw new UnsupportedOperationException("Span of ART is 8 bits, so Node256 is the largest node type."); + } + + @Override + public @Nullable Node first() { + int i = 0; + while (child[i] == null) i++; + return child[i]; + } + + @Override + public boolean isFull() { + return noOfChildren == NODE_SIZE; + } + } + } +} diff --git a/rewrite-core/src/test/java/org/openrewrite/internal/AdaptiveRadixTreeTest.java b/rewrite-core/src/test/java/org/openrewrite/internal/AdaptiveRadixTreeTest.java new file mode 100644 index 00000000000..3d6932b87b0 --- /dev/null +++ b/rewrite-core/src/test/java/org/openrewrite/internal/AdaptiveRadixTreeTest.java @@ -0,0 +1,386 @@ +/* + * Copyright 2024 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.internal; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class AdaptiveRadixTreeTest { + + @Test + public void insertAndSearch_SingleKey() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("cat", 1); + + assertThat(tree.search("cat")).isEqualTo(1); + assertThat(tree.search("ca")).isNull(); + assertThat(tree.search("c")).isNull(); + assertThat(tree.search("dog")).isNull(); + } + + @Test + public void copy() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("cat", 1); + AdaptiveRadixTree copy = tree.copy(); + assertThat(copy.search("cat")).isEqualTo(1); + assertThat(copy.search("ca")).isNull(); + assertThat(copy.search("c")).isNull(); + assertThat(copy.search("dog")).isNull(); + } + + @Test + public void insertAndSearch_MultipleKeys() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("cat", 1); + tree.insert("car", 2); + tree.insert("cart", 3); + tree.insert("dog", 4); + + assertThat(tree.search("cat")).isEqualTo(1); + assertThat(tree.search("car")).isEqualTo(2); + assertThat(tree.search("cart")).isEqualTo(3); + assertThat(tree.search("dog")).isEqualTo(4); + + assertThat(tree.search("ca")).isNull(); + assertThat(tree.search("c")).isNull(); + assertThat(tree.search("do")).isNull(); + assertThat(tree.search("dogs")).isNull(); + } + + @Test + public void insertAndSearch_OverlappingKeys() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("test", 1); + tree.insert("testing", 2); + tree.insert("tester", 3); + + assertThat(tree.search("test")).isEqualTo(1); + assertThat(tree.search("testing")).isEqualTo(2); + assertThat(tree.search("tester")).isEqualTo(3); + + assertThat(tree.search("tes")).isNull(); + assertThat(tree.search("testers")).isNull(); + } + + @Test + public void insertAndSearch_PrefixKeys() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("a", 1); + tree.insert("ab", 2); + tree.insert("abc", 3); + tree.insert("abcd", 4); + + assertThat(tree.search("a")).isEqualTo(1); + assertThat(tree.search("ab")).isEqualTo(2); + assertThat(tree.search("abc")).isEqualTo(3); + assertThat(tree.search("abcd")).isEqualTo(4); + + assertThat(tree.search("abcde")).isNull(); + assertThat(tree.search("abce")).isNull(); + } + + @Test + public void insertAndSearch_EmptyString() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("", 1); + + assertThat(tree.search("")).isEqualTo(1); + assertThat(tree.search(" ")).isNull(); + assertThat(tree.search("a")).isNull(); + } + + @Test + public void insertAndSearch_SpecialCharacters() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("hello-world", 1); + tree.insert("hello_world", 2); + tree.insert("hello world", 3); + + assertThat(tree.search("hello-world")).isEqualTo(1); + assertThat(tree.search("hello_world")).isEqualTo(2); + assertThat(tree.search("hello world")).isEqualTo(3); + + assertThat(tree.search("hello")).isNull(); + assertThat(tree.search("world")).isNull(); + } + + @Test + public void insertAndSearch_CaseSensitivity() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("Apple", 1); + tree.insert("apple", 2); + + assertThat(tree.search("Apple")).isEqualTo(1); + assertThat(tree.search("apple")).isEqualTo(2); + + assertThat(tree.search("Appl")).isNull(); + assertThat(tree.search("APPLE")).isNull(); + } + + @Test + public void insertAndSearch_NumericKeys() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("123", 1); + tree.insert("1234", 2); + tree.insert("12345", 3); + + assertThat(tree.search("123")).isEqualTo(1); + assertThat(tree.search("1234")).isEqualTo(2); + assertThat(tree.search("12345")).isEqualTo(3); + + assertThat(tree.search("12")).isNull(); + assertThat(tree.search("123456")).isNull(); + } + + @Test + public void insertAndSearch_MixedCharacterKeys() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("user1", 1); + tree.insert("user2", 2); + tree.insert("user10", 3); + tree.insert("user20", 4); + + assertThat(tree.search("user1")).isEqualTo(1); + assertThat(tree.search("user2")).isEqualTo(2); + assertThat(tree.search("user10")).isEqualTo(3); + assertThat(tree.search("user20")).isEqualTo(4); + + assertThat(tree.search("user")).isNull(); + assertThat(tree.search("user3")).isNull(); + } + + @Test + public void insertAndSearch_LongKeys() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + String longKey1 = "a".repeat(1000); + String longKey2 = "a".repeat(999) + "b"; + + tree.insert(longKey1, 1); + tree.insert(longKey2, 2); + + assertThat(tree.search(longKey1)).isEqualTo(1); + assertThat(tree.search(longKey2)).isEqualTo(2); + + assertThat(tree.search("a".repeat(500))).isNull(); + } + + @Test + public void insertAndSearch_NullValue() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("key", null); + + assertThat(tree.search("key")).isNull(); + assertThat(tree.search("ke")).isNull(); + } + + @Test + public void insertAndSearch_UnicodeKeys() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("こんにちは", 1); // "Hello" in Japanese + tree.insert("こんばんは", 2); // "Good evening" in Japanese + tree.insert("你好", 3); // "Hello" in Chinese + + assertThat(tree.search("こんにちは")).isEqualTo(1); + assertThat(tree.search("こんばんは")).isEqualTo(2); + assertThat(tree.search("你好")).isEqualTo(3); + + assertThat(tree.search("こん")).isNull(); + assertThat(tree.search("你好嗎")).isNull(); + } + + @Test + public void insertAndSearch_EmptyTree() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + assertThat(tree.search("anykey")).isNull(); + } + + @Test + public void insert_DuplicateKeys() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("duplicate", 1); + tree.insert("duplicate", 2); + + assertThat(tree.search("duplicate")).isEqualTo(2); + } + + @Test + public void Search_NonExistentKeys() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("exist", 1); + + assertThat(tree.search("nonexist")).isNull(); + assertThat(tree.search("exis")).isNull(); + assertThat(tree.search("exists")).isNull(); + } + + @Test + public void insertAndSearch_SimilarKeys() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("ab", 1); + tree.insert("abc", 2); + tree.insert("abcd", 3); + tree.insert("abcde", 4); + tree.insert("abcdef", 5); + + assertThat(tree.search("ab")).isEqualTo(1); + assertThat(tree.search("abc")).isEqualTo(2); + assertThat(tree.search("abcd")).isEqualTo(3); + assertThat(tree.search("abcde")).isEqualTo(4); + assertThat(tree.search("abcdef")).isEqualTo(5); + } + + @Test + public void insertAndSearch_CommonPrefixes() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("prefix", 1); + tree.insert("preface", 2); + tree.insert("preform", 3); + tree.insert("preposition", 4); + tree.insert("presentation", 5); + + assertThat(tree.search("prefix")).isEqualTo(1); + assertThat(tree.search("preface")).isEqualTo(2); + assertThat(tree.search("preform")).isEqualTo(3); + assertThat(tree.search("preposition")).isEqualTo(4); + assertThat(tree.search("presentation")).isEqualTo(5); + + assertThat(tree.search("pre")).isNull(); + assertThat(tree.search("president")).isNull(); + } + + @Test + public void insertAndSearch_SingleCharacterKeys() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("a", 1); + tree.insert("b", 2); + tree.insert("c", 3); + + assertThat(tree.search("a")).isEqualTo(1); + assertThat(tree.search("b")).isEqualTo(2); + assertThat(tree.search("c")).isEqualTo(3); + + assertThat(tree.search("d")).isNull(); + } + + @Test + public void insertAndSearch_NullKey() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + assertThatThrownBy(() -> tree.insert(null, 1)) + .isInstanceOf(NullPointerException.class); + } + + @Test + public void insertAndSearch_SpecialCaseKeys() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("user_name", 1); + tree.insert("user-name", 2); + tree.insert("user.name", 3); + tree.insert("user@name", 4); + tree.insert("user#name", 5); + + assertThat(tree.search("user_name")).isEqualTo(1); + assertThat(tree.search("user-name")).isEqualTo(2); + assertThat(tree.search("user.name")).isEqualTo(3); + assertThat(tree.search("user@name")).isEqualTo(4); + assertThat(tree.search("user#name")).isEqualTo(5); + + assertThat(tree.search("user%name")).isNull(); + } + + @Test + public void insertAndSearch_CyrillicKeys() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("привет", 1); // "Hello" in Russian + tree.insert("проект", 2); // "Project" in Russian + + assertThat(tree.search("привет")).isEqualTo(1); + assertThat(tree.search("проект")).isEqualTo(2); + + assertThat(tree.search("про")).isNull(); + assertThat(tree.search("прив")).isNull(); + } + + @Test + public void insertAndSearch_EmojiKeys() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("😀", 1); + tree.insert("😀😁", 2); + tree.insert("😀😁😂", 3); + + assertThat(tree.search("😀")).isEqualTo(1); + assertThat(tree.search("😀😁")).isEqualTo(2); + assertThat(tree.search("😀😁😂")).isEqualTo(3); + + assertThat(tree.search("😀😁😂🤣")).isNull(); + } + + @Test + public void insertAndSearch_MixedLanguageKeys() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("hello世界", 1); // "hello world" mixing English and Chinese + tree.insert("こんにちはworld", 2); // "hello world" mixing Japanese and English + + assertThat(tree.search("hello世界")).isEqualTo(1); + assertThat(tree.search("こんにちはworld")).isEqualTo(2); + + assertThat(tree.search("hello")).isNull(); + assertThat(tree.search("world")).isNull(); + } + + @Test + public void insertAndSearch_SpacesInKeys() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("key with spaces", 1); + tree.insert("another key with spaces", 2); + + assertThat(tree.search("key with spaces")).isEqualTo(1); + assertThat(tree.search("another key with spaces")).isEqualTo(2); + + assertThat(tree.search("key with")).isNull(); + assertThat(tree.search("another key")).isNull(); + } + + @Test + public void insertAndSearch_SpecialUnicodeCharacters() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("naïve", 1); + tree.insert("café", 2); + tree.insert("résumé", 3); + + assertThat(tree.search("naïve")).isEqualTo(1); + assertThat(tree.search("café")).isEqualTo(2); + assertThat(tree.search("résumé")).isEqualTo(3); + + assertThat(tree.search("naive")).isNull(); + assertThat(tree.search("cafe")).isNull(); + } + + @Test + public void insertAndSearch_ControlCharacters() { + AdaptiveRadixTree tree = new AdaptiveRadixTree<>(); + tree.insert("line1\nline2", 1); + tree.insert("tab\tcharacter", 2); + + assertThat(tree.search("line1\nline2")).isEqualTo(1); + assertThat(tree.search("tab\tcharacter")).isEqualTo(2); + + assertThat(tree.search("line1")).isNull(); + assertThat(tree.search("tab")).isNull(); + } +} 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 new file mode 100644 index 00000000000..d0a3c5ebea2 --- /dev/null +++ b/rewrite-java/src/main/java/org/openrewrite/java/internal/AdaptiveRadixJavaTypeCache.java @@ -0,0 +1,49 @@ +/* + * 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; + +@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(signature); + } + + @Override + public void put(String signature, Object o) { + typeCache.insert(signature, o); + } + + @Override + public void clear() { + typeCache.clear(); + } + + @Override + public AdaptiveRadixJavaTypeCache clone() { + AdaptiveRadixJavaTypeCache clone = (AdaptiveRadixJavaTypeCache) super.clone(); + clone.typeCache = this.typeCache.copy(); + return clone; + } +}