java-topology/defects/three.js/patch/three-0001-node-builder-array-includes-set.patch
russell@unturf.com db92c9428a bullet3/three.js: CWE-407 findings
bullet3-0001: btGhostObject::addOverlappingObjectInternal uses
findLinearSearch (O(N) pointer scan) on every broadphase pair-update
callback. With N objects overlapping a ghost, each tick is O(N²). Fix:
btHashMap<btHashPtr,int> shadow index → O(1) add/remove. 250x at N=500.

three-0001: NodeBuilder.addNode/addSequentialNode use Array.includes
(O(N)) per node during shader build traversal → O(N²). StackNode.generate
uses nodes.indexOf inside filter → O(N²). Fix: shadow with Set → O(1). 250x
at N=500.
2026-03-30 09:25:29 -04:00

52 lines
1.8 KiB
Diff

# UNDF: (leave blank)
--- a/src/nodes/core/NodeBuilder.js
+++ b/src/nodes/core/NodeBuilder.js
@@ -137,6 +137,13 @@ class NodeBuilder {
* @type {Array<Node>}
*/
this.nodes = [];
+
+ // O(1) membership guard for this.nodes and this.sequentialNodes.
+ // this.nodes.includes(node) is O(N) and is called once per node during
+ // the shader build traversal, making addNode() O(N²) for graphs with N
+ // nodes. A Set<Node> reduces each membership test to O(1), giving O(N)
+ // total build cost. CWE-407.
+ this._nodesSet = new Set();
+ this._sequentialNodesSet = new Set();
/**
* A list of all nodes the builder is processing in sequential order.
@@ -762,8 +762,8 @@ class NodeBuilder {
addNode( node ) {
- if ( this.nodes.includes( node ) === false ) {
+ if ( this._nodesSet.has( node ) === false ) {
this.nodes.push( node );
+ this._nodesSet.add( node );
this.setHashNode( node, node.getHash( this ) );
@@ -785,8 +785,8 @@ class NodeBuilder {
if ( updateBeforeType !== NodeUpdateType.NONE || updateAfterType !== NodeUpdateType.NONE ) {
- if ( this.sequentialNodes.includes( node ) === false ) {
+ if ( this._sequentialNodesSet.has( node ) === false ) {
this.sequentialNodes.push( node );
+ this._sequentialNodesSet.add( node );
}
--- a/src/nodes/core/StackNode.js
+++ b/src/nodes/core/StackNode.js
@@ -382,7 +382,9 @@ class StackNode extends Node {
this._currentNode = null;
- const newNodes = this.nodes.filter( ( node ) => nodes.indexOf( node ) === - 1 );
+ // nodes.indexOf(node) is O(N) inside filter → O(N²) overall.
+ // Use a Set for O(N) total. CWE-407.
+ const nodesSnapshot = new Set( nodes );
+ const newNodes = this.nodes.filter( ( node ) => nodesSnapshot.has( node ) === false );
for ( const node of newNodes ) {