wave15: sm-0003/0004 + threejs-0006 + varnish-0002 + mongodb-0008 — 533/240
This commit is contained in:
parent
7146714143
commit
31850d5ef6
13 changed files with 1893 additions and 9 deletions
275
defects/threejs/unit/ThreeJSObject3DTest.java
Normal file
275
defects/threejs/unit/ThreeJSObject3DTest.java
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
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");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue