Skip to content

ImportLayoutStyle: cache classpath conflict data per JavaSourceSet#7911

Draft
knutwannheden wants to merge 3 commits into
mainfrom
restore-orderimports-classpath-fast-path
Draft

ImportLayoutStyle: cache classpath conflict data per JavaSourceSet#7911
knutwannheden wants to merge 3 commits into
mainfrom
restore-orderimports-classpath-fast-path

Conversation

@knutwannheden

@knutwannheden knutwannheden commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Motivation

ImportLayoutStyle.ImportLayoutConflictDetection walks the entire classpath for every file in setJVMClassNames() and mapNamesInPackageToPackages() — a fresh detector is created per file by OrderImports/AddImport, so the cost is O(files × classpath) of getPackageName()/getFullyQualifiedName() string work plus HashMap churn. CPU profiling of a static-analysis run over a large multi-module project showed ImportLayoutStyle at ~15% of the whole run. With a lazily-resolved classpath it is worse, because the full per-file walk forces materializing the entire lazy classpath on every file.

What changed

JavaSourceSet gains a lazily-built, per-instance index of classpath types grouped by package, exposed as a single generic accessor (import-conflict policy stays in ImportLayoutStyle):

public List<JavaType.FullyQualified> typesInPackage(String packageName);

It is a Lombok @Getter(lazy = true) field: thread-safe lazy init, and excluded from serialization, equals/hashCode, toString, and the with* constructor threading — so a source set produced by withClasspath (e.g. addTypesForGav/removeTypesForGav) rebuilds the index for its new classpath rather than reusing a stale one.

ImportLayoutConflictDetection resolves only the imported packages through that index when a source set is available, instead of walking the whole classpath:

// new overloads; the existing ones delegate with sourceSet = null (unchanged full-walk behavior)
style.orderImports(imports, classpath, classpathDirty, sourceSet);
style.addImport(imports, toAdd, pkg, classpath, classpathDirty, sourceSet);

OrderImports/AddImport pass the CU's JavaSourceSet. The classpathDirty safe path and the cheap classpath.isEmpty() guard are unchanged, and the cached path is byte-for-byte equivalent to the full walk (mirrors the per-type semantics of the original loop, including the static-member/FQN branch).

Summary

  • Add JavaSourceSet.typesInPackage(String) backed by a lazily-built Map<packageName, List<FullyQualified>> (single classpath walk per instance, @Getter(lazy = true)).
  • ImportLayoutConflictDetection uses the per-source-set index when present; falls back to the existing full walk when no source set is supplied.
  • New orderImports/addImport overloads carrying the JavaSourceSet; old overloads retained and delegate with null.
  • OrderImports and AddImport thread the source set through.

Scope note

The cache is keyed to a JavaSourceSet instance, so it benefits every file that shares that instance (the in-memory parse/recipe path). On serialized pipelines where each file is re-hydrated into its own marker instance, the transient index is cold per file (same as the existing transient typeFactory); a follow-up can add an ExecutionContext-id-keyed backstop if profiling shows that path still hot. Memoizing JavaType.FullyQualified.getPackageName() is a complementary, independently valuable follow-up.

Test plan

  • JavaSourceSetTest: typesInPackage returns the types in a package; the classpath is walked exactly once across repeated calls (counting collection); the lazy index is excluded from the Jackson payload and the marker round-trips.
  • ImportLayoutStyleTest: the new orderImports(..., sourceSet) overload is byte-for-byte equal to the full-walk path, both when a java.lang collision must suppress folding and when folding must proceed.
  • Regression green: OrderImportsTest, AddImportTest (incl. doNotFoldPackageWithJavaLangClassNames, now exercising the cached path), full :rewrite-java, DirtyJavaSourceSetsProducerTest, ChangeTypeTest/ChangePackageTest/RemoveImportTest/RemoveUnusedImportsTest.

ImportLayoutConflictDetection walked the entire classpath for every file
in setJVMClassNames() and mapNamesInPackageToPackages(), since a fresh
detector is created per file by OrderImports/AddImport. That is O(files x
classpath) string work and, with a lazy classpath, forces materializing
the whole classpath on every file.

Add a lazily-built, per-instance package index to JavaSourceSet
(typesInPackage(String)) and resolve only the imported packages through it,
so the classpath is walked once per source set instead of once per file.
New orderImports/addImport overloads thread the source set through; the
existing overloads delegate with a null source set (unchanged full-walk
behavior). The classpathDirty safe path is preserved.

A fast path along these lines was added in #7528 and later reverted in
#7845 as an unused SPI; this restores it without any SPI surface.
@knutwannheden knutwannheden force-pushed the restore-orderimports-classpath-fast-path branch from 178a6d1 to 7528ef7 Compare June 5, 2026 12:19
buildTypesByPackage() iterated the `classpath` field directly. A subclass
that hydrates its classpath lazily — passing an empty list to super(...)
and serving the real classpath only via an overridden getClasspath() —
would therefore index an empty classpath, silently disabling
ImportLayoutStyle's conflict detection (folding imports as if nothing
conflicts). Read through getClasspath() so the override is honored; the
lazy getter runs after construction, and behavior is unchanged for the
base class, whose getter returns the field.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

1 participant