wave15: sm-0003/0004 + threejs-0006 + varnish-0002 + mongodb-0008 — 533/240

This commit is contained in:
russell@unturf.com 2026-03-27 19:22:37 -04:00
parent 7146714143
commit 31850d5ef6
13 changed files with 1893 additions and 9 deletions

View file

@ -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