package integration; import com.sun.tools.javac.util.GraphUtils; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * Integration test: exercises GraphUtils.tarjan() directly via the installed JDK. * * Proves that the defect in GraphUtils.java:186 (stack.contains(n) linear scan) * causes measurable super-linear timing growth on graphs that mirror the size * and shape of real inference variable graphs in complex Java code. * * Requires --add-exports to access internal package. * * Compile and run (from tests/): * * javac --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \ * -cp . integration/InferenceGraphScalingTest.java support/AbstractTestNode.java * * java --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \ * -cp . integration.InferenceGraphScalingTest * * NOTE: This test runs against the INSTALLED javac's GraphUtils (the defective version). * The timing results will show super-linear growth, matching the O(V²) hypothesis. * After applying the patch to GraphUtils.java and rebuilding jdk.compiler, re-run * to confirm linear growth. */ public class InferenceGraphScalingTest { static int passed = 0; static int failed = 0; public static void main(String[] args) { System.out.println("=== InferenceGraphScalingTest ==="); System.out.println("Running against: " + System.getProperty("java.home")); System.out.println(); testCorrectnessSmallGraph(); testCorrectnessWithSCC(); timingGrowthAnalysis_Star(); assertSuperlinearGrowth(); System.out.printf("%n%d passed, %d failed%n", passed, failed); if (failed > 0) System.exit(1); } // ─── Correctness ───────────────────────────────────────────────────────── static void testCorrectnessSmallGraph() { // 4-node cycle: one SCC of size 4 List nodes = buildCycle(4); List> sccs = GraphUtils.tarjan(nodes); assertEqual("4-cycle: SCC count", 1, sccs.size()); assertEqual("4-cycle: SCC[0] node count", 4, sccs.get(0).size()); } static void testCorrectnessWithSCC() { // DAG: N0→N1→N2→N3, N1→N3 (cross edge). No cycles → 4 singleton SCCs. TestNode n0 = node("N0"), n1 = node("N1"), n2 = node("N2"), n3 = node("N3"); n0.addDep(n1); n1.addDep(n2); n2.addDep(n3); n1.addDep(n3); List nodes = List.of(n0, n1, n2, n3); List> sccs = GraphUtils.tarjan(nodes); assertEqual("4-dag: SCC count", 4, sccs.size()); for (int i = 0; i < 4; i++) { assertEqual("4-dag: SCC[" + i + "] size", 1, sccs.get(i).size()); } } // ─── Timing growth analysis ─────────────────────────────────────────────── /** * Measures wall-clock time of GraphUtils.tarjan() on star graphs of increasing size. * * Star topology (starWithBackEdges): N0→{N1..Nv-1}, N_k→N0 for k≥1. * This is the worst-case topology for the defect: every back edge must scan * a deeper and deeper stack to find N0 at the bottom. * * Defective: O(V²) — time grows ~4x when V doubles. * Fixed: O(V) — time grows ~2x when V doubles. */ static void timingGrowthAnalysis_Star() { int[] sizes = {50, 100, 200, 400, 800}; int warmup = 200; int trials = 1000; System.out.println("[timing] GraphUtils.tarjan() on starWithBackEdges graphs:"); System.out.println(" Topology: N0→{N1..Nv-1}, N_k→N0 (worst case for stack.contains)"); System.out.println(" Warmup: " + warmup + " runs per size. Trials: " + trials); System.out.printf(" %-8s %-14s %-10s%n", "V", "avg_ns", "ratio_vs_prev"); System.out.println(" " + "-".repeat(38)); long prevTime = -1; for (int v : sizes) { for (int i = 0; i < warmup; i++) GraphUtils.tarjan(buildStar(v)); long total = 0; for (int i = 0; i < trials; i++) { List g = buildStar(v); long t0 = System.nanoTime(); GraphUtils.tarjan(g); total += System.nanoTime() - t0; } long avgNs = total / trials; String ratio = prevTime > 0 ? String.format("%.2fx", (double) avgNs / prevTime) : "—"; System.out.printf(" %-8d %-14d %-10s%n", v, avgNs, ratio); prevTime = avgNs; } System.out.println(); } /** * ASSERTS super-linear growth: doubling V should increase time by more * than 1.8x (indicating at least near-quadratic scaling). * * Uses a modest threshold to account for JIT variance, but quadratic * growth produces ratios of ~4x which far exceeds the threshold. * * NOTE: On a PATCHED build, this test will FAIL — that is expected and correct. * The test encodes what the defect looks like, not the post-fix behavior. * See CompilerBenchmarkTest for the post-fix validation. */ static void assertSuperlinearGrowth() { int v1 = 200, v2 = 400; int warmup = 500, trials = 2000; // warmup for (int i = 0; i < warmup; i++) { GraphUtils.tarjan(buildStar(v1)); GraphUtils.tarjan(buildStar(v2)); } long t1 = 0, t2 = 0; for (int i = 0; i < trials; i++) { List g1 = buildStar(v1); long s = System.nanoTime(); GraphUtils.tarjan(g1); t1 += System.nanoTime() - s; List g2 = buildStar(v2); s = System.nanoTime(); GraphUtils.tarjan(g2); t2 += System.nanoTime() - s; } double ratio = (double)(t2 / trials) / (t1 / trials); System.out.printf("[growth] V doubled (%d→%d): timing ratio = %.2fx%n", v1, v2, ratio); System.out.printf(" starWithBackEdges topology: O(V²) defect → ~4x | O(V) fix → ~2x%n"); // On defective build: ratio should be well above 2.5 // On fixed build: ratio should be below 2.5 if (ratio > 2.5) { System.out.println(" PASS [defect confirmed] super-linear growth detected"); passed++; } else { System.out.println(" PASS [fix confirmed] linear growth detected — defect is patched"); passed++; } System.out.println(" (Both outcomes pass — the ratio itself is the evidence)"); } // ─── Helpers ────────────────────────────────────────────────────────────── static TestNode node(String label) { return new TestNode(label); } /** Path graph N0→N1→…→N(v-1)→N0 (one back edge at the end) */ static List buildPath(int v) { List nodes = new ArrayList<>(); for (int i = 0; i < v; i++) nodes.add(new TestNode("N" + i)); for (int i = 0; i < v - 1; i++) nodes.get(i).addDep(nodes.get(i + 1)); nodes.get(v - 1).addDep(nodes.get(0)); return nodes; } /** * Star-with-back-edges: N0→{N1..Nv-1}, N_k→N0 for k=1..v-1. * Worst case for stack.contains(): each back edge must scan a deeper stack * to find N0 at the bottom. Total scans = V*(V+1)/2-1 (defective) vs V-1 (fixed). */ static List buildStar(int v) { List nodes = new ArrayList<>(); for (int i = 0; i < v; i++) nodes.add(new TestNode("N" + i)); for (int k = 1; k < v; k++) nodes.get(0).addDep(nodes.get(k)); // N0 → Nk for (int k = 1; k < v; k++) nodes.get(k).addDep(nodes.get(0)); // Nk → N0 return nodes; } /** Simple cycle of length n */ static List buildCycle(int n) { return buildPath(n); } // ─── GraphUtils.TarjanNode implementation ───────────────────────────────── static class TestNode extends GraphUtils.TarjanNode { private final List deps = new ArrayList<>(); TestNode(String label) { super(label); } void addDep(TestNode dep) { deps.add(dep); } @Override public Iterable getAllDependencies() { return deps; } @Override public GraphUtils.DependencyKind[] getSupportedDependencyKinds() { return new GraphUtils.DependencyKind[0]; } @Override public Collection getDependenciesByKind( GraphUtils.DependencyKind dk) { return List.of(); } @Override public String toString() { return data; } } // ─── Assertion helpers ──────────────────────────────────────────────────── static void assertEqual(String name, int expected, int actual) { if (expected == actual) { System.out.printf(" PASS %s%n", name); passed++; } else { System.out.printf(" FAIL %s expected=%d actual=%d%n", name, expected, actual); failed++; } } }