Skip to content

Cache ContentModuleScanner results to fix Junie perf issue - #1567

Open
Flórián Garaba (fgaraba) wants to merge 1 commit into
masterfrom
perf/content-module-scan-cache
Open

Cache ContentModuleScanner results to fix Junie perf issue#1567
Flórián Garaba (fgaraba) wants to merge 1 commit into
masterfrom
perf/content-module-scan-cache

Conversation

@fgaraba

@fgaraba Flórián Garaba (fgaraba) commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Cache lib/ scan results in FileBasedModuleDescriptorResolver to fix content-module resolution bottleneck

Junie (plugin 1072850) declares 64 content modules, none of which use the fast path (lib/modules/.jar) -- they all land in a single JAR under lib/. This caused FileBasedModuleDescriptorResolver.getModuleCreator to fall through to LibDirectoryPluginLoader for every module, which sequentially opens all JARs in lib/ to find the one containing the descriptor. With 243 JARs in Junie's lib/, that's 15,552 JAR operations per plugin parse - adding ~2 minutes to structure parsing in the Marketplace's PluginStructureParser.

ContentModuleScanner already scans all JARs in lib/ and lib/modules/ and builds a moduleId -> artifactPath map; it was previously used only for classpath setup. This change reuses it inside FileBasedModuleDescriptorResolver: on the first miss of the lib/modules/<name>.jar fast path, the resolver calls ContentModuleScanner once and caches the result in a map keyed by plugin artifact path. Subsequent modules resolve via O(1) map lookup.

) : ModuleDescriptorResolver<FileBasedModule>() {

private val contentModuleScanner = ContentModuleScanner(fileSystemProvider)
private val moduleArtifactCache = ConcurrentHashMap<Path, Map<String, Path>>()

@chrkv Ivan Chirkov (chrkv) Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scope this cache to a single resolveContentModules pass rather than the lifetime of IdePluginManager. ContentModuleLoader is retained by IdePluginManager, and the manager can be reused to parse many plugin artifacts, so this map otherwise retains every scanned artifact and its module paths indefinitely.


private fun resolveModuleArtifact(pluginArtifactPath: Path, moduleName: String): Path? {
val moduleMap = moduleArtifactCache.computeIfAbsent(pluginArtifactPath) { path ->
contentModuleScanner.getContentModules(path).modules.associate { it.id to it.artifactPath }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

associate silently collapses duplicate module IDs. If the same root-level module descriptor exists in two JARs under lib/, we now pick one artifact and bypass LibDirectoryPluginLoader, which previously reported MultiplePluginDescriptors.

Please preserve all candidates, e.g. as Map<String, List<Path>>, and use the direct-JAR fast path only when singleOrNull() returns a unique match. For duplicates, fall back to the existing loader or report the ambiguity explicitly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants