Fixes #6886. dependencyTree, dependencyBrowseTree, and inspect tree re-explore the same node once per incoming edge. In a DAG with N levels and M children per node the rendered output is O(M^N) -- the OP needed >16 GB heap, #7360 has a 6 GB heap dump, and Friendseeker's analysis on the issue showed the exponential re-traversal directly. Fix: track a visited set across the renderer's recursion. The first time a node is encountered it is rendered in full; on subsequent visits the entry collapses to a one-line +- <id> (*) (ASCII) or a <id> (*) leaf (JSON), matching Maven's dependency:tree (*) convention. Cycle detection (separate parents set, (cycle) marker) is unchanged. Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2.2 KiB
Dependency tree: duplicate-subtree collapse
dependencyTree, dependencyBrowseTree, and inspect tree now collapse
duplicate subtrees in a DAG to a single line marked (*), matching the
convention used by Maven's dependency:tree. The first occurrence is
rendered in full; subsequent occurrences appear as +- <id> (*).
This fixes #6886: rendering a deep diamond DAG no longer
produces O(M^N) output (and the OOMs that came with it).
Output change
Before:
o:root_2.13:0.1
+-o:subA_2.13:0.1 [S]
| +-o:common_2.13:0.1 [S]
| | +-org.scala-lang:scala-library:2.13.16 [S]
| +-org.scala-lang:scala-library:2.13.16 [S]
+-o:subB_2.13:0.1 [S]
| +-o:common_2.13:0.1 [S] # full subtree again
| | +-org.scala-lang:scala-library:2.13.16 [S]
| +-org.scala-lang:scala-library:2.13.16 [S]
+-org.scala-lang:scala-library:2.13.16 [S]
After:
o:root_2.13:0.1
+-o:subA_2.13:0.1 [S]
| +-o:common_2.13:0.1 [S]
| | +-org.scala-lang:scala-library:2.13.16 [S]
| +-org.scala-lang:scala-library:2.13.16 [S]
+-o:subB_2.13:0.1 [S]
| +-o:common_2.13:0.1 [S] (*) # collapsed
| +-org.scala-lang:scala-library:2.13.16 [S] (*)
+-org.scala-lang:scala-library:2.13.16 [S] (*)
Affected surfaces
dependencyTree: ASCII output (alsodependencyTreeList,dependencyTreeStats).dependencyBrowseTree: JSON / HTML view.inspect tree: the setting-graph renderer is the same code path, so the(*)marker shows up there too. Most users don't think ofinspect treeas "the dependency tree" -- this note is the heads-up.
Contract for tooling consumers
A line whose entry ends with (*) is a reference to the canonical
(first-rendered) occurrence of that node within the same render.
Tools parsing dependencyTree / dependencyBrowseTree output should
treat <id> (*) as a back-pointer rather than a distinct dependency.
Scope
Dedup is currently within a single root's subtree. Cross-root dedup
(when a ModuleGraph has multiple roots that share a transitive
closure) is tracked separately as #9227.