java-topology/defects/libgdx/unit/LibGDXTest.java

325 lines
14 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.*;
/**
* LibGDXTest — libgdx-0001..0004
*
* 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)
*
* 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;
}
public static void main(String[] args) {
System.out.println("=== UNIT libgdx-0001..0004: 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);
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++;
System.out.printf("%d/5 PASS%n", pass);
}
}