java-topology/defects/ruffle-0002/test/RuffleTest.java
russell@unturf.com 7786adc4c0 ruffle: 2 CWE-407 defects, MOADs 0002-0005 documented
ruffle-0001: AVM2 optimizer type_aware.rs process_jump() worklist dedup
  Vec<usize>.contains() inside while-loop over basic blocks -> O(B^2).
  Fix: companion HashSet<usize> for O(1) dedup. 249.5x at fanWidth=500.

ruffle-0002: MovieClip goto_commands depth lookup O(F*D^2) -> O(F*D).
  goto_place_object/goto_remove_object use iter().position(|o| o.depth()==d)
  inside frame-scan while-loop. Fix: HashMap<Depth, usize> index alongside
  Vec; swap_remove displacement handled correctly. 51.5x at F=500 D=100.

MOAD-0002 (Intertangle): UpdateContext god-object couples GC/AVM1/AVM2/
  audio/video/renderer/navigator/UI/storage/log/timers/input in one struct.
  Architectural, not a single-patch fix.
MOAD-0003 (Leaked Context): CURRENT_CONTEXT thread_local in web/src/lib.rs
  holds raw *mut UpdateContext<'static> (request-scoped in thread scope).
  Desktop thread_locals CALLSTACK/RENDER_INFO/SWF_INFO carry per-SWF state.
MOAD-0004: CLEAN. No verbatim credential logging found.
MOAD-0005: CLEAN. Arc<Mutex<Player>> used consistently, no unsynchronized
  cache double-check pattern.

2/2 unit tests PASS.
2026-03-31 19:57:30 -04:00

234 lines
9.4 KiB
Java

