You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/08/28 19:48:47 UTC
(maven) branch maven-4.0.x updated: [backport 4.0.x] Optimize reactor sort and phase comparator performance (#12901)
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch maven-4.0.x
in repository https://gitbox.apache.org/repos/asf/maven.git
The following commit(s) were added to refs/heads/maven-4.0.x by this push:
new 4ea1bc4729 [backport 4.0.x] Optimize reactor sort and phase comparator performance (#12901)
4ea1bc4729 is described below
commit 4ea1bc4729113b951e1e05177101e393d977fe7c
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Fri Aug 28 21:48:42 2026 +0200
[backport 4.0.x] Optimize reactor sort and phase comparator performance (#12901)
* chore: optimize reactor sort, model pool, and phase comparator performance (#12652)
JFR profiling of a 4,383-module reactor revealed three hotspots that
together consume ~35% of CPU time during dependency resolution:
1. DefaultGraphBuilder: result.sort(comparing(sortedProjects::indexOf))
uses O(n) ArrayList.indexOf per comparison, causing O(n² log n) total
MavenProject.equals calls (~230M for 4,383 modules). Replace with a
HashMap<MavenProject, Integer> index built in O(n), reducing sort
comparisons to O(1) each. Affects 3 call sites.
2. DefaultModelObjectPool.getPooledTypes(): re-parses a comma-separated
property string into a new Stream → Set on every process() call.
Cache the parsed Set at construction time. Also add hashCode
fast-rejection to PoolKey.equals() to skip expensive deep equality
when hashes differ.
3. DefaultModelObjectPool.PoolKey.dependencyHashCode(): Objects.hash()
with 12 arguments allocates a new Object[12] on every call. Inline
the hash computation to eliminate the varargs allocation.
4. PhaseComparator: List.indexOf() in compare() is O(n) per call.
Pre-build a HashMap<String, Integer> in the constructor for O(1)
phase index lookups.
Co-authored-by: Claude Opus 4.6 <no...@anthropic.com>
* chore: remove DefaultModelObjectPool from backport
The DefaultModelObjectPool optimization is out of scope for this
backport PR. Keep only the reactor sort and phase comparator
performance improvements.
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <no...@anthropic.com>
---
.../apache/maven/graph/DefaultGraphBuilder.java | 21 +++++++++++++++------
.../maven/lifecycle/internal/PhaseComparator.java | 22 ++++++++++++++--------
2 files changed, 29 insertions(+), 14 deletions(-)
diff --git a/impl/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java b/impl/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java
index 5f20f38e59..4c2e72e335 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java
@@ -157,8 +157,7 @@ private List<MavenProject> trimProjectsToRequest(
if (request.getPom() != null) {
result = getProjectsInRequestScope(request, activeProjects);
- List<MavenProject> sortedProjects = graph.getSortedProjects();
- result.sort(comparing(sortedProjects::indexOf));
+ result.sort(comparing(buildProjectIndexMap(graph.getSortedProjects())::get));
result = includeAlsoMakeTransitively(result, request, graph);
}
@@ -189,8 +188,7 @@ private List<MavenProject> trimSelectedProjects(
result = includeAlsoMakeTransitively(result, request, graph);
// Order the new list in the original order
- List<MavenProject> sortedProjects = graph.getSortedProjects();
- result.sort(comparing(sortedProjects::indexOf));
+ result.sort(comparing(buildProjectIndexMap(graph.getSortedProjects())::get));
}
}
@@ -290,13 +288,24 @@ private List<MavenProject> includeAlsoMakeTransitively(
result = new ArrayList<>(projectsSet);
// Order the new list in the original order
- List<MavenProject> sortedProjects = graph.getSortedProjects();
- result.sort(comparing(sortedProjects::indexOf));
+ result.sort(comparing(buildProjectIndexMap(graph.getSortedProjects())::get));
}
return result;
}
+ /**
+ * Builds a Map from MavenProject to its index in the sorted list, enabling O(1) index lookups
+ * for sorting instead of O(n) ArrayList.indexOf() scans that cause O(n² log n) sort performance.
+ */
+ private static Map<MavenProject, Integer> buildProjectIndexMap(List<MavenProject> sortedProjects) {
+ Map<MavenProject, Integer> indexMap = new HashMap<>(sortedProjects.size() * 2);
+ for (int i = 0; i < sortedProjects.size(); i++) {
+ indexMap.put(sortedProjects.get(i), i);
+ }
+ return indexMap;
+ }
+
private void enrichRequestFromResumptionData(List<MavenProject> projects, MavenExecutionRequest request) {
if (request.isResume()) {
projects.stream()
diff --git a/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseComparator.java b/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseComparator.java
index 2a1efccfbf..575c70ab03 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseComparator.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseComparator.java
@@ -19,16 +19,19 @@
package org.apache.maven.lifecycle.internal;
import java.util.Comparator;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/**
* Compares phases within the context of a specific lifecycle with secondary sorting based on the {@link PhaseId}.
*/
public class PhaseComparator implements Comparator<String> {
/**
- * The lifecycle phase ordering.
+ * Map from phase name to its index in the lifecycle, enabling O(1) lookups
+ * instead of O(n) List.indexOf() scans on every comparison.
*/
- private final List<String> lifecyclePhases;
+ private final Map<String, Integer> phaseIndexMap;
/**
* Constructor.
@@ -36,24 +39,27 @@ public class PhaseComparator implements Comparator<String> {
* @param lifecyclePhases the lifecycle phase ordering.
*/
public PhaseComparator(List<String> lifecyclePhases) {
- this.lifecyclePhases = lifecyclePhases;
+ this.phaseIndexMap = new HashMap<>(lifecyclePhases.size() * 2);
+ for (int i = 0; i < lifecyclePhases.size(); i++) {
+ phaseIndexMap.put(lifecyclePhases.get(i), i);
+ }
}
@Override
public int compare(String o1, String o2) {
PhaseId p1 = PhaseId.of(o1);
PhaseId p2 = PhaseId.of(o2);
- int i1 = lifecyclePhases.indexOf(p1.executionPoint().prefix() + p1.phase());
- int i2 = lifecyclePhases.indexOf(p2.executionPoint().prefix() + p2.phase());
- if (i1 == -1 && i2 == -1) {
+ Integer i1 = phaseIndexMap.get(p1.executionPoint().prefix() + p1.phase());
+ Integer i2 = phaseIndexMap.get(p2.executionPoint().prefix() + p2.phase());
+ if (i1 == null && i2 == null) {
// unknown phases, leave in existing order
return 0;
}
- if (i1 == -1) {
+ if (i1 == null) {
// second one is known, so it comes first
return 1;
}
- if (i2 == -1) {
+ if (i2 == null) {
// first one is known, so it comes first
return -1;
}