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 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 allNodes) { List 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 nodes, Set nodeSet, Object node) { if (!nodeSet.contains(node)) { // O(1) nodes.add(node); nodeSet.add(node); } } static long buildShader_set(List allNodes) { List visited = new ArrayList<>(); Set 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 allNodes, List snapshot) { long ops = 0; List 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 allNodes, List snapshot) { long ops = 0; Set snapshotSet = new HashSet<>(snapshot); List 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 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 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 arrayVisited = new ArrayList<>(); List setVisited = new ArrayList<>(); Set 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"); } }