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) 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 (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 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 O(1) per tag --- static long fixedGoto(int frames, int depthsPerFrame) { List cmds = new ArrayList<>(); Map 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 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 cmds = new ArrayList<>(); Map 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 refCmds = new ArrayList<>(); Map 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 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"); } }