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.
154 lines
5.6 KiB
Java
154 lines
5.6 KiB
Java
import java.util.*;
|
|
|
|
/**
|
|
* CWE-407 unit test for three.js NodeBuilder defect.
|
|
*
|
|
* three-0001: src/nodes/core/NodeBuilder.js addNode() / addSequentialNode()
|
|
* src/nodes/core/StackNode.js generate()
|
|
*
|
|
* NodeBuilder.addNode() guards deduplication with:
|
|
* if ( this.nodes.includes( node ) === false ) { ... }
|
|
* Array.includes() is O(N). It is called once per node during the shader
|
|
* build traversal (Node.build → builder.addNode), making the full build
|
|
* O(N²) in the number of unique nodes.
|
|
*
|
|
* addSequentialNode() has the identical pattern with sequentialNodes.
|
|
*
|
|
* StackNode.generate() compounds this:
|
|
* const newNodes = this.nodes.filter( n => nodes.indexOf(n) === -1 );
|
|
* filter is O(N), indexOf is O(N) per element → O(N²) to compute the
|
|
* newly-added-node delta.
|
|
*
|
|
* Fix: shadow Array with Set for O(1) has() checks.
|
|
*
|
|
* Severity: MEDIUM — called during shader compilation (material first-frame
|
|
* or material.needsUpdate). Complex scenes with N=300+ nodes (particles,
|
|
* post-processing chains) stall on first render due to quadratic build cost.
|
|
*
|
|
* Speedup: O(N²) → O(N); measured ~250x op-count reduction at N=500.
|
|
*/
|
|
public class ThreeJsTest {
|
|
|
|
// --- defect simulation: Array.includes dedup (O(N) per call) ---
|
|
|
|
static void addNode_array(List<Object> nodes, Object node) {
|
|
if (!nodes.contains(node)) { // O(N)
|
|
nodes.add(node);
|
|
}
|
|
}
|
|
|
|
// Build simulation: traverse N nodes, each calling addNode
|
|
static long buildShader_array(List<Object> allNodes) {
|
|
List<Object> visited = new ArrayList<>();
|
|
long ops = 0;
|
|
for (Object node : allNodes) {
|
|
// simulate nodes.includes(node): scan visited
|
|
boolean found = false;
|
|
for (Object v : visited) { ops++; if (v == node) { found = true; break; } }
|
|
if (!found) {
|
|
visited.add(node);
|
|
ops++; // the push
|
|
}
|
|
}
|
|
return ops;
|
|
}
|
|
|
|
// --- fix simulation: Set.has dedup (O(1) per call) ---
|
|
|
|
static void addNode_set(List<Object> nodes, Set<Object> nodeSet, Object node) {
|
|
if (!nodeSet.contains(node)) { // O(1)
|
|
nodes.add(node);
|
|
nodeSet.add(node);
|
|
}
|
|
}
|
|
|
|
static long buildShader_set(List<Object> allNodes) {
|
|
List<Object> visited = new ArrayList<>();
|
|
Set<Object> visitedSet = new HashSet<>();
|
|
long ops = 0;
|
|
for (Object node : allNodes) {
|
|
ops++; // O(1) set lookup
|
|
if (!visitedSet.contains(node)) {
|
|
visited.add(node);
|
|
visitedSet.add(node);
|
|
}
|
|
}
|
|
return ops;
|
|
}
|
|
|
|
// --- filter delta simulation: nodes.indexOf vs Set.has ---
|
|
|
|
static long filterDelta_indexOf(List<Object> allNodes, List<Object> snapshot) {
|
|
long ops = 0;
|
|
List<Object> newNodes = new ArrayList<>();
|
|
for (Object node : allNodes) {
|
|
// indexOf O(N) per element
|
|
boolean found = false;
|
|
for (Object s : snapshot) { ops++; if (s == node) { found = true; break; } }
|
|
if (!found) newNodes.add(node);
|
|
}
|
|
return ops;
|
|
}
|
|
|
|
static long filterDelta_set(List<Object> allNodes, List<Object> snapshot) {
|
|
long ops = 0;
|
|
Set<Object> snapshotSet = new HashSet<>(snapshot);
|
|
List<Object> newNodes = new ArrayList<>();
|
|
for (Object node : allNodes) {
|
|
ops++; // O(1) set lookup
|
|
if (!snapshotSet.contains(node)) newNodes.add(node);
|
|
}
|
|
return ops;
|
|
}
|
|
|
|
static void testThree0001() throws Exception {
|
|
int N = 500;
|
|
|
|
// Build N unique node objects
|
|
List<Object> allNodes = new ArrayList<>();
|
|
for (int i = 0; i < N; i++) allNodes.add(new Object());
|
|
|
|
// Shader build dedup
|
|
long arrayOps = buildShader_array(allNodes);
|
|
long setOps = buildShader_set(allNodes);
|
|
|
|
double buildRatio = (double) arrayOps / setOps;
|
|
System.out.printf("three-0001 addNode N=%d array_ops=%d set_ops=%d ratio=%.1fx%n",
|
|
N, arrayOps, setOps, buildRatio);
|
|
if (buildRatio < 2.0) {
|
|
throw new Exception("FAIL: addNode ratio expected >= 2x, got " + buildRatio);
|
|
}
|
|
|
|
// StackNode.generate filter-delta
|
|
// snapshot = first half of nodes, allNodes = all N
|
|
List<Object> snapshot = new ArrayList<>(allNodes.subList(0, N / 2));
|
|
long idxOps = filterDelta_indexOf(allNodes, snapshot);
|
|
long setOps2 = filterDelta_set(allNodes, snapshot);
|
|
|
|
double filterRatio = (double) idxOps / setOps2;
|
|
System.out.printf("three-0001 filter N=%d indexOf_ops=%d set_ops=%d ratio=%.1fx%n",
|
|
N, idxOps, setOps2, filterRatio);
|
|
if (filterRatio < 2.0) {
|
|
throw new Exception("FAIL: filter ratio expected >= 2x, got " + filterRatio);
|
|
}
|
|
|
|
// Correctness: final visited sets must be equal
|
|
List<Object> arrayVisited = new ArrayList<>();
|
|
List<Object> setVisited = new ArrayList<>();
|
|
Set<Object> setMirror = new HashSet<>();
|
|
for (Object node : allNodes) {
|
|
addNode_array(arrayVisited, node);
|
|
addNode_set(setVisited, setMirror, node);
|
|
}
|
|
if (!new HashSet<>(arrayVisited).equals(new HashSet<>(setVisited))) {
|
|
throw new Exception("FAIL: visited sets differ between array and set implementations");
|
|
}
|
|
|
|
System.out.println("three-0001 PASS");
|
|
}
|
|
|
|
public static void main(String[] args) throws Exception {
|
|
testThree0001();
|
|
System.out.println("All three.js tests PASS");
|
|
}
|
|
}
|