Optimize reactor sort, model pool, and phase comparator performance - #12652
Optimize reactor sort, model pool, and phase comparator performance#12652gnodet wants to merge 1 commit into
Conversation
…mance 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 <noreply@anthropic.com>
gnodet
left a comment
There was a problem hiding this comment.
Clean, well-motivated performance optimization replacing O(n) linear scans with O(1) HashMap lookups in three hot paths identified by JFR profiling. All changes preserve semantic equivalence and are low risk.
Notable details:
- The inlined
dependencyHashCodecomputation is mathematically identical toObjects.hash(...), avoiding the varargs allocation. - The hash-code fast-rejection in
PoolKey.equals()is a textbook optimization. MavenProjecthas properequals()/hashCode()based on GAV, confirming it is safe as a HashMap key.
No new tests needed since the changes are purely algorithmic (same inputs, same outputs, different time complexity).
Note: Cannot submit as APPROVE because the PR author matches the review account.
This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of gnodet
gnodet
left a comment
There was a problem hiding this comment.
Clean, well-motivated performance optimizations backed by JFR profiling data. All three changes preserve semantic equivalence — the complexity reductions (O(n² log n) → O(n log n) for reactor sort, O(k) → O(1) for pooled types, varargs allocation elimination for hash codes) are real and significant for large reactor builds.
One minor inconsistency:
In PhaseComparator, the Map.get() result is defensively null-checked (i1 == null / i2 == null). In DefaultGraphBuilder.buildProjectIndexMap, Comparator.comparing(buildProjectIndexMap(...)::get) would throw NullPointerException if a project were absent, whereas the original comparing(sortedProjects::indexOf) returned -1. In practice this cannot arise since all elements come from graph-derived data, but getOrDefault(project, -1) would align the two patterns in the same PR.
Own PR — LGTM.
This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of Guillaume Nodet
Summary
JFR profiling of a 4,383-module reactor build revealed three hotspots that together consume ~35% of CPU time. All three share the same root cause: O(n) linear scans used inside tight loops or comparators.
Fixes
DefaultGraphBuilder(23.5% CPU): Three call sites useresult.sort(comparing(sortedProjects::indexOf))whereArrayList.indexOf()is O(n), causing O(n² log n) totalMavenProject.equals()calls (~230M for 4,383 modules). Replaced with aHashMap<MavenProject, Integer>index built in O(n), reducing each sort comparison to O(1).DefaultModelObjectPool(~8% CPU, ~2.4 GB allocation pressure):getPooledTypes()re-parses a comma-separated property string into a newStream→Seton everyprocess()call (hundreds of thousands of times). Cached at construction time.PoolKey.dependencyHashCode()usesObjects.hash()with 12 arguments, allocating anew Object[12]on every call. Inlined the hash computation.PoolKey.equals()to skip expensive deep equality when hashes differ.PhaseComparator(~2% CPU):List.indexOf()incompare()is O(n) per call. Pre-built aHashMap<String, Integer>in the constructor for O(1) phase index lookups.JFR Profile (before these fixes)
MavenProject.equalsArrayList.indexOfinDefaultGraphBuildersortDefaultModelObjectPool$PoolKey.*Objects.hashvarargsDefaultDependencyManagementImporter.importManagementPhaseComparator.compareList.indexOfper comparisonComplexity reduction
DefaultGraphBuildersortDefaultModelObjectPool.process()PoolKey.dependencyHashCodePhaseComparator.compareTest plan
DefaultModelObjectPoolTestpassesmaven-impltest suite passesDefaultGraphBuilderTest(blocked by pre-existingProjectBuildLogAppendercompile error on master — unrelated to this PR)🤖 Generated with Claude Code