import java.util.*; /** * CWE-407 simulation tests for Natron defects. * * natron-0001: Node graph traversal visited-set O(N^2) via std::list + std::find * * Natron's Node::computeHashRecursive() and related recursive traversal * functions pass a std::list as a visited/marked set and call * std::find() at each node visit to detect already-visited nodes. * std::find on std::list is O(N) per call. With N nodes in the graph, * total cost is O(N^2). The fix replaces std::list with std::unordered_set * for O(1) average membership test, reducing total cost to O(N). * * Hot paths affected: * - computeHashRecursive(): called every parameter change to propagate * cache invalidation downstream through the node graph * - markInputRelatedDataDirtyRecursiveInternal(): called on input changes * - clearPersistentMessageRecursive(): called on connection changes * - addIdentityNodesRecursively(): called per-frame during composition */ public class NatronTest { // --------------------------------------------------------------- // natron-0001: node graph traversal visited-set O(N^2) // --------------------------------------------------------------- /** * Simulate defective graph traversal: std::list + std::find O(N). * * Models computeHashRecursive() which visits each node and checks the * marked list with std::find before adding this node and recursing. * Each visit is O(visited_so_far) due to list scan. * Total for N nodes visited linearly: 0 + 1 + 2 + ... + (N-1) = O(N^2). * * @param numNodes number of nodes in the linear graph chain * @return total comparison operations performed */ static long graphTraversalDefective(int numNodes) { long ops = 0; // Simulate std::list as ArrayList (O(N) contains) List marked = new ArrayList<>(); for (int node = 0; node < numNodes; node++) { // std::find on the list — O(marked.size()) for (int i = 0; i < marked.size(); i++) { ops++; if (marked.get(i).equals(node)) break; } // Not found — push_back marked.add(node); // computeHashInternal() — O(1) per node (not counted here) // Recurse to next node (implicit in sequential model) } return ops; } /** * Simulate fixed graph traversal: std::unordered_set + count() O(1). * * @param numNodes number of nodes in the linear graph chain * @return total comparison operations performed */ static long graphTraversalFixed(int numNodes) { long ops = 0; // Simulate std::unordered_set — O(1) lookup Set marked = new HashSet<>(); for (int node = 0; node < numNodes; node++) { ops++; // hash lookup: O(1) if (!marked.contains(node)) { marked.add(node); } } return ops; } /** * Simulate the DAG (diamond) case: N nodes with fan-out F, * where many nodes are visited multiple times before the visited * check fires. Worse case for defect since std::find grows larger. */ static long dagTraversalDefective(int numNodes, int fanOut) { long ops = 0; List marked = new ArrayList<>(); // Simulate visiting numNodes nodes with shared nodes visited multiple times for (int visit = 0; visit < numNodes * fanOut; visit++) { int node = visit % numNodes; // std::find scan: O(marked.size()) boolean found = false; for (int i = 0; i < marked.size(); i++) { ops++; if (marked.get(i).equals(node)) { found = true; break; } } if (!found) { marked.add(node); } } return ops; } static long dagTraversalFixed(int numNodes, int fanOut) { long ops = 0; Set marked = new HashSet<>(); for (int visit = 0; visit < numNodes * fanOut; visit++) { int node = visit % numNodes; ops++; // O(1) hash lookup marked.add(node); } return ops; } static void testLinearGraph() { int N = 500; // 500-node composition (realistic for complex VFX) long defectOps = graphTraversalDefective(N); long fixedOps = graphTraversalFixed(N); double ratio = (double) defectOps / fixedOps; System.out.printf("natron-0001 linear graph traversal (N=%d nodes):%n", N); System.out.printf(" defect_ops=%d fixed_ops=%d ratio=%.1fx%n", defectOps, fixedOps, ratio); assert ratio > 100.0 : "Expected significant overhead, got " + ratio; System.out.println(" PASS"); } static void testDagGraph() { int N = 200; // unique nodes int FAN = 5; // fan-out factor (shared nodes visited multiple times) long defectOps = dagTraversalDefective(N, FAN); long fixedOps = dagTraversalFixed(N, FAN); double ratio = (double) defectOps / fixedOps; System.out.printf("natron-0001 DAG traversal (N=%d nodes, fan=%d):%n", N, FAN); System.out.printf(" defect_ops=%d fixed_ops=%d ratio=%.1fx%n", defectOps, fixedOps, ratio); assert ratio > 50.0 : "Expected significant overhead, got " + ratio; System.out.println(" PASS"); } static void testHashInvalidationStorm() { // Simulate parameter change on root node: all N downstream nodes // have their hash recomputed via computeHashRecursive. With std::list // visited set, the traversal itself is O(N^2) before any hashing. int N = 300; long defectOps = graphTraversalDefective(N); long fixedOps = graphTraversalFixed(N); double ratio = (double) defectOps / fixedOps; System.out.printf("natron-0001 hash invalidation storm (N=%d nodes):%n", N); System.out.printf(" defect_ops=%d fixed_ops=%d ratio=%.1fx%n", defectOps, fixedOps, ratio); assert ratio > 50.0 : "Expected significant overhead, got " + ratio; System.out.println(" PASS"); } // --------------------------------------------------------------- // Main // --------------------------------------------------------------- public static void main(String[] args) { testLinearGraph(); testDagGraph(); testHashInvalidationStorm(); System.out.println("\nAll Natron CWE-407 tests PASS"); } }