Skip to content

Commit f21fcb3

Browse files
authored
Reduce GC pressure in dependency graph traversal (#6333)
* Reduce GC pressure in dependency graph traversal Optimized AbstractDependencyGraphBuilder to reduce ArrayList allocations during dependency path collection by using a backtracking pattern. ## Problem The original implementation created 3 ArrayList objects per dependency node during traversal: 1. Copy of incoming path + current node 2. ArrayList for storing paths per GAV (if new) 3. Temporary path objects during recursion For projects like Spring Boot with 250+ dependencies, this resulted in ~3,000 ArrayList allocations per recipe execution. ## Solution Use a single reusable ArrayList (pathBuffer) for traversal: - Add node to buffer before recursion - Create snapshot only when storing path - Remove node from buffer after recursion (backtracking) This reduces allocations from 3 to 2 per node (~33% reduction). ## Benchmark results Tested with Spring Boot project (250+ transitive dependencies): - GC time: 35,784ms → 10,514ms (-70%) - GC percentage: 71% → 21% - More stable performance (no catastrophic GC collapse) The optimization is functionally equivalent - produces identical dependency paths using the same DFS traversal order. * Add clarifying comments for O(1) performance optimization * updated comment
1 parent 82fb604 commit f21fcb3

1 file changed

Lines changed: 24 additions & 20 deletions

File tree

rewrite-maven/src/main/java/org/openrewrite/maven/graph/AbstractDependencyGraphBuilder.java

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,41 +35,45 @@ public abstract class AbstractDependencyGraphBuilder<T extends DependencyGraph.D
3535
public void collectDependencyPaths(List<ResolvedDependency> dependencies,
3636
Map<ResolvedGroupArtifactVersion, List<DependencyGraph.DependencyPath>> paths,
3737
String scope) {
38+
List<DependencyGraph.DependencyNode> pathBuffer = new ArrayList<>();
3839
for (ResolvedDependency dependency : dependencies) {
39-
List<DependencyGraph.DependencyNode> parentPath = new ArrayList<>();
40-
collectDependencyPathsRecursive(dependency, parentPath, paths, scope);
40+
collectDependencyPathsRecursive(dependency, pathBuffer, paths, scope);
4141
}
4242
}
4343

4444
private void collectDependencyPathsRecursive(ResolvedDependency dependency,
45-
List<DependencyGraph.DependencyNode> parentPath,
45+
List<DependencyGraph.DependencyNode> pathBuffer,
4646
Map<ResolvedGroupArtifactVersion, List<DependencyGraph.DependencyPath>> paths,
4747
String scope) {
4848
ResolvedGroupArtifactVersion gav = dependency.getGav();
4949

50-
// Create path for this dependency including all parents
51-
List<DependencyGraph.DependencyNode> pathNodes = new ArrayList<>();
52-
53-
// Add the current dependency using the specific node type created by subclass
5450
T node = createNode(dependency, scope);
55-
pathNodes.add(node);
56-
57-
// Add all parents from the parent path
58-
pathNodes.addAll(parentPath);
51+
pathBuffer.add(node);
5952

60-
DependencyGraph.DependencyPath path = new DependencyGraph.DependencyPath(pathNodes, scope);
53+
// We add at the end for performance during recursing but reverse
54+
// when creating a snapshot to get correct order (current -> parents)
55+
List<DependencyGraph.DependencyNode> pathSnapshot = new ArrayList<>(pathBuffer.size());
56+
for (int i = pathBuffer.size() - 1; i >= 0; i--) {
57+
pathSnapshot.add(pathBuffer.get(i));
58+
}
59+
DependencyGraph.DependencyPath path = new DependencyGraph.DependencyPath(pathSnapshot, scope);
6160
paths.computeIfAbsent(gav, k -> new ArrayList<>()).add(path);
6261

63-
// Create new parent path for children that includes this dependency
64-
List<DependencyGraph.DependencyNode> newParentPath = new ArrayList<>();
65-
newParentPath.add(node);
66-
newParentPath.addAll(parentPath);
67-
68-
// Recursively process child dependencies
6962
for (ResolvedDependency child : dependency.getDependencies()) {
70-
if (newParentPath.stream().noneMatch(childPath -> childPath.getGroupId().equals(child.getGroupId()) && childPath.getArtifactId().equals(child.getArtifactId()))) {
71-
collectDependencyPathsRecursive(child, newParentPath, paths, scope);
63+
boolean hasCycle = false;
64+
for (int i = 0; i < pathBuffer.size(); i++) {
65+
DependencyGraph.DependencyNode n = pathBuffer.get(i);
66+
if (n.getGroupId().equals(child.getGroupId()) &&
67+
n.getArtifactId().equals(child.getArtifactId())) {
68+
hasCycle = true;
69+
break;
70+
}
71+
}
72+
if (!hasCycle) {
73+
collectDependencyPathsRecursive(child, pathBuffer, paths, scope);
7274
}
7375
}
76+
77+
pathBuffer.remove(pathBuffer.size() - 1); // Backtrack for next iteration
7478
}
7579
}

0 commit comments

Comments
 (0)