262 lines
11 KiB
Java
262 lines
11 KiB
Java
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.");
|
||
}
|
||
}
|