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:
russell@unturf.com 2026-03-26 19:48:18 -04:00
parent 0a580b313d
commit db29a08762
1311 changed files with 371202 additions and 1188 deletions

View 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()`.

View 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.

View 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.

View 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.

View 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
View 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)
```

View 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"]
}
}

View 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]
}

View 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]
}

View 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"]
}

View 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]
}