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 {