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 outFuncs = new ArrayList<>(); Node(String name) { this.name = name; } void addEdge(String target) { outFuncs.add(target); } } // Result from traversal static class Result { final List sortedNodes; final long listScans; Result(List 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 nodes, List sortedNodes, List 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 newSeen = new ArrayList<>(seen); newSeen.add(n); traverseDefective(n, nodes, sortedNodes, newSeen); } } } } static Result runDefective(Map nodes, String start) { defectiveScans = 0; List sortedNodes = new ArrayList<>(); List 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 nodes, LinkedHashMap sortedNodes, Set 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 nodes, String start) { fixedScans = 0; LinkedHashMap sortedNodes = new LinkedHashMap<>(); Set 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 linearChain(int n) { Map 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 diamondDAG(int n) { Map 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 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 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 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 small = linearChain(N / 4); Map 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."); } }