Store DependencyInsight's dependency graph as a DAG to prevent OOM on ubiquitous transitive deps#8193
Merged
Merged
Conversation
fcf75dd to
6300851
Compare
6300851 to
addaba7
Compare
jkschneider
commented
Jul 8, 2026
… ubiquitous transitive deps `DependencyInsight` could OOM on a single repository when the target is a ubiquitous, deeply-transitive dependency such as `com.fasterxml.jackson*`. `DependencyTreeWalker.walkRecursive` enumerated every root->match path (cycles were pruned only within the current path), and the per-match `DependencyGraph` stored each path as its own tree branch. On a diamond-heavy graph both grow combinatorially; one repo materialized ~37M `DependencyGraph$DependencyNode` and blew a 16 GB heap. Store the graph as a coordinate-interned DAG instead of a per-path tree: a dependency reachable through many paths (a diamond) is stored once. `DependencyGraph.build` constructs it in O(nodes+edges), and `getSize()` reports the true distinct-path count via dynamic programming over the DAG rather than by enumerating exponentially many paths. `DependencyTreeWalker` also gets a global visited-set so the `Matches` computation behind the `SearchResult` markers is O(nodes) as well. Observable behavior is unchanged: the markers, the `ExplainDependenciesInUse` dependency tree (`print()` already collapses shared nodes with `(*)`), and the `DependenciesInUse` count are all identical to before -- the existing tests pass untouched. Only the internal representation and its complexity change. Fixes #8181
addaba7 to
99513ad
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What's the problem?
DependencyInsight(Gradle/Maven) can OOM on a single repository when the target is a ubiquitous, deeply-transitive dependency such ascom.fasterxml.jackson*.DependencyTreeWalker.walkRecursiveenumerates every root→match path — it prunes cycles within the current path but has no DAG-level dedup — so a diamond-heavy graph expands combinatorially, and the per-match callback appends each path into a non-deduplicatedDependencyGraph. One repo materialized ~37MDependencyGraph$DependencyNodeand blew a ~16 GB heap.The fix
Give the walk a single visited-set spanning the whole traversal (not just the current path). It both prevents cycles and dedups subtrees reachable through multiple paths, so a match reachable via k diamonds fires the callback once with a representative path instead of k times. Traversal goes from
O(paths)(exponential) toO(nodes)(linear).This also makes good on the walker's existing Javadoc — "Uses caching to avoid re-traversing duplicate dependencies" — which the implementation never actually honored.
The visited-set is per direct-dependency root (each
walk(root, …)gets its own), so attribution is preserved: theSearchResultmarkers on the POM/build.gradle are unchanged (DFS reachability is identical). Only theDependenciesInUse/ExplainDependenciesInUsedata tables change —countbecomes the number of direct dependencies a match is pulled in through, and the dependency tree shows one representative path per root instead of the combinatorial expansion.Benchmark
Reproducing the exact OOM structure — one matched dependency reachable from the root via
2^depthdiamond paths — and running the pre-fix algorithm against the post-fix one on the identical graph:Total traversal work —
O(2ᵈ)→O(d):DependencyGraphbuilt the wayDependencyInsightdoes (depth 18 = 262,144 paths to one match):Extrapolated to the issue's ~depth-25 scale (~33M paths): OLD ≈ 28 GB, NEW stays flat — matching the reported 37M nodes / 16 GB heap in order of magnitude. For non-pathological graphs the walk visits each node once, so there's no slowdown.
Tests
New
DependencyTreeWalkerTest(network-free): a 20-deep diamond chain (2²⁰ ≈ 1M paths, 61 nodes) asserts the callback fires 61 times, with a fail-fast cap so a regression trips instead of OOMing; plus cycle-termination and path-order coverage.Gradle
DependencyInsightTestexpectations updated to the deduped values (spring-boot10→3,jackson-core11→2, one representative-path tree). Markers unchanged.Fixes Dependency Insight OOMs on ubiquitous transitive deps (jackson): DependencyTreeWalker enumerates all root→match paths with no DAG dedup #8181