natron-0001: Node graph traversal visited-set O(N^2) via std::list+std::find Engine/Node.cpp computeHashRecursive and 3+ sibling functions use std::list<Node*> as visited set with O(N) std::find per visit = O(N^2). Fix: std::unordered_set<Node*>. 249.5x at N=500 nodes. 3/3 PASS. ardour-0001: PluginManager blacklist/rescan PluginInfoList O(I*N) libs/ardour/plugin_manager.cc blacklist() and rescan_plugin() call std::find on pil (N plugins) for each of I scan-log entries = O(I*N). Fix: unordered_set + remove_if. 19.4x at N=1000 I=20. 3/3 PASS. MOADs 0002-0005: CLEAN with notes in SCAN-MOAD-0002-0005.md each.
167 lines
6.5 KiB
Java
167 lines
6.5 KiB
Java
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<Node*> 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<Node*> + 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<Node*> as ArrayList (O(N) contains)
|
|
List<Integer> 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<Node*> + 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<Node*> — O(1) lookup
|
|
Set<Integer> 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<Integer> 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<Integer> 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");
|
|
}
|
|
}
|