You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by sj...@apache.org on 2026/08/31 20:59:09 UTC

(maven-install-plugin) branch maven-install-plugin-3.x updated: Cache projectsUsingPlugin to fix O(N²) reactor scan

This is an automated email from the ASF dual-hosted git repository.

slawekjaranowski pushed a commit to branch maven-install-plugin-3.x
in repository https://gitbox.apache.org/repos/asf/maven-install-plugin.git


The following commit(s) were added to refs/heads/maven-install-plugin-3.x by this push:
     new 9148a8f  Cache projectsUsingPlugin to fix O(N²) reactor scan
9148a8f is described below

commit 9148a8fba14a56c7f677eebee8ce8be75b6e88fe
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Sun Aug 30 22:30:37 2026 +0200

    Cache projectsUsingPlugin to fix O(N²) reactor scan
    
    * Cache projectsUsingPlugin list to avoid O(N²) reactor scan
    
    InstallMojo.execute() computes the list of reactor projects using
    the install plugin on every module invocation by streaming over all
    projects and calling getPluginsAsMap() for each one. In a 4383-module
    reactor, this produces ~19.2M filter evaluations (4383² calls).
    
    JFR profiling shows this as 5.6% of total CPU time, all in
    PluginContainer.getPluginsAsMap() called from usingPlugin().
    
    Fix: cache the computed list in the first reactor project's plugin
    context on first invocation. The list is invariant during a build —
    which projects have the install plugin configured does not change
    between module invocations. This turns O(N²) into O(N).
    
    Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
    
    * fix: synchronize cache population for parallel builds
    
    The get/check/put on the plugin context map is a TOCTOU race — in
    parallel builds (-T), multiple threads can see the cache as null
    and each recompute the full reactor scan, defeating the cache.
    
    Wrap the compound operation in synchronized(ctx) so only the first
    thread computes and all others reuse the result. Session.getPluginContext()
    returns Map (not ConcurrentMap), so synchronized is the safest choice
    that does not depend on the underlying implementation.
    
    Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
    
    * refactor: use computeIfAbsent instead of synchronized block
    
    Replace the synchronized get/check/put with a single computeIfAbsent
    call. The plugin context map is a ConcurrentHashMap at runtime, so
    computeIfAbsent is atomic and guarantees single-invocation. Even if
    the underlying Map implementation changed, the worst case is redundant
    computation of the same invariant list — a perf regression, not a bug.
    
    Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 <no...@anthropic.com>
---
 .../apache/maven/plugins/install/InstallMojo.java  | 57 +++++++++++-----------
 1 file changed, 29 insertions(+), 28 deletions(-)

diff --git a/src/main/java/org/apache/maven/plugins/install/InstallMojo.java b/src/main/java/org/apache/maven/plugins/install/InstallMojo.java
index 63be467..96f40c7 100644
--- a/src/main/java/org/apache/maven/plugins/install/InstallMojo.java
+++ b/src/main/java/org/apache/maven/plugins/install/InstallMojo.java
@@ -21,15 +21,14 @@ package org.apache.maven.plugins.install;
 import javax.inject.Inject;
 
 import java.io.File;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 import org.apache.maven.RepositoryUtils;
 import org.apache.maven.execution.MavenSession;
 import org.apache.maven.model.Plugin;
-import org.apache.maven.model.PluginExecution;
 import org.apache.maven.plugin.AbstractMojo;
 import org.apache.maven.plugin.MojoExecutionException;
 import org.apache.maven.plugin.descriptor.PluginDescriptor;
@@ -106,6 +105,7 @@ public class InstallMojo extends AbstractMojo {
     }
 
     private static final String INSTALL_PROCESSED_MARKER = InstallMojo.class.getName() + ".processed";
+    private static final String PROJECTS_USING_PLUGIN_KEY = InstallMojo.class.getName() + ".projectsUsingPlugin";
 
     private void putState(State state) {
         getPluginContext().put(INSTALL_PROCESSED_MARKER, state.name());
@@ -121,6 +121,28 @@ public class InstallMojo extends AbstractMojo {
         return pluginContext.containsKey(INSTALL_PROCESSED_MARKER);
     }
 
+    /**
+     * Returns the list of reactor projects that have this plugin configured, cached on first call.
+     * The list is invariant during a build and is stored in the current project's plugin
+     * context to avoid recomputing it on every module invocation (O(N) total instead of O(N²)).
+     */
+    @SuppressWarnings("unchecked")
+    private List<MavenProject> getProjectsUsingPlugin() {
+        List<MavenProject> allProjects = session.getProjects();
+        if (allProjects.isEmpty()) {
+            return Collections.emptyList();
+        }
+        Map<String, Object> ctx = session.getPluginContext(pluginDescriptor, allProjects.get(0));
+        return (List<MavenProject>) ctx.computeIfAbsent(
+                PROJECTS_USING_PLUGIN_KEY,
+                k -> allProjects.stream().filter(this::usingPlugin).collect(Collectors.toList()));
+    }
+
+    private boolean usingPlugin(MavenProject project) {
+        Plugin plugin = project.getPlugin("org.apache.maven.plugins:maven-install-plugin");
+        return plugin != null && plugin.getExecutions().stream().anyMatch(e -> !"none".equalsIgnoreCase(e.getPhase()));
+    }
+
     @Override
     public void execute() throws MojoExecutionException {
         if (skip) {
@@ -139,11 +161,10 @@ public class InstallMojo extends AbstractMojo {
             }
         }
 
-        List<MavenProject> allProjectsUsingPlugin = getAllProjectsUsingPlugin();
-
-        if (allProjectsMarked(allProjectsUsingPlugin)) {
+        List<MavenProject> projectsUsingPlugin = getProjectsUsingPlugin();
+        if (allProjectsMarked(projectsUsingPlugin)) {
             InstallRequest request = new InstallRequest();
-            for (MavenProject reactorProject : allProjectsUsingPlugin) {
+            for (MavenProject reactorProject : projectsUsingPlugin) {
                 State state = getState(reactorProject);
                 if (state == State.TO_BE_INSTALLED) {
                     processProject(reactorProject, request);
@@ -153,28 +174,8 @@ public class InstallMojo extends AbstractMojo {
         }
     }
 
-    private boolean allProjectsMarked(List<MavenProject> allProjectsUsingPlugin) {
-        return allProjectsUsingPlugin.stream().allMatch(this::hasState);
-    }
-
-    private final Predicate<MavenProject> hasMavenInstallPluginExecution =
-            rp -> hasExecution(rp.getPlugin("org.apache.maven.plugins:maven-install-plugin"));
-
-    private List<MavenProject> getAllProjectsUsingPlugin() {
-        return session.getProjects().stream()
-                .filter(hasMavenInstallPluginExecution)
-                .collect(Collectors.toList());
-    }
-
-    private final Predicate<PluginExecution> havingGoals = pe -> !pe.getGoals().isEmpty();
-    private final Predicate<PluginExecution> nonePhase = pe -> !"none".equalsIgnoreCase(pe.getPhase());
-
-    private boolean hasExecution(Plugin plugin) {
-        if (plugin == null) {
-            return false;
-        }
-
-        return plugin.getExecutions().stream().filter(havingGoals).anyMatch(nonePhase);
+    private boolean allProjectsMarked(List<MavenProject> projectsUsingPlugin) {
+        return projectsUsingPlugin.stream().allMatch(this::hasState);
     }
 
     private void installProject(InstallRequest request) throws MojoExecutionException {