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;
         }