Skip to content

Performance: Maven 4 at parity with Maven 3 on large reactors (2:45 → 1:18) #12667

Description

@gnodet

Summary

JFR profiling of Maven 4 RC5/RC6 on a 4,383-module generated reactor project revealed several performance regressions compared to Maven 3. A clean install -DskipTests that takes ~1:15 on Maven 3.9.16 was taking ~2:45 on Maven 4 RC6.

After a series of targeted optimizations across 4 repositories, Maven 4 now completes the same build in ~1:18 — at parity with Maven 3.

Benchmark Results

Test machine: Apple M4 Pro, JDK 21. Project: 4,383-module diamond-graph reactor, clean install -DskipTests -q.

Configuration Wall time vs RC6
Maven 3.9.16 1:14 – 1:18
Maven 4 RC6 (unpatched) 2:45 baseline
+ PathConflictResolver default 1:45 -36%
+ resolver optimizations 1:22 -50%
+ all optimizations 1:18 -53%

Hotspots & Fixes

Each optimization was identified via JFR CPU profiling. The fixes are grouped by repository and listed in order of impact.

1. Conflict Resolver: O(N²) → O(N) — apache/maven#12662

JFR: ClassicConflictResolver.gatherConflictItems — 46.7% CPU

The ClassicConflictResolver performs O(N×M) recursive DFS in gatherConflictItems(). Switching the default from classic to path (PathConflictResolver, available since resolver 2.0.11, same resolution results) reduces this to O(N). Single biggest win: 2:45 → 1:45.

2. TransitiveDependencyManager — apache/maven-resolver#2014

JFR: deriveChildManager + Key.equals + MMap.done — 28% CPU combined

TransitiveDependencyManager (Maven 4 default, deriveUntil=Integer.MAX_VALUE) creates a new child manager at every graph node. Four layered fixes:

  • Instance self-reuse — when no new management data is collected (common for transitive POMs without <dependencyManagement>), return this instead of a new instance. Restores pool cache transparency.
  • Key coordinate caching — cache groupId/artifactId/extension/classifier strings eagerly to avoid RelocatedArtifact virtual dispatch per equals().
  • Cons-list parent pointer — replace O(depth)-copied ArrayList<AbstractDependencyManager> with a single parent reference. O(1) derive, cascading hashCode, identity-shortcircuit equals.
  • Varargs elimination — replace Objects.hash(Object...) with manual 31*h + field.hashCode() chains throughout Key, constructor, Holder, and MMap to eliminate ~10 temporary Object[] allocations per derive call.
  • MEMO_CACHE_SIZE 4 → 16 — self-reuse causes long-lived managers to serve children from many BFS wavefronts.
  • PathConflictResolver O(1) cycle detection — each Path node maintains a Set<String> of all conflict IDs from root, making hasConflictIdOnPathToRoot a Set.contains() instead of O(depth) parent walk.

3. InstallMojo / DeployMojo: O(N²) → O(N) — apache/maven-install-plugin#427, apache/maven-deploy-plugin#684

JFR: PluginContainer.getPluginsAsMap — 5.6% CPU

Both InstallMojo.execute() and DeployMojo.execute() scan the entire reactor project list on every module invocation to check which projects use the plugin. In a 4,383-module build this is ~19.2M filter evaluations. Fixed by caching the filtered list in the first reactor project's plugin context on first invocation.

4. Reactor Sort: O(N² log N) → O(N log N) — apache/maven#12652

JFR: MavenProject.equals — 23.5% CPU

DefaultGraphBuilder uses result.sort(comparing(sortedProjects::indexOf)) where ArrayList.indexOf() is O(n), causing O(N² log N) total MavenProject.equals() calls (~230M for 4,383 modules). Replaced with a HashMap<MavenProject, Integer> index for O(1) lookups.

Also includes:

  • DefaultModelObjectPool (~8% CPU) — cache getPooledTypes() set at construction, inline Objects.hash() varargs in PoolKey, add hashCode fast-rejection in equals()
  • PhaseComparator (~2% CPU) — pre-build HashMap<String, Integer> for O(1) phase lookups

5. Model Building Pipeline — apache/maven#12653

JFR: DefaultModelObjectPool.PoolKey + Dependency builders — 37% + 18% CPU

  • Add Builder getters and *ToBuilder merger variants to generated model classes
  • Defer Dependency.build() in DefaultDependencyManagementInjector — accumulate as Builder objects, build once at end
  • Optimize computeLocations() — replace Stream.concat().collect() with HashMap.putAll() + Map.copyOf()
  • Precompute locationsHashCode at build time for fast inequality checks

6. PrintWriter Lock Contention — apache/maven#12654

JFR: 1,470ms blocked time on PrintWriter.println()

AsyncDrainWriter wraps the logging Consumer<String> with a lock-free ConcurrentLinkedQueue + non-blocking drain via ReentrantLock.tryLock(). Eliminates all contention during parallel model building (-T1C).

7. Location Tracking Wire-up — apache/maven#12655

JFR: 97 InputLocation.of() allocations per POM

ModelBuilderRequest.isLocationTracking() existed but was only checked in one place. Now wired through XmlReaderRequestDefaultModelXmlFactoryMavenStaxReader so the parser actually skips all InputLocation.of() calls when tracking is disabled.

All PRs

Repository PR Status Description
apache/maven #12662 ✅ Ready Enable PathConflictResolver by default
apache/maven #12652 ✅ Ready Optimize reactor sort, model pool, phase comparator
apache/maven #12653 ✅ Ready Optimize model building pipeline
apache/maven #12654 ✅ Ready AsyncDrainWriter — lock contention elimination
apache/maven #12655 ✅ Ready Wire location tracking to XML parser
apache/maven-resolver #2014 ✅ Ready TransitiveDependencyManager performance
apache/maven-install-plugin #427 ✅ Ready Cache projectsUsingPlugin — O(N²) → O(N)
apache/maven-deploy-plugin #684 ✅ Ready Cache projectsWithDeployExecution — O(N²) → O(N)

Reproducing

# Generate the test project
git clone https://github.com/maven-turbo-reactor/maven-multiproject-generator
cd maven-multiproject-generator && ./generate.sh

# Build with gnodet's bench branch that includes all resolver + maven patches
git clone -b bench/resolver-2.0.22 https://github.com/gnodet/maven
cd maven && mvn install -DskipTests -q

# Benchmark
cd generated && time path/to/patched-maven/bin/mvn clean install -DskipTests -q

See also: benchmark gist

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions