wave15: v8-0002/0003 Intl+revectorizer (125x/25x) + bullet/box2d ticket files — 528/240
This commit is contained in:
parent
c4026333ba
commit
7146714143
13 changed files with 1520 additions and 11 deletions
253
defects/box2d/unit/Box2DAlgorithm.java
Normal file
253
defects/box2d/unit/Box2DAlgorithm.java
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
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 71–88
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue