275 lines
11 KiB
Java
275 lines
11 KiB
Java
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");
|
||
}
|
||
}
|