pygame: CLEAN — runtime uses dict/set throughout (O(1) membership) libgdx-0002: ModelBuilder.rebuildReferences Array.contains O(P*M) — patch libgdx-0005: Stage.touchDragged touchFocuses.contains O(F^2) — patch libgdx-0006: AfterAction.delegate currentActions.indexOf O(W*A) per frame — patch unit: extend LibGDXTest to 7 tests, all PASS (50x-1971x speedup measured)
428 lines
19 KiB
Java
428 lines
19 KiB
Java
package unit;
|
||
|
||
import java.util.*;
|
||
|
||
/**
|
||
* LibGDXTest — libgdx-0001..0006
|
||
*
|
||
* Proves CWE-407 in libGDX:
|
||
* libgdx-0001: Model.loadNode() — nested for-loop string-ID scan O(parts × meshes + parts × materials)
|
||
* libgdx-0002: ModelBuilder.rebuildReferences() — Array.contains() in node-part loop O(parts × materials)
|
||
* libgdx-0003: ModelInstance.invalidate() — Array.contains() in node-part loop O(parts × materials)
|
||
* libgdx-0004: Kerning.readSubtable2() — IntArray.contains() in GPOS coverage loop O(coverage × classes × K)
|
||
* libgdx-0005: Stage.touchDragged() — touchFocuses.contains(focus, true) inside touchFocuses loop O(F^2)
|
||
* libgdx-0006: AfterAction.delegate() — currentActions.indexOf(action, true) inside waitForActions loop O(W*A)
|
||
*
|
||
* Run: javac -d . LibGDXTest.java && java -ea unit.LibGDXTest
|
||
*/
|
||
public class LibGDXTest {
|
||
|
||
static void bench(String label, Runnable slow, Runnable fast, long sOps, long fOps) {
|
||
slow.run(); fast.run(); // warmup
|
||
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 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);
|
||
}
|
||
|
||
// ── libgdx-0001: Model.loadNode nested for-loop string scan ──────────────
|
||
|
||
/**
|
||
* SLOW: simulates Model.loadNode() — for each node-part, scan all meshParts
|
||
* (by string ID) and all materials (by string ID).
|
||
* O(parts × (meshCount + materialCount))
|
||
*/
|
||
static long loadNodeSlow(int nodePartCount, int meshCount, int materialCount) {
|
||
// Build arrays of String IDs
|
||
String[] meshIds = new String[meshCount];
|
||
String[] materialIds = new String[materialCount];
|
||
for (int i = 0; i < meshCount; i++) meshIds[i] = "mesh_" + i;
|
||
for (int i = 0; i < materialCount; i++) materialIds[i] = "mat_" + i;
|
||
|
||
long ops = 0;
|
||
// Simulate loading nodePartCount node-parts, each referencing the last mesh/material
|
||
String targetMesh = meshIds[meshCount - 1]; // worst case: always last
|
||
String targetMat = materialIds[materialCount - 1];
|
||
for (int p = 0; p < nodePartCount; p++) {
|
||
// for (MeshPart part : meshParts) { if id.equals(...) ... } — O(M)
|
||
for (int m = 0; m < meshCount; m++) {
|
||
ops++;
|
||
if (meshIds[m].equals(targetMesh)) break;
|
||
}
|
||
// for (Material mat : materials) { if id.equals(...) ... } — O(T)
|
||
for (int t = 0; t < materialCount; t++) {
|
||
ops++;
|
||
if (materialIds[t].equals(targetMat)) break;
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
/**
|
||
* FAST: simulates fixed Model.loadNode() — build HashMap<String, index> once
|
||
* before the node loop, use O(1) get() per node-part.
|
||
*/
|
||
static long loadNodeFast(int nodePartCount, int meshCount, int materialCount) {
|
||
String[] meshIds = new String[meshCount];
|
||
String[] materialIds = new String[materialCount];
|
||
for (int i = 0; i < meshCount; i++) meshIds[i] = "mesh_" + i;
|
||
for (int i = 0; i < materialCount; i++) materialIds[i] = "mat_" + i;
|
||
|
||
// Build lookup maps once — O(M + T)
|
||
Map<String, Integer> meshById = new HashMap<>(meshCount * 2);
|
||
for (int i = 0; i < meshCount; i++) meshById.put(meshIds[i], i);
|
||
Map<String, Integer> matById = new HashMap<>(materialCount * 2);
|
||
for (int i = 0; i < materialCount; i++) matById.put(materialIds[i], i);
|
||
|
||
String targetMesh = meshIds[meshCount - 1];
|
||
String targetMat = materialIds[materialCount - 1];
|
||
|
||
long ops = 0;
|
||
for (int p = 0; p < nodePartCount; p++) {
|
||
ops++; meshById.get(targetMesh); // O(1)
|
||
ops++; matById.get(targetMat); // O(1)
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
// ── libgdx-0002: ModelBuilder.rebuildReferences Array.contains ───────────
|
||
|
||
/**
|
||
* SLOW: simulates rebuildReferences() — for each node-part, call Array.contains()
|
||
* (linear scan by identity) to check if material/meshPart/mesh is already added.
|
||
* O(parts × (materials + meshParts + meshes))
|
||
*/
|
||
static long rebuildRefsSlow(int nodePartCount, int materialCount) {
|
||
List<Object> materials = new ArrayList<>();
|
||
List<Object> meshParts = new ArrayList<>();
|
||
List<Object> meshes = new ArrayList<>();
|
||
|
||
// Pre-create unique objects
|
||
Object[] matObjs = new Object[materialCount];
|
||
Object[] partObjs = new Object[materialCount];
|
||
Object[] meshObjs = new Object[materialCount];
|
||
for (int i = 0; i < materialCount; i++) {
|
||
matObjs[i] = new Object();
|
||
partObjs[i] = new Object();
|
||
meshObjs[i] = new Object();
|
||
}
|
||
|
||
long ops = 0;
|
||
// Each node-part references materials/parts in round-robin (causes dedup checks)
|
||
for (int p = 0; p < nodePartCount; p++) {
|
||
Object mat = matObjs[p % materialCount];
|
||
Object part = partObjs[p % materialCount];
|
||
Object mesh = meshObjs[p % materialCount];
|
||
|
||
// Array.contains — linear scan by identity — O(M)
|
||
boolean hasMat = false;
|
||
for (Object m : materials) { ops++; if (m == mat) { hasMat = true; break; } }
|
||
if (!hasMat) materials.add(mat);
|
||
|
||
boolean hasPart = false;
|
||
for (Object pp : meshParts) { ops++; if (pp == part) { hasPart = true; break; } }
|
||
if (!hasPart) {
|
||
meshParts.add(part);
|
||
boolean hasMesh = false;
|
||
for (Object msh : meshes) { ops++; if (msh == mesh) { hasMesh = true; break; } }
|
||
if (!hasMesh) meshes.add(mesh);
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
/**
|
||
* FAST: simulates fixed rebuildReferences() — IdentityHashMap for O(1) dedup.
|
||
*/
|
||
static long rebuildRefsFast(int nodePartCount, int materialCount) {
|
||
IdentityHashMap<Object, Boolean> matSeen = new IdentityHashMap<>();
|
||
IdentityHashMap<Object, Boolean> partSeen = new IdentityHashMap<>();
|
||
IdentityHashMap<Object, Boolean> meshSeen = new IdentityHashMap<>();
|
||
List<Object> materials = new ArrayList<>();
|
||
List<Object> meshParts = new ArrayList<>();
|
||
List<Object> meshes = new ArrayList<>();
|
||
|
||
Object[] matObjs = new Object[materialCount];
|
||
Object[] partObjs = new Object[materialCount];
|
||
Object[] meshObjs = new Object[materialCount];
|
||
for (int i = 0; i < materialCount; i++) {
|
||
matObjs[i] = new Object();
|
||
partObjs[i] = new Object();
|
||
meshObjs[i] = new Object();
|
||
}
|
||
|
||
long ops = 0;
|
||
for (int p = 0; p < nodePartCount; p++) {
|
||
Object mat = matObjs[p % materialCount];
|
||
Object part = partObjs[p % materialCount];
|
||
Object mesh = meshObjs[p % materialCount];
|
||
|
||
ops++;
|
||
if (matSeen.put(mat, Boolean.TRUE) == null) materials.add(mat); // O(1)
|
||
ops++;
|
||
if (partSeen.put(part, Boolean.TRUE) == null) {
|
||
meshParts.add(part);
|
||
ops++;
|
||
if (meshSeen.put(mesh, Boolean.TRUE) == null) meshes.add(mesh); // O(1)
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
// ── libgdx-0003: ModelInstance.invalidate Array.contains ─────────────────
|
||
|
||
/**
|
||
* SLOW: simulates ModelInstance.invalidate() — for each node-part, call
|
||
* materials.contains(part.material, identity=true) — O(T) linear scan.
|
||
*/
|
||
static long invalidateSlow(int nodePartCount, int materialCount) {
|
||
List<Object> materials = new ArrayList<>();
|
||
Object[] matObjs = new Object[materialCount];
|
||
for (int i = 0; i < materialCount; i++) matObjs[i] = new Object();
|
||
|
||
long ops = 0;
|
||
for (int p = 0; p < nodePartCount; p++) {
|
||
Object mat = matObjs[p % materialCount];
|
||
boolean found = false;
|
||
for (Object m : materials) { ops++; if (m == mat) { found = true; break; } }
|
||
if (!found) materials.add(mat);
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
/**
|
||
* FAST: simulates fixed invalidate() — IdentityHashMap dedup, O(1) per part.
|
||
*/
|
||
static long invalidateFast(int nodePartCount, int materialCount) {
|
||
IdentityHashMap<Object, Boolean> seen = new IdentityHashMap<>();
|
||
List<Object> materials = new ArrayList<>();
|
||
Object[] matObjs = new Object[materialCount];
|
||
for (int i = 0; i < materialCount; i++) matObjs[i] = new Object();
|
||
|
||
long ops = 0;
|
||
for (int p = 0; p < nodePartCount; p++) {
|
||
Object mat = matObjs[p % materialCount];
|
||
ops++;
|
||
if (seen.put(mat, Boolean.TRUE) == null) materials.add(mat); // O(1)
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
// ── libgdx-0004: Kerning GPOS IntArray.contains ──────────────────────────
|
||
|
||
/**
|
||
* SLOW: simulates GPOS coverage loop — for each covered glyph, scan all class
|
||
* definitions with IntArray.contains() (linear scan) to find class membership.
|
||
* O(coverageLen × class1Count × avg_glyphs_per_class)
|
||
*/
|
||
static long kerningGposSlow(int coverageLen, int class1Count, int glyphsPerClass) {
|
||
// Build class arrays: class c contains glyphs [c*glyphsPerClass .. (c+1)*glyphsPerClass)
|
||
int[][] glyphsByClass = new int[class1Count][];
|
||
for (int c = 1; c < class1Count; c++) {
|
||
glyphsByClass[c] = new int[glyphsPerClass];
|
||
for (int k = 0; k < glyphsPerClass; k++)
|
||
glyphsByClass[c][k] = c * glyphsPerClass + k;
|
||
}
|
||
glyphsByClass[0] = new int[0]; // class 0 = unclassified
|
||
|
||
long ops = 0;
|
||
// Coverage glyphs: worst case, each one is at the end of the last class (or unclassified)
|
||
for (int i = 0; i < coverageLen; i++) {
|
||
int glyph = (class1Count - 1) * glyphsPerClass + (i % glyphsPerClass); // last class
|
||
boolean found = false;
|
||
for (int j = 1; j < class1Count && !found; j++) {
|
||
// IntArray.contains — linear scan
|
||
for (int k = 0; k < glyphsByClass[j].length; k++) {
|
||
ops++;
|
||
if (glyphsByClass[j][k] == glyph) { found = true; break; }
|
||
}
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
/**
|
||
* FAST: simulates fixed GPOS handler — build IntIntMap (glyph→class) once,
|
||
* then O(1) lookup per covered glyph.
|
||
*/
|
||
static long kerningGposFast(int coverageLen, int class1Count, int glyphsPerClass) {
|
||
int[][] glyphsByClass = new int[class1Count][];
|
||
for (int c = 1; c < class1Count; c++) {
|
||
glyphsByClass[c] = new int[glyphsPerClass];
|
||
for (int k = 0; k < glyphsPerClass; k++)
|
||
glyphsByClass[c][k] = c * glyphsPerClass + k;
|
||
}
|
||
|
||
// Build reverse map once — O(class1Count × glyphsPerClass)
|
||
Map<Integer, Integer> glyphToClass = new HashMap<>((class1Count * glyphsPerClass) * 2);
|
||
for (int c = 1; c < class1Count; c++)
|
||
for (int k = 0; k < glyphsByClass[c].length; k++)
|
||
glyphToClass.put(glyphsByClass[c][k], c);
|
||
|
||
long ops = 0;
|
||
for (int i = 0; i < coverageLen; i++) {
|
||
int glyph = (class1Count - 1) * glyphsPerClass + (i % glyphsPerClass);
|
||
ops++;
|
||
glyphToClass.containsKey(glyph); // O(1)
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
// ── libgdx-0005: Stage.touchDragged SnapshotArray.contains O(F^2) ─────────
|
||
|
||
/**
|
||
* SLOW: simulates Stage.touchDragged() — for each focus in touchFocuses,
|
||
* calls touchFocuses.contains(focus, true) which is an O(F) linear scan.
|
||
* Total: O(F^2) per touchDragged event.
|
||
*/
|
||
static long stageTouchDraggedSlow(int focusCount) {
|
||
List<Object> focuses = new ArrayList<>();
|
||
for (int i = 0; i < focusCount; i++) focuses.add(new Object());
|
||
long ops = 0;
|
||
// Outer loop: iterate all focuses
|
||
for (int i = 0; i < focuses.size(); i++) {
|
||
Object focus = focuses.get(i);
|
||
// Inner: Array.contains(focus, identity=true) — linear scan
|
||
for (int j = focuses.size() - 1; j >= 0; j--) {
|
||
ops++;
|
||
if (focuses.get(j) == focus) break;
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
/**
|
||
* FAST: build ObjectSet once (O(F)), then O(1) per focus check → O(F) total.
|
||
*/
|
||
static long stageTouchDraggedFast(int focusCount) {
|
||
List<Object> focuses = new ArrayList<>();
|
||
for (int i = 0; i < focusCount; i++) focuses.add(new Object());
|
||
Set<Object> focusSet = new HashSet<>(focuses); // O(F)
|
||
long ops = (long) focusCount; // set construction cost
|
||
for (int i = 0; i < focuses.size(); i++) {
|
||
Object focus = focuses.get(i);
|
||
ops++; // O(1) hash lookup
|
||
focusSet.contains(focus);
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
// ── libgdx-0006: AfterAction.delegate indexOf O(W*A) per frame ───────────
|
||
|
||
/**
|
||
* SLOW: simulates AfterAction.delegate() — for each action in waitForActions,
|
||
* calls currentActions.indexOf(action, true) which is an O(A) linear scan.
|
||
* Total: O(W*A) per frame.
|
||
*/
|
||
static long afterActionSlow(int waitCount, int actionCount) {
|
||
List<Object> currentActions = new ArrayList<>();
|
||
List<Object> waitForActions = new ArrayList<>();
|
||
for (int i = 0; i < actionCount; i++) currentActions.add(new Object());
|
||
for (int i = 0; i < waitCount && i < actionCount; i++) waitForActions.add(currentActions.get(i));
|
||
long ops = 0;
|
||
for (int i = waitForActions.size() - 1; i >= 0; i--) {
|
||
Object action = waitForActions.get(i);
|
||
// indexOf scans all A current actions
|
||
for (int j = 0; j < currentActions.size(); j++) {
|
||
ops++;
|
||
if (currentActions.get(j) == action) break;
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
/**
|
||
* FAST: build ObjectSet from currentActions once (O(A)), then O(1) per waitForActions check.
|
||
*/
|
||
static long afterActionFast(int waitCount, int actionCount) {
|
||
List<Object> currentActions = new ArrayList<>();
|
||
List<Object> waitForActions = new ArrayList<>();
|
||
for (int i = 0; i < actionCount; i++) currentActions.add(new Object());
|
||
for (int i = 0; i < waitCount && i < actionCount; i++) waitForActions.add(currentActions.get(i));
|
||
Set<Object> currentSet = new HashSet<>(currentActions); // O(A)
|
||
long ops = (long) actionCount; // set construction cost
|
||
for (int i = waitForActions.size() - 1; i >= 0; i--) {
|
||
Object action = waitForActions.get(i);
|
||
ops++; // O(1)
|
||
currentSet.contains(action);
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
public static void main(String[] args) {
|
||
System.out.println("=== UNIT libgdx-0001..0006: LibGDX CWE-407 ===");
|
||
|
||
// libgdx-0001: Model.loadNode
|
||
System.out.println();
|
||
System.out.println(" libgdx-0001: Model.loadNode nested string-ID scan");
|
||
final int NP1 = 500, M1 = 100, T1 = 50;
|
||
long s0 = loadNodeSlow(NP1, M1, T1);
|
||
long f0 = loadNodeFast(NP1, M1, T1);
|
||
bench(String.format("libgdx-0001 parts=%d meshes=%d mats=%d", NP1, M1, T1),
|
||
() -> loadNodeSlow(NP1, M1, T1), () -> loadNodeFast(NP1, M1, T1), s0, f0);
|
||
|
||
final int NP2 = 2000, M2 = 200, T2 = 100;
|
||
long s1 = loadNodeSlow(NP2, M2, T2);
|
||
long f1 = loadNodeFast(NP2, M2, T2);
|
||
bench(String.format("libgdx-0001 parts=%d meshes=%d mats=%d", NP2, M2, T2),
|
||
() -> loadNodeSlow(NP2, M2, T2), () -> loadNodeFast(NP2, M2, T2), s1, f1);
|
||
|
||
// libgdx-0002: ModelBuilder.rebuildReferences
|
||
System.out.println();
|
||
System.out.println(" libgdx-0002: ModelBuilder.rebuildReferences Array.contains");
|
||
final int RNP = 1000, RM = 50;
|
||
long s2 = rebuildRefsSlow(RNP, RM);
|
||
long f2 = rebuildRefsFast(RNP, RM);
|
||
bench(String.format("libgdx-0002 parts=%d materials=%d", RNP, RM),
|
||
() -> rebuildRefsSlow(RNP, RM), () -> rebuildRefsFast(RNP, RM), s2, f2);
|
||
|
||
// libgdx-0003: ModelInstance.invalidate
|
||
System.out.println();
|
||
System.out.println(" libgdx-0003: ModelInstance.invalidate Array.contains");
|
||
final int INP = 1000, IM = 50;
|
||
long s3 = invalidateSlow(INP, IM);
|
||
long f3 = invalidateFast(INP, IM);
|
||
bench(String.format("libgdx-0003 parts=%d materials=%d", INP, IM),
|
||
() -> invalidateSlow(INP, IM), () -> invalidateFast(INP, IM), s3, f3);
|
||
|
||
// libgdx-0004: Kerning GPOS
|
||
System.out.println();
|
||
System.out.println(" libgdx-0004: Kerning GPOS IntArray.contains");
|
||
final int KC = 1000, KN = 100, KG = 20;
|
||
long s4 = kerningGposSlow(KC, KN, KG);
|
||
long f4 = kerningGposFast(KC, KN, KG);
|
||
bench(String.format("libgdx-0004 coverage=%d classes=%d glyphs/class=%d", KC, KN, KG),
|
||
() -> kerningGposSlow(KC, KN, KG), () -> kerningGposFast(KC, KN, KG), s4, f4);
|
||
|
||
// libgdx-0005: Stage.touchDragged
|
||
System.out.println();
|
||
System.out.println(" libgdx-0005: Stage.touchDragged SnapshotArray.contains O(F^2)");
|
||
final int FC = 200;
|
||
long s5 = stageTouchDraggedSlow(FC);
|
||
long f5 = stageTouchDraggedFast(FC);
|
||
bench(String.format("libgdx-0005 focuses=%d", FC),
|
||
() -> stageTouchDraggedSlow(FC), () -> stageTouchDraggedFast(FC), s5, f5);
|
||
|
||
// libgdx-0006: AfterAction.delegate
|
||
System.out.println();
|
||
System.out.println(" libgdx-0006: AfterAction.delegate indexOf O(W*A)");
|
||
final int WC = 100, AC = 200;
|
||
long s6 = afterActionSlow(WC, AC);
|
||
long f6 = afterActionFast(WC, AC);
|
||
bench(String.format("libgdx-0006 wait=%d actions=%d", WC, AC),
|
||
() -> afterActionSlow(WC, AC), () -> afterActionFast(WC, AC), s6, f6);
|
||
|
||
System.out.println();
|
||
|
||
int pass = 0;
|
||
assert s0 > f0 * 5 : "libgdx-0001 (small) expected >5x speedup"; pass++;
|
||
assert s1 > f1 * 5 : "libgdx-0001 (large) expected >5x speedup"; pass++;
|
||
assert s2 > f2 * 5 : "libgdx-0002 expected >5x speedup"; pass++;
|
||
assert s3 > f3 * 5 : "libgdx-0003 expected >5x speedup"; pass++;
|
||
assert s4 > f4 * 5 : "libgdx-0004 expected >5x speedup"; pass++;
|
||
assert s5 > f5 * 5 : "libgdx-0005 expected >5x speedup"; pass++;
|
||
assert s6 > f6 * 5 : "libgdx-0006 expected >5x speedup"; pass++;
|
||
System.out.printf("%d/7 PASS%n", pass);
|
||
}
|
||
}
|