java-topology/defects/neo4j/unit/Neo4jTest.java

172 lines
6 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package unit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* neo4j-0001: Dijkstra predecessors List.contains() — O(E×P) vs O(E)
*
* Simulates the predecessor deduplication guard from Dijkstra.java:324.
* The defective path uses List<String> (ArrayList.contains = O(N)).
* The fixed path uses Set<String> (HashSet.contains = O(1)).
*
* Each "relationship" is a unique String object.
* We measure how many equality comparisons .contains() triggers
* by wrapping items in a counted comparator object.
*/
public class Neo4jTest {
static int slowContainsOps = 0;
static int fastContainsOps = 0;
static class CountedRel {
final int id;
CountedRel(int id) { this.id = id; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CountedRel)) return false;
// Count each comparison
slowContainsOps++;
return this.id == ((CountedRel) o).id;
}
@Override
public int hashCode() {
return id;
}
}
static class CountedRelFast {
final int id;
CountedRelFast(int id) { this.id = id; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CountedRelFast)) return false;
fastContainsOps++;
return this.id == ((CountedRelFast) o).id;
}
@Override
public int hashCode() {
return id; // proper hash — Set.contains() can short-circuit
}
}
/**
* Defective path: List<Rel> predecessors.
* For N predecessors, each contains() call costs O(N) in the worst case
* (item not present triggers full scan).
*/
static void slowPath(int n) {
Map<Integer, List<CountedRel>> predecessors = new HashMap<>();
// Simulate adding n unique relationships to the predecessor list for node 0
predecessors.put(0, new ArrayList<>());
for (int i = 0; i < n; i++) {
CountedRel rel = new CountedRel(i);
List<CountedRel> preds = predecessors.get(0);
// This is the defective guard: O(P) scan per call
if (!preds.contains(rel)) {
preds.add(rel);
}
}
// Now simulate a second pass: trying to add the same rels again (duplicates)
// Each contains() now scans all n existing entries
for (int i = 0; i < n; i++) {
CountedRel rel = new CountedRel(i);
List<CountedRel> preds = predecessors.get(0);
if (!preds.contains(rel)) {
preds.add(rel);
}
}
}
/**
* Fixed path: Set<Rel> predecessors.
* HashSet.contains() is O(1) — hash lookup, equals only called on collision.
*/
static void fastPath(int n) {
Map<Integer, Set<CountedRelFast>> predecessors = new HashMap<>();
predecessors.put(0, new LinkedHashSet<>());
for (int i = 0; i < n; i++) {
CountedRelFast rel = new CountedRelFast(i);
Set<CountedRelFast> preds = predecessors.get(0);
preds.add(rel); // Set.add() deduplicates; no explicit contains() needed
}
// Duplicate pass: set semantics are idempotent
for (int i = 0; i < n; i++) {
CountedRelFast rel = new CountedRelFast(i);
predecessors.get(0).add(rel);
}
}
public static void main(String[] args) {
int N = 500;
int PASSES = 3;
// Warm up
slowPath(10);
fastPath(10);
slowContainsOps = 0;
fastContainsOps = 0;
// Measure
for (int p = 0; p < PASSES; p++) {
slowPath(N);
fastPath(N);
}
// For the slow path: first pass adds N items with 0..N-1 scans = N*(N-1)/2 ops
// Second pass finds all N items present: each scan goes full N = N*N ops
// Total per call: ~N^2/2 + N^2 = ~1.5*N^2 comparisons
// For fast path: hash collisions are rare; ideally ~0 equals() calls for unique ids
long expectedSlowMin = (long)(N * N / 4); // conservative lower bound per pass
long expectedFastMax = (long)(N * PASSES * 2); // generous upper bound
System.out.println("N=" + N + " PASSES=" + PASSES);
System.out.println("slow contains ops : " + slowContainsOps + " (expected >=" + expectedSlowMin + ")");
System.out.println("fast contains ops : " + fastContainsOps + " (expected <=" + expectedFastMax + ")");
int passed = 0;
int total = 0;
total++;
if (slowContainsOps >= expectedSlowMin) {
System.out.println("PASS 1/" + total + ": slow path O(n^2) confirmed (ops=" + slowContainsOps + " >= " + expectedSlowMin + ")");
passed++;
} else {
System.out.println("FAIL 1/" + total + ": slow path did not show expected O(n^2) ops");
}
total++;
if (fastContainsOps <= expectedFastMax) {
System.out.println("PASS 2/" + total + ": fast path O(1) confirmed (ops=" + fastContainsOps + " <= " + expectedFastMax + ")");
passed++;
} else {
System.out.println("FAIL 2/" + total + ": fast path showed too many comparisons: " + fastContainsOps);
}
total++;
// Ratio must be at least 10x
boolean ratioOk = slowContainsOps >= fastContainsOps * 10;
if (fastContainsOps == 0 || ratioOk) {
System.out.println("PASS 3/" + total + ": ratio slow/fast is " +
(fastContainsOps == 0 ? "inf" : (slowContainsOps / fastContainsOps)) + "x (expected >=10x)");
passed++;
} else {
System.out.println("FAIL 3/" + total + ": ratio too small: slow=" + slowContainsOps + " fast=" + fastContainsOps);
}
System.out.println(passed + "/" + total + " PASS");
if (passed != total) System.exit(1);
}
}