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.
234 lines
9.4 KiB
Java
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");
|
|
}
|
|
}
|