java-topology/defects/hudi/unit/HudiTimelineListContainsAlgorithm.java

148 lines
5.5 KiB
Java
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Instant> appendLoadedInstants_slow(List<Instant> existing, List<Instant> loaded) {
// existingInstants is a plain List
List<Instant> existingInstants = new ArrayList<>(existing);
return loaded.stream()
.filter(instant -> !existingInstants.contains(instant))
.collect(Collectors.toList());
}
// Fixed: Set.contains — O(N+M)
static List<Instant> appendLoadedInstants_fast(List<Instant> existing, List<Instant> loaded) {
Set<Instant> 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<Instant> existing = new ArrayList<>();
for (int i = 0; i < existingSize; i++) existing.add(new Instant(i));
// loaded = first overlapSize are duplicates, rest are new
List<Instant> 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<Instant> resultSlow = appendLoadedInstants_slow(existing, loaded);
long slowCount = Instant.compareCount;
Instant.compareCount = 0;
List<Instant> 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<Integer> slowIds = resultSlow.stream().map(x -> x.id).collect(Collectors.toSet());
Set<Integer> 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<Instant> existing500 = new ArrayList<>();
for (int i = 0; i < 500; i++) existing500.add(new Instant(i));
List<Instant> 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");
}
}