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
|
|
@ -0,0 +1,88 @@
|
|||
# threejs-0006: EventDispatcher.addEventListener() O(N²) via indexOf dedup on every add
|
||||
|
||||
**File:** `src/core/EventDispatcher.js`
|
||||
**Lines:** 43 (`addEventListener` indexOf dedup), 64 (`hasEventListener` indexOf)
|
||||
**Severity:** MEDIUM
|
||||
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
|
||||
|
||||
## Description
|
||||
|
||||
`EventDispatcher.addEventListener(type, listener)` checks for duplicate
|
||||
listeners using a linear array scan:
|
||||
|
||||
```js
|
||||
addEventListener( type, listener ) {
|
||||
...
|
||||
if ( listeners[ type ] === undefined ) {
|
||||
listeners[ type ] = [];
|
||||
}
|
||||
|
||||
if ( listeners[ type ].indexOf( listener ) === - 1 ) { // O(N) scan — CWE-407
|
||||
listeners[ type ].push( listener );
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`listeners[type]` is a plain `Array`. `Array.prototype.indexOf()` performs a
|
||||
linear scan over all existing listeners for that event type.
|
||||
|
||||
When `addEventListener` is called N times for the same event type (e.g. adding
|
||||
N unique disposal/resize handlers in a scene setup loop, or attaching per-object
|
||||
listeners in a particle system), the total dedup cost is:
|
||||
|
||||
```
|
||||
O(0) + O(1) + O(2) + ... + O(N-1) = O(N²/2)
|
||||
```
|
||||
|
||||
`hasEventListener(type, listener)` has the same O(N) scan. If called in a
|
||||
render loop that validates listeners exist before dispatching, it compounds the
|
||||
problem.
|
||||
|
||||
Three.js objects — `Material`, `BufferGeometry`, `Texture`, `Object3D` — all
|
||||
extend `EventDispatcher`. A scene with M materials each gaining N listeners
|
||||
during load would incur O(M × N²) total work.
|
||||
|
||||
## Fix
|
||||
|
||||
Replace the per-type `Array` with a `Map<type, Set<listener>>`:
|
||||
|
||||
```js
|
||||
addEventListener( type, listener ) {
|
||||
if ( this._listenerSets === undefined ) this._listenerSets = new Map();
|
||||
|
||||
let listenerSet = this._listenerSets.get( type );
|
||||
if ( listenerSet === undefined ) {
|
||||
listenerSet = new Set();
|
||||
this._listenerSets.set( type, listenerSet );
|
||||
}
|
||||
|
||||
if ( ! listenerSet.has( listener ) ) { // O(1) — fixed
|
||||
listenerSet.add( listener );
|
||||
}
|
||||
}
|
||||
|
||||
hasEventListener( type, listener ) {
|
||||
if ( this._listenerSets === undefined ) return false;
|
||||
const listenerSet = this._listenerSets.get( type );
|
||||
return listenerSet !== undefined && listenerSet.has( listener ); // O(1)
|
||||
}
|
||||
```
|
||||
|
||||
`dispatchEvent` iterates the set (same O(N) iteration, no change in
|
||||
semantics since insertion order is preserved by `Set`).
|
||||
|
||||
## Complexity
|
||||
|
||||
| Path | Before | After |
|
||||
|------|--------|-------|
|
||||
| `addEventListener()` per call | O(N) | O(1) |
|
||||
| Total for N unique listeners, same type | O(N²) | O(N) |
|
||||
| `hasEventListener()` per call | O(N) | O(1) |
|
||||
|
||||
**Speedup:** ~N/2 × at N=500: ~250×
|
||||
|
||||
## References
|
||||
|
||||
- `Material`, `BufferGeometry`, `Texture`, `Object3D` all extend `EventDispatcher`
|
||||
- `WebGLRenderer` calls `material.addEventListener('dispose', ...)` — guarded but pattern propagates
|
||||
- threejs-0001..0005: previous CWE-407 defects in Three.js
|
||||
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