import java.util.*;
/**
* ruffle-0002: MovieClip.gotoAndPlay() goto_commands depth lookup O(F*D^2) -> O(F*D)
*
* Defect: goto_place_object() and goto_remove_object() both search goto_commands
* (a Vec<GotoPlaceObject>) by depth using iter().position(|o| o.depth() == depth).
* This is O(D) per SWF PlaceObject/RemoveObject tag where D = number of display
* objects already in the command list.
*
* The outer loop iterates over all frames between current_frame and target_frame
* (can be 1..thousands), each potentially containing hundreds of place/remove tags.
* Total complexity: O(F * T * D) where T = tags per frame, D = depths in command list.
* Worst case when depths accumulate across frames: O(F * D^2).
*
* Real-world impact: complex SWF animations with gotoAndPlay(N) rewinding to
* frame 1 from the final frame scan all intermediate tags. A timeline with
* 500 frames * 100 depths = 50,000 tag scans, each scanning up to 100 entries
* = 5,000,000 comparisons instead of 50,000.
*
* Fix: carry a companion HashMap<Depth, usize> (goto_depth_index) alongside the Vec.
* - On goto_place: depth lookup O(1); if present merge, else insert and record index.
* - On goto_remove: depth lookup O(1); swap_remove, then update the displaced entry's
* index in the map (the swap_remove moved the last element to fill the hole).
* Total complexity drops to O(F * T) amortized.
*/
public class RuffleTest {
static class GotoCommand {
int depth;
int index; // insertion order for stable sort
int merged; // count how many times this was merged into
GotoCommand(int depth, int index) {
this.depth = depth;
this.index = index;
this.merged = 0;
}
void merge() { merged++; }
}
// --- Defective: iter().position() O(D) per tag ---
static long defectiveGoto(int frames, int depthsPerFrame) {
List<GotoCommand> cmds = new ArrayList<>();
long ops = 0;
int idx = 0;
for (int f = 0; f < frames; f++) {
for (int d = 0; d < depthsPerFrame; d++) {
ops++; // cost of the scan
// Simulate iter().position(|o| o.depth() == d)
int found = -1;
for (int k = 0; k < cmds.size(); k++) {
ops++; // each comparison
if (cmds.get(k).depth == d) {
found = k;
break;
}
}
if (found >= 0) {
cmds.get(found).merge();
} else {
cmds.add(new GotoCommand(d, idx++));
}
}
}
return ops;
}
// --- Fixed: HashMap<Depth, usize> O(1) per tag ---
static long fixedGoto(int frames, int depthsPerFrame) {
List<GotoCommand> cmds = new ArrayList<>();
Map<Integer, Integer> depthIndex = new HashMap<>();
long ops = 0;
int idx = 0;
for (int f = 0; f < frames; f++) {
for (int d = 0; d < depthsPerFrame; d++) {
ops++; // cost of the HashMap lookup (O(1))
Integer found = depthIndex.get(d);
if (found != null) {
cmds.get(found).merge();
} else {
depthIndex.put(d, cmds.size());
cmds.add(new GotoCommand(d, idx++));
}
}
}
return ops;
}
// Simulate remove with swap_remove and correct index maintenance
static long defectiveGotoWithRemove(int frames, int depthsPerFrame) {
List<GotoCommand> cmds = new ArrayList<>();
long ops = 0;
int idx = 0;
for (int f = 0; f < frames; f++) {
// Alternate: even frames place, odd frames remove half
boolean remove = (f % 2 == 1);
for (int d = 0; d < depthsPerFrame; d++) {
ops++;
int found = -1;
for (int k = 0; k < cmds.size(); k++) {
ops++;
if (cmds.get(k).depth == d) { found = k; break; }
}
if (remove) {
if (found >= 0) {
// swap_remove
int last = cmds.size() - 1;
cmds.set(found, cmds.get(last));
cmds.remove(last);
}
} else {
if (found >= 0) {
cmds.get(found).merge();
} else {
cmds.add(new GotoCommand(d, idx++));
}
}
}
}
return ops;
}
static long fixedGotoWithRemove(int frames, int depthsPerFrame) {
List<GotoCommand> cmds = new ArrayList<>();
Map<Integer, Integer> depthIndex = new HashMap<>();
long ops = 0;
int idx = 0;
for (int f = 0; f < frames; f++) {
boolean remove = (f % 2 == 1);
for (int d = 0; d < depthsPerFrame; d++) {
ops++;
Integer found = depthIndex.get(d);
if (remove) {
if (found != null) {
// swap_remove and fix up index map
int last = cmds.size() - 1;
int displacedDepth = cmds.get(last).depth;
cmds.set(found, cmds.get(last));
cmds.remove(last);
depthIndex.remove(d);
if (displacedDepth != d) {
depthIndex.put(displacedDepth, found);
}
}
} else {
if (found != null) {
cmds.get(found).merge();
} else {
depthIndex.put(d, cmds.size());
cmds.add(new GotoCommand(d, idx++));
}
}
}
}
return ops;
}
public static void main(String[] args) {
System.out.println("ruffle-0002: MovieClip goto_commands depth lookup O(F*D^2) -> O(F*D)");
// Small smoke test
long defSmall = defectiveGoto(10, 5);
long fixSmall = fixedGoto(10, 5);
System.out.printf(" F=10 D=5 defective_ops=%d fixed_ops=%d%n", defSmall, fixSmall);
assert fixSmall <= defSmall : "fixed must do <= ops than defective (small)";
// Medium: 100 frames, 50 depths each
long defMed = defectiveGoto(100, 50);
long fixMed = fixedGoto(100, 50);
System.out.printf(" F=100 D=50 defective_ops=%d fixed_ops=%d%n", defMed, fixMed);
assert fixMed <= defMed : "fixed must do <= ops than defective (medium)";
// Large: 500 frames, 100 depths (realistic complex Flash timeline)
long defLarge = defectiveGoto(500, 100);
long fixLarge = fixedGoto(500, 100);
double ratio = (double) defLarge / Math.max(1, fixLarge);
System.out.printf(" F=500 D=100 defective_ops=%d fixed_ops=%d ratio=%.1fx%n",
defLarge, fixLarge, ratio);
assert ratio >= 20.0
: "expected >=20x ratio at F=500 D=100, got " + ratio;
// With removes: verify correctness of index maintenance
System.out.println(" Testing with removes + swap_remove index correction...");
int F = 40, D = 20;
// Build reference list using defective (known correct)
List<GotoCommand> refCmds = new ArrayList<>();
Map<Integer, Integer> fixIndex = new HashMap<>();
for (int f = 0; f < F; f++) {
boolean remove = (f % 2 == 1);
for (int d = 0; d < D; d++) {
Integer found = fixIndex.get(d);
if (remove) {
if (found != null) {
int last = refCmds.size() - 1;
int displacedDepth = refCmds.get(last).depth;
refCmds.set(found, refCmds.get(last));
refCmds.remove(last);
fixIndex.remove(d);
if (displacedDepth != d) fixIndex.put(displacedDepth, found);
}
} else {
if (found != null) {
refCmds.get(found).merge();
} else {
fixIndex.put(d, refCmds.size());
refCmds.add(new GotoCommand(d, 0));
}
}
}
}
// Verify index map consistency: every entry in fixIndex must point to correct depth
for (Map.Entry<Integer, Integer> e : fixIndex.entrySet()) {
int depth = e.getKey();
int idx = e.getValue();
assert refCmds.get(idx).depth == depth
: "index map inconsistent: depth=" + depth + " -> idx=" + idx
+ " but cmd.depth=" + refCmds.get(idx).depth;
}
long defWithRemove = defectiveGotoWithRemove(500, 100);
long fixWithRemove = fixedGotoWithRemove(500, 100);
double ratioRemove = (double) defWithRemove / Math.max(1, fixWithRemove);
System.out.printf(" F=500 D=100 with-removes defective_ops=%d fixed_ops=%d ratio=%.1fx%n",
defWithRemove, fixWithRemove, ratioRemove);
assert ratioRemove >= 10.0
: "expected >=10x ratio with removes, got " + ratioRemove;
System.out.println("PASS");
}
}