Skip to content

Commit ce8a026

Browse files
sambsnydJenson3210
andauthored
Build fast "has type" lookup into TypesInUse cache, speeding up repeated type queries (#7483)
* Fast type lookup via TypesInUse. LSTs already have a TypesInUse field which is a WeakReference'd cache of type information present in a given LST. But UsesType and UsesMethod (another very popular precondition / implementation detail) still iterate over every single type in use looking for matches. So in a large composite recipe: thousands of recipe instance are, via UsesType / UsesMethod, iterating over lists of hundreds or thousands of types, for hundreds or thousands of source files. It explains why AddDependency dominates the scanning phase in terms of time spent in the spring boot migration, followed closely by ChangeType. So build a lazily produced prefix tree into TypesInUse. Big savings in the asociated benchmark. In the benchmark which simulates running 100 HasType in succession this is 100x faster * Get rid of useless MethodMatcher cache * TypesInUse test coverage * Implement Knut's feedback * Fallback to old code paths to minimize operational risk during rollout period --------- Co-authored-by: Jente Sondervorst <jentesondervorst@gmail.com>
1 parent 85e77cb commit ce8a026

6 files changed

Lines changed: 1002 additions & 54 deletions

File tree

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
/*
2+
* Copyright 2026 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.benchmarks.java;
17+
18+
import org.openjdk.jmh.annotations.*;
19+
import org.openjdk.jmh.infra.Blackhole;
20+
import org.openjdk.jmh.runner.Runner;
21+
import org.openjdk.jmh.runner.RunnerException;
22+
import org.openjdk.jmh.runner.options.Options;
23+
import org.openjdk.jmh.runner.options.OptionsBuilder;
24+
import org.openrewrite.InMemoryExecutionContext;
25+
import org.openrewrite.SourceFile;
26+
import org.openrewrite.java.JavaParser;
27+
import org.openrewrite.java.internal.TypesInUse;
28+
import org.openrewrite.java.tree.J;
29+
import org.openrewrite.java.tree.JavaSourceFile;
30+
import org.openrewrite.java.tree.JavaType;
31+
import org.openrewrite.java.tree.TypeUtils;
32+
33+
import java.lang.reflect.Field;
34+
import java.util.Arrays;
35+
import java.util.List;
36+
import java.util.concurrent.TimeUnit;
37+
import java.util.stream.Collectors;
38+
39+
/**
40+
* Compares {@link TypesInUse#hasType(String, boolean)} (the closure-cached path) against
41+
* the iteration that {@code UsesType.visit} performed before the cache was added. Each
42+
* benchmark issues 100 successive queries against a single parsed compilation unit using
43+
* different FQNs.
44+
*/
45+
@Fork(1)
46+
@Warmup(iterations = 3, time = 2)
47+
@Measurement(iterations = 5, time = 3)
48+
@BenchmarkMode(Mode.AverageTime)
49+
@OutputTimeUnit(TimeUnit.MICROSECONDS)
50+
@State(Scope.Benchmark)
51+
public class UsesTypeBenchmark {
52+
53+
/**
54+
* 100 FQNs to query: a mix of hits (reachable through types-in-use, imports, or supertype
55+
* walks) and misses (types not used by the sample file). The proportions are roughly
56+
* representative of recipe preconditions in a mixed migration composite — most queries miss.
57+
*/
58+
static final List<String> QUERIES = Arrays.asList(
59+
// ~30 likely hits (common JDK types reachable via imports + supertype walks)
60+
"java.lang.Object", "java.lang.String", "java.lang.Comparable", "java.lang.CharSequence",
61+
"java.lang.Throwable", "java.lang.Exception", "java.lang.RuntimeException", "java.lang.Iterable",
62+
"java.lang.Number", "java.lang.Integer", "java.lang.Boolean", "java.lang.Long",
63+
"java.util.Collection", "java.util.List", "java.util.ArrayList", "java.util.Map",
64+
"java.util.HashMap", "java.util.Set", "java.util.HashSet", "java.util.Iterator",
65+
"java.util.function.Function", "java.util.function.Predicate", "java.util.function.Consumer",
66+
"java.util.function.Supplier", "java.util.stream.Stream", "java.util.Optional",
67+
"java.io.Serializable", "java.lang.AutoCloseable", "java.util.AbstractList", "java.util.AbstractMap",
68+
// ~70 likely misses (types not in the sample file)
69+
"javax.swing.JFrame", "javax.swing.JButton", "javax.swing.JPanel", "javax.swing.JTable",
70+
"java.awt.Color", "java.awt.Graphics", "java.awt.event.ActionListener",
71+
"java.sql.Connection", "java.sql.PreparedStatement", "java.sql.ResultSet",
72+
"javax.servlet.http.HttpServletRequest", "javax.servlet.http.HttpServletResponse",
73+
"javax.servlet.http.HttpServlet", "javax.servlet.ServletContext",
74+
"org.springframework.context.ApplicationContext", "org.springframework.beans.factory.BeanFactory",
75+
"org.springframework.web.bind.annotation.RestController", "org.springframework.stereotype.Service",
76+
"org.springframework.stereotype.Component", "org.springframework.stereotype.Repository",
77+
"org.springframework.boot.SpringApplication", "org.springframework.data.jpa.repository.JpaRepository",
78+
"org.springframework.transaction.annotation.Transactional",
79+
"org.junit.jupiter.api.Test", "org.junit.jupiter.api.BeforeEach", "org.junit.jupiter.api.AfterEach",
80+
"org.junit.jupiter.api.BeforeAll", "org.junit.jupiter.api.AfterAll", "org.junit.jupiter.api.Disabled",
81+
"org.mockito.Mockito", "org.mockito.Mock", "org.mockito.InjectMocks", "org.mockito.Spy",
82+
"org.assertj.core.api.Assertions", "org.assertj.core.api.AssertionsForClassTypes",
83+
"org.hibernate.Session", "org.hibernate.SessionFactory", "org.hibernate.Transaction",
84+
"javax.persistence.Entity", "javax.persistence.Id", "javax.persistence.Column", "javax.persistence.Table",
85+
"javax.persistence.GeneratedValue", "javax.persistence.OneToMany", "javax.persistence.ManyToOne",
86+
"com.fasterxml.jackson.annotation.JsonProperty", "com.fasterxml.jackson.databind.ObjectMapper",
87+
"com.fasterxml.jackson.core.JsonGenerator", "com.fasterxml.jackson.databind.JsonNode",
88+
"ch.qos.logback.classic.Logger", "ch.qos.logback.classic.LoggerContext",
89+
"org.slf4j.Logger", "org.slf4j.LoggerFactory",
90+
"io.netty.channel.Channel", "io.netty.channel.ChannelHandler", "io.netty.bootstrap.ServerBootstrap",
91+
"reactor.core.publisher.Mono", "reactor.core.publisher.Flux",
92+
"kotlinx.coroutines.flow.Flow", "kotlin.coroutines.Continuation",
93+
"scala.collection.immutable.List", "scala.Option", "scala.concurrent.Future",
94+
"groovy.lang.Closure", "groovy.lang.MetaClass",
95+
"com.example.MadeUpType", "com.example.AnotherMadeUp", "com.example.foo.Bar", "com.example.foo.Baz",
96+
"org.example.never.exists.Foo", "org.example.never.exists.Bar"
97+
);
98+
99+
JavaSourceFile sample;
100+
Field trieField;
101+
102+
@Setup(Level.Trial)
103+
public void setup() throws Exception {
104+
// Use a non-trivial Java file: many imports, a class hierarchy, and a healthy amount of
105+
// type usage. Inlined here so the benchmark is self-contained and reproducible.
106+
@org.intellij.lang.annotations.Language("java")
107+
String source = """
108+
package org.openrewrite.benchmarks.sample;
109+
110+
import java.util.ArrayList;
111+
import java.util.Collection;
112+
import java.util.Collections;
113+
import java.util.HashMap;
114+
import java.util.HashSet;
115+
import java.util.LinkedHashMap;
116+
import java.util.LinkedList;
117+
import java.util.List;
118+
import java.util.Map;
119+
import java.util.Optional;
120+
import java.util.Set;
121+
import java.util.TreeMap;
122+
import java.util.UUID;
123+
import java.util.concurrent.ConcurrentHashMap;
124+
import java.util.function.Function;
125+
import java.util.function.Predicate;
126+
import java.util.stream.Collectors;
127+
import java.util.stream.Stream;
128+
import java.io.IOException;
129+
import java.io.UncheckedIOException;
130+
import java.nio.file.Path;
131+
import java.nio.file.Paths;
132+
import java.nio.file.Files;
133+
134+
public class Sample {
135+
private final Map<String, List<UUID>> grouped = new HashMap<>();
136+
private final Set<String> seen = new HashSet<>();
137+
private final ConcurrentHashMap<String, Integer> counters = new ConcurrentHashMap<>();
138+
139+
public Optional<List<UUID>> lookup(String key) {
140+
return Optional.ofNullable(grouped.get(key));
141+
}
142+
143+
public List<String> sortedKeys(Predicate<String> filter) {
144+
return grouped.keySet().stream()
145+
.filter(filter)
146+
.sorted()
147+
.collect(Collectors.toList());
148+
}
149+
150+
public Map<String, Integer> counts(Function<String, Integer> mapper) {
151+
Map<String, Integer> out = new LinkedHashMap<>();
152+
for (String key : seen) {
153+
out.put(key, mapper.apply(key));
154+
}
155+
return out;
156+
}
157+
158+
public void incrementAll(Collection<String> keys) {
159+
for (String key : keys) {
160+
counters.merge(key, 1, Integer::sum);
161+
}
162+
}
163+
164+
public Stream<Path> walkTree(Path root) {
165+
try {
166+
return Files.walk(root);
167+
} catch (IOException e) {
168+
throw new UncheckedIOException(e);
169+
}
170+
}
171+
172+
public TreeMap<String, LinkedList<String>> grouped(List<String> input) {
173+
TreeMap<String, LinkedList<String>> by = new TreeMap<>();
174+
for (String s : input) {
175+
by.computeIfAbsent(s.substring(0, 1), k -> new LinkedList<>()).add(s);
176+
}
177+
return by;
178+
}
179+
180+
public List<UUID> ids(int n) {
181+
List<UUID> out = new ArrayList<>(n);
182+
for (int i = 0; i < n; i++) {
183+
out.add(UUID.randomUUID());
184+
}
185+
return Collections.unmodifiableList(out);
186+
}
187+
188+
public static Path resolve(String first, String... more) {
189+
return Paths.get(first, more);
190+
}
191+
}
192+
""";
193+
194+
List<SourceFile> parsed = JavaParser.fromJavaVersion()
195+
.build()
196+
.parse(new InMemoryExecutionContext(), source)
197+
.toList();
198+
sample = (JavaSourceFile) parsed.getFirst();
199+
200+
// Ensure type information is realized once up front so it doesn't pollute the first
201+
// measurement iteration. Doesn't build the closure — that's the cache we're measuring.
202+
sample.getTypesInUse().getTypesInUse().size();
203+
204+
trieField = TypesInUse.class.getDeclaredField("trie");
205+
trieField.setAccessible(true);
206+
}
207+
208+
/**
209+
* Closure already built (warmed up). Measures the steady-state cost of 100 successive
210+
* {@code hasType} queries — pure {@code Map.get}.
211+
*/
212+
@Benchmark
213+
public void cachedHot(Blackhole bh) {
214+
TypesInUse tiu = sample.getTypesInUse();
215+
for (String fqn : QUERIES) {
216+
bh.consume(tiu.hasType(fqn, false));
217+
}
218+
}
219+
220+
/**
221+
* Trie cleared before each invocation. Measures the cost of building the trie once plus 100
222+
* successive queries against it. This is the worst case for the cached path.
223+
*/
224+
@Benchmark
225+
public void cachedCold(Blackhole bh) throws IllegalAccessException {
226+
TypesInUse tiu = sample.getTypesInUse();
227+
trieField.set(tiu, null);
228+
for (String fqn : QUERIES) {
229+
bh.consume(tiu.hasType(fqn, false));
230+
}
231+
}
232+
233+
/**
234+
* Replicates the iteration {@code UsesType.visit} performed before this change: per query,
235+
* iterate types-in-use and imports calling {@code TypeUtils.isAssignableTo}. This is what
236+
* 100 successive {@code UsesType} invocations cost without any caching.
237+
*/
238+
@Benchmark
239+
public void uncachedIteration(Blackhole bh) {
240+
for (String fqn : QUERIES) {
241+
bh.consume(legacyHasType(sample, fqn));
242+
}
243+
}
244+
245+
private static boolean legacyHasType(JavaSourceFile cu, String fqn) {
246+
for (JavaType type : cu.getTypesInUse().getTypesInUse()) {
247+
JavaType checkType = type instanceof JavaType.Primitive ? type : TypeUtils.asFullyQualified(type);
248+
if (checkType != null && TypeUtils.isAssignableTo(fqn, checkType)) {
249+
return true;
250+
}
251+
}
252+
for (J.Import anImport : cu.getImports()) {
253+
JavaType target = anImport.isStatic()
254+
? anImport.getQualid().getTarget().getType()
255+
: anImport.getQualid().getType();
256+
JavaType checkType = TypeUtils.asFullyQualified(target);
257+
if (checkType != null && TypeUtils.isAssignableTo(fqn, checkType)) {
258+
return true;
259+
}
260+
}
261+
return false;
262+
}
263+
264+
public static void main(String[] args) throws RunnerException {
265+
Options opt = new OptionsBuilder()
266+
.include(UsesTypeBenchmark.class.getSimpleName())
267+
.build();
268+
new Runner(opt).run();
269+
}
270+
}

0 commit comments

Comments
 (0)