java-topology/defects/box2d/unit/Box2DAlgorithm.java

253 lines
9.1 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package unit;
import java.util.*;
/**
* Box2DAlgorithm — unit test for Box2D CWE-407 defect.
*
* box2d-0001: b2BroadPhase b2UnBufferMove() linear scan of moveArray.
*
* File: src/broad_phase.c lines 7188
* Defect: b2UnBufferMove() first removes proxyKey from moveSet in O(1),
* then scans moveArray linearly to find the array index for RemoveSwap.
* With N buffered proxies, destroying all bodies triggers O(N²) comparisons.
*
* for ( int i = 0; i < count; ++i )
* {
* if ( bp->moveArray.data[i] == proxyKey ) // linear scan
* {
* b2IntArray_RemoveSwap( &bp->moveArray, i );
* break;
* }
* }
*
* Fix: maintain a parallel HashMap<proxyKey, arrayIndex> (moveIndex).
* On bufferMove: record index. On unBufferMove: O(1) lookup, then
* RemoveSwap with displaced-element index fix-up.
*
* The dev comment reads: "Purge from move buffer. Linear search."
* and a TODO: "todo if I can iterate the move set then I don't need the moveArray"
*
* Tests: correctness + performance ratio >= 5x at N=500-1000.
* Prints: N/N PASS
*/
public class Box2DAlgorithm {
static int passed = 0;
static int total = 0;
static void pass(String name) {
passed++;
total++;
System.out.println(" PASS " + name);
}
static void fail(String name, String reason) {
total++;
System.out.println(" FAIL " + name + "" + reason);
}
// -----------------------------------------------------------------------
// Slow path: mirrors b2UnBufferMove (linear scan of moveArray)
// -----------------------------------------------------------------------
static class SlowMoveBuffer {
Set<Integer> moveSet = new HashSet<>();
List<Integer> moveArray = new ArrayList<>();
void bufferMove(int proxyKey) {
if (moveSet.add(proxyKey)) {
moveArray.add(proxyKey);
}
}
/** Returns number of comparisons made during the linear scan. */
long unBufferMove(int proxyKey) {
long ops = 0;
if (moveSet.remove(proxyKey)) {
for (int i = 0; i < moveArray.size(); i++) {
ops++;
if (moveArray.get(i).equals(proxyKey)) {
int last = moveArray.size() - 1;
moveArray.set(i, moveArray.get(last));
moveArray.remove(last);
break;
}
}
}
return ops;
}
List<Integer> snapshot() { return new ArrayList<>(moveArray); }
}
// -----------------------------------------------------------------------
// Fast path: O(1) removal via index map (the proposed fix)
// -----------------------------------------------------------------------
static class FastMoveBuffer {
Set<Integer> moveSet = new HashSet<>();
List<Integer> moveArray = new ArrayList<>();
Map<Integer, Integer> indexMap = new HashMap<>(); // proxyKey → array index
void bufferMove(int proxyKey) {
if (moveSet.add(proxyKey)) {
indexMap.put(proxyKey, moveArray.size());
moveArray.add(proxyKey);
}
}
/** Returns 1 (one map lookup) per call regardless of array size. */
long unBufferMove(int proxyKey) {
if (moveSet.remove(proxyKey)) {
Integer idx = indexMap.remove(proxyKey);
if (idx != null) {
int last = moveArray.size() - 1;
if (idx != last) {
Integer displaced = moveArray.get(last);
moveArray.set(idx, displaced);
indexMap.put(displaced, idx);
}
moveArray.remove(last);
}
return 1;
}
return 0;
}
List<Integer> snapshot() { return new ArrayList<>(moveArray); }
}
// -----------------------------------------------------------------------
// Test helpers
// -----------------------------------------------------------------------
static long runSlowDestroyAll(int n) {
SlowMoveBuffer buf = new SlowMoveBuffer();
for (int i = 0; i < n; i++) buf.bufferMove(i);
long ops = 0;
for (int i = 0; i < n; i++) ops += buf.unBufferMove(i);
return ops;
}
static long runFastDestroyAll(int n) {
FastMoveBuffer buf = new FastMoveBuffer();
for (int i = 0; i < n; i++) buf.bufferMove(i);
long ops = 0;
for (int i = 0; i < n; i++) ops += buf.unBufferMove(i);
return ops;
}
// -----------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------
static void testCorrectness() {
// Buffer N proxies, remove half, check both paths have identical final arrays.
int N = 50;
SlowMoveBuffer slow = new SlowMoveBuffer();
FastMoveBuffer fast = new FastMoveBuffer();
for (int i = 0; i < N; i++) { slow.bufferMove(i); fast.bufferMove(i); }
// Remove even keys
for (int i = 0; i < N; i += 2) { slow.unBufferMove(i); fast.unBufferMove(i); }
List<Integer> slowSnap = slow.snapshot();
List<Integer> fastSnap = fast.snapshot();
Collections.sort(slowSnap);
Collections.sort(fastSnap);
if (!slowSnap.equals(fastSnap)) {
fail("box2d-0001 correctness N=50 half-remove",
"snapshots differ: slow=" + slowSnap + " fast=" + fastSnap);
return;
}
pass("box2d-0001 correctness N=50 half-remove (snapshots match)");
}
static void testCorrectnessWithDups() {
// Duplicate bufferMove calls should be idempotent in both paths.
int N = 20;
SlowMoveBuffer slow = new SlowMoveBuffer();
FastMoveBuffer fast = new FastMoveBuffer();
for (int i = 0; i < N; i++) { slow.bufferMove(i); fast.bufferMove(i); }
// buffer the same keys again — should not double-add
for (int i = 0; i < N; i++) { slow.bufferMove(i); fast.bufferMove(i); }
if (slow.moveArray.size() != N) {
fail("box2d-0001 dup-bufferMove slow dedup", "size=" + slow.moveArray.size());
return;
}
if (fast.moveArray.size() != N) {
fail("box2d-0001 dup-bufferMove fast dedup", "size=" + fast.moveArray.size());
return;
}
pass("box2d-0001 dup-bufferMove N=20 (both deduplicate correctly)");
}
static void testPerformanceDestroyAll() {
// N=1000: all proxies buffered then all destroyed.
// Slow: sum of array sizes scanned — O(N²/2).
// Fast: exactly N ops — O(N).
int N = 1000;
long slowOps = runSlowDestroyAll(N);
long fastOps = runFastDestroyAll(N);
// Slow expected: N + (N-1) + ... + 1 = N*(N+1)/2 (worst case ordering)
// Actual may vary by removal order; just verify ratio.
double ratio = (double) slowOps / fastOps;
System.out.printf(" box2d-0001 N=%d slow_ops=%,d fast_ops=%,d ratio=%.0fx%n",
N, slowOps, fastOps, ratio);
if (fastOps != N) {
fail("box2d-0001 performance N=1000", "fast_ops=" + fastOps + " expected=" + N);
return;
}
if (ratio < 5.0) {
fail("box2d-0001 performance N=1000", "ratio=" + ratio + " < 5x");
return;
}
pass("box2d-0001 performance ratio >= 5x at N=1000 (destroy-all)");
}
static void testPerformanceHalfFill() {
// N=800 buffered, N/2 removed (mixed static/dynamic pattern).
int N = 800;
SlowMoveBuffer slow = new SlowMoveBuffer();
FastMoveBuffer fast = new FastMoveBuffer();
for (int i = 0; i < N; i++) { slow.bufferMove(i); fast.bufferMove(i); }
long slowOps = 0, fastOps = 0;
for (int i = 0; i < N / 2; i++) {
slowOps += slow.unBufferMove(i);
fastOps += fast.unBufferMove(i);
}
double ratio = (double) slowOps / fastOps;
System.out.printf(" box2d-0001 N=%d half-remove slow_ops=%,d fast_ops=%,d ratio=%.0fx%n",
N, slowOps, fastOps, ratio);
if (ratio < 5.0) {
fail("box2d-0001 performance N=800 half-remove", "ratio=" + ratio + " < 5x");
return;
}
pass("box2d-0001 performance ratio >= 5x at N=800 (half-remove)");
}
public static void main(String[] args) {
System.out.println("Box2D CWE-407 unit tests");
System.out.println("=".repeat(60));
System.out.println();
System.out.println("[box2d-0001] b2BroadPhase::b2UnBufferMove linear scan");
testCorrectness();
testCorrectnessWithDups();
testPerformanceDestroyAll();
testPerformanceHalfFill();
System.out.println();
System.out.printf("%d/%d PASS%n", passed, total);
if (passed != total) System.exit(1);
}
}