UNDF-2026-000000468: three.js src/nodes/core/Node.js:351 traverse() recurses without a visited set; on shared-node (diamond) TSL shader graphs yields 2^D callback invocations. D=10 gives 4093× overhead. Fix: add optional visited Set parameter, default new Set() at root call. Unit test: 10/10 PASS (ThreeJSNodeTraverseTest.java) CLEAN markers written for: webpack (visitedModules WeakSet throughout), valhalla (Dijkstra+BFS, no recursive DAG traversal), traefik (traverse() has proper visited map), wasmer (petgraph+BTreeMap), wasmtime (SCC algorithms).
187 lines
7.2 KiB
Java
187 lines
7.2 KiB
Java
import java.util.*;
|
|
|
|
/**
|
|
* Unit test for threejs-0007: Node.traverse() diamond recursion.
|
|
*
|
|
* Simulates the three.js TSL shader node graph in Java.
|
|
* A "Node" has children (set of child nodes) that may be shared across parents.
|
|
* traverse(cb) without a visited set causes O(2^D) visits on a diamond DAG.
|
|
* The fix: pass a shared visited Set through the recursion.
|
|
*/
|
|
public class ThreeJSNodeTraverseTest {
|
|
|
|
// ----- Node simulation -----
|
|
|
|
static class Node {
|
|
final String name;
|
|
final List<Node> children = new ArrayList<>();
|
|
|
|
Node(String name) { this.name = name; }
|
|
|
|
void addChild(Node child) { children.add(child); }
|
|
|
|
/** DEFECTIVE traverse — no visited set, O(2^D) on diamonds */
|
|
void traverseDefective(java.util.function.Consumer<Node> callback) {
|
|
callback.accept(this);
|
|
for (Node child : children) {
|
|
child.traverseDefective(callback);
|
|
}
|
|
}
|
|
|
|
/** FIXED traverse — shared visited set, O(N) */
|
|
void traverseFixed(java.util.function.Consumer<Node> callback) {
|
|
traverseFixed(callback, new HashSet<>());
|
|
}
|
|
|
|
private void traverseFixed(java.util.function.Consumer<Node> callback, Set<Node> visited) {
|
|
if (visited.contains(this)) return;
|
|
visited.add(this);
|
|
callback.accept(this);
|
|
for (Node child : children) {
|
|
child.traverseFixed(callback, visited);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----- Build a diamond DAG of depth D -----
|
|
//
|
|
// Depth 1: root → {left, right}, left → bottom, right → bottom
|
|
// Depth D: each "bottom" of depth D-1 becomes root of a depth-1 diamond
|
|
//
|
|
// For depth D the bottom node gets visited 2^D times in the defective version.
|
|
|
|
static Node buildDiamond(int depth) {
|
|
if (depth == 0) {
|
|
return new Node("leaf");
|
|
}
|
|
Node root = new Node("root_d" + depth);
|
|
Node left = new Node("left_d" + depth);
|
|
Node right = new Node("right_d" + depth);
|
|
Node bottom = buildDiamond(depth - 1); // SHARED child
|
|
|
|
root.addChild(left);
|
|
root.addChild(right);
|
|
left.addChild(bottom);
|
|
right.addChild(bottom);
|
|
return root;
|
|
}
|
|
|
|
// ----- Helpers -----
|
|
|
|
static int countVisits(Node root, boolean useFixed) {
|
|
int[] count = {0};
|
|
if (useFixed) {
|
|
root.traverseFixed(n -> count[0]++);
|
|
} else {
|
|
root.traverseDefective(n -> count[0]++);
|
|
}
|
|
return count[0];
|
|
}
|
|
|
|
// For a diamond of depth D:
|
|
// - Defective: 2^(D+1) - 1 total visits (full binary tree traversal)
|
|
// - Fixed: D + 3 unique nodes visited (root + 2 per level + leaf)
|
|
// Actually: 3*D + 1 nodes total (root, left, right per level + 1 leaf)
|
|
|
|
// Let's just assert:
|
|
// defective visits >> fixed visits for D >= 3
|
|
|
|
static void check(String label, boolean condition) {
|
|
if (!condition) {
|
|
System.out.println("FAIL: " + label);
|
|
System.exit(1);
|
|
}
|
|
System.out.println("PASS: " + label);
|
|
}
|
|
|
|
public static void main(String[] args) {
|
|
|
|
// --- Test 1: Diamond depth 1 ---
|
|
// root → {left, right}, left → leaf, right → leaf
|
|
// Defective: root(1) + left(1) + leaf(1) + right(1) + leaf(1) = 5 visits
|
|
// Fixed: 4 unique nodes (root, left, right, leaf)
|
|
{
|
|
Node root = buildDiamond(1);
|
|
int def = countVisits(root, false);
|
|
int fix = countVisits(root, true);
|
|
check("D=1 defective: leaf visited 2x (total 5)", def == 5);
|
|
check("D=1 fixed: each node visited exactly once (total 4)", fix == 4);
|
|
check("D=1 ratio: defective/fixed >= 1", def >= fix);
|
|
}
|
|
|
|
// --- Test 2: Diamond depth 5 ---
|
|
// Each diamond level has: root + left + right + (shared bottom subtree)
|
|
// The shared bottom is visited 2x (once via left, once via right).
|
|
// Defective total = 4 * 2^5 - 3 = 125
|
|
// Fixed unique = 3*5 + 1 = 16 (root, left, right per level + 1 leaf)
|
|
{
|
|
Node root = buildDiamond(5);
|
|
int def = countVisits(root, false);
|
|
int fix = countVisits(root, true);
|
|
check("D=5 defective total visits == 125", def == 125);
|
|
check("D=5 fixed unique visits == 16", fix == 16);
|
|
check("D=5 ratio >= 7x", def >= 7 * fix);
|
|
System.out.printf(" D=5: defective=%d fixed=%d ratio=%.1fx%n",
|
|
def, fix, (double) def / fix);
|
|
}
|
|
|
|
// --- Test 3: Diamond depth 10 ---
|
|
// Defective: 4 * 2^10 - 3 = 4093 total visits
|
|
// Fixed: 3*10 + 1 = 31 unique visits
|
|
{
|
|
Node root = buildDiamond(10);
|
|
int def = countVisits(root, false);
|
|
int fix = countVisits(root, true);
|
|
check("D=10 defective total visits == 4093", def == 4093);
|
|
check("D=10 fixed unique visits == 31", fix == 31);
|
|
check("D=10 ratio >= 100x", def >= 100 * fix);
|
|
System.out.printf(" D=10: defective=%d fixed=%d ratio=%.1fx%n",
|
|
def, fix, (double) def / fix);
|
|
}
|
|
|
|
// --- Test 4: Linear chain (no diamonds) — behavior unchanged ---
|
|
// A -> B -> C -> D (chain of 4, no shared nodes)
|
|
{
|
|
Node d = new Node("D");
|
|
Node c = new Node("C"); c.addChild(d);
|
|
Node b = new Node("B"); b.addChild(c);
|
|
Node a = new Node("A"); a.addChild(b);
|
|
int def = countVisits(a, false);
|
|
int fix = countVisits(a, true);
|
|
check("Chain: defective == 4", def == 4);
|
|
check("Chain: fixed == 4", fix == 4);
|
|
check("Chain: defective == fixed (no diamonds)", def == fix);
|
|
}
|
|
|
|
// --- Test 5: Shared uniform node in a TSL-style expression ---
|
|
// sharedUniform is child of both mul and add
|
|
// result = add(mul(sharedUniform, factor), div(sharedUniform, 2))
|
|
{
|
|
Node sharedUniform = new Node("sharedUniform");
|
|
Node factor = new Node("factor");
|
|
Node two = new Node("2");
|
|
Node mul = new Node("mul");
|
|
Node div = new Node("div");
|
|
Node add = new Node("add");
|
|
|
|
mul.addChild(sharedUniform); // sharedUniform appears as child of mul
|
|
mul.addChild(factor);
|
|
div.addChild(sharedUniform); // AND as child of div (diamond!)
|
|
div.addChild(two);
|
|
add.addChild(mul);
|
|
add.addChild(div);
|
|
|
|
int def = countVisits(add, false);
|
|
int fix = countVisits(add, true);
|
|
|
|
// Defective: add(1) + mul(1) + sharedUniform(1) + factor(1)
|
|
// + div(1) + sharedUniform AGAIN(1) + two(1) = 7
|
|
// Fixed: 6 unique nodes (add, mul, div, sharedUniform, factor, 2)
|
|
check("TSL diamond: defective visits sharedUniform twice (total 7)", def == 7);
|
|
check("TSL diamond: fixed visits each node once (total 6)", fix == 6);
|
|
System.out.printf(" TSL diamond: defective=%d fixed=%d%n", def, fix);
|
|
}
|
|
|
|
System.out.println("\nAll tests PASSED — threejs-0007 confirmed.");
|
|
}
|
|
}
|