From 5cdab6adb275c8cd95c110a5635d716363090f4f Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Mon, 29 Jun 2026 22:25:10 +0200 Subject: [PATCH] Put PCR aside for now It's proven a bit problematic, and memory hungry, while the promise of O(N) over O(N^2) is cool. --- .../graph/transformer/ConflictResolver.java | 91 ++----------------- 1 file changed, 10 insertions(+), 81 deletions(-) diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java index fc8bf87ab3..9fb6eb31f9 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java @@ -18,7 +18,6 @@ */ package org.eclipse.aether.util.graph.transformer; -import java.util.ArrayDeque; import java.util.Arrays; import java.util.Collection; @@ -43,31 +42,19 @@ *

* Available Implementations: *

*

* Implementation Selection Guide: *

*

* Usage Example: *

{@code
- * // Recommended: High-performance path-based resolver
- * DependencyGraphTransformer transformer = new ChainedDependencyGraphTransformer(
- *     new PathConflictResolver(
- *         new NearestVersionSelector(),
- *         new JavaScopeSelector(),
- *         new SimpleOptionalitySelector(),
- *         new JavaScopeDeriver()),
- *     // other transformers...
- * );
- *
- * // Legacy: Classic resolver for backward compatibility
+ * // Classic resolver
  * DependencyGraphTransformer legacyTransformer = new ChainedDependencyGraphTransformer(
  *     new ClassicConflictResolver(
  *         new NearestVersionSelector(),
@@ -112,8 +99,8 @@
  * existing information about conflict ids. In absence of this information, it will automatically invoke the
  * {@link ConflictIdSorter} to calculate it.
  *
- * @see PathConflictResolver
  * @see ClassicConflictResolver
+ * @see PathConflictResolver
  */
 public class ConflictResolver implements DependencyGraphTransformer {
 
@@ -132,9 +119,8 @@ public class ConflictResolver implements DependencyGraphTransformer {
     /**
      * The name of the conflict resolver implementation to use: "auto" (default), "path", or "classic" (same as Maven 3).
      * 

- * When set to "auto", the resolver estimates whether the Path tree would fit in available heap memory. - * If it would consume more than 25% of available heap, the classic (in-place) resolver is used instead - * to avoid OutOfMemoryErrors on very large dependency graphs. + * When set to "auto", the resolver will currently just use "classic". The idea here, is that this value will + * always select the best (most robust, most performant) one, which currently is "classic". * * @since 2.0.11 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} @@ -264,18 +250,15 @@ public ConflictResolver( } @Override - @SuppressWarnings("unchecked") public DependencyNode transformGraph(DependencyNode node, DependencyGraphTransformationContext context) throws RepositoryException { String cf = ConfigUtils.getString( context.getSession(), DEFAULT_CONFLICT_RESOLVER_IMPL, CONFIG_PROP_CONFLICT_RESOLVER_IMPL); ConflictResolver delegate; - if (AUTO_CONFLICT_RESOLVER.equals(cf)) { - delegate = selectConflictResolver(node, context); + if (AUTO_CONFLICT_RESOLVER.equals(cf) || CLASSIC_CONFLICT_RESOLVER.equals(cf)) { + delegate = new ClassicConflictResolver(versionSelector, scopeSelector, optionalitySelector, scopeDeriver); } else if (PATH_CONFLICT_RESOLVER.equals(cf)) { delegate = new PathConflictResolver(versionSelector, scopeSelector, optionalitySelector, scopeDeriver); - } else if (CLASSIC_CONFLICT_RESOLVER.equals(cf)) { - delegate = new ClassicConflictResolver(versionSelector, scopeSelector, optionalitySelector, scopeDeriver); } else { throw new IllegalArgumentException("Unknown conflict resolver: " + cf + "; known are " + Arrays.asList(AUTO_CONFLICT_RESOLVER, PATH_CONFLICT_RESOLVER, CLASSIC_CONFLICT_RESOLVER)); @@ -283,60 +266,6 @@ public DependencyNode transformGraph(DependencyNode node, DependencyGraphTransfo return delegate.transformGraph(node, context); } - /** - * Selects the most appropriate conflict resolver based on graph size and available memory. - *

- * PathConflictResolver builds a parallel tree of Path objects that costs ~200 bytes per node. - * For very large dependency graphs (millions of nodes), this can exhaust the heap. - * In such cases, ClassicConflictResolver is used instead — it works in-place with no parallel - * structure, trading O(N²) worst-case time for O(1) extra space. - */ - private ConflictResolver selectConflictResolver(DependencyNode node, DependencyGraphTransformationContext context) - throws RepositoryException { - // Ensure conflict IDs are computed — both implementations need this anyway - if (context.get(TransformationContextKeys.SORTED_CONFLICT_IDS) == null) { - new ConflictIdSorter().transformGraph(node, context); - } - - Runtime rt = Runtime.getRuntime(); - long available = rt.maxMemory() - (rt.totalMemory() - rt.freeMemory()); - - // Estimate the maximum number of Path tree nodes that would fit in 25% of available heap. - // Each Path object costs ~200 bytes (object header + fields + children list entry). - int maxPathNodes = (int) Math.min(available / (4L * 200), Integer.MAX_VALUE); - - // Walk the dependency tree to count total nodes (including diamond-expanded duplicates). - // The Path tree mirrors this structure, so the count directly reflects Path tree size. - // Use early-exit: stop counting once we exceed the threshold — no need to measure the - // full tree if we already know it's too large. - if (treeExceedsThreshold(node, maxPathNodes)) { - return new ClassicConflictResolver(versionSelector, scopeSelector, optionalitySelector, scopeDeriver); - } else { - return new PathConflictResolver(versionSelector, scopeSelector, optionalitySelector, scopeDeriver); - } - } - - /** - * Checks whether the total number of nodes in the dependency tree (including diamond-expanded - * duplicates) exceeds the given threshold. Uses an iterative walk with early exit to avoid - * measuring the full tree when it's clearly too large. - */ - private boolean treeExceedsThreshold(DependencyNode root, int threshold) { - int count = 0; - ArrayDeque stack = new ArrayDeque<>(); - stack.push(root); - while (!stack.isEmpty()) { - DependencyNode n = stack.pop(); - if (++count > threshold) { - return true; - } - for (DependencyNode child : n.getChildren()) { - stack.push(child); - } - } - return false; - } - /** * A context used to hold information that is relevant for deriving the scope of a child dependency. *