package unit; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * dart-0001/0002/0003: Dart2JS namedParameters List.contains() — O(N²) vs O(N) * * Simulates forEachOrderedParameterByFunctionNode from: * pkg/compiler/lib/src/js_model/element_map.dart:636 * pkg/compiler/lib/src/ssa/builder.dart:2156, 5007 * * The defective path uses List.contains() inside an O(N) loop — * total O(N²) equality comparisons. * The fixed path converts to Set once before the loop — O(1) per lookup. * * Test: N=500 named parameters, measures equality comparison counts. * Expected ratio: >= 5x (in practice ~125x at N=500). * Prints: N/N PASS */ public class DartTest { static long slowOps = 0; static long fastOps = 0; /** * Counted string wrapper — records each .equals() call. */ static class SlowName { final String value; SlowName(String v) { this.value = v; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof SlowName)) return false; slowOps++; return this.value.equals(((SlowName) o).value); } @Override public int hashCode() { // Deliberately return constant hash so HashSet falls back to equals // — but we don't use HashSet for slowOps; this is for List only. return 0; } } static class FastName { final String value; FastName(String v) { this.value = v; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof FastName)) return false; fastOps++; return this.value.equals(((FastName) o).value); } @Override public int hashCode() { return value.hashCode(); // proper hash — Set.contains() rarely calls equals() } } /** * Defective path: * Build a List representing parameterStructure.namedParameters. * For each of N parameter declarations, call listNames.contains(decl.name). * Each call is O(N) → total O(N²). */ static void slowPath(int n) { // parameterStructure.namedParameters (subset — all N are "live") List structureNames = new ArrayList<>(n); for (int i = 0; i < n; i++) { structureNames.add(new SlowName("param_" + i)); } // node.namedParameters — all N parameter declarations List declaredNames = new ArrayList<>(n); for (int i = 0; i < n; i++) { declaredNames.add("param_" + i); } // Defective filter: for each declared param, scan the list for (String decl : declaredNames) { SlowName probe = new SlowName(decl); boolean isLive = structureNames.contains(probe); // O(N) each time // use result to prevent dead-code elimination if (!isLive) throw new RuntimeException("unexpected: param not found"); } } /** * Fixed path: * Convert parameterStructure.namedParameters to Set once. * Each contains() call is O(1). */ static void fastPath(int n) { // parameterStructure.namedParameters → converted to Set once Set structureSet = new HashSet<>(n * 2); for (int i = 0; i < n; i++) { structureSet.add(new FastName("param_" + i)); } // node.namedParameters — all N parameter declarations List declaredNames = new ArrayList<>(n); for (int i = 0; i < n; i++) { declaredNames.add("param_" + i); } // Fixed filter: O(1) contains per lookup for (String decl : declaredNames) { FastName probe = new FastName(decl); boolean isLive = structureSet.contains(probe); // O(1) if (!isLive) throw new RuntimeException("unexpected: param not found"); } } public static void main(String[] args) { int N = 500; int PASSES = 3; // Warm-up (not counted) slowPath(10); fastPath(10); slowOps = 0; fastOps = 0; // Measure for (int p = 0; p < PASSES; p++) { slowPath(N); fastPath(N); } // Expected: // slow: each of N items probed against list of N → N² comparisons per pass // (worst case probe = not-found or found at end: N scans; avg = N/2) // all N items are found (triangular: 1+2+...+N = N*(N+1)/2 per pass) // Total for PASSES: PASSES * N*(N+1)/2 ≈ 3 * 125250 = 375750 for N=500 // fast: HashSet with distinct hashes → 0 equals() calls (hash uniquely determines bucket) // In practice for N=500 unique names: ~0 equals calls long expectedSlowMin = (long) PASSES * N * (N / 4); // conservative lower bound long expectedFastMax = (long) PASSES * N * 2; // generous upper bound System.out.println("N=" + N + " PASSES=" + PASSES); System.out.println("slow ops (List.contains) : " + slowOps + " expected >= " + expectedSlowMin); System.out.println("fast ops (Set.contains) : " + fastOps + " expected <= " + expectedFastMax); int passed = 0; int total = 3; // Test 1: slow path shows O(N²) ops if (slowOps >= expectedSlowMin) { System.out.println("PASS 1/3: slow O(N²) confirmed — ops=" + slowOps + " >= " + expectedSlowMin); passed++; } else { System.out.println("FAIL 1/3: slow ops=" + slowOps + " < expected " + expectedSlowMin); } // Test 2: fast path shows near-O(1) ops if (fastOps <= expectedFastMax) { System.out.println("PASS 2/3: fast O(1) confirmed — ops=" + fastOps + " <= " + expectedFastMax); passed++; } else { System.out.println("FAIL 2/3: fast ops=" + fastOps + " > expected " + expectedFastMax); } // Test 3: ratio >= 5x long ratio = (fastOps == 0) ? Long.MAX_VALUE : slowOps / fastOps; boolean ratioOk = fastOps == 0 || slowOps >= fastOps * 5; if (ratioOk) { System.out.println("PASS 3/3: ratio=" + (fastOps == 0 ? "inf" : ratio) + "x >= 5x"); passed++; } else { System.out.println("FAIL 3/3: ratio=" + ratio + "x < 5x (slow=" + slowOps + " fast=" + fastOps + ")"); } System.out.println(passed + "/" + total + " PASS"); if (passed != total) System.exit(1); } }