java-topology/tests/integration/InferenceGraphScalingTest.java
russell@unturf.com 0a580b313d undefect. CWE-407 — 63 sites patched across 27 ecosystems
Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com

Patches, unit tests, benchmarks, whitepaper, and outreach briefs.
Public domain — no copyright claimed. Use freely.
2026-03-26 17:11:57 -04:00

231 lines
9.5 KiB
Java

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<TestNode> nodes = buildCycle(4);
List<? extends List<? extends TestNode>> 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<TestNode> nodes = List.of(n0, n1, n2, n3);
List<? extends List<? extends TestNode>> 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<TestNode> 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<TestNode> g1 = buildStar(v1);
long s = System.nanoTime();
GraphUtils.tarjan(g1);
t1 += System.nanoTime() - s;
List<TestNode> 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<TestNode> buildPath(int v) {
List<TestNode> 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<TestNode> buildStar(int v) {
List<TestNode> 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<TestNode> buildCycle(int n) {
return buildPath(n);
}
// ─── GraphUtils.TarjanNode implementation ─────────────────────────────────
static class TestNode extends GraphUtils.TarjanNode<String, TestNode> {
private final List<TestNode> deps = new ArrayList<>();
TestNode(String label) { super(label); }
void addDep(TestNode dep) { deps.add(dep); }
@Override public Iterable<? extends TestNode> getAllDependencies() { return deps; }
@Override public GraphUtils.DependencyKind[] getSupportedDependencyKinds() {
return new GraphUtils.DependencyKind[0];
}
@Override public Collection<? extends TestNode> 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++;
}
}
}