Copilot commented on code in PR #1938:
URL: https://github.com/apache/maven-resolver/pull/1938#discussion_r3487784159


##########
maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/PathConflictResolver.java:
##########
@@ -170,28 +167,33 @@ public DependencyNode transformGraph(DependencyNode node, 
DependencyGraphTransfo
                 scopeDeriver.getInstance(node, context),
                 optionalitySelector.getInstance(node, context),
                 conflictIds,
+                sortedConflictIds.size(),
                 node);
 
         // loop over topographically sorted conflictIds
         for (String conflictId : sortedConflictIds) {
-            // paths in given conflict group to consider
-            Set<Path> paths = state.partitions.get(conflictId);
-            if (paths.isEmpty()) {
+            // paths in given conflict group to consider; filter out those 
moved out of scope
+            List<Path> allPaths = state.partitions.get(conflictId);
+            List<Path> activePaths = new ArrayList<>(allPaths.size());
+            List<ConflictItem> items = new ArrayList<>(allPaths.size());
+            for (Path p : allPaths) {
+                if (!p.outOfScope) {
+                    activePaths.add(p);
+                    items.add(new ConflictItem(p));
+                }
+            }
+            if (activePaths.isEmpty()) {

Review Comment:
   `partitions` retains out-of-scope `Path` entries indefinitely. Even though 
`activePaths` filters them out, the backing `allPaths` list still strongly 
references losers (and entire detached subtrees in `Verbosity.NONE`/redundant 
`STANDARD`), which can prevent GC and raise peak memory — the opposite of the 
intended OOM mitigation. Consider compacting `allPaths` in-place while 
filtering (or otherwise dropping references to out-of-scope entries) so 
resolved/losing branches don’t stay retained via `partitions`.



##########
maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java:
##########
@@ -258,22 +264,58 @@ 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 (PATH_CONFLICT_RESOLVER.equals(cf)) {
+        if (AUTO_CONFLICT_RESOLVER.equals(cf)) {
+            delegate = selectConflictResolver(node, context);
+        } 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(PATH_CONFLICT_RESOLVER, 
CLASSIC_CONFLICT_RESOLVER));
+                    + Arrays.asList(AUTO_CONFLICT_RESOLVER, 
PATH_CONFLICT_RESOLVER, CLASSIC_CONFLICT_RESOLVER));
         }
         return delegate.transformGraph(node, context);
     }
 
+    /**
+     * Selects the most appropriate conflict resolver based on graph size and 
available memory.
+     * <p>
+     * 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.

Review Comment:
   The new default "auto" resolver selection path isn’t covered by unit tests 
(existing tests exercise `PathConflictResolver` and `ClassicConflictResolver` 
directly). Adding tests that validate both branches of the heuristic would help 
prevent regressions (e.g., by refactoring heap-availability and/or estimate 
calculation behind overridable helpers or injectable suppliers so tests can 
deterministically force either selection).



##########
maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/PathConflictResolver.java:
##########
@@ -610,43 +643,53 @@ private boolean isDirectDependencyOnPathToRoot(Artifact 
artifact) {
          * verbosity mode we remove "redundant" nodes (of a range) leaving 
only "winner equal" loser, that have same GACEV as winner.
          */
         private int relatedSiblingsCount(Artifact artifact, Path parent) {
-            String ga = artifact.getGroupId() + ":" + artifact.getArtifactId();
-            return Math.toIntExact(parent.children.stream()
-                    .map(n -> n.dn.getArtifact())
-                    .filter(a -> ga.equals(a.getGroupId() + ":" + 
a.getArtifactId()))
-                    .count());
+            if (parent.children == null) {
+                return 0;
+            }
+            String groupId = artifact.getGroupId();
+            String artifactId = artifact.getArtifactId();
+            int count = 0;
+            for (Path n : parent.children) {
+                Artifact a = n.dn.getArtifact();
+                if (Objects.equals(groupId, a.getGroupId()) && 
Objects.equals(artifactId, a.getArtifactId())) {
+                    count++;
+                }
+            }
+            return count;
         }
 
         /**
-         * Removes this and all child {@link Path} nodes from winner selection 
scope; essentially marks whole subtree
+         * Marks this and all child {@link Path} nodes as out of scope; 
essentially marks whole subtree
          * from "this and below" as loser, to not be considered in subsequent 
winner selections.
-         * Uses {@link LinkedHashSet} for O(1) removal instead of the previous 
O(N) ArrayList.remove.
+         * Uses a boolean flag instead of removing from partition collections, 
avoiding the per-entry
+         * overhead of LinkedHashSet (~48 bytes/entry). Out-of-scope paths are 
filtered at query time.
          */
         private void moveOutOfScope() {
-            this.state.partitions.get(this.conflictId).remove(this);
-            for (Path child : this.children) {
-                child.moveOutOfScope();
+            this.outOfScope = true;
+            if (this.children != null) {
+                for (Path child : this.children) {
+                    child.moveOutOfScope();

Review Comment:
   `moveOutOfScope()` is still recursive. Deep dependency chains can still 
trigger `StackOverflowError` when an early node becomes a loser (the subtree 
walk recurses once per level). Converting this to an explicit stack keeps the 
“no recursion” goal consistent with the new iterative `gatherCRNodes()` change.



-- 
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