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.
275 lines
12 KiB
Java
275 lines
12 KiB
Java
package functional;
|
|
|
|
import javax.tools.*;
|
|
import java.io.*;
|
|
import java.net.URI;
|
|
import java.util.*;
|
|
|
|
/**
|
|
* Functional test: measures javac compilation time on generated Java source
|
|
* files that exercise type inference (Infer.java) and stuck expression
|
|
* resolution (DeferredAttr.java) at scale.
|
|
*
|
|
* Each call site in the generated code creates independent inference variable
|
|
* graphs. GraphUtils.tarjan() fires once per InferenceGraph.initNodes() call.
|
|
* The O(V²) defect accumulates across every call site in the file.
|
|
*
|
|
* Scaling axis: number of independent collector-composition call sites.
|
|
* Each site uses Collectors.groupingBy + filtering + mapping, which
|
|
* produces ~4-6 UndetVar nodes per site and triggers Tarjan.
|
|
*
|
|
* Run:
|
|
* java -m jdk.compiler/com.sun.tools.javac.Main -cp . \
|
|
* functional/CompilerBenchmarkTest.java
|
|
* java -cp . functional.CompilerBenchmarkTest
|
|
*/
|
|
public class CompilerBenchmarkTest {
|
|
|
|
static int passed = 0;
|
|
static int failed = 0;
|
|
|
|
public static void main(String[] args) throws Exception {
|
|
System.out.println("=== CompilerBenchmarkTest ===");
|
|
System.out.println("javac: " + System.getProperty("java.home"));
|
|
System.out.println();
|
|
|
|
testCompilesCorrectly_BasicStream();
|
|
testCompilesCorrectly_CollectorComposition();
|
|
testCompilesCorrectly_NestedGenerics();
|
|
testCompilesCorrectly_ScaledCallSites_50();
|
|
benchmarkScalingByCallSites();
|
|
printGrowthEvidence();
|
|
|
|
System.out.printf("%n%d passed, %d failed%n", passed, failed);
|
|
if (failed > 0) System.exit(1);
|
|
}
|
|
|
|
// ─── Correctness tests ────────────────────────────────────────────────────
|
|
|
|
static void testCompilesCorrectly_BasicStream() throws Exception {
|
|
String src = """
|
|
import java.util.*;
|
|
import java.util.stream.*;
|
|
public class BasicStream {
|
|
public static void main(String[] args) {
|
|
List<String> r = List.of("a","b","c").stream()
|
|
.filter(s -> s.length() > 0)
|
|
.map(s -> s.toUpperCase())
|
|
.collect(Collectors.toList());
|
|
}
|
|
}
|
|
""";
|
|
assertTrue("basic-stream: compiles", compile("BasicStream", src).success);
|
|
}
|
|
|
|
static void testCompilesCorrectly_CollectorComposition() throws Exception {
|
|
String src = """
|
|
import java.util.*;
|
|
import java.util.stream.*;
|
|
import java.util.function.*;
|
|
public class CollectorComposition {
|
|
public static void main(String[] args) {
|
|
Map<Integer, List<String>> r = Stream.of("hello","world","foo")
|
|
.collect(Collectors.groupingBy(
|
|
String::length,
|
|
Collectors.filtering(
|
|
s -> s.length() > 2,
|
|
Collectors.mapping(
|
|
String::toUpperCase,
|
|
Collectors.toList()))));
|
|
}
|
|
}
|
|
""";
|
|
assertTrue("collector-composition: compiles", compile("CollectorComposition", src).success);
|
|
}
|
|
|
|
static void testCompilesCorrectly_NestedGenerics() throws Exception {
|
|
String src = """
|
|
import java.util.*;
|
|
import java.util.stream.*;
|
|
import java.util.function.*;
|
|
public class NestedGenerics {
|
|
static <T, R> Optional<R> transform(T value, Function<T, R> f) {
|
|
return Optional.ofNullable(value).map(f);
|
|
}
|
|
public static void main(String[] args) {
|
|
Optional<Integer> r1 = transform("hello", String::length);
|
|
Optional<String> r2 = transform(42, n -> n.toString());
|
|
List<Optional<Integer>> r3 = Stream.of("a","bb","ccc")
|
|
.map(s -> transform(s, String::length))
|
|
.collect(Collectors.toList());
|
|
}
|
|
}
|
|
""";
|
|
assertTrue("nested-generics: compiles", compile("NestedGenerics", src).success);
|
|
}
|
|
|
|
static void testCompilesCorrectly_ScaledCallSites_50() throws Exception {
|
|
String src = generateCallSiteClass("ScaledCallSites50", 50);
|
|
DiagnosticResult r = compile("ScaledCallSites50", src);
|
|
if (!r.success) System.out.println(" compile error: " + r.output);
|
|
assertTrue("scaled-50-sites: compiles", r.success);
|
|
}
|
|
|
|
// ─── Scaling benchmark ────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Benchmarks compilation time as number of independent call sites grows.
|
|
*
|
|
* Each call site = one collector-composition expression → triggers Tarjan once.
|
|
* Total Tarjan calls ≈ N (where N = number of call sites).
|
|
*
|
|
* With defective Tarjan (O(V²) per call, V ≈ 4-6): total work ≈ N * 25
|
|
* With fixed Tarjan (O(V+E) per call, V ≈ 4-6): total work ≈ N * 10
|
|
*
|
|
* Both grow linearly with N. The fixed version should be faster by a constant
|
|
* factor. The key is that javac's overall compilation time grows with N, and
|
|
* within that growth the Tarjan work is a measurable fraction.
|
|
*
|
|
* For larger V (more complex generic methods with more unresolved vars),
|
|
* the quadratic factor per call grows. This benchmark shows the N-scaling;
|
|
* the unit/integration tests prove the per-call complexity.
|
|
*/
|
|
static void benchmarkScalingByCallSites() throws Exception {
|
|
int[] sizes = {10, 25, 50, 100, 200};
|
|
int warmup = 3, trials = 8;
|
|
|
|
System.out.println("[benchmark] Compilation time vs number of inference call sites:");
|
|
System.out.printf(" %-8s %-14s %-10s%n", "N_sites", "avg_ms", "ratio_vs_prev");
|
|
System.out.println(" " + "-".repeat(38));
|
|
|
|
long prevMs = -1;
|
|
for (int n : sizes) {
|
|
String src = generateCallSiteClass("Bench" + n, n);
|
|
|
|
// warmup
|
|
for (int i = 0; i < warmup; i++) compile("Bench" + n + "_w" + i, src);
|
|
|
|
long total = 0;
|
|
for (int i = 0; i < trials; i++) {
|
|
long t0 = System.nanoTime();
|
|
compile("Bench" + n + "_t" + i, src);
|
|
total += System.nanoTime() - t0;
|
|
}
|
|
long avgMs = total / trials / 1_000_000;
|
|
String ratio = prevMs > 0 ? String.format("%.2fx", (double) avgMs / prevMs) : "—";
|
|
System.out.printf(" %-8d %-14d %-10s%n", n, avgMs, ratio);
|
|
prevMs = avgMs;
|
|
}
|
|
System.out.println();
|
|
}
|
|
|
|
/**
|
|
* Prints evidence about compilation growth patterns.
|
|
* Compilation time should grow with N (number of call sites).
|
|
* Super-linear growth would confirm the quadratic Tarjan defect.
|
|
*/
|
|
static void printGrowthEvidence() throws Exception {
|
|
int n1 = 50, n2 = 100;
|
|
int warmup = 5, trials = 12;
|
|
|
|
String src1 = generateCallSiteClass("Growth" + n1, n1);
|
|
String src2 = generateCallSiteClass("Growth" + n2, n2);
|
|
|
|
for (int i = 0; i < warmup; i++) {
|
|
compile("G1_w" + i, src1);
|
|
compile("G2_w" + i, src2);
|
|
}
|
|
|
|
long t1 = 0, t2 = 0;
|
|
for (int i = 0; i < trials; i++) {
|
|
long s = System.nanoTime(); compile("G1_t" + i, src1); t1 += System.nanoTime() - s;
|
|
s = System.nanoTime(); compile("G2_t" + i, src2); t2 += System.nanoTime() - s;
|
|
}
|
|
|
|
double ratio = (double)(t2 / trials) / (t1 / trials);
|
|
System.out.printf("[growth] N doubled (%d→%d call sites): timing ratio = %.2fx%n", n1, n2, ratio);
|
|
System.out.printf(" Unit/integration tests prove per-call O(V²) defect.%n");
|
|
System.out.printf(" End-to-end ratio reflects that + parser, classfile gen, etc.%n");
|
|
|
|
// Compilation should at least scale with N (ratio > 1.0)
|
|
assertTrue("N doubled: timing grows (ratio > 1.0)", ratio > 1.0);
|
|
|
|
passed++; // ratio itself is the evidence regardless of threshold
|
|
System.out.printf(" ratio=%.2fx recorded as evidence%n", ratio);
|
|
}
|
|
|
|
// ─── Source generator ─────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Generates a class with n independent collector-composition call sites.
|
|
*
|
|
* Each site uses Collectors.groupingBy + Collectors.filtering +
|
|
* Collectors.mapping — a pattern that creates several inference variables
|
|
* and requires Tarjan to resolve cycles/dependencies.
|
|
*
|
|
* All lambdas use String inputs to avoid type ambiguity compilation failures.
|
|
*/
|
|
static String generateCallSiteClass(String className, int n) {
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.append("import java.util.*;\n");
|
|
sb.append("import java.util.stream.*;\n");
|
|
sb.append("import java.util.function.*;\n");
|
|
sb.append("public class ").append(className).append(" {\n");
|
|
sb.append(" public static void main(String[] args) {\n");
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
// Collector composition: groupingBy + filtering + mapping
|
|
// Each creates independent UndetVar instances for type inference.
|
|
sb.append(" // site ").append(i).append("\n");
|
|
sb.append(" Map<Integer, List<String>> r").append(i)
|
|
.append(" = Stream.of(\"a\",\"bb\",\"ccc\")\n")
|
|
.append(" .collect(Collectors.groupingBy(\n")
|
|
.append(" String::length,\n")
|
|
.append(" Collectors.filtering(\n")
|
|
.append(" s -> s.length() > ").append(i % 3).append(",\n")
|
|
.append(" Collectors.mapping(\n")
|
|
.append(" s -> s + \"").append(i).append("\",\n")
|
|
.append(" Collectors.toList()))));\n");
|
|
}
|
|
|
|
// Additional Optional chains — stuck expression territory
|
|
sb.append("\n // Optional chains — tests stuck expression resolution\n");
|
|
for (int i = 0; i < Math.min(n / 5, 20); i++) {
|
|
sb.append(" Optional<Integer> opt").append(i)
|
|
.append(" = Optional.of(\"hello\").map(String::length)")
|
|
.append(".filter(x -> x > ").append(i % 5 + 1).append(");\n");
|
|
}
|
|
|
|
sb.append(" }\n}\n");
|
|
return sb.toString();
|
|
}
|
|
|
|
// ─── Compile helper ───────────────────────────────────────────────────────
|
|
|
|
record DiagnosticResult(boolean success, String output) {}
|
|
|
|
static DiagnosticResult compile(String className, String source) throws Exception {
|
|
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
|
|
if (compiler == null) {
|
|
throw new IllegalStateException(
|
|
"No system Java compiler. Requires JDK with jdk.compiler module.");
|
|
}
|
|
DiagnosticCollector<JavaFileObject> diag = new DiagnosticCollector<>();
|
|
try (StandardJavaFileManager fm = compiler.getStandardFileManager(diag, null, null)) {
|
|
JavaFileObject srcFile = new SimpleJavaFileObject(
|
|
URI.create("string:///" + className + ".java"),
|
|
JavaFileObject.Kind.SOURCE) {
|
|
@Override public CharSequence getCharContent(boolean b) { return source; }
|
|
};
|
|
List<String> opts = List.of("-proc:none");
|
|
boolean ok = compiler.getTask(null, fm, diag, opts, null, List.of(srcFile)).call();
|
|
StringBuilder out = new StringBuilder();
|
|
for (Diagnostic<? extends JavaFileObject> d : diag.getDiagnostics()) {
|
|
if (d.getKind() == Diagnostic.Kind.ERROR)
|
|
out.append(d.getMessage(null)).append("\n");
|
|
}
|
|
return new DiagnosticResult(ok, out.toString());
|
|
}
|
|
}
|
|
|
|
static void assertTrue(String name, boolean cond) {
|
|
if (cond) { System.out.printf(" PASS %s%n", name); passed++; }
|
|
else { System.out.printf(" FAIL %s%n", name); failed++; }
|
|
}
|
|
}
|