java-topology/defects/threejs/unit/ThreeJSObject3DTest.java

275 lines
11 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.HashSet;
import java.util.Map;
import java.util.Set;
/**
* ThreeJSObject3DTest — threejs-0006
*
* Models the CWE-407 defect in Three.js EventDispatcher.addEventListener():
*
* Defective: listeners[type] is a plain Array.
* addEventListener() calls listeners[type].indexOf(listener) before push.
* When N unique listeners are added for the same type:
* O(0) + O(1) + ... + O(N-1) = O(N²)
*
* Fixed: listeners[type] is a Set (Map<type, Set<listener>>).
* addEventListener() calls listenerSet.has(listener) — O(1).
* Total for N additions: O(N).
*
* "Listener" is modelled as an Integer ID. Comparison counts are instrumented
* explicitly — not wall-clock timing.
*
* Run: javac -d . ThreeJSObject3DTest.java && java -ea unit.ThreeJSObject3DTest
*/
public class ThreeJSObject3DTest {
// -----------------------------------------------------------------------
// Defective EventDispatcher: listeners[type] is ArrayList
// indexOf(listener) called before every push — O(N) per add
// -----------------------------------------------------------------------
static class DefectiveEventDispatcher {
private final Map<String, ArrayList<Integer>> listeners = new HashMap<>();
long comparisons = 0;
/**
* Models: if (listeners[type].indexOf(listener) === -1) listeners[type].push(listener)
*/
void addEventListener(String type, int listener) {
listeners.computeIfAbsent(type, k -> new ArrayList<>());
ArrayList<Integer> arr = listeners.get(type);
// O(N) indexOf scan — the defect
for (int existing : arr) {
comparisons++;
if (existing == listener) return; // already present
}
arr.add(listener);
}
/**
* Models: listeners[type] !== undefined && listeners[type].indexOf(listener) !== -1
*/
boolean hasEventListener(String type, int listener) {
ArrayList<Integer> arr = listeners.get(type);
if (arr == null) return false;
for (int existing : arr) {
comparisons++;
if (existing == listener) return true;
}
return false;
}
int listenerCount(String type) {
ArrayList<Integer> arr = listeners.get(type);
return arr == null ? 0 : arr.size();
}
}
// -----------------------------------------------------------------------
// Fixed EventDispatcher: listeners[type] is Set
// has(listener) is O(1) amortised
// -----------------------------------------------------------------------
static class FixedEventDispatcher {
private final Map<String, Set<Integer>> listenerSets = new HashMap<>();
long lookups = 0;
void addEventListener(String type, int listener) {
listenerSets.computeIfAbsent(type, k -> new HashSet<>());
Set<Integer> set = listenerSets.get(type);
lookups++; // one O(1) hash probe
set.add(listener);
}
boolean hasEventListener(String type, int listener) {
Set<Integer> set = listenerSets.get(type);
if (set == null) return false;
lookups++;
return set.contains(listener);
}
int listenerCount(String type) {
Set<Integer> set = listenerSets.get(type);
return set == null ? 0 : set.size();
}
}
// -----------------------------------------------------------------------
// Test 1 — correctness: both dispatchers agree on listener count and has()
// -----------------------------------------------------------------------
static void test1_correctness() {
DefectiveEventDispatcher slow = new DefectiveEventDispatcher();
FixedEventDispatcher fast = new FixedEventDispatcher();
// Add 10 unique listeners for "dispose", with 3 duplicates
int[] listeners = {1, 2, 3, 4, 5, 1, 6, 7, 2, 8, 9, 10, 5};
for (int l : listeners) {
slow.addEventListener("dispose", l);
fast.addEventListener("dispose", l);
}
int slowCount = slow.listenerCount("dispose");
int fastCount = fast.listenerCount("dispose");
assert slowCount == 10 : "defective: expected 10 unique listeners, got " + slowCount;
assert fastCount == 10 : "fixed: expected 10 unique listeners, got " + fastCount;
// Both should agree on hasEventListener
for (int l : new int[]{1, 5, 10, 99}) {
boolean inSlow = slow.hasEventListener("dispose", l);
boolean inFast = fast.hasEventListener("dispose", l);
assert inSlow == inFast
: "disagreement on listener " + l + ": slow=" + inSlow + " fast=" + inFast;
}
System.out.printf("test1: 10 unique listeners (13 adds with 3 dupes), both agree — CORRECT%n");
}
// -----------------------------------------------------------------------
// Test 2 — ratio >= 5x at N=100 unique listeners, same event type
// -----------------------------------------------------------------------
static void test2_ratioAt100() {
int N = 100;
DefectiveEventDispatcher slow = new DefectiveEventDispatcher();
FixedEventDispatcher fast = new FixedEventDispatcher();
for (int i = 0; i < N; i++) {
slow.addEventListener("resize", i);
fast.addEventListener("resize", i);
}
double ratio = (double) slow.comparisons / Math.max(1, fast.lookups);
System.out.printf("test2: N=%d defect=%d fixed=%d ratio=%.1fx%n",
N, slow.comparisons, fast.lookups, ratio);
assert ratio >= 5.0 : "expected ratio >= 5x at N=100, got " + ratio;
}
// -----------------------------------------------------------------------
// Test 3 — ratio >= 50x at N=500 unique listeners, same event type
// -----------------------------------------------------------------------
static void test3_ratioAt500() {
int N = 500;
DefectiveEventDispatcher slow = new DefectiveEventDispatcher();
FixedEventDispatcher fast = new FixedEventDispatcher();
for (int i = 0; i < N; i++) {
slow.addEventListener("update", i);
fast.addEventListener("update", i);
}
double ratio = (double) slow.comparisons / Math.max(1, fast.lookups);
System.out.printf("test3: N=%d defect=%d fixed=%d ratio=%.1fx%n",
N, slow.comparisons, fast.lookups, ratio);
assert ratio >= 50.0 : "expected ratio >= 50x at N=500, got " + ratio;
}
// -----------------------------------------------------------------------
// Test 4 — quadratic growth: doubling N should roughly quadruple defect ops
// -----------------------------------------------------------------------
static void test4_quadraticGrowth() {
int N1 = 200, N2 = 400;
DefectiveEventDispatcher slow1 = new DefectiveEventDispatcher();
DefectiveEventDispatcher slow2 = new DefectiveEventDispatcher();
FixedEventDispatcher fast1 = new FixedEventDispatcher();
FixedEventDispatcher fast2 = new FixedEventDispatcher();
for (int i = 0; i < N1; i++) { slow1.addEventListener("change", i); fast1.addEventListener("change", i); }
for (int i = 0; i < N2; i++) { slow2.addEventListener("change", i); fast2.addEventListener("change", i); }
double defectGrowth = (double) slow2.comparisons / Math.max(1, slow1.comparisons);
double fixedGrowth = (double) fast2.lookups / Math.max(1, fast1.lookups);
System.out.printf("test4: N=%d→%d defect %d→%d (%.2fx) fixed %d→%d (%.2fx)%n",
N1, N2,
slow1.comparisons, slow2.comparisons, defectGrowth,
fast1.lookups, fast2.lookups, fixedGrowth);
// Defect should grow quadratically (~4x when N doubles)
assert defectGrowth >= 3.5
: "expected defect ~4x when N doubles, got " + defectGrowth;
// Fixed should grow linearly (~2x when N doubles)
assert fixedGrowth >= 1.5 && fixedGrowth <= 2.5
: "fixed should grow ~2x when N doubles, got " + fixedGrowth;
}
// -----------------------------------------------------------------------
// Test 5 — duplicate suppression: adding same listener M times should not
// grow the listener array, and total comparison cost is O(M×current_size)
// -----------------------------------------------------------------------
static void test5_duplicateSuppression() {
int N = 20; // unique listeners
int M = 50; // times each listener is re-added
DefectiveEventDispatcher slow = new DefectiveEventDispatcher();
FixedEventDispatcher fast = new FixedEventDispatcher();
// Add N unique listeners first
for (int i = 0; i < N; i++) {
slow.addEventListener("click", i);
fast.addEventListener("click", i);
}
assert slow.listenerCount("click") == N;
assert fast.listenerCount("click") == N;
// Re-add all N listeners M times (simulating sloppy register-on-each-render)
for (int r = 0; r < M; r++) {
for (int i = 0; i < N; i++) {
slow.addEventListener("click", i);
fast.addEventListener("click", i);
}
}
// Size should remain N
assert slow.listenerCount("click") == N
: "defective size wrong after re-adds: " + slow.listenerCount("click");
assert fast.listenerCount("click") == N
: "fixed size wrong after re-adds: " + fast.listenerCount("click");
double ratio = (double) slow.comparisons / Math.max(1, fast.lookups);
System.out.printf("test5: N=%d M=%d re-adds defect=%d fixed=%d ratio=%.1fx — size both=%d%n",
N, M, slow.comparisons, fast.lookups, ratio, N);
assert ratio >= 5.0 : "expected ratio >= 5x for re-add pattern, got " + ratio;
}
// -----------------------------------------------------------------------
// Main
// -----------------------------------------------------------------------
public static void main(String[] args) {
System.out.println("=== ThreeJSObject3DTest ===");
System.out.println("Modelling CWE-407 threejs-0006: EventDispatcher.addEventListener() indexOf O(N²)");
System.out.println("Location: src/core/EventDispatcher.js addEventListener() + hasEventListener()");
System.out.println();
test1_correctness();
System.out.println(" PASS test1_correctness");
test2_ratioAt100();
System.out.println(" PASS test2_ratioAt100");
test3_ratioAt500();
System.out.println(" PASS test3_ratioAt500");
test4_quadraticGrowth();
System.out.println(" PASS test4_quadraticGrowth");
test5_duplicateSuppression();
System.out.println(" PASS test5_duplicateSuppression");
System.out.println();
System.out.println("5/5 PASS");
}
}