wave15: v8-0002/0003 Intl+revectorizer (125x/25x) + bullet/box2d ticket files — 528/240

This commit is contained in:
russell@unturf.com 2026-03-27 19:16:28 -04:00
parent c4026333ba
commit 7146714143
13 changed files with 1520 additions and 11 deletions

View file

@ -0,0 +1,78 @@
# box2d-0001: O(N²) proxy destruction — `b2UnBufferMove` linear scan of `moveArray`
**Severity:** LOW-MEDIUM
**CWE:** CWE-407 (Algorithmic Complexity — Insufficient Control of Quadratic Complexity)
**Target:** erincatto/box2d
**File:** `src/broad_phase.c`
**Lines:** 71–88
**Status:** PATCHED (unit test PASS)
## Description
`b2UnBufferMove` removes a proxy from the broadphase move buffer. It first removes
the key from `moveSet` in O(1), then scans `moveArray` linearly to find the index
for `RemoveSwap`:
```c
// broad_phase.c:77–87 — developer comment: "Purge from move buffer. Linear search."
int count = bp->moveArray.count;
for ( int i = 0; i < count; ++i )
{
if ( bp->moveArray.data[i] == proxyKey )
{
b2IntArray_RemoveSwap( &bp->moveArray, i );
break;
}
}
```
The developer comment reads: *"Purge from move buffer. Linear search."* with a TODO:
*"todo if I can iterate the move set then I don't need the moveArray"*
`b2UnBufferMove` is called when a shape's filter is changed or when a body is
destroyed. With N bodies all buffered for movement (e.g. scene teardown, filter
mass-update), destroying all bodies is O(N²).
Real-world impact: destroying 1000 dynamic bodies in a single step on a scene
reset requires ~500 000 comparisons instead of ~1000.
Note: this is a lower-severity defect than Bullet's hot-path cases — `b2UnBufferMove`
is called on body/shape destroy, not on every step. However it is a direct CWE-407
pattern with an acknowledged O(N) scan inside a destruction loop.
## Root Cause
`moveArray` is a plain `b2IntArray` with no parallel index structure. The hash set
`moveSet` already provides O(1) membership, but the array index for `RemoveSwap`
requires a linear scan.
## Fix
Maintain a parallel hash table `moveIndex` that maps `proxyKey+1 → arrayIndex`.
On `b2BufferMove`, record the new index. On `b2UnBufferMove`, look up the index in
O(1), perform `RemoveSwap`, and update the displaced element's entry in `moveIndex`.
**Patch:** `patch/box2d-0001-broad-phase-index-map.patch`
## Complexity
| Scenario | Before | After |
|----------|--------|-------|
| Destroy N buffered bodies | O(N²) | O(N) |
| N=1000 body-destroy sweep | ~500 000 comparisons | ~1 000 ops |
| N=800 half-fill remove | ~80 000 comparisons | ~400 ops |
| Speedup at N=1000 | — | ~250x |
| Speedup at N=800 half-fill | — | ~200x |
## Unit Test
`unit/Box2DAlgorithm.java`
Correctness: both paths produce identical final arrays for half-remove and dup-buffer scenarios.
Performance: op-count ratio >= 5x verified at N=1000 (measured 251x) and N=800 half-fill (201x).
Run:
```
javac -d /tmp/out defects/box2d/unit/Box2DAlgorithm.java
java -cp /tmp/out unit.Box2DAlgorithm
```
Output: `4/4 PASS`

View 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);
}
}