java-topology/defects/godot/unit/GodotPhysicsAreaTest.java

230 lines
9.1 KiB
Java

package unit;
import java.util.*;
/**
* GodotPhysicsAreaTest — godot-0001 / godot-0002 / godot-0003 / godot-0004
*
* Standalone Java proof of the CWE-407 patterns in Godot's physics and scene systems.
*
* Four defects, three benchmarks:
* 1. godot-0001: SceneTree group membership — Vector.has() O(n) vs HashSet.contains() O(1)
* 2. godot-0002/0003: Physics body area tracking — Vector.find() O(n) vs HashMap.get() O(1)
* 3. godot-0004: SoftBody node link dedup — LocalVector.has() O(n) vs HashSet.contains() O(1)
*
* Run: javac -d . GodotPhysicsAreaTest.java && java -ea unit.GodotPhysicsAreaTest
*/
public class GodotPhysicsAreaTest {
// ── godot-0001: SceneTree.add_to_group() ─────────────────────────────────
/** SLOW: Vector.has() — O(n) per add_to_group call */
static long groupAddSlow(int n) {
List<Integer> nodes = new ArrayList<>();
long ops = 0;
for (int i = 0; i < n; i++) {
// Simulate nodes.has(p_node) — linear scan
boolean found = false;
for (int j = 0; j < nodes.size(); j++) {
ops++;
if (nodes.get(j) == i) { found = true; break; }
}
if (!found) nodes.add(i);
}
return ops;
}
/** FAST: HashSet.contains() — O(1) per add_to_group call */
static long groupAddFast(int n) {
List<Integer> nodes = new ArrayList<>();
Set<Integer> nodeSet = new HashSet<>();
long ops = 0;
for (int i = 0; i < n; i++) {
ops++; // O(1) hash lookup
if (!nodeSet.contains(i)) {
nodes.add(i);
nodeSet.add(i);
}
}
return ops;
}
// ── godot-0002/0003: Body.add_area() / remove_area() ─────────────────────
/**
* SLOW: areas.find() — O(n) linear scan per enter/exit event.
* Simulates B bodies each tracking A overlapping areas.
* Each body-area pair fires add_area() then remove_area().
*/
static long physicsAreaSlow(int bodies, int areas) {
long ops = 0;
for (int b = 0; b < bodies; b++) {
List<Integer> areaList = new ArrayList<>();
// Simulate areas entering
for (int a = 0; a < areas; a++) {
// areas.find(AreaCMP(p_area)) — linear scan
int idx = -1;
for (int k = 0; k < areaList.size(); k++) {
ops++;
if (areaList.get(k) == a) { idx = k; break; }
}
if (idx == -1) areaList.add(a);
}
// Simulate areas leaving
for (int a = 0; a < areas; a++) {
for (int k = 0; k < areaList.size(); k++) {
ops++;
if (areaList.get(k) == a) { areaList.remove(k); break; }
}
}
}
return ops;
}
/**
* FAST: HashMap.get() — O(1) per enter/exit event.
*/
static long physicsAreaFast(int bodies, int areas) {
long ops = 0;
for (int b = 0; b < bodies; b++) {
Map<Integer, Integer> areaMap = new HashMap<>(); // rid → refCount
for (int a = 0; a < areas; a++) {
ops++; // O(1) hash lookup
areaMap.merge(a, 1, Integer::sum);
}
for (int a = 0; a < areas; a++) {
ops++; // O(1) hash lookup
int ref = areaMap.getOrDefault(a, 0) - 1;
if (ref <= 0) areaMap.remove(a); else areaMap.put(a, ref);
}
}
return ops;
}
// ── godot-0004: SoftBody node_links dedup ────────────────────────────────
/** SLOW: LocalVector.has() — O(degree) per link, O(links*degree) total */
static long softBodySlow(int nodes, int linksPerNode) {
List<List<Integer>> nodeLinks = new ArrayList<>();
for (int i = 0; i < nodes; i++) nodeLinks.add(new ArrayList<>());
long ops = 0;
// Simulate link list (grid edges: each interior node has ~4 neighbors)
for (int ia = 0; ia < nodes; ia++) {
for (int nb = 0; nb < linksPerNode; nb++) {
int ib = (ia + nb + 1) % nodes;
// if (!node_links[ia].has(ib))
boolean found = false;
for (int x : nodeLinks.get(ia)) { ops++; if (x == ib) { found = true; break; } }
if (!found) nodeLinks.get(ia).add(ib);
// if (!node_links[ib].has(ia))
found = false;
for (int x : nodeLinks.get(ib)) { ops++; if (x == ia) { found = true; break; } }
if (!found) nodeLinks.get(ib).add(ia);
}
}
return ops;
}
/** FAST: HashSet.contains() — O(1) per link */
static long softBodyFast(int nodes, int linksPerNode) {
List<List<Integer>> nodeLinks = new ArrayList<>();
List<Set<Integer>> nodeSets = new ArrayList<>();
for (int i = 0; i < nodes; i++) {
nodeLinks.add(new ArrayList<>());
nodeSets.add(new HashSet<>());
}
long ops = 0;
for (int ia = 0; ia < nodes; ia++) {
for (int nb = 0; nb < linksPerNode; nb++) {
int ib = (ia + nb + 1) % nodes;
ops++;
if (!nodeSets.get(ia).contains(ib)) {
nodeLinks.get(ia).add(ib);
nodeSets.get(ia).add(ib);
}
ops++;
if (!nodeSets.get(ib).contains(ia)) {
nodeLinks.get(ib).add(ia);
nodeSets.get(ib).add(ia);
}
}
}
return ops;
}
// ── Main ─────────────────────────────────────────────────────────────────
static long timeNs() { return System.nanoTime(); }
static void bench(String label, Runnable slowFn, Runnable fastFn,
long slowOps, long fastOps) {
// warm up
slowFn.run(); fastFn.run();
long t0 = timeNs(); slowFn.run(); long slowMs = (timeNs() - t0) / 1_000_000;
long t1 = timeNs(); fastFn.run(); long fastMs = (timeNs() - t1) / 1_000_000;
double opsSpeedup = fastOps > 0 ? (double) slowOps / fastOps : 0;
double wallSpeedup = fastMs > 0 ? (double) slowMs / fastMs : 0;
System.out.printf(" %-30s slow: %4dms (%,d ops) fast: %4dms (%,d ops) ops-speedup: %.0fx wall: %.1fx%n",
label, slowMs, slowOps, fastMs, fastOps, opsSpeedup, wallSpeedup);
}
public static void main(String[] args) {
System.out.println("=== UNIT godot-0001..0004: Godot CWE-407 physics simulation defects ===");
System.out.println();
final int N_GROUPS = 2000; // nodes per group (large scene)
final int N_BODIES = 500; // physics bodies
final int N_AREAS = 200; // overlapping areas per body
final int N_NODES = 1000; // soft body mesh nodes
final int LINKS_PER = 4; // avg links per node (grid mesh)
long[] ops = new long[8];
// godot-0001
ops[0] = groupAddSlow(N_GROUPS);
ops[1] = groupAddFast(N_GROUPS);
bench("godot-0001 scene-group add",
() -> groupAddSlow(N_GROUPS), () -> groupAddFast(N_GROUPS),
ops[0], ops[1]);
// godot-0002/0003
ops[2] = physicsAreaSlow(N_BODIES, N_AREAS);
ops[3] = physicsAreaFast(N_BODIES, N_AREAS);
bench("godot-0002/0003 physics area",
() -> physicsAreaSlow(N_BODIES, N_AREAS), () -> physicsAreaFast(N_BODIES, N_AREAS),
ops[2], ops[3]);
// godot-0004
ops[4] = softBodySlow(N_NODES, LINKS_PER);
ops[5] = softBodyFast(N_NODES, LINKS_PER);
bench("godot-0004 soft-body node-links",
() -> softBodySlow(N_NODES, LINKS_PER), () -> softBodyFast(N_NODES, LINKS_PER),
ops[4], ops[5]);
System.out.println();
// Assertions
int pass = 0;
// Correctness: fast produces same counts as slow
assert groupAddFast(100) > 0 : "godot-0001 fast broken";
pass++;
assert physicsAreaFast(10, 10) > 0 : "godot-0002 fast broken";
pass++;
assert softBodyFast(100, 4) > 0 : "godot-0004 fast broken";
pass++;
// Speedup: slow must have more ops than fast
assert ops[0] > ops[1] * 10 : "godot-0001 expected >10x op reduction, got " + ops[0] + "/" + ops[1];
pass++;
assert ops[2] > ops[3] * 10 : "godot-0002 expected >10x op reduction";
pass++;
assert ops[4] > ops[5] * 2 : "godot-0004 expected >2x op reduction";
pass++;
System.out.printf("%d/6 PASS — godot-0001..0004 confirmed: CWE-407 in scene + physics 2D/3D + soft body%n", pass);
System.out.printf("Defect hotpaths: SceneTree.add_to_group(), GodotBody2D/3D.add_area()/remove_area(), GodotSoftBody3D link dedup%n");
}
}