gnodet opened a new issue, #12667:
URL: https://github.com/apache/maven/issues/12667

   ## Summary
   
   JFR profiling of Maven 4 RC5/RC6 on a [4,383-module generated reactor 
project](https://github.com/maven-turbo-reactor/maven-multiproject-generator) 
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](https://github.com/apache/maven/pull/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](https://github.com/apache/maven-resolver/pull/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](https://github.com/apache/maven-install-plugin/pull/427),
 
[apache/maven-deploy-plugin#684](https://github.com/apache/maven-deploy-plugin/pull/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](https://github.com/apache/maven/pull/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](https://github.com/apache/maven/pull/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](https://github.com/apache/maven/pull/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](https://github.com/apache/maven/pull/12655)
   
   **JFR: 97 `InputLocation.of()` allocations per POM**
   
   `ModelBuilderRequest.isLocationTracking()` existed but was only checked in 
one place. Now wired through `XmlReaderRequest` → `DefaultModelXmlFactory` → 
`MavenStaxReader` so the parser actually skips all `InputLocation.of()` calls 
when tracking is disabled.
   
   ## All PRs
   
   | Repository | PR | Status | Description |
   |---|---|---|---|
   | apache/maven | [#12662](https://github.com/apache/maven/pull/12662) | ✅ 
Ready | Enable PathConflictResolver by default |
   | apache/maven | [#12652](https://github.com/apache/maven/pull/12652) | ✅ 
Ready | Optimize reactor sort, model pool, phase comparator |
   | apache/maven | [#12653](https://github.com/apache/maven/pull/12653) | ✅ 
Ready | Optimize model building pipeline |
   | apache/maven | [#12654](https://github.com/apache/maven/pull/12654) | ✅ 
Ready | AsyncDrainWriter — lock contention elimination |
   | apache/maven | [#12655](https://github.com/apache/maven/pull/12655) | ✅ 
Ready | Wire location tracking to XML parser |
   | apache/maven-resolver | 
[#2014](https://github.com/apache/maven-resolver/pull/2014) | ✅ Ready | 
TransitiveDependencyManager performance |
   | apache/maven-install-plugin | 
[#427](https://github.com/apache/maven-install-plugin/pull/427) | ✅ Ready | 
Cache projectsUsingPlugin — O(N²) → O(N) |
   | apache/maven-deploy-plugin | 
[#684](https://github.com/apache/maven-deploy-plugin/pull/684) | ✅ Ready | 
Cache projectsWithDeployExecution — O(N²) → O(N) |
   
   ## Reproducing
   
   ```bash
   # 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](https://gist.github.com/gnodet/5d324a772d2be64bdb8c0b73a35e4354)


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to