B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections. Squash of 94 local commits onto remote master.
3.9 KiB
0001 — Tarjan SCC: O(V²) stack membership via stack.contains(n)
Status: open
Severity: performance — high
Component: jdk.compiler / com.sun.tools.javac.util.GraphUtils
Affects: comp/Infer.java (type inference), comp/DeferredAttr.java (stuck expression resolution)
Root Cause
GraphUtils.java:186 — inner class Tarjan.findSCC():
// DEFECTIVE — O(n) per edge traversal
} else if (stack.contains(n)) {
v.lowlink = Math.min(v.lowlink, n.index);
}
stack is a ListBuffer<N>. ListBuffer.contains() performs a full linear scan.
TarjanNode (line 132–148) already carries an active field:
public abstract static class TarjanNode<D, N extends TarjanNode<D, N>> {
boolean active; // set true in visitNode(), false in addSCC()
...
}
active is set true when a node is pushed (line 202) and false when popped (line 210). It exists precisely to be the O(1) on-stack check. It is never read.
Fix — one line:
// FIXED — O(1)
} else if (n.active) {
v.lowlink = Math.min(v.lowlink, n.index);
}
Complexity Analysis
| Metric | Defective | Fixed |
|---|---|---|
| Stack membership check | O(|stack|) per edge | O(1) per edge |
| Overall Tarjan | O(V²) worst case | O(V+E) |
| Growth pattern | Quadratic | Linear |
Worst case is a path graph: V₀→V₁→→…→Vₙ→V₀ (one back edge). Each forward step adds one node to the stack. When the back edge is reached, stack.contains() scans all V nodes. Total comparisons: V + (V-1) + … + 1 = V(V+1)/2.
Call Sites
comp/Infer.java:1908 — type inference graph solver
for (List<? extends Node> conSubGraph : GraphUtils.tarjan(nodes)) {
Called once per GraphSolver.solve() invocation — which fires for every method call site with unresolved inference variables. Complex generics (streams, collectors, builders) accumulate many inference variables per call site.
comp/DeferredAttr.java:675 — stuck expression resolver
List<? extends StuckNode> csn = GraphUtils.tarjan(stuckGraph).get(0);
Called in pickDeferredNode() during each stuck-expression resolution attempt. Firing frequency scales with lambda/method-reference density.
Diagrams
Source for all diagrams is in docs/tickets/diagrams/. Render with:
dot -Tsvg diagrams/0001-inference-graph.dot -o diagrams/0001-inference-graph.svg
dot -Tsvg diagrams/0001-tarjan-defect.dot -o diagrams/0001-tarjan-defect.svg
dot -Tsvg diagrams/0001-stack-scan.dot -o diagrams/0001-stack-scan.svg
Diagram 1 — Inference Variable Dependency Graph
Shows the graph structure that Tarjan processes during type inference. Cycles indicate mutually-dependent inference variables that must be merged into super-nodes.
See: diagrams/0001-inference-graph.dot
Diagram 2 — Tarjan Execution With Defect Highlighted
Shows the DFS traversal, stack growth, and the O(n) scan that fires when a back edge is encountered.
See: diagrams/0001-tarjan-defect.dot
Diagram 3 — Linear Scan vs O(1) Check
Side-by-side comparison of the defective and fixed stack membership check, showing the work done per edge traversal.
See: diagrams/0001-stack-scan.dot
Tests
| Test | File | Purpose |
|---|---|---|
| Unit | tests/unit/TarjanComplexityTest.java |
Proves O(V²) vs O(V+E) operation counts |
| Integration | tests/integration/InferenceGraphScalingTest.java |
Proves scaling impact on type inference graph |
| Functional | tests/functional/CompilerBenchmarkTest.java |
Proves end-to-end compilation latency impact |
Run all: make -C tests
Fix Location
src/jdk.compiler/share/classes/com/sun/tools/javac/util/GraphUtils.java
line 186: stack.contains(n) → n.active
No other changes required. The active field is already correctly maintained by visitNode() and addSCC().