java-topology/defects/bullet/unit/BulletTest.java

220 lines
9.2 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.*;
/**
* BulletTest — Java analogs of three Bullet Physics CWE-407 defects.
*
* Defect 1 (bullet-0001): btGhostObject — findLinearSearch on m_overlappingObjects
* in addOverlappingObjectInternal / removeOverlappingObjectInternal.
* Called per broadphase pair per simulation step.
* Slow: ArrayList.contains / indexOf — O(N) per call.
* Fast: HashMap with index — O(1) per call.
*
* Defect 2 (bullet-0002): btCollisionObject::checkCollideWithOverride —
* findLinearSearch on m_objectsWithoutCollisionCheck.
* Called inside needsCollision() for every pair in processAllOverlappingPairs.
* Slow: ArrayList.contains — O(E) per pair.
* Fast: HashSet.contains — O(1) per pair.
*
* Defect 3 (bullet-0003): btSortedOverlappingPairCache::findPair /
* removeOverlappingPair — findLinearSearch on m_overlappingPairArray.
* Called during broadphase pair removal phase.
* Slow: ArrayList.indexOf — O(P) per removal.
* Fast: HashMap<key, index> — O(1) per removal.
*/
public class BulletTest {
static void bench(String label, Runnable slow, Runnable fast, long sOps, long fOps) {
// warmup
slow.run();
fast.run();
long t0 = System.nanoTime();
slow.run();
long sMs = (System.nanoTime() - t0) / 1_000_000;
long t1 = System.nanoTime();
fast.run();
long fMs = (System.nanoTime() - t1) / 1_000_000;
double ratio = fOps > 0 ? (double) sOps / fOps : 0;
System.out.printf(" %-56s slow:%5dms (%,d ops) fast:%5dms (%,d ops) speedup:%.0fx%n",
label, sMs, sOps, fMs, fOps, ratio);
}
// -----------------------------------------------------------------------
// bullet-0001: btGhostObject overlapping-object add/remove per broadphase step
// -----------------------------------------------------------------------
static void benchGhostOverlapping(int P, int steps) {
// P objects start overlapping a ghost, then all leave — simulated over steps
// SLOW: ArrayList membership check — O(P) per add and remove
Runnable slow = () -> {
List<Object> overlapping = new ArrayList<>(P);
Object[] bodies = new Object[P];
for (int i = 0; i < P; i++) bodies[i] = new Object();
for (int s = 0; s < steps; s++) {
// add phase (each new overlap checks if already present)
overlapping.clear();
for (int i = 0; i < P; i++) {
if (!overlapping.contains(bodies[i])) // O(P)
overlapping.add(bodies[i]);
}
// remove phase
for (int i = 0; i < P; i++) {
int idx = overlapping.indexOf(bodies[i]); // O(P)
if (idx >= 0) {
overlapping.set(idx, overlapping.get(overlapping.size() - 1));
overlapping.remove(overlapping.size() - 1);
}
}
}
};
// FAST: HashMap<Object, Integer> index — O(1) per add and remove
Runnable fast = () -> {
List<Object> overlapping = new ArrayList<>(P);
Map<Object, Integer> index = new HashMap<>(P * 2);
Object[] bodies = new Object[P];
for (int i = 0; i < P; i++) bodies[i] = new Object();
for (int s = 0; s < steps; s++) {
// add phase
overlapping.clear();
index.clear();
for (int i = 0; i < P; i++) {
if (!index.containsKey(bodies[i])) { // O(1)
index.put(bodies[i], overlapping.size());
overlapping.add(bodies[i]);
}
}
// remove phase
for (int i = P - 1; i >= 0; i--) {
Integer pos = index.remove(bodies[i]); // O(1)
if (pos != null) {
int last = overlapping.size() - 1;
if (pos != last) {
Object moved = overlapping.get(last);
overlapping.set(pos, moved);
index.put(moved, pos);
}
overlapping.remove(last);
}
}
}
};
long sOps = (long) steps * P * P; // P adds × O(P) + P removes × O(P)
long fOps = (long) steps * P;
bench(String.format("bullet-0001 btGhostObject overlapping P=%d steps=%d", P, steps),
slow, fast, sOps, fOps);
}
// -----------------------------------------------------------------------
// bullet-0002: btCollisionObject::checkCollideWithOverride per pair per step
// -----------------------------------------------------------------------
static void benchCheckCollideWith(int M, int E, int steps) {
// M total pairs, E exclusions per object
Object[] colliders = new Object[E + 1];
for (int i = 0; i <= E; i++) colliders[i] = new Object();
// SLOW: ArrayList.contains — O(E) per pair
Runnable slow = () -> {
List<Object> exclusions = new ArrayList<>(Arrays.asList(colliders).subList(1, E + 1));
long dummy = 0;
for (int s = 0; s < steps; s++) {
for (int p = 0; p < M; p++) {
Object candidate = colliders[p % (E + 1)];
if (!exclusions.contains(candidate)) // O(E)
dummy++;
}
}
if (dummy < 0) System.out.println("never");
};
// FAST: HashSet.contains — O(1) per pair
Runnable fast = () -> {
Set<Object> exclusions = new HashSet<>(Arrays.asList(colliders).subList(1, E + 1));
long dummy = 0;
for (int s = 0; s < steps; s++) {
for (int p = 0; p < M; p++) {
Object candidate = colliders[p % (E + 1)];
if (!exclusions.contains(candidate)) // O(1)
dummy++;
}
}
if (dummy < 0) System.out.println("never");
};
long sOps = (long) steps * M * E;
long fOps = (long) steps * M;
bench(String.format("bullet-0002 checkCollideWith M=%d E=%d steps=%d", M, E, steps),
slow, fast, sOps, fOps);
}
// -----------------------------------------------------------------------
// bullet-0003: btSortedOverlappingPairCache::removeOverlappingPair — O(P) per remove
// -----------------------------------------------------------------------
static void benchSortedPairCacheRemove(int P) {
// P pairs, remove all of them (broadphase pair removal phase)
Integer[] pairs = new Integer[P];
for (int i = 0; i < P; i++) pairs[i] = i;
// SLOW: ArrayList.indexOf + remove — O(P) per removal
Runnable slow = () -> {
List<Integer> pairArray = new ArrayList<>(Arrays.asList(pairs));
for (int i = 0; i < P; i++) {
int idx = pairArray.indexOf(pairs[i]); // O(P)
if (idx >= 0) {
pairArray.set(idx, pairArray.get(pairArray.size() - 1));
pairArray.remove(pairArray.size() - 1);
}
}
};
// FAST: HashMap<key, index> — O(1) per removal
Runnable fast = () -> {
List<Integer> pairArray = new ArrayList<>(Arrays.asList(pairs));
Map<Integer, Integer> pairIndex = new HashMap<>(P * 2);
for (int i = 0; i < P; i++) pairIndex.put(pairs[i], i);
for (int i = 0; i < P; i++) {
Integer pos = pairIndex.remove(pairs[i]); // O(1)
if (pos != null) {
int last = pairArray.size() - 1;
if (pos != last) {
Integer moved = pairArray.get(last);
pairArray.set(pos, moved);
pairIndex.put(moved, pos);
}
pairArray.remove(last);
}
}
};
long sOps = (long) P * P / 2; // average scan P/2 × P removals
long fOps = P;
bench(String.format("bullet-0003 btSortedPairCache removeOverlappingPair P=%d", P),
slow, fast, sOps, fOps);
}
public static void main(String[] args) {
System.out.println("Bullet Physics CWE-407 defect benchmarks");
System.out.println("=".repeat(100));
System.out.println("\n[bullet-0001] btGhostObject::addOverlappingObjectInternal/removeOverlappingObjectInternal");
benchGhostOverlapping(50, 100);
benchGhostOverlapping(200, 50);
benchGhostOverlapping(500, 20);
System.out.println("\n[bullet-0002] btCollisionObject::checkCollideWithOverride per pair per step");
benchCheckCollideWith(500, 10, 100);
benchCheckCollideWith(1_000, 20, 50);
benchCheckCollideWith(2_000, 50, 20);
System.out.println("\n[bullet-0003] btSortedOverlappingPairCache::removeOverlappingPair");
benchSortedPairCacheRemove(500);
benchSortedPairCacheRemove(2_000);
benchSortedPairCacheRemove(10_000);
}
}