java-topology/defects/eclipse-jdt/unit/MinimalErasedCandidatesAlgorithm.java

129 lines
4.7 KiB
Java

package unit;
import java.util.*;
/**
* eclipse-jdt-0001: minimalErasedCandidatesAlgorithm BFS work-queue dedup
*
* Demonstrates O(N²) ArrayList.contains vs O(N) HashSet.add dedup when
* building the supertype-hierarchy work-queue in Scope.minimalErasedCandidates.
*
* The real BFS (Scope.java ~4295-4383) walks a type's superinterfaces AND
* superclass, calling typesToVisit.contains() for each candidate. In a deep
* diamond interface graph each BFS step checks K candidates (K = branching
* factor), so total contains-calls = K * N steps * avg-list-size N/2 = O(K*N²).
*
* This test models K=4 candidates per step (one superclass + 3 interfaces),
* which matches the worst-case pattern in the Eclipse JDT source.
*
* SLOW: List<Integer> typesToVisit + typesToVisit.contains(x) → O(N²) ops
* FAST: List + parallel HashSet visitedSet + visitedSet.add(x) → O(N) ops
*/
public class MinimalErasedCandidatesAlgorithm {
// Branching factor: number of supertypes added per BFS node
// (mirrors: superclass + up to 3 interfaces in typical Java hierarchy)
private static final int BRANCH = 4;
// ── SLOW path (ArrayList dedup — mirrors JDT defect) ──────────────────────
// Each BFS step checks BRANCH candidate supertypes via typesToVisit.contains
static long slowBfs(int n) {
List<Integer> typesToVisit = new ArrayList<>();
typesToVisit.add(0);
int max = 1;
long ops = 0;
for (int i = 0; i < max && max < n; i++) {
int base = typesToVisit.get(i);
// Simulate checking BRANCH supertypes per BFS node
for (int k = 1; k <= BRANCH && max < n; k++) {
int candidate = base + k;
ops += typesToVisit.size(); // cost of ArrayList.contains
if (!typesToVisit.contains(candidate)) {
typesToVisit.add(candidate);
max++;
}
}
}
return ops;
}
// ── FAST path (HashSet dedup — the fix) ───────────────────────────────────
static long fastBfs(int n) {
List<Integer> typesToVisit = new ArrayList<>();
Set<Integer> visitedSet = new HashSet<>();
typesToVisit.add(0);
visitedSet.add(0);
int max = 1;
long ops = 0;
for (int i = 0; i < max && max < n; i++) {
int base = typesToVisit.get(i);
for (int k = 1; k <= BRANCH && max < n; k++) {
int candidate = base + k;
ops++; // O(1) hash lookup cost
if (visitedSet.add(candidate)) {
typesToVisit.add(candidate);
max++;
}
}
}
return ops;
}
// ── Test harness ──────────────────────────────────────────────────────────
static class Result {
final String label;
final int n;
final long slowOps;
final long fastOps;
final boolean pass;
Result(String label, int n, long slowOps, long fastOps) {
this.label = label;
this.n = n;
this.slowOps = slowOps;
this.fastOps = fastOps;
double ratio = fastOps > 0 ? (double) slowOps / fastOps : slowOps;
this.pass = ratio >= 5.0;
}
}
public static void main(String[] args) {
int[] sizes = {50, 100, 200, 500};
List<Result> results = new ArrayList<>();
for (int n : sizes) {
long slow = slowBfs(n);
long fast = fastBfs(n);
results.add(new Result("N=" + n, n, slow, fast));
}
System.out.println("eclipse-jdt-0001 MinimalErasedCandidatesAlgorithm");
System.out.println("==================================================");
System.out.printf("%-8s %10s %10s %8s %s%n",
"N", "SLOW ops", "FAST ops", "Ratio", "PASS");
System.out.println("-".repeat(55));
int passed = 0;
int total = results.size();
for (Result r : results) {
double ratio = r.fastOps > 0 ? (double) r.slowOps / r.fastOps : r.slowOps;
String status = r.pass ? "PASS" : "FAIL";
System.out.printf("%-8s %10d %10d %8.1fx %s%n",
r.label, r.slowOps, r.fastOps, ratio, status);
if (r.pass) passed++;
}
System.out.println("-".repeat(55));
System.out.printf("%d/%d PASS%n", passed, total);
if (passed < total) {
System.exit(1);
}
}
}