undefect. CWE-407 — 92 sites, 42 ecosystems
B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections. Squash of 94 local commits onto remote master.
This commit is contained in:
parent
0a580b313d
commit
db29a08762
1311 changed files with 371202 additions and 1188 deletions
126
docs/tickets/0001-tarjan-ov2-stack-contains.md
Normal file
126
docs/tickets/0001-tarjan-ov2-stack-contains.md
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
# 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()`:
|
||||
|
||||
```java
|
||||
// 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:
|
||||
|
||||
```java
|
||||
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:**
|
||||
|
||||
```java
|
||||
// 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
|
||||
|
||||
```java
|
||||
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
|
||||
|
||||
```java
|
||||
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()`.
|
||||
112
docs/tickets/0002-inference-graph-findnode-linear-scan.md
Normal file
112
docs/tickets/0002-inference-graph-findnode-linear-scan.md
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# 0002 — InferenceGraph.findNode(): O(N) linear scan, called O(N²·S) times
|
||||
|
||||
**Status:** open
|
||||
**Severity:** performance — high
|
||||
**Component:** `jdk.compiler / com.sun.tools.javac.comp.Infer$GraphSolver$InferenceGraph`
|
||||
**Depends on:** 0001 (Tarjan fix removes some pressure, but this is independent)
|
||||
|
||||
---
|
||||
|
||||
## Root Cause
|
||||
|
||||
`Infer.java:1850` — `InferenceGraph.findNode()`:
|
||||
|
||||
```java
|
||||
public Node findNode(Type t) {
|
||||
for (Node n : nodes) { // O(N) linear scan over ArrayList
|
||||
if (n.data.contains(t)) {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
`nodes` is an `ArrayList<Node>`. Every lookup is a full scan. There is no index.
|
||||
|
||||
---
|
||||
|
||||
## Call Site: `DeferredAttr.buildStuckGraph()`
|
||||
|
||||
`DeferredAttr.java:690-695` — nested loop:
|
||||
|
||||
```java
|
||||
for (StuckNode sn1 : nodes) {
|
||||
for (StuckNode sn2 : nodes) {
|
||||
if (sn1 != sn2 && canInfluence(graph, sn2, sn1)) { // O(N²) calls
|
||||
sn1.deps.add(sn2);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`canInfluence()` (`DeferredAttr.java:700-715`):
|
||||
|
||||
```java
|
||||
boolean canInfluence(InferenceGraph graph, StuckNode sn1, StuckNode sn2) {
|
||||
for (Type inputVar : sn2.data.deferredStuckPolicy.stuckVars()) {
|
||||
InferenceGraph.Node inputNode = graph.findNode(inputVar); // O(N) scan
|
||||
if (inputNode != null) {
|
||||
Set<InferenceGraph.Node> inputClosure = inputNode.closure(); // O(V+E) DFS, NOT cached
|
||||
if (outputVars.stream()
|
||||
.map(graph::findNode) // O(N) scan per output var
|
||||
.anyMatch(inputClosure::contains)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complexity
|
||||
|
||||
| Layer | Defect | Cost |
|
||||
|-------|--------|------|
|
||||
| `buildStuckGraph()` outer loop | O(N²) calls to `canInfluence()` | inherent |
|
||||
| `canInfluence()` → `findNode()` | O(N) scan per call | should be O(1) |
|
||||
| `canInfluence()` → `closure()` | O(V+E) per call, **recomputed every time** | should be cached |
|
||||
| `canInfluence()` → `outputVars.map(findNode)` | O(S·N) per call | should be O(S) |
|
||||
|
||||
**Total: O(N² · S · N) = O(N³)** where N = inference variable count, S = stuck variables per node.
|
||||
|
||||
With a HashMap index: `findNode()` becomes O(1) → total drops to **O(N² · S · (V+E))**.
|
||||
With closure caching: drops further to **O(N² · S)**.
|
||||
|
||||
---
|
||||
|
||||
## Fix
|
||||
|
||||
**Fix 1 — index `nodes` by type:**
|
||||
```java
|
||||
// Replace ArrayList<Node> nodes with:
|
||||
Map<Type, Node> nodeIndex = new LinkedHashMap<>();
|
||||
|
||||
public Node findNode(Type t) {
|
||||
return nodeIndex.get(t); // O(1)
|
||||
}
|
||||
```
|
||||
|
||||
**Fix 2 — cache `closure()` per node:**
|
||||
```java
|
||||
private Set<Node> cachedClosure = null;
|
||||
|
||||
protected Set<Node> closure() {
|
||||
if (cachedClosure == null) {
|
||||
cachedClosure = new LinkedHashSet<>();
|
||||
closureInternal(cachedClosure);
|
||||
}
|
||||
return cachedClosure;
|
||||
}
|
||||
// invalidate cachedClosure in graphChanged() and mergeWith()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Relationship to 0001
|
||||
|
||||
Ticket 0001 fixes Tarjan from O(V²) to O(V+E). This ticket fixes the caller layer:
|
||||
`buildStuckGraph()` calls Tarjan AFTER `canInfluence()` builds the stuck graph.
|
||||
Both defects are independent and compound: fixing 0001 alone doesn't fix 0002.
|
||||
91
docs/tickets/0003-module-hasher-topo-deque-contains.md
Normal file
91
docs/tickets/0003-module-hasher-topo-deque-contains.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# 0003 — ModuleHashesBuilder$TopoSorter: O(N) Deque.contains() for cycle detection
|
||||
|
||||
**Status:** open
|
||||
**Severity:** performance — medium
|
||||
**Component:** `java.base / jdk.internal.module.ModuleHashesBuilder$TopoSorter`
|
||||
**Module:** `java.base` — ships in every JDK and JRE
|
||||
|
||||
---
|
||||
|
||||
## Root Cause
|
||||
|
||||
`ModuleHashesBuilder.java` (inner class `TopoSorter.visit()`), bytecode instruction 57:
|
||||
|
||||
```
|
||||
invokeinterface java/util/Deque.contains:(Ljava/lang/Object;)Z
|
||||
```
|
||||
|
||||
The `visit()` method uses an `ArrayDeque` as a DFS stack and calls `Deque.contains()` to detect back edges (cycles in the module dependency graph). `ArrayDeque.contains()` is a linear scan — O(N).
|
||||
|
||||
This is the same structural defect as ticket 0001 (`GraphUtils.Tarjan`) in a different module and a different layer of the stack.
|
||||
|
||||
Source pattern (reconstructed from bytecode):
|
||||
|
||||
```java
|
||||
private void visit(T node, Set<T> visited, Deque<T> stack) {
|
||||
if (visited.contains(node)) {
|
||||
if (stack.contains(node)) { // O(N) — THE DEFECT
|
||||
throw new IllegalArgumentException("Cycle detected: " + node + " -> " + children(node));
|
||||
}
|
||||
return;
|
||||
}
|
||||
visited.add(node);
|
||||
stack.push(node);
|
||||
children(node).forEach(child -> visit(child, visited, stack));
|
||||
stack.pop();
|
||||
result.addLast(node);
|
||||
}
|
||||
```
|
||||
|
||||
**Fix — one boolean field per node, or a `HashSet<T> onStack`:**
|
||||
|
||||
```java
|
||||
Set<T> onStack = new HashSet<>(); // O(1) contains
|
||||
|
||||
private void visit(T node, Set<T> visited, Deque<T> stack, Set<T> onStack) {
|
||||
if (visited.contains(node)) {
|
||||
if (onStack.contains(node)) { // O(1)
|
||||
throw new IllegalArgumentException("Cycle: " + node);
|
||||
}
|
||||
return;
|
||||
}
|
||||
visited.add(node);
|
||||
stack.push(node);
|
||||
onStack.add(node); // O(1)
|
||||
children(node).forEach(...);
|
||||
stack.pop();
|
||||
onStack.remove(node); // O(1)
|
||||
result.addLast(node);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Where This Fires
|
||||
|
||||
`ModuleHashesBuilder` is called by `jlink` during custom runtime image creation:
|
||||
|
||||
```
|
||||
jlink → ModuleHashesBuilder.computeHashes() → new TopoSorter(graph) → visit()
|
||||
```
|
||||
|
||||
The module dependency graph fed to this sorter is the full transitive closure of the modules included in the image. For a typical server JDK image with 20–50 modules, this is manageable. For large multi-module applications using `jlink` with 100+ modules, the O(N²) degradation is measurable.
|
||||
|
||||
---
|
||||
|
||||
## Significance
|
||||
|
||||
This defect lives in `java.base` — the lowest-level module present in every JDK/JRE. The same pattern (`Deque.contains()` for on-stack check) replicated here independently of `GraphUtils.Tarjan` shows this is a **systemic pattern** in the JDK codebase, not an isolated incident.
|
||||
|
||||
---
|
||||
|
||||
## Complexity
|
||||
|
||||
| V (modules) | Defective contains() calls | Fixed contains() calls |
|
||||
|---|---|---|
|
||||
| 20 | ≤ 190 | ≤ 20 |
|
||||
| 50 | ≤ 1,225 | ≤ 50 |
|
||||
| 100 | ≤ 4,950 | ≤ 100 |
|
||||
|
||||
For small module graphs, impact is negligible. For large `jlink` builds or module graphs
|
||||
with deep dependency chains, this compounds with other O(N²) patterns.
|
||||
49
docs/tickets/0004-dependencies-node-list-contains.md
Normal file
49
docs/tickets/0004-dependencies-node-list-contains.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# 0004 — Dependencies$GraphDependencies$Node: List.contains() dedup on every addDependency()
|
||||
|
||||
**Status:** open
|
||||
**Severity:** performance — low-medium
|
||||
**Component:** `jdk.compiler / com.sun.tools.javac.util.Dependencies$GraphDependencies$Node`
|
||||
|
||||
---
|
||||
|
||||
## Root Cause
|
||||
|
||||
`Dependencies.java:199`:
|
||||
|
||||
```java
|
||||
void addDependency(DependencyKind depKind, Node dep) {
|
||||
List<Node> deps = depsByKind.get(depKind);
|
||||
if (!deps.contains(dep)) { // O(N) linear scan before every add
|
||||
deps.add(dep);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`deps` is a `java.util.ArrayList<Node>`. The deduplication check scans the entire list on every `addDependency()` call. Should be a `LinkedHashSet` (O(1) add with deduplication, preserves insertion order).
|
||||
|
||||
---
|
||||
|
||||
## Fix
|
||||
|
||||
```java
|
||||
// Replace ArrayList with LinkedHashSet in Node constructor:
|
||||
EnumMap<CompletionCause, Set<Node>> depsByKind; // Set, not List
|
||||
|
||||
Node(ClassSymbol value) {
|
||||
super(value);
|
||||
this.depsByKind = new EnumMap<>(CompletionCause.class);
|
||||
for (CompletionCause depKind : CompletionCause.values()) {
|
||||
depsByKind.put(depKind, new LinkedHashSet<>()); // O(1) add + dedup
|
||||
}
|
||||
}
|
||||
|
||||
void addDependency(DependencyKind depKind, Node dep) {
|
||||
depsByKind.get(depKind).add(dep); // dedup is free, no contains() needed
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
`GraphDependencies` is only active when the `debug.completionDeps` option is set (disabled by default). Impact is limited to debug/diagnostic compilation runs. Severity lower than 0001–0003.
|
||||
66
docs/tickets/0005-inference-context-isequiv-containsall.md
Normal file
66
docs/tickets/0005-inference-context-isequiv-containsall.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# 0005 — InferenceContext.isEquiv(): O(B²) bound-list equality via List.containsAll()
|
||||
|
||||
**Status:** open
|
||||
**Severity:** performance — low
|
||||
**Component:** `jdk.compiler / com.sun.tools.javac.comp.InferenceContext$ReachabilityVisitor`
|
||||
|
||||
---
|
||||
|
||||
## Root Cause
|
||||
|
||||
`InferenceContext.java:506` — `isEquiv()`:
|
||||
|
||||
```java
|
||||
boolean isEquiv(UndetVar from, Type t, InferenceBound boundKind) {
|
||||
UndetVar uv = (UndetVar)asUndetVar(t);
|
||||
for (InferenceBound ib : InferenceBound.values()) {
|
||||
List<Type> b1 = from.getBounds(ib);
|
||||
...
|
||||
List<Type> b2 = uv.getBounds(ib);
|
||||
...
|
||||
if (!b1.containsAll(b2) || !b2.containsAll(b1)) { // O(B²)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
`b1.containsAll(b2)` on `com.sun.tools.javac.util.List` (javac's own linked-list) does a linear `contains()` check per element of `b2` — O(|b1| × |b2|). Called twice.
|
||||
|
||||
This is a set-equality check expressed as two mutual containment checks on linear lists. Should use `Set` or sort-and-compare.
|
||||
|
||||
---
|
||||
|
||||
## Call Context
|
||||
|
||||
`isEquiv()` is called from `ReachabilityVisitor.visitUndetVar()` which is called during inference context minimization (`InferenceContext.min()`). Minimization fires when the compiler attempts to reduce the inference context to its minimal reachable set before solving.
|
||||
|
||||
Bounds lists are typically small (1–10 elements), so O(B²) is not severe in practice. Impact is lower than 0001–0003 but follows the same pattern.
|
||||
|
||||
---
|
||||
|
||||
## Fix
|
||||
|
||||
```java
|
||||
// Replace containsAll on linked lists with Set comparison:
|
||||
if (!new LinkedHashSet<>(b1).equals(new LinkedHashSet<>(b2))) {
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
Or, if `getBounds()` returned a `Set` instead of a `List`, the comparison would be direct.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| # | Location | Defect | Complexity | Module |
|
||||
|---|----------|--------|-----------|--------|
|
||||
| 0001 | GraphUtils$Tarjan | stack.contains(n) | O(V²) Tarjan | jdk.compiler |
|
||||
| 0002 | InferenceGraph.findNode() + closure() | O(N) scan + uncached DFS | O(N³) total | jdk.compiler |
|
||||
| 0003 | ModuleHashesBuilder$TopoSorter | Deque.contains() | O(V²) topo sort | java.base |
|
||||
| 0004 | Dependencies$Node.addDependency() | List.contains() dedup | O(N) per add | jdk.compiler |
|
||||
| 0005 | InferenceContext.isEquiv() | List.containsAll() | O(B²) per call | jdk.compiler |
|
||||
|
||||
All five share the root pattern: **linear collection membership check where O(1) is achievable.**
|
||||
40
docs/tickets/README.md
Normal file
40
docs/tickets/README.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Tickets
|
||||
|
||||
| # | Title | Module | Severity | Status |
|
||||
|---|-------|--------|----------|--------|
|
||||
| [0001](0001-tarjan-ov2-stack-contains.md) | Tarjan SCC: `stack.contains(n)` — O(V²) | jdk.compiler | high | open |
|
||||
| [0002](0002-inference-graph-findnode-linear-scan.md) | `InferenceGraph.findNode()` O(N) scan, called O(N³) total | jdk.compiler | high | open |
|
||||
| [0003](0003-module-hasher-topo-deque-contains.md) | `ModuleHashesBuilder$TopoSorter` `Deque.contains()` | **java.base** | medium | open |
|
||||
| [0004](0004-dependencies-node-list-contains.md) | `Dependencies$Node.addDependency()` `List.contains()` dedup | jdk.compiler | low | open |
|
||||
| [0005](0005-inference-context-isequiv-containsall.md) | `InferenceContext.isEquiv()` `List.containsAll()` bound comparison | jdk.compiler | low | open |
|
||||
|
||||
## Pattern
|
||||
|
||||
Same defect replicated in 4 locations across 2 modules:
|
||||
**O(n) linear collection membership check where O(1) is available.**
|
||||
|
||||
In every case a flag field, HashSet, or dedicated boolean already exists or is trivially addable.
|
||||
The background bytecode scan of all 69 JDK modules confirmed no additional hits beyond these five.
|
||||
The `active` field in `TarjanNode` (0001) is the most direct evidence: maintained correctly, never read.
|
||||
|
||||
## Cascade
|
||||
|
||||
```
|
||||
javac compilation
|
||||
└─ type inference (Infer.java)
|
||||
├─ GraphSolver.solve()
|
||||
│ └─ InferenceGraph.initNodes()
|
||||
│ └─ GraphUtils.tarjan() ← DEFECT 0001: O(V²)
|
||||
└─ DeferredAttr.buildStuckGraph()
|
||||
└─ canInfluence() × N²
|
||||
├─ findNode() ← DEFECT 0002: O(N) per call → O(N³) total
|
||||
└─ closure() ← DEFECT 0002: uncached O(V+E) per call
|
||||
|
||||
jlink image creation
|
||||
└─ ModuleHashesBuilder.computeHashes()
|
||||
└─ TopoSorter.visit() ← DEFECT 0003: Deque.contains() O(N)
|
||||
|
||||
debug compilation (-XDcompletionDeps)
|
||||
└─ Dependencies$GraphDependencies
|
||||
└─ Node.addDependency() ← DEFECT 0004: List.contains() O(N)
|
||||
```
|
||||
70
docs/tickets/diagrams/0001-inference-graph.dot
Normal file
70
docs/tickets/diagrams/0001-inference-graph.dot
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// Diagram 1: Inference Variable Dependency Graph
|
||||
// Represents the graph passed to GraphUtils.tarjan() in Infer.java:1908
|
||||
// Nodes = unresolved inference variables (UndetVar)
|
||||
// Edges = bound dependencies (T_i appears in bounds of T_j)
|
||||
//
|
||||
// Render: dot -Tsvg 0001-inference-graph.dot -o 0001-inference-graph.svg
|
||||
|
||||
digraph inference_graph {
|
||||
graph [
|
||||
label="Inference Variable Dependency Graph\n(input to GraphUtils.tarjan() — Infer.java:1908)"
|
||||
labelloc=t
|
||||
fontsize=14
|
||||
fontname="monospace"
|
||||
bgcolor="#f8f8f8"
|
||||
pad=0.5
|
||||
]
|
||||
node [fontname="monospace" fontsize=11]
|
||||
edge [fontname="monospace" fontsize=10]
|
||||
|
||||
// ── Unresolved inference variables ──────────────────────────────────────
|
||||
T1 [label="T1\n(UndetVar)" shape=ellipse style=filled fillcolor="#fff8a0" penwidth=2]
|
||||
T2 [label="T2\n(UndetVar)" shape=ellipse style=filled fillcolor="#fff8a0" penwidth=2]
|
||||
T3 [label="T3\n(UndetVar)" shape=ellipse style=filled fillcolor="#fff8a0" penwidth=2]
|
||||
T4 [label="T4\n(UndetVar)" shape=ellipse style=filled fillcolor="#fff8a0" penwidth=2]
|
||||
T5 [label="T5\n(UndetVar)" shape=ellipse style=filled fillcolor="#fff8a0" penwidth=2]
|
||||
|
||||
// ── Resolved variable (leaf, no deps) ───────────────────────────────────
|
||||
T6 [label="T6\n(resolved)" shape=ellipse style=filled fillcolor="#b8f0b8"]
|
||||
|
||||
// ── Bound dependencies ──────────────────────────────────────────────────
|
||||
T1 -> T2 [label="bound" style=dashed color="#555555"]
|
||||
T2 -> T3 [label="bound" style=dashed color="#555555"]
|
||||
T3 -> T1 [label="bound" style=dashed color="#cc0000" penwidth=2 label="back edge\n(creates cycle)"]
|
||||
T4 -> T2 [label="bound" style=dashed color="#555555"]
|
||||
T4 -> T5 [label="bound" style=dashed color="#555555"]
|
||||
T5 -> T6 [label="bound" style=dashed color="#555555"]
|
||||
|
||||
// ── SCC: T1+T2+T3 form a strongly connected component ──────────────────
|
||||
subgraph cluster_scc {
|
||||
label="SCC → merged into super-node {T1,T2,T3}"
|
||||
style=dashed
|
||||
color="#cc0000"
|
||||
penwidth=2
|
||||
fontcolor="#cc0000"
|
||||
T1; T2; T3;
|
||||
}
|
||||
|
||||
// ── After Tarjan: acyclic graph ─────────────────────────────────────────
|
||||
supernode [
|
||||
label="{T1,T2,T3}\nsuper-node"
|
||||
shape=rectangle
|
||||
style="filled,rounded"
|
||||
fillcolor="#ffcccc"
|
||||
penwidth=2
|
||||
]
|
||||
|
||||
T4_copy [label="T4" shape=ellipse style=filled fillcolor="#fff8a0"]
|
||||
T5_copy [label="T5" shape=ellipse style=filled fillcolor="#fff8a0"]
|
||||
T6_copy [label="T6\n(resolved)" shape=ellipse style=filled fillcolor="#b8f0b8"]
|
||||
|
||||
subgraph cluster_acyclic {
|
||||
label="Acyclic graph after Tarjan (Infer.java:1907-1918)"
|
||||
style=solid
|
||||
color="#0055aa"
|
||||
fontcolor="#0055aa"
|
||||
T4_copy -> supernode [label="bound" style=dashed color="#555555"]
|
||||
T4_copy -> T5_copy [label="bound" style=dashed color="#555555"]
|
||||
T5_copy -> T6_copy [label="bound" style=dashed color="#555555"]
|
||||
}
|
||||
}
|
||||
74
docs/tickets/diagrams/0001-stack-scan.dot
Normal file
74
docs/tickets/diagrams/0001-stack-scan.dot
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
// Diagram 3: Complexity Comparison — stack.contains(n) vs n.active
|
||||
// Shows operation count growth as graph size V increases.
|
||||
// Defective: V*(V+1)/2 comparisons for a path graph with back edge.
|
||||
// Fixed: 1 comparison per edge regardless of stack depth.
|
||||
//
|
||||
// Render: dot -Tsvg 0001-stack-scan.dot -o 0001-stack-scan.svg
|
||||
|
||||
digraph complexity_comparison {
|
||||
graph [
|
||||
label="Stack Membership Check: O(V²) Defect vs O(1) Fix\nComparisons required when back edge is reached on a path graph of V nodes"
|
||||
labelloc=t
|
||||
fontsize=13
|
||||
fontname="monospace"
|
||||
bgcolor="#f8f8f8"
|
||||
pad=0.6
|
||||
]
|
||||
node [fontname="monospace" fontsize=10]
|
||||
edge [fontname="monospace" fontsize=9]
|
||||
|
||||
// ── Defective: stack.contains(n) ────────────────────────────────────────
|
||||
subgraph cluster_defect {
|
||||
label="DEFECTIVE: stack.contains(n) — GraphUtils.java:186"
|
||||
style=filled fillcolor="#fff0f0" color="#cc0000" penwidth=2
|
||||
fontcolor="#cc0000"
|
||||
|
||||
d_v5 [label="V=5\n15 comparisons" shape=rect style=filled fillcolor="#ff9999" height=0.6 width=1.4]
|
||||
d_v10 [label="V=10\n55 comparisons" shape=rect style=filled fillcolor="#ff6666" height=1.0 width=1.4]
|
||||
d_v50 [label="V=50\n1275 comparisons" shape=rect style=filled fillcolor="#ff3333" height=2.0 width=1.4]
|
||||
d_v100[label="V=100\n5050 comparisons" shape=rect style=filled fillcolor="#cc0000" fontcolor=white height=3.0 width=1.4]
|
||||
|
||||
d_v5 -> d_v10 -> d_v50 -> d_v100 [style=invis]
|
||||
|
||||
d_label [label="O(V²)\ngrowth" shape=none fontcolor="#cc0000" fontsize=12]
|
||||
}
|
||||
|
||||
// ── Fixed: n.active ─────────────────────────────────────────────────────
|
||||
subgraph cluster_fix {
|
||||
label="FIXED: n.active — O(1) boolean read"
|
||||
style=filled fillcolor="#f0fff0" color="#006600" penwidth=2
|
||||
fontcolor="#006600"
|
||||
|
||||
f_v5 [label="V=5\n1 comparison" shape=rect style=filled fillcolor="#99ff99" height=0.25 width=1.4]
|
||||
f_v10 [label="V=10\n1 comparison" shape=rect style=filled fillcolor="#99ff99" height=0.25 width=1.4]
|
||||
f_v50 [label="V=50\n1 comparison" shape=rect style=filled fillcolor="#99ff99" height=0.25 width=1.4]
|
||||
f_v100[label="V=100\n1 comparison" shape=rect style=filled fillcolor="#99ff99" height=0.25 width=1.4]
|
||||
|
||||
f_v5 -> f_v10 -> f_v50 -> f_v100 [style=invis]
|
||||
|
||||
f_label [label="O(1)\nper edge" shape=none fontcolor="#006600" fontsize=12]
|
||||
}
|
||||
|
||||
// ── Source location ──────────────────────────────────────────────────────
|
||||
src_defect [
|
||||
label="GraphUtils.java:186\n} else if (stack.contains(n)) {\n // ListBuffer linear scan"
|
||||
shape=box style="filled,rounded" fillcolor="#ffeeee"
|
||||
fontcolor="#cc0000" penwidth=2
|
||||
]
|
||||
src_fix [
|
||||
label="GraphUtils.java:186 (patched)\n} else if (n.active) {\n // TarjanNode.active field — O(1)"
|
||||
shape=box style="filled,rounded" fillcolor="#eeffee"
|
||||
fontcolor="#006600" penwidth=2
|
||||
]
|
||||
|
||||
src_defect -> d_v5 [label="drives" color="#cc0000" style=dashed]
|
||||
src_fix -> f_v5 [label="drives" color="#006600" style=dashed]
|
||||
|
||||
// ── Where Tarjan fires ───────────────────────────────────────────────────
|
||||
callers [
|
||||
label="Call sites:\nInfer.java:1908 (type inference — every call site with unresolved vars)\nDeferredAttr.java:675 (stuck expression resolution — every lambda/method-ref)"
|
||||
shape=box style=filled fillcolor="#e8e8ff"
|
||||
]
|
||||
callers -> src_defect [style=dotted]
|
||||
callers -> src_fix [style=dotted]
|
||||
}
|
||||
84
docs/tickets/diagrams/0001-tarjan-defect.dot
Normal file
84
docs/tickets/diagrams/0001-tarjan-defect.dot
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// Diagram 2: Tarjan DFS Execution — Defect Highlighted
|
||||
// Shows the algorithm walking a path graph N0→N1→N2→N3→N4→N0 (back edge).
|
||||
// At each step, the stack grows. When the back edge N4→N0 is reached,
|
||||
// stack.contains(N0) scans ALL 5 stack entries instead of reading N0.active.
|
||||
//
|
||||
// Render: dot -Tsvg 0001-tarjan-defect.dot -o 0001-tarjan-defect.svg
|
||||
|
||||
digraph tarjan_defect {
|
||||
graph [
|
||||
label="Tarjan SCC: Execution Trace on Path Graph N0→N1→N2→N3→N4→N0\nDefect: stack.contains(N0) at back-edge fires O(n) scan (GraphUtils.java:186)"
|
||||
labelloc=t
|
||||
fontsize=13
|
||||
fontname="monospace"
|
||||
bgcolor="#f8f8f8"
|
||||
rankdir=LR
|
||||
pad=0.5
|
||||
]
|
||||
node [fontname="monospace" fontsize=10]
|
||||
edge [fontname="monospace" fontsize=9]
|
||||
|
||||
// ── Input graph ─────────────────────────────────────────────────────────
|
||||
subgraph cluster_input {
|
||||
label="Input graph"
|
||||
style=solid color="#333333"
|
||||
rankdir=LR
|
||||
|
||||
N0 [label="N0\nindex=0" shape=circle style=filled fillcolor="#cce5ff"]
|
||||
N1 [label="N1\nindex=1" shape=circle style=filled fillcolor="#cce5ff"]
|
||||
N2 [label="N2\nindex=2" shape=circle style=filled fillcolor="#cce5ff"]
|
||||
N3 [label="N3\nindex=3" shape=circle style=filled fillcolor="#cce5ff"]
|
||||
N4 [label="N4\nindex=4" shape=circle style=filled fillcolor="#cce5ff"]
|
||||
|
||||
N0 -> N1 [label="forward"]
|
||||
N1 -> N2 [label="forward"]
|
||||
N2 -> N3 [label="forward"]
|
||||
N3 -> N4 [label="forward"]
|
||||
N4 -> N0 [label="BACK EDGE" color="#cc0000" penwidth=2 style=dashed]
|
||||
}
|
||||
|
||||
// ── Stack at moment of back-edge check ──────────────────────────────────
|
||||
subgraph cluster_stack {
|
||||
label="Stack at step 5 (visiting N4, checking neighbour N0)"
|
||||
style=solid color="#cc0000" fontcolor="#cc0000"
|
||||
rankdir=TB
|
||||
|
||||
s_top [label="N4 ← top" shape=record style=filled fillcolor="#ffcccc"]
|
||||
s_3 [label="N3" shape=record style=filled fillcolor="#ffe0cc"]
|
||||
s_2 [label="N2" shape=record style=filled fillcolor="#ffe0cc"]
|
||||
s_1 [label="N1" shape=record style=filled fillcolor="#ffe0cc"]
|
||||
s_bot [label="N0 ← bottom / TARGET" shape=record style=filled fillcolor="#ffeeaa" penwidth=2]
|
||||
|
||||
s_top -> s_3 [style=invis]
|
||||
s_3 -> s_2 [style=invis]
|
||||
s_2 -> s_1 [style=invis]
|
||||
s_1 -> s_bot [style=invis]
|
||||
}
|
||||
|
||||
// ── The defective scan ──────────────────────────────────────────────────
|
||||
subgraph cluster_scan {
|
||||
label="stack.contains(N0) — LINEAR SCAN (GraphUtils.java:186)"
|
||||
style=filled fillcolor="#fff0f0" color="#cc0000" fontcolor="#cc0000"
|
||||
|
||||
cmp1 [label="N4 == N0 ? NO" shape=diamond style=filled fillcolor="#ffcccc"]
|
||||
cmp2 [label="N3 == N0 ? NO" shape=diamond style=filled fillcolor="#ffcccc"]
|
||||
cmp3 [label="N2 == N0 ? NO" shape=diamond style=filled fillcolor="#ffcccc"]
|
||||
cmp4 [label="N1 == N0 ? NO" shape=diamond style=filled fillcolor="#ffcccc"]
|
||||
cmp5 [label="N0 == N0 ? YES" shape=diamond style=filled fillcolor="#aaff88" penwidth=2]
|
||||
|
||||
cmp1 -> cmp2 -> cmp3 -> cmp4 -> cmp5
|
||||
}
|
||||
|
||||
// ── The fix ─────────────────────────────────────────────────────────────
|
||||
subgraph cluster_fix {
|
||||
label="n.active — O(1) CHECK (fix)"
|
||||
style=filled fillcolor="#f0fff0" color="#006600" fontcolor="#006600"
|
||||
|
||||
fix [label="N0.active == true ? YES\n→ done, 1 operation" shape=diamond style=filled fillcolor="#aaff88" penwidth=2]
|
||||
}
|
||||
|
||||
// ── Connections ─────────────────────────────────────────────────────────
|
||||
N4 -> cmp1 [label="back edge triggers\nstack.contains(N0)" color="#cc0000" penwidth=2]
|
||||
N4 -> fix [label="fix: n.active" color="#006600" penwidth=2 style=dashed]
|
||||
s_top -> cmp1 [label="scan starts\nat top" color="#cc0000" style=dotted]
|
||||
}
|
||||
79
docs/tickets/diagrams/0002-buildstuckgraph-cascade.dot
Normal file
79
docs/tickets/diagrams/0002-buildstuckgraph-cascade.dot
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
// Diagram: buildStuckGraph() complexity cascade (ticket 0002)
|
||||
// Shows how three compounding inefficiencies in DeferredAttr+Infer
|
||||
// produce O(N³) total work where O(N²) should be the floor.
|
||||
//
|
||||
// Render: dot -Tsvg 0002-buildstuckgraph-cascade.dot -o 0002-buildstuckgraph-cascade.svg
|
||||
|
||||
digraph buildstuck_cascade {
|
||||
graph [
|
||||
label="buildStuckGraph() complexity cascade (DeferredAttr.java:685 + Infer.java:1850)"
|
||||
labelloc=t fontsize=13 fontname="monospace" bgcolor="#f8f8f8" pad=0.6
|
||||
rankdir=TB
|
||||
]
|
||||
node [fontname="monospace" fontsize=10]
|
||||
edge [fontname="monospace" fontsize=9]
|
||||
|
||||
// ── Entry point ──────────────────────────────────────────────────────────
|
||||
bsg [
|
||||
label="buildStuckGraph()\nDeferredAttr.java:685\nN stuck nodes"
|
||||
shape=box style="filled,rounded" fillcolor="#dde8ff" penwidth=2
|
||||
]
|
||||
|
||||
// ── Outer N² loop ────────────────────────────────────────────────────────
|
||||
loop [
|
||||
label="for sn1 in nodes:\n for sn2 in nodes:\n canInfluence(sn2, sn1)\n\nO(N²) calls"
|
||||
shape=box style=filled fillcolor="#fff0cc" penwidth=2
|
||||
]
|
||||
|
||||
// ── canInfluence internals ────────────────────────────────────────────────
|
||||
ci [
|
||||
label="canInfluence()\nDeferredAttr.java:700"
|
||||
shape=box style="filled,rounded" fillcolor="#ffe8cc"
|
||||
]
|
||||
|
||||
fn [
|
||||
label="findNode(inputVar)\nInfer.java:1850\nO(N) ArrayList scan\n← DEFECT 0002a"
|
||||
shape=box style=filled fillcolor="#ffcccc" penwidth=2
|
||||
]
|
||||
|
||||
cl [
|
||||
label="inputNode.closure()\nInfer.java:1747\nO(V+E) DFS\nNOT CACHED ← DEFECT 0002b"
|
||||
shape=box style=filled fillcolor="#ffcccc" penwidth=2
|
||||
]
|
||||
|
||||
fn2 [
|
||||
label="outputVars.map(findNode)\nInfer.java:1850\nO(S·N) per call\n← DEFECT 0002a (again)"
|
||||
shape=box style=filled fillcolor="#ffcccc" penwidth=2
|
||||
]
|
||||
|
||||
// ── Tarjan (already in 0001) ──────────────────────────────────────────────
|
||||
tarjan [
|
||||
label="GraphUtils.tarjan(stuckGraph)\nDeferredAttr.java:675\n← DEFECT 0001 (separate)"
|
||||
shape=box style=filled fillcolor="#ffeebb" penwidth=2
|
||||
]
|
||||
|
||||
// ── Total complexity ─────────────────────────────────────────────────────
|
||||
total_defective [
|
||||
label="DEFECTIVE TOTAL\nO(N²) × O(S·N) × O(V+E)\n= O(N³·S·(V+E))"
|
||||
shape=rect style="filled,rounded" fillcolor="#ff8888" fontcolor=white penwidth=3
|
||||
]
|
||||
total_fixed [
|
||||
label="FIXED TOTAL (with HashMap + closure cache)\nO(N²) × O(S) × O(1) [amortized]\n= O(N²·S)"
|
||||
shape=rect style="filled,rounded" fillcolor="#88cc88" penwidth=3
|
||||
]
|
||||
|
||||
bsg -> loop
|
||||
loop -> ci [label="N² calls"]
|
||||
ci -> fn [label="per stuckVar"]
|
||||
ci -> cl [label="per inputNode"]
|
||||
ci -> fn2 [label="per outputVar"]
|
||||
loop -> tarjan [label="after loop\n(once)"]
|
||||
|
||||
fn -> total_defective [style=dashed color="#cc0000"]
|
||||
cl -> total_defective [style=dashed color="#cc0000"]
|
||||
fn2 -> total_defective [style=dashed color="#cc0000"]
|
||||
|
||||
fn -> total_fixed [style=dashed color="#006600" label="HashMap O(1)"]
|
||||
cl -> total_fixed [style=dashed color="#006600" label="cached"]
|
||||
fn2 -> total_fixed [style=dashed color="#006600"]
|
||||
}
|
||||
60
docs/tickets/diagrams/0003-module-topo-deque.dot
Normal file
60
docs/tickets/diagrams/0003-module-topo-deque.dot
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
// Diagram: ModuleHashesBuilder$TopoSorter defect (ticket 0003)
|
||||
// Shows the Deque.contains() pattern in java.base and its scope.
|
||||
//
|
||||
// Render: dot -Tsvg 0003-module-topo-deque.dot -o 0003-module-topo-deque.svg
|
||||
|
||||
digraph module_topo {
|
||||
graph [
|
||||
label="ModuleHashesBuilder$TopoSorter: Deque.contains() in java.base\n(Same pattern as 0001, different module, different layer)"
|
||||
labelloc=t fontsize=13 fontname="monospace" bgcolor="#f8f8f8" pad=0.6
|
||||
rankdir=TB
|
||||
]
|
||||
node [fontname="monospace" fontsize=10]
|
||||
edge [fontname="monospace" fontsize=9]
|
||||
|
||||
// ── Call chain ────────────────────────────────────────────────────────────
|
||||
jlink [
|
||||
label="jlink\n(custom runtime image builder)"
|
||||
shape=box style="filled,rounded" fillcolor="#dde8ff"
|
||||
]
|
||||
|
||||
mhb [
|
||||
label="ModuleHashesBuilder.computeHashes()\njava.base/jdk.internal.module"
|
||||
shape=box style="filled,rounded" fillcolor="#dde8ff"
|
||||
]
|
||||
|
||||
topo [
|
||||
label="new TopoSorter(graph)\n→ sort() → visit()"
|
||||
shape=box style="filled,rounded" fillcolor="#fff0cc"
|
||||
]
|
||||
|
||||
// ── The defect ────────────────────────────────────────────────────────────
|
||||
defect [
|
||||
label="visit(node, visited, stack)\n\nDeque.contains(node) ← DEFECT\nbytecode instr 57:\ninvokeinterface Deque.contains:(Object)Z\n\nArrayDeque.contains() = O(N) linear scan"
|
||||
shape=box style=filled fillcolor="#ffcccc" penwidth=3
|
||||
]
|
||||
|
||||
fix [
|
||||
label="FIX: HashSet<T> onStack\nonStack.contains(node) ← O(1)\nAdd on push, remove on pop"
|
||||
shape=box style="filled,rounded" fillcolor="#ccffcc" penwidth=2
|
||||
]
|
||||
|
||||
// ── Module scope ──────────────────────────────────────────────────────────
|
||||
scope [
|
||||
label="Scope: java.base\nPresent in every JDK + JRE\nLoaded by every JVM process"
|
||||
shape=ellipse style=filled fillcolor="#ffeeaa" penwidth=2
|
||||
]
|
||||
|
||||
// ── Comparison to 0001 ────────────────────────────────────────────────────
|
||||
cmp [
|
||||
label="0001: GraphUtils$Tarjan (jdk.compiler)\n stack.contains(n) → n.active\n\n0003: ModuleHashesBuilder$TopoSorter (java.base)\n stack.contains(n) → onStack.contains(n)\n\nSame pattern. Different modules. Independent fixes."
|
||||
shape=note style=filled fillcolor="#fffff0"
|
||||
]
|
||||
|
||||
jlink -> mhb
|
||||
mhb -> topo
|
||||
topo -> defect
|
||||
defect -> fix [label="fix" style=dashed color="#006600"]
|
||||
defect -> scope [style=dotted]
|
||||
defect -> cmp [style=dotted]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue