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.
231 lines
9.5 KiB
Java
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++;
|
|
}
|
|
}
|
|
}
|