package unit; import java.util.*; /** * Box2D CWE-407 benchmark: b2UnBufferMove linear scan vs O(1) index map. * * Defect: src/broad_phase.c b2UnBufferMove() lines 77-87 * Linear scan of moveArray to find the index for removal after * O(1) hash-set removal. With N shapes all buffered for movement, * destroying all bodies triggers N × N/2 comparisons on average. * * Fix: Maintain a parallel int[] indexMap[proxyKey] → array position. * On RemoveSwap, update the displaced element's entry. O(1) removal. * * ticket: docs/tickets/box2d-0001-broad-phase-unbuffer-move-linear-scan.md */ public class Box2DTest { // ----------------------------------------------------------------------- // SLOW: mirrors b2UnBufferMove — hash set O(1) + linear array scan O(n) // ----------------------------------------------------------------------- static class SlowMoveBuffer { Set moveSet = new HashSet<>(); List moveArray = new ArrayList<>(); void bufferMove(int proxyKey) { if (moveSet.add(proxyKey)) { moveArray.add(proxyKey); } } /** Linear search — exact mirror of box2d broad_phase.c lines 77-87. */ void unBufferMove(int proxyKey) { if (moveSet.remove(proxyKey)) { // "Purge from move buffer. Linear search." for (int i = 0; i < moveArray.size(); i++) { if (moveArray.get(i) == proxyKey) { // RemoveSwap: replace with last, shrink int last = moveArray.size() - 1; moveArray.set(i, moveArray.get(last)); moveArray.remove(last); break; } } } } } // ----------------------------------------------------------------------- // FAST: O(1) removal via index map (the proposed fix) // ----------------------------------------------------------------------- static class FastMoveBuffer { Set moveSet = new HashSet<>(); int[] moveArray = new int[4096]; int count = 0; Map indexMap = new HashMap<>(); // proxyKey → array index void bufferMove(int proxyKey) { if (moveSet.add(proxyKey)) { if (count == moveArray.length) { moveArray = Arrays.copyOf(moveArray, count * 2); } indexMap.put(proxyKey, count); moveArray[count++] = proxyKey; } } void unBufferMove(int proxyKey) { if (moveSet.remove(proxyKey)) { int idx = indexMap.remove(proxyKey); int last = count - 1; if (idx != last) { int displaced = moveArray[last]; moveArray[idx] = displaced; indexMap.put(displaced, idx); } count--; } } } // ----------------------------------------------------------------------- // Bench harness // ----------------------------------------------------------------------- static void bench(String label, Runnable slow, Runnable fast, long sOps, long fOps) { // Warmup slow.run(); fast.run(); // Slow timing long t0 = System.nanoTime(); slow.run(); long sMs = (System.nanoTime() - t0) / 1_000_000; // Fast timing long t1 = System.nanoTime(); fast.run(); long fMs = (System.nanoTime() - t1) / 1_000_000; double r = fOps > 0 ? (double) sOps / fOps : 0; System.out.printf(" %-52s slow:%4dms (%,d ops) fast:%4dms (%,d ops) speedup:%.0fx%n", label, sMs, sOps, fMs, fOps, r); } // ----------------------------------------------------------------------- // Scenarios // ----------------------------------------------------------------------- /** N shapes all buffered, then all destroyed (body-destroy sweep). */ static Runnable slowDestroyAll(int n) { return () -> { SlowMoveBuffer buf = new SlowMoveBuffer(); for (int i = 0; i < n; i++) buf.bufferMove(i); for (int i = 0; i < n; i++) buf.unBufferMove(i); }; } static Runnable fastDestroyAll(int n) { return () -> { FastMoveBuffer buf = new FastMoveBuffer(); for (int i = 0; i < n; i++) buf.bufferMove(i); for (int i = 0; i < n; i++) buf.unBufferMove(i); }; } /** Interleaved add/remove (shape filter update pattern): N pairs. */ static Runnable slowInterleaved(int n) { return () -> { SlowMoveBuffer buf = new SlowMoveBuffer(); for (int i = 0; i < n; i++) { buf.bufferMove(i); buf.unBufferMove(i); } }; } static Runnable fastInterleaved(int n) { return () -> { FastMoveBuffer buf = new FastMoveBuffer(); for (int i = 0; i < n; i++) { buf.bufferMove(i); buf.unBufferMove(i); } }; } /** Fill half, then remove all (ContactManager pattern: some shapes static). */ static Runnable slowHalfFill(int n) { return () -> { SlowMoveBuffer buf = new SlowMoveBuffer(); for (int i = 0; i < n; i++) buf.bufferMove(i); // remove only the dynamic half for (int i = 0; i < n / 2; i++) buf.unBufferMove(i); }; } static Runnable fastHalfFill(int n) { return () -> { FastMoveBuffer buf = new FastMoveBuffer(); for (int i = 0; i < n; i++) buf.bufferMove(i); for (int i = 0; i < n / 2; i++) buf.unBufferMove(i); }; } public static void main(String[] args) { System.out.println("Box2D CWE-407: b2UnBufferMove linear scan vs O(1) index map"); System.out.println(" defect: src/broad_phase.c lines 77-87"); System.out.println(); int N = 800; long ops = (long) N * N / 2; // approx comparisons in slow path bench(String.format("destroy-all N=%d (body-destroy sweep)", N), slowDestroyAll(N), fastDestroyAll(N), ops, N); bench(String.format("interleaved N=%d (shape-filter update)", N), slowInterleaved(N), fastInterleaved(N), ops, N); bench(String.format("half-fill N=%d (mixed static/dynamic)", N), slowHalfFill(N), fastHalfFill(N), ops, N); System.out.println(); System.out.println("Fix: maintain HashMap alongside moveArray."); System.out.println(" On RemoveSwap, update displaced element's index entry."); System.out.println(" All operations O(1). See patch box2d-0001-broad-phase-index-map.patch"); } }