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>). * 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> 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 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 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 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> listenerSets = new HashMap<>(); long lookups = 0; void addEventListener(String type, int listener) { listenerSets.computeIfAbsent(type, k -> new HashSet<>()); Set set = listenerSets.get(type); lookups++; // one O(1) hash probe set.add(listener); } boolean hasEventListener(String type, int listener) { Set set = listenerSets.get(type); if (set == null) return false; lookups++; return set.contains(listener); } int listenerCount(String type) { Set 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"); } }