package unit; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; /** * hudi-0001: BaseHoodieTimeline.appendLoadedInstants List.contains O(N×M) → HashSet O(N+M) * * Simulates the deduplication filter in appendLoadedInstants using a lightweight * Instant stand-in (just an integer ID) to count comparison operations. * * Compile: javac -d . HudiTimelineListContainsAlgorithm.java * Run: java -ea unit.HudiTimelineListContainsAlgorithm */ public class HudiTimelineListContainsAlgorithm { // Lightweight stand-in for HoodieInstant static class Instant { final int id; static long compareCount = 0; Instant(int id) { this.id = id; } @Override public boolean equals(Object o) { compareCount++; if (this == o) return true; if (!(o instanceof Instant)) return false; return this.id == ((Instant) o).id; } @Override public int hashCode() { return Integer.hashCode(id); } } // Defective: List.contains — O(N×M) static List appendLoadedInstants_slow(List existing, List loaded) { // existingInstants is a plain List List existingInstants = new ArrayList<>(existing); return loaded.stream() .filter(instant -> !existingInstants.contains(instant)) .collect(Collectors.toList()); } // Fixed: Set.contains — O(N+M) static List appendLoadedInstants_fast(List existing, List loaded) { Set existingSet = new HashSet<>(existing); return loaded.stream() .filter(instant -> !existingSet.contains(instant)) .collect(Collectors.toList()); } static void test(String name, int existingSize, int loadedSize, int overlapSize) { List existing = new ArrayList<>(); for (int i = 0; i < existingSize; i++) existing.add(new Instant(i)); // loaded = first overlapSize are duplicates, rest are new List loaded = new ArrayList<>(); for (int i = 0; i < overlapSize; i++) loaded.add(new Instant(i)); // duplicates for (int i = existingSize; i < existingSize + (loadedSize - overlapSize); i++) { loaded.add(new Instant(i)); // new instants } Instant.compareCount = 0; List resultSlow = appendLoadedInstants_slow(existing, loaded); long slowCount = Instant.compareCount; Instant.compareCount = 0; List resultFast = appendLoadedInstants_fast(existing, loaded); long fastCount = Instant.compareCount; // Correctness: both must return the same new instants assert resultSlow.size() == resultFast.size() : "Size mismatch slow=" + resultSlow.size() + " fast=" + resultFast.size(); Set slowIds = resultSlow.stream().map(x -> x.id).collect(Collectors.toSet()); Set fastIds = resultFast.stream().map(x -> x.id).collect(Collectors.toSet()); assert slowIds.equals(fastIds) : "Result mismatch"; long ratio = fastCount == 0 ? 0 : slowCount / fastCount; System.out.printf(" %-40s existing=%4d loaded=%4d overlap=%4d slow=%8d fast=%8d ratio=%4dx%n", name, existingSize, loadedSize, overlapSize, slowCount, fastCount, ratio); } public static void main(String[] args) { int pass = 0; int fail = 0; System.out.println("hudi-0001: BaseHoodieTimeline.appendLoadedInstants"); // Test 1: correctness with small input try { test("correctness-small", 10, 10, 5); pass++; } catch (AssertionError e) { System.out.println(" FAIL correctness-small: " + e.getMessage()); fail++; } // Test 2: slow path is measurably more expensive than fast path at N=500 Instant.compareCount = 0; List existing500 = new ArrayList<>(); for (int i = 0; i < 500; i++) existing500.add(new Instant(i)); List loaded500 = new ArrayList<>(); for (int i = 0; i < 250; i++) loaded500.add(new Instant(i)); // duplicates for (int i = 500; i < 750; i++) loaded500.add(new Instant(i)); // new Instant.compareCount = 0; appendLoadedInstants_slow(existing500, loaded500); long slowOps = Instant.compareCount; Instant.compareCount = 0; appendLoadedInstants_fast(existing500, loaded500); long fastOps = Instant.compareCount; System.out.printf(" %-40s slow=%8d fast=%8d%n", "N=500-ops-comparison", slowOps, fastOps); if (slowOps > fastOps * 10) { System.out.println(" PASS slow-path > 10x fast-path at N=500"); pass++; } else { System.out.println(" FAIL expected slow > 10x fast"); fail++; } // Test 3: no overlap — all new instants try { test("all-new", 200, 200, 0); pass++; } catch (AssertionError e) { System.out.println(" FAIL all-new: " + e.getMessage()); fail++; } // Test 4: full overlap — all duplicates try { test("all-dup", 200, 100, 100); pass++; } catch (AssertionError e) { System.out.println(" FAIL all-dup: " + e.getMessage()); fail++; } System.out.println(pass + "/" + (pass + fail) + " PASS"); if (fail > 0) throw new RuntimeException(fail + " tests failed"); } }