java-topology/defects/metaflow/unit/MetaflowGraphAlgorithm.java

262 lines
11 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package unit;
import java.util.*;
/**
* metaflow-0001 — O(N²) graph traversal in FlowGraph._traverse_graph()
*
* Simulates the defective pattern from:
* metaflow/graph.py lines 300-340
*
* Defect 1: sorted_nodes.remove(node.name) is O(N) inside a DFS that visits N nodes → O(N²)
* Defect 2: seen is a List, so `n not in seen` is O(depth) per edge → O(N²) summed over all edges
*
* Fix 1: Use a LinkedHashMap (insertion-ordered map) for sorted_nodes → O(1) remove/put
* Fix 2: Use a HashSet for seen → O(1) membership test
*
* The op-count measures "list scans": how many elements were examined across all
* remove() and `contains()` calls. In the defective version this is O(N²);
* in the fixed version it is O(N + E).
*/
public class MetaflowGraphAlgorithm {
static void check(String desc, boolean cond) {
System.out.println((cond ? "PASS" : "FAIL") + ": " + desc);
if (!cond) throw new AssertionError("FAIL: " + desc);
}
// Simulates a DAGNode with successors
static class Node {
final String name;
final List<String> outFuncs = new ArrayList<>();
Node(String name) { this.name = name; }
void addEdge(String target) { outFuncs.add(target); }
}
// Result from traversal
static class Result {
final List<String> sortedNodes;
final long listScans;
Result(List<String> sortedNodes, long listScans) {
this.sortedNodes = sortedNodes;
this.listScans = listScans;
}
}
// -----------------------------------------------------------------------
// DEFECTIVE: sorted_nodes is an ArrayList, seen is a List
// -----------------------------------------------------------------------
static long defectiveScans;
static void traverseDefective(
String name,
Map<String, Node> nodes,
List<String> sortedNodes,
List<String> seen) {
// O(N) scan: remove from list
int sizeBefore = sortedNodes.size();
sortedNodes.remove(name);
defectiveScans += sizeBefore; // counted even if not found
sortedNodes.add(name);
for (String n : nodes.get(name).outFuncs) {
// O(depth) scan: check membership
defectiveScans += seen.size();
if (!seen.contains(n)) {
if (nodes.containsKey(n)) {
List<String> newSeen = new ArrayList<>(seen);
newSeen.add(n);
traverseDefective(n, nodes, sortedNodes, newSeen);
}
}
}
}
static Result runDefective(Map<String, Node> nodes, String start) {
defectiveScans = 0;
List<String> sortedNodes = new ArrayList<>();
List<String> seen = new ArrayList<>();
seen.add(start);
traverseDefective(start, nodes, sortedNodes, seen);
return new Result(sortedNodes, defectiveScans);
}
// -----------------------------------------------------------------------
// FIXED: sorted_nodes is a LinkedHashMap, seen is a HashSet
// -----------------------------------------------------------------------
static long fixedScans;
static void traverseFixed(
String name,
Map<String, Node> nodes,
LinkedHashMap<String, Boolean> sortedNodes,
Set<String> seen) {
// O(1) remove + put
sortedNodes.remove(name); // no scan needed
sortedNodes.put(name, true);
fixedScans += 1; // count the O(1) hash op
for (String n : nodes.get(name).outFuncs) {
// O(1) hash lookup
fixedScans += 1;
if (!seen.contains(n)) {
if (nodes.containsKey(n)) {
seen.add(n);
traverseFixed(n, nodes, sortedNodes, seen);
}
}
}
}
static Result runFixed(Map<String, Node> nodes, String start) {
fixedScans = 0;
LinkedHashMap<String, Boolean> sortedNodes = new LinkedHashMap<>();
Set<String> seen = new HashSet<>();
seen.add(start);
traverseFixed(start, nodes, sortedNodes, seen);
return new Result(new ArrayList<>(sortedNodes.keySet()), fixedScans);
}
// -----------------------------------------------------------------------
// Build a linear chain: start → s1 → s2 → ... → sN → end
// -----------------------------------------------------------------------
static Map<String, Node> linearChain(int n) {
Map<String, Node> nodes = new LinkedHashMap<>();
Node start = new Node("start");
nodes.put("start", start);
String prev = "start";
for (int i = 1; i <= n; i++) {
String name = "step" + i;
Node node = new Node(name);
nodes.put(name, node);
nodes.get(prev).addEdge(name);
prev = name;
}
Node end = new Node("end");
nodes.put("end", end);
nodes.get(prev).addEdge("end");
return nodes;
}
// -----------------------------------------------------------------------
// Build a diamond DAG (wide split): start → N parallel → join → end
// -----------------------------------------------------------------------
static Map<String, Node> diamondDAG(int n) {
Map<String, Node> nodes = new LinkedHashMap<>();
Node start = new Node("start");
nodes.put("start", start);
Node join = new Node("join");
nodes.put("join", join);
for (int i = 0; i < n; i++) {
String name = "branch" + i;
Node b = new Node(name);
nodes.put(name, b);
start.addEdge(name);
b.addEdge("join");
}
Node end = new Node("end");
nodes.put("end", end);
join.addEdge("end");
return nodes;
}
public static void main(String[] args) {
System.out.println("=== metaflow-0001: O(N²) graph traverse in FlowGraph._traverse_graph ===");
// ------ Test 1: correctness on small linear chain ------
{
Map<String, Node> nodes = linearChain(4);
Result def = runDefective(nodes, "start");
Result fix = runFixed(nodes, "start");
check("linear-4: defective produces correct topo order",
def.sortedNodes.equals(Arrays.asList("start","step1","step2","step3","step4","end")));
check("linear-4: fixed produces correct topo order",
fix.sortedNodes.equals(Arrays.asList("start","step1","step2","step3","step4","end")));
check("linear-4: fixed uses fewer scans",
fix.listScans <= def.listScans);
}
// ------ Test 2: O(N²) vs O(N) scaling on linear chain ------
{
int N = 200;
Map<String, Node> nodes = linearChain(N);
Result def200 = runDefective(nodes, "start");
Result fix200 = runFixed(nodes, "start");
// Defective: remove scans grow as 0+1+2+...+N ≈ N²/2; seen grows too
// Fixed: each op is O(1)
long ratio = def200.listScans / Math.max(fix200.listScans, 1);
System.out.printf(" N=%d: defective scans=%d, fixed scans=%d, ratio=%dx%n",
N, def200.listScans, fix200.listScans, ratio);
check("linear-200: defective scan count > 1000 (quadratic evidence)",
def200.listScans > 1_000);
check("linear-200: fixed scan count <= 2*(N+2) (linear)",
fix200.listScans <= 2L * (N + 2));
check("linear-200: ratio >= 10x",
ratio >= 10);
// Verify both produce same sorted order
check("linear-200: same sorted order",
def200.sortedNodes.equals(fix200.sortedNodes));
}
// ------ Test 3: diamond DAG correctness ------
{
int N = 50;
Map<String, Node> nodes = diamondDAG(N);
Result def = runDefective(nodes, "start");
Result fix = runFixed(nodes, "start");
// start must be first in both versions, all nodes visited, same count
check("diamond-50: start is first (defective)", def.sortedNodes.get(0).equals("start"));
check("diamond-50: start is first (fixed)", fix.sortedNodes.get(0).equals("start"));
// Both must contain all N+3 nodes: start, N branches, join, end
check("diamond-50: defective visits all nodes", def.sortedNodes.size() == N + 3);
check("diamond-50: fixed visits all nodes", fix.sortedNodes.size() == N + 3);
// join must appear before end in both (topological constraint)
int defJoinIdx = def.sortedNodes.indexOf("join");
int defEndIdx = def.sortedNodes.indexOf("end");
int fixJoinIdx = fix.sortedNodes.indexOf("join");
int fixEndIdx = fix.sortedNodes.indexOf("end");
check("diamond-50: join before end (defective)", defJoinIdx < defEndIdx);
check("diamond-50: join before end (fixed)", fixJoinIdx < fixEndIdx);
// all branches are present in fixed output
check("diamond-50: fixed contains branch0", fix.sortedNodes.contains("branch0"));
check("diamond-50: fixed contains branch" + (N-1), fix.sortedNodes.contains("branch" + (N-1)));
}
// ------ Test 4: large chain — quadratic cost is clear ------
{
int N = 400;
Map<String, Node> small = linearChain(N / 4);
Map<String, Node> large = linearChain(N);
Result defSmall = runDefective(small, "start");
Result defLarge = runDefective(large, "start");
// Quadratic: scans should grow roughly 16× (4× nodes → 16× scans)
double growthRatio = (double) defLarge.listScans / Math.max(defSmall.listScans, 1);
System.out.printf(" Defective: N=%d scans=%d, N=%d scans=%d, growth=%.1fx%n",
N/4, defSmall.listScans, N, defLarge.listScans, growthRatio);
check("quadratic growth: defLarge.scans > 4x defSmall.scans (O(N²) evidence)",
growthRatio > 4.0);
Result fixSmall = runFixed(small, "start");
Result fixLarge = runFixed(large, "start");
double fixGrowthRatio = (double) fixLarge.listScans / Math.max(fixSmall.listScans, 1);
System.out.printf(" Fixed: N=%d scans=%d, N=%d scans=%d, growth=%.1fx%n",
N/4, fixSmall.listScans, N, fixLarge.listScans, fixGrowthRatio);
check("linear growth: fixLarge.scans < 5x fixSmall.scans (O(N) evidence)",
fixGrowthRatio < 5.0);
}
System.out.println("All tests PASS.");
}
}