Skip to content

Commit cf2eaef

Browse files
authored
Put PCR aside for now (#1942)
It's proven a bit problematic, and memory hungry, while the promise of O(N) over O(N^2) is cool.
1 parent d331083 commit cf2eaef

1 file changed

Lines changed: 10 additions & 81 deletions

File tree

maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java

Lines changed: 10 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
*/
1919
package org.eclipse.aether.util.graph.transformer;
2020

21-
import java.util.ArrayDeque;
2221
import java.util.Arrays;
2322
import java.util.Collection;
2423

@@ -43,31 +42,19 @@
4342
* <p>
4443
* <strong>Available Implementations:</strong>
4544
* <ul>
46-
* <li><strong>{@link PathConflictResolver}</strong> - Recommended high-performance implementation with O(N) complexity</li>
47-
* <li><strong>{@link ClassicConflictResolver}</strong> - Legacy implementation for backward compatibility (O(N²) worst-case)</li>
45+
* <li><strong>{@link ClassicConflictResolver}</strong> - Original implementation (O(N²) worst-case)</li>
46+
* <li><strong>{@link PathConflictResolver}</strong> - Not yet recommended for production; high-performance implementation with O(N) complexity</li>
4847
* </ul>
4948
* <p>
5049
* <strong>Implementation Selection Guide:</strong>
5150
* <ul>
52-
* <li><strong>New Projects:</strong> Use {@link PathConflictResolver} for optimal performance</li>
53-
* <li><strong>Large Multi-Module Projects:</strong> Use {@link PathConflictResolver} to avoid performance bottlenecks</li>
54-
* <li><strong>Maven 4+ Environments:</strong> Use {@link PathConflictResolver} for best build performance</li>
55-
* <li><strong>Legacy Compatibility:</strong> Use {@link ClassicConflictResolver} only when exact Maven 3.x behavior is required</li>
51+
* <li><strong>All projects:</strong> Use {@link ClassicConflictResolver} for optimal correctness and Maven 3.x behavior</li>
52+
* <li><strong>Experimenters:</strong> Use {@link PathConflictResolver} but no guarantees it will work</li>
5653
* </ul>
5754
* <p>
5855
* <strong>Usage Example:</strong>
5956
* <pre>{@code
60-
* // Recommended: High-performance path-based resolver
61-
* DependencyGraphTransformer transformer = new ChainedDependencyGraphTransformer(
62-
* new PathConflictResolver(
63-
* new NearestVersionSelector(),
64-
* new JavaScopeSelector(),
65-
* new SimpleOptionalitySelector(),
66-
* new JavaScopeDeriver()),
67-
* // other transformers...
68-
* );
69-
*
70-
* // Legacy: Classic resolver for backward compatibility
57+
* // Classic resolver
7158
* DependencyGraphTransformer legacyTransformer = new ChainedDependencyGraphTransformer(
7259
* new ClassicConflictResolver(
7360
* new NearestVersionSelector(),
@@ -112,8 +99,8 @@
11299
* existing information about conflict ids. In absence of this information, it will automatically invoke the
113100
* {@link ConflictIdSorter} to calculate it.
114101
*
115-
* @see PathConflictResolver
116102
* @see ClassicConflictResolver
103+
* @see PathConflictResolver
117104
*/
118105
public class ConflictResolver implements DependencyGraphTransformer {
119106

@@ -132,9 +119,8 @@ public class ConflictResolver implements DependencyGraphTransformer {
132119
/**
133120
* The name of the conflict resolver implementation to use: "auto" (default), "path", or "classic" (same as Maven 3).
134121
* <p>
135-
* When set to "auto", the resolver estimates whether the Path tree would fit in available heap memory.
136-
* If it would consume more than 25% of available heap, the classic (in-place) resolver is used instead
137-
* to avoid OutOfMemoryErrors on very large dependency graphs.
122+
* When set to "auto", the resolver will currently just use "classic". The idea here, is that this value will
123+
* always select the best (most robust, most performant) one, which currently is "classic".
138124
*
139125
* @since 2.0.11
140126
* @configurationSource {@link RepositorySystemSession#getConfigProperties()}
@@ -264,79 +250,22 @@ public ConflictResolver(
264250
}
265251

266252
@Override
267-
@SuppressWarnings("unchecked")
268253
public DependencyNode transformGraph(DependencyNode node, DependencyGraphTransformationContext context)
269254
throws RepositoryException {
270255
String cf = ConfigUtils.getString(
271256
context.getSession(), DEFAULT_CONFLICT_RESOLVER_IMPL, CONFIG_PROP_CONFLICT_RESOLVER_IMPL);
272257
ConflictResolver delegate;
273-
if (AUTO_CONFLICT_RESOLVER.equals(cf)) {
274-
delegate = selectConflictResolver(node, context);
258+
if (AUTO_CONFLICT_RESOLVER.equals(cf) || CLASSIC_CONFLICT_RESOLVER.equals(cf)) {
259+
delegate = new ClassicConflictResolver(versionSelector, scopeSelector, optionalitySelector, scopeDeriver);
275260
} else if (PATH_CONFLICT_RESOLVER.equals(cf)) {
276261
delegate = new PathConflictResolver(versionSelector, scopeSelector, optionalitySelector, scopeDeriver);
277-
} else if (CLASSIC_CONFLICT_RESOLVER.equals(cf)) {
278-
delegate = new ClassicConflictResolver(versionSelector, scopeSelector, optionalitySelector, scopeDeriver);
279262
} else {
280263
throw new IllegalArgumentException("Unknown conflict resolver: " + cf + "; known are "
281264
+ Arrays.asList(AUTO_CONFLICT_RESOLVER, PATH_CONFLICT_RESOLVER, CLASSIC_CONFLICT_RESOLVER));
282265
}
283266
return delegate.transformGraph(node, context);
284267
}
285268

286-
/**
287-
* Selects the most appropriate conflict resolver based on graph size and available memory.
288-
* <p>
289-
* PathConflictResolver builds a parallel tree of Path objects that costs ~200 bytes per node.
290-
* For very large dependency graphs (millions of nodes), this can exhaust the heap.
291-
* In such cases, ClassicConflictResolver is used instead — it works in-place with no parallel
292-
* structure, trading O(N²) worst-case time for O(1) extra space.
293-
*/
294-
private ConflictResolver selectConflictResolver(DependencyNode node, DependencyGraphTransformationContext context)
295-
throws RepositoryException {
296-
// Ensure conflict IDs are computed — both implementations need this anyway
297-
if (context.get(TransformationContextKeys.SORTED_CONFLICT_IDS) == null) {
298-
new ConflictIdSorter().transformGraph(node, context);
299-
}
300-
301-
Runtime rt = Runtime.getRuntime();
302-
long available = rt.maxMemory() - (rt.totalMemory() - rt.freeMemory());
303-
304-
// Estimate the maximum number of Path tree nodes that would fit in 25% of available heap.
305-
// Each Path object costs ~200 bytes (object header + fields + children list entry).
306-
int maxPathNodes = (int) Math.min(available / (4L * 200), Integer.MAX_VALUE);
307-
308-
// Walk the dependency tree to count total nodes (including diamond-expanded duplicates).
309-
// The Path tree mirrors this structure, so the count directly reflects Path tree size.
310-
// Use early-exit: stop counting once we exceed the threshold — no need to measure the
311-
// full tree if we already know it's too large.
312-
if (treeExceedsThreshold(node, maxPathNodes)) {
313-
return new ClassicConflictResolver(versionSelector, scopeSelector, optionalitySelector, scopeDeriver);
314-
} else {
315-
return new PathConflictResolver(versionSelector, scopeSelector, optionalitySelector, scopeDeriver);
316-
}
317-
}
318-
319-
/**
320-
* Checks whether the total number of nodes in the dependency tree (including diamond-expanded
321-
* duplicates) exceeds the given threshold. Uses an iterative walk with early exit to avoid
322-
* measuring the full tree when it's clearly too large.
323-
*/
324-
private boolean treeExceedsThreshold(DependencyNode root, int threshold) {
325-
int count = 0;
326-
ArrayDeque<DependencyNode> stack = new ArrayDeque<>();
327-
stack.push(root);
328-
while (!stack.isEmpty()) {
329-
DependencyNode n = stack.pop();
330-
if (++count > threshold) {
331-
return true;
332-
}
333-
for (DependencyNode child : n.getChildren()) {
334-
stack.push(child);
335-
}
336-
}
337-
return false;
338-
}
339-
340269
/**
341270
* A context used to hold information that is relevant for deriving the scope of a child dependency.
342271
*

0 commit comments

Comments
 (0)