B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections. Squash of 94 local commits onto remote master.
248 lines
9.9 KiB
Java
248 lines
9.9 KiB
Java
package unit;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
|
|
/**
|
|
* SpiderMonkeyLinearSumTest
|
|
*
|
|
* Models the CWE-407 defect in LinearSum::add(MDefinition* term, int32_t scale):
|
|
*
|
|
* Defective: Vector<LinearTerm, 2, JitAllocPolicy> searched by linear pointer
|
|
* scan on every add() call. At T distinct terms and N total add()
|
|
* calls, cost is O(N * T).
|
|
*
|
|
* Fixed: HashMap<MDefinition*, int32_t, ...> with lookupOrAdd(), O(1)
|
|
* amortised per call regardless of T.
|
|
*
|
|
* "Term" is modelled as an Integer ID. Comparison counts are instrumented
|
|
* explicitly — not wall-clock timing.
|
|
*/
|
|
public class SpiderMonkeyLinearSumTest {
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Instrumented defective LinearSum: ArrayList<int[]> — {termId, scale}
|
|
// Linear scan on every add()
|
|
// -----------------------------------------------------------------------
|
|
|
|
static long defectiveLinearSumBuild(int[] termIds, int[] scales) {
|
|
ArrayList<int[]> terms = new ArrayList<>(); // {termId, scale}
|
|
long comparisons = 0;
|
|
for (int i = 0; i < termIds.length; i++) {
|
|
int termId = termIds[i];
|
|
int scale = scales[i];
|
|
boolean found = false;
|
|
// O(T) linear scan — the defect
|
|
for (int[] entry : terms) {
|
|
comparisons++;
|
|
if (entry[0] == termId) {
|
|
entry[1] += scale;
|
|
if (entry[1] == 0) {
|
|
terms.remove(entry);
|
|
}
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
terms.add(new int[]{termId, scale});
|
|
}
|
|
}
|
|
return comparisons;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Instrumented fixed LinearSum: HashMap<Integer,Integer> (termId -> scale)
|
|
// O(1) amortised per add()
|
|
// -----------------------------------------------------------------------
|
|
|
|
static long fixedLinearSumBuild(int[] termIds, int[] scales) {
|
|
HashMap<Integer, Integer> terms = new HashMap<>();
|
|
long lookups = 0;
|
|
for (int i = 0; i < termIds.length; i++) {
|
|
int termId = termIds[i];
|
|
int scale = scales[i];
|
|
lookups++; // one O(1) hash lookup per call
|
|
Integer existing = terms.get(termId);
|
|
if (existing != null) {
|
|
int newScale = existing + scale;
|
|
if (newScale == 0) {
|
|
terms.remove(termId);
|
|
} else {
|
|
terms.put(termId, newScale);
|
|
}
|
|
} else {
|
|
terms.put(termId, scale);
|
|
}
|
|
}
|
|
return lookups;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Helper: build add() call sequence — N calls over T distinct term IDs,
|
|
// cycling through IDs so each term is visited multiple times.
|
|
// -----------------------------------------------------------------------
|
|
|
|
static int[][] makeAddCalls(int N, int T) {
|
|
int[] termIds = new int[N];
|
|
int[] scales = new int[N];
|
|
for (int i = 0; i < N; i++) {
|
|
termIds[i] = i % T;
|
|
scales[i] = 1;
|
|
}
|
|
return new int[][]{termIds, scales};
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Test 1 — T=20 terms, N=100 add() calls: defect comparisons > fixed lookups
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test1_basicRatio() {
|
|
int T = 20, N = 100;
|
|
int[][] calls = makeAddCalls(N, T);
|
|
long defectOps = defectiveLinearSumBuild(calls[0], calls[1]);
|
|
long fixedOps = fixedLinearSumBuild(calls[0], calls[1]);
|
|
|
|
System.out.printf("test1: T=%d N=%d defect=%d fixed=%d%n",
|
|
T, N, defectOps, fixedOps);
|
|
|
|
assert defectOps > fixedOps
|
|
: "defect must do more work than fix: " + defectOps + " vs " + fixedOps;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Test 2 — Ratio > 10x at T=20, N=100
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test2_ratioExceedsTenX() {
|
|
int T = 20, N = 100;
|
|
int[][] calls = makeAddCalls(N, T);
|
|
long defectOps = defectiveLinearSumBuild(calls[0], calls[1]);
|
|
long fixedOps = fixedLinearSumBuild(calls[0], calls[1]);
|
|
|
|
double ratio = (double) defectOps / Math.max(1, fixedOps);
|
|
System.out.printf("test2: T=%d N=%d defect=%d fixed=%d ratio=%.1fx%n",
|
|
T, N, defectOps, fixedOps, ratio);
|
|
|
|
assert ratio > 10.0
|
|
: "expected ratio > 10x, got " + ratio;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Test 3 — All N add() calls use the same term (T=1, N=200)
|
|
// Defect: each call scans list of length 1 → N-1 comparisons (hitting first).
|
|
// Actually on first call list is empty (0 comparisons), subsequent hits find it.
|
|
// But scale accumulates; zero-cancellation only if scale wraps.
|
|
// We use non-zero final scale; list stays length 1 throughout.
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test3_singleTermRepeat() {
|
|
int N = 200;
|
|
int[] termIds = new int[N];
|
|
int[] scales = new int[N];
|
|
for (int i = 0; i < N; i++) { termIds[i] = 7; scales[i] = 1; }
|
|
|
|
long defectOps = defectiveLinearSumBuild(termIds, scales);
|
|
long fixedOps = fixedLinearSumBuild(termIds, scales);
|
|
|
|
// Defect: 0 comparisons for first call (list empty), 1 comparison each for calls 2..N.
|
|
// Total: N-1 comparisons.
|
|
System.out.printf("test3: T=1 N=%d defect=%d (expect %d) fixed=%d%n",
|
|
N, defectOps, N - 1, fixedOps);
|
|
|
|
assert defectOps == N - 1
|
|
: "expected " + (N-1) + " defect comparisons for T=1, got " + defectOps;
|
|
assert fixedOps == N
|
|
: "expected " + N + " fixed lookups for T=1, got " + fixedOps;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Test 4 — Scale cancellation: add term with +1 then -1 repeatedly.
|
|
// Fixed must handle zero-scale removal correctly.
|
|
// Defect still scans; the removal path is exercised on both sides.
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test4_scaleCancellation() {
|
|
int T = 10;
|
|
int N = 40; // 20 pairs of (+1,-1) over T terms
|
|
int[] termIds = new int[N];
|
|
int[] scales = new int[N];
|
|
for (int i = 0; i < N; i++) {
|
|
termIds[i] = i % T;
|
|
scales[i] = (i / T % 2 == 0) ? 1 : -1;
|
|
}
|
|
|
|
long defectOps = defectiveLinearSumBuild(termIds, scales);
|
|
long fixedOps = fixedLinearSumBuild(termIds, scales);
|
|
|
|
System.out.printf("test4: T=%d N=%d cancellation defect=%d fixed=%d%n",
|
|
T, N, defectOps, fixedOps);
|
|
|
|
// Both must complete (no assertion error = correct semantics)
|
|
// Defect should still be >= fixed
|
|
assert defectOps >= fixedOps
|
|
: "defect should not be cheaper than fix: " + defectOps + " vs " + fixedOps;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Test 5 — Scaling: doubling T roughly doubles defect per add() call,
|
|
// while fixed remains O(1) per call.
|
|
// -----------------------------------------------------------------------
|
|
|
|
static void test5_linearVsConstantScaling() {
|
|
int N = 200;
|
|
int T1 = 10;
|
|
int T2 = 20; // double T
|
|
|
|
int[][] calls1 = makeAddCalls(N, T1);
|
|
int[][] calls2 = makeAddCalls(N, T2);
|
|
|
|
long d1 = defectiveLinearSumBuild(calls1[0], calls1[1]);
|
|
long d2 = defectiveLinearSumBuild(calls2[0], calls2[1]);
|
|
long f1 = fixedLinearSumBuild(calls1[0], calls1[1]);
|
|
long f2 = fixedLinearSumBuild(calls2[0], calls2[1]);
|
|
|
|
double defectGrowth = (double) d2 / Math.max(1, d1);
|
|
double fixedGrowth = (double) f2 / Math.max(1, f1);
|
|
|
|
System.out.printf("test5: N=%d T1=%d→T2=%d defect_growth=%.2fx fixed_growth=%.2fx%n",
|
|
N, T1, T2, defectGrowth, fixedGrowth);
|
|
|
|
// Defect average scan length grows with T → more than 2x when T doubles
|
|
// (for N >> T, each call hits an average-length list growing with T)
|
|
// We need at least that doubling T increases defect more than it increases fixed.
|
|
assert defectGrowth > fixedGrowth
|
|
: "defect growth should exceed fixed growth when T doubles";
|
|
// Fixed lookups should remain essentially constant (N lookups regardless of T)
|
|
assert fixedGrowth <= 1.1
|
|
: "fixed lookups should not grow meaningfully with T, got " + fixedGrowth;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Main
|
|
// -----------------------------------------------------------------------
|
|
|
|
public static void main(String[] args) {
|
|
System.out.println("=== SpiderMonkeyLinearSumTest ===");
|
|
System.out.println("Modelling CWE-407: LinearSum::add() linear scan vs HashMap O(1)");
|
|
System.out.println();
|
|
|
|
test1_basicRatio();
|
|
System.out.println(" PASS test1_basicRatio");
|
|
|
|
test2_ratioExceedsTenX();
|
|
System.out.println(" PASS test2_ratioExceedsTenX");
|
|
|
|
test3_singleTermRepeat();
|
|
System.out.println(" PASS test3_singleTermRepeat");
|
|
|
|
test4_scaleCancellation();
|
|
System.out.println(" PASS test4_scaleCancellation");
|
|
|
|
test5_linearVsConstantScaling();
|
|
System.out.println(" PASS test5_linearVsConstantScaling");
|
|
|
|
System.out.println();
|
|
System.out.println("All 5 tests PASSED.");
|
|
}
|
|
}
|