java-topology/defects/argo-workflows/unit/ArgoGetTaskAlgorithm.java

245 lines
9.4 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.*;
/**
* argo-0001 — O(N) linear GetTask() scan inside O(N) DAG execution loop
*
* Simulates the defective GetTask() from:
* workflow/controller/dag.go:83-89
*
* Defect: dagContext stores tasks as []DAGTask (slice). GetTask(name) does
* linear scan. Called O(N) times per executeDAG() cycle → O(N²) total.
*
* Fix: Build a map[string]*DAGTask at dagContext construction time → O(1) lookup.
*
* The unit test simulates:
* 1. A DAGContext with N tasks (stored as list in defective, map in fixed)
* 2. An executeDAG loop that calls GetTask() once per targetTask
* 3. Counts total comparisons to confirm O(N²) vs O(N)
*/
public class ArgoGetTaskAlgorithm {
static int opCount = 0;
static void check(String desc, boolean cond) {
System.out.println((cond ? "PASS" : "FAIL") + ": " + desc);
if (!cond) throw new AssertionError("FAIL: " + desc);
}
// -----------------------------------------------------------------------
// Simulated DAGTask
// -----------------------------------------------------------------------
static class DAGTask {
final String name;
final List<String> dependencies;
DAGTask(String name, List<String> deps) {
this.name = name;
this.dependencies = deps;
}
}
// -----------------------------------------------------------------------
// DEFECTIVE: tasks stored as List<DAGTask>, GetTask does linear scan
// -----------------------------------------------------------------------
static class DefectiveDagContext {
final List<DAGTask> tasks;
DefectiveDagContext(List<DAGTask> tasks) {
this.tasks = new ArrayList<>(tasks);
}
DAGTask getTask(String taskName) {
for (DAGTask task : tasks) { // O(N) scan
opCount++;
if (task.name.equals(taskName)) {
return task;
}
}
throw new RuntimeException("task not found: " + taskName);
}
// Simulates executeDAG: for each target task, call getTask() 3 times
// (matches the 3 GetTask calls on lines 292, 300, 307 in dag.go)
void executeDAG(List<String> targetTasks) {
for (String taskName : targetTasks) {
getTask(taskName); // line 292: task := dagCtx.GetTask(ctx, taskName)
getTask(taskName); // line 300: dagCtx.GetTask(ctx, taskName).Hooks
getTask(taskName); // line 307: dagCtx.GetTask(ctx, taskName).GetExitHook(...)
}
}
}
// -----------------------------------------------------------------------
// FIXED: tasks stored as Map<String, DAGTask>, GetTask does O(1) lookup
// -----------------------------------------------------------------------
static class FixedDagContext {
final List<DAGTask> tasks;
final Map<String, DAGTask> taskByName;
FixedDagContext(List<DAGTask> tasks) {
this.tasks = new ArrayList<>(tasks);
this.taskByName = new HashMap<>(tasks.size() * 2);
for (DAGTask t : tasks) {
this.taskByName.put(t.name, t);
}
}
DAGTask getTask(String taskName) {
opCount++; // count each lookup
DAGTask task = taskByName.get(taskName);
if (task == null) throw new RuntimeException("task not found: " + taskName);
return task;
}
void executeDAG(List<String> targetTasks) {
for (String taskName : targetTasks) {
getTask(taskName);
getTask(taskName);
getTask(taskName);
}
}
}
// Build N tasks (all as target tasks — worst case: getTask scans entire list)
static List<DAGTask> buildTasks(int n) {
List<DAGTask> tasks = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
tasks.add(new DAGTask("task-" + i, Collections.emptyList()));
}
return tasks;
}
public static void main(String[] args) {
int passed = 0;
int total = 0;
// ------------------------------------------------------------------
// Test 1: Correctness — both return same task
// ------------------------------------------------------------------
total++;
{
List<DAGTask> tasks = buildTasks(10);
DefectiveDagContext def = new DefectiveDagContext(tasks);
FixedDagContext fix = new FixedDagContext(tasks);
opCount = 0;
DAGTask defTask = def.getTask("task-7");
opCount = 0;
DAGTask fixTask = fix.getTask("task-7");
check("Correctness: both find task-7", defTask.name.equals("task-7") && fixTask.name.equals("task-7"));
passed++;
}
// ------------------------------------------------------------------
// Test 2: Op-count for getTask on last element — defective is O(N)
// ------------------------------------------------------------------
total++;
{
int n = 100;
List<DAGTask> tasks = buildTasks(n);
DefectiveDagContext def = new DefectiveDagContext(tasks);
FixedDagContext fix = new FixedDagContext(tasks);
opCount = 0;
def.getTask("task-99"); // last element -> worst case: N comparisons
long defOps = opCount;
opCount = 0;
fix.getTask("task-99");
long fixOps = opCount;
System.out.printf(" Single getTask (last of N=%d): defective=%d ops, fixed=%d ops%n",
n, defOps, fixOps);
check("Defective getTask(last) scans all N elements", defOps == n);
check("Fixed getTask is O(1)", fixOps == 1);
passed += 2;
total++;
}
// ------------------------------------------------------------------
// Test 3: executeDAG op-count ratio — O(N²) vs O(N)
// ------------------------------------------------------------------
total++;
{
int n = 300;
List<DAGTask> tasks = buildTasks(n);
List<String> targetTasks = new ArrayList<>();
for (DAGTask t : tasks) targetTasks.add(t.name);
DefectiveDagContext def = new DefectiveDagContext(tasks);
FixedDagContext fix = new FixedDagContext(tasks);
opCount = 0;
def.executeDAG(targetTasks);
long defOps = opCount;
opCount = 0;
fix.executeDAG(targetTasks);
long fixOps = opCount;
double ratio = (double) defOps / fixOps;
System.out.printf(" executeDAG N=%d: defective=%d ops, fixed=%d ops, ratio=%.1fx%n",
n, defOps, fixOps, ratio);
// Defective: 3 calls × avg N/2 scans × N tasks = 3N²/2
// Fixed: 3 calls × 1 lookup × N tasks = 3N
// Expected ratio ≈ N/2 = 150 for N=300; require >= 50x conservatively
check("Op-count ratio >= 50x for executeDAG N=" + n, ratio >= 50.0);
passed++;
}
// ------------------------------------------------------------------
// Test 4: Quadratic growth — ops at N=200 should be ~4x ops at N=100
// ------------------------------------------------------------------
total++;
{
List<DAGTask> tasks100 = buildTasks(100);
List<DAGTask> tasks200 = buildTasks(200);
List<String> target100 = new ArrayList<>(); for (DAGTask t : tasks100) target100.add(t.name);
List<String> target200 = new ArrayList<>(); for (DAGTask t : tasks200) target200.add(t.name);
opCount = 0;
new DefectiveDagContext(tasks100).executeDAG(target100);
long ops100 = opCount;
opCount = 0;
new DefectiveDagContext(tasks200).executeDAG(target200);
long ops200 = opCount;
double growthRatio = (double) ops200 / ops100;
System.out.printf(" Defective growth (200 vs 100): %.2fx (expected ~4x for O(N²))%n",
growthRatio);
check("Defective shows quadratic growth (ratio >= 3.5x)", growthRatio >= 3.5);
passed++;
}
// ------------------------------------------------------------------
// Test 5: Linear growth for fixed
// ------------------------------------------------------------------
total++;
{
List<DAGTask> tasks100 = buildTasks(100);
List<DAGTask> tasks200 = buildTasks(200);
List<String> target100 = new ArrayList<>(); for (DAGTask t : tasks100) target100.add(t.name);
List<String> target200 = new ArrayList<>(); for (DAGTask t : tasks200) target200.add(t.name);
opCount = 0;
new FixedDagContext(tasks100).executeDAG(target100);
long ops100 = opCount;
opCount = 0;
new FixedDagContext(tasks200).executeDAG(target200);
long ops200 = opCount;
double growthRatio = (double) ops200 / ops100;
System.out.printf(" Fixed growth (200 vs 100): %.2fx (expected ~2x for O(N))%n",
growthRatio);
check("Fixed shows linear growth (ratio < 3.0x)", growthRatio < 3.0);
passed++;
}
System.out.printf("%n%d/%d PASS%n", passed, total);
}
}