package unit; import java.util.*; /** * Standalone unit test for leveldb-001: * Version::GetOverlappingInputs Level-0 quadratic restart scan. * * Models the exact C++ algorithm from db/version_set.cc lines 512-537. * Compile: javac -d . *.java * Run: java unit.GetOverlappingInputsAlgorithm */ public class GetOverlappingInputsAlgorithm { // ------------------------------------------------------------------------- // Model types // ------------------------------------------------------------------------- static class FileMetaData { final int smallest; final int largest; FileMetaData(int s, int l) { this.smallest = s; this.largest = l; } public String toString() { return "[" + smallest + "," + largest + "]"; } } // ------------------------------------------------------------------------- // Defective algorithm (exact port from LevelDB, db/version_set.cc:512-537) // ------------------------------------------------------------------------- static class DefectiveFinder { static Result find(List level0, int beginKey, int endKey) { int comparisons = 0; int userBegin = beginKey; int userEnd = endKey; List inputs = new ArrayList<>(); for (int i = 0; i < level0.size();) { FileMetaData f = level0.get(i++); int fileStart = f.smallest; int fileLimit = f.largest; comparisons++; if (fileLimit < userBegin) { // completely before — skip } else if (fileStart > userEnd) { // completely after — skip } else { inputs.add(f); // level == 0: check for range expansion → restart if (fileStart < userBegin) { userBegin = fileStart; inputs.clear(); i = 0; // ← restart (O(F²)) } else if (fileLimit > userEnd) { userEnd = fileLimit; inputs.clear(); i = 0; // ← restart (O(F²)) } } } return new Result(new ArrayList<>(inputs), comparisons); } } // ------------------------------------------------------------------------- // Fixed algorithm: O(F) — extend range until stable, no restarts // ------------------------------------------------------------------------- static class FixedFinder { static Result find(List level0, int beginKey, int endKey) { int comparisons = 0; int curBegin = beginKey; int curEnd = endKey; Set inSet = new HashSet<>(); List inputs = new ArrayList<>(); boolean changed = true; while (changed) { changed = false; for (FileMetaData f : level0) { comparisons++; if (inSet.contains(f)) continue; int fs = f.smallest; int fl = f.largest; if (fl < curBegin || fs > curEnd) continue; inSet.add(f); inputs.add(f); if (fs < curBegin) { curBegin = fs; changed = true; } if (fl > curEnd) { curEnd = fl; changed = true; } } } return new Result(new ArrayList<>(inputs), comparisons); } } // ------------------------------------------------------------------------- // Result type // ------------------------------------------------------------------------- static class Result { final List files; final int comparisons; Result(List f, int c) { files = f; comparisons = c; } } // ------------------------------------------------------------------------- // Worst-case file set construction // // Pattern that maximises restarts: // Query: [M, M] (single point in middle) // Files ordered so that each new file adds to the edge of the range // and forces a restart that eventually reaches all F files. // // Specifically: arrange files as a chain where the first file [M, M+1] // triggers an expansion, which expands to include [M+1, M+2], etc., BUT // each expansion of the RIGHT edge forces restart from i=0 which must // re-scan all already-visited files. // // Construction: place files at positions F, F-1, ..., 1 (sorted // largest-limit first in the list). // File i: [M - (F-i), M - (F-i) + F] — a chain expanding left AND right. // // Simpler: interleave left-expanding and right-expanding files so // every other add triggers a restart. // // Most direct: build F files where file[0] barely overlaps the query, // file[1] expands left to include file[0], file[2] expands right, etc. // When file[k] is found, restart skips to i=0 re-scanning k files. // Total: 1 + 2 + 3 + ... + F = O(F²). // ------------------------------------------------------------------------- /** * Build worst-case file set for the LevelDB restart defect. * * Strategy: F files with alternating left/right expansion. * Query starts at [CENTER, CENTER]. * * Files placed in the list so that: * - First F/2 files expand the LEFT boundary (placed at the END of list) * - Second F/2 files expand the RIGHT boundary (placed at the END too) * - Interleaved so each add triggers a restart, re-scanning all previous. * * Simpler approach: all F files form a staircase to the right. * File[i] = [i, i+1]. * List order: [F-1, F], [F-2, F-1], ..., [0, 1] — sorted by smallest desc. * Query: [F-1, F-1]. * * First match: file[0] = [F-1, F] — expands right (userEnd = F). * → restart i=0 * Next match: [F-1, F] again (already in cleared list). No expansion. * Then [F-2, F-1] — 5 ≤ userEnd=F and F-2 ≤ userEnd=F, in range → add. * F-2 < userBegin=F-1 → expand left, restart. * etc. * * After k restarts, we've re-scanned k*F comparisons total. */ static List buildWorstCaseFiles(int F, int[] queryOut) { // Files form a right-expanding chain: // File i covers [F-1-i, F-i] // Listed in order of decreasing start: file[0]=[F-1,F], file[1]=[F-2,F-1], ... // Query starts at [F-1, F-1]. // File[0]=[F-1,F]: matches [F-1,F-1], expands right to F → restart // File[0]=[F-1,F]: matches again, no expansion // File[1]=[F-2,F-1]: matches [F-1,F], F-2 < F-1 → expand left → restart // ... // This gives ~F restarts of increasing length. List files = new ArrayList<>(); for (int i = 0; i < F; i++) { files.add(new FileMetaData(F - 1 - i, F - i)); } queryOut[0] = F - 1; // begin queryOut[1] = F - 1; // end return files; } /** * Alternative worst case: all F files have identical range [0, F], * arranged so the query expands to include all of them. * But since they're identical, no expansion-on-add → O(F) not O(F²). * * Better: staircase where each file has UNIQUE boundaries and * each forces an expansion in different direction. * * Proven worst case: files are a chain of length F, listed in * REVERSE order so that the scan finds the "last" file first, * which expands the range to include the "second-to-last", etc. * Each new discovery expands the range by 1 unit, forcing a restart. * * Files: [0,1], [1,2], [2,3], ..., [F-2, F-1] (F-1 files) * Listed in order: [F-2,F-1], [F-3,F-2], ..., [0,1] * Query: [F-2, F-2]. * * Round 1: find [F-2,F-1] → expands right → restart (scan F-1 files again) * Round 2: find [F-2,F-1] (no new expansion), then [F-3,F-2] (expands left) → restart * Round 3: find [F-2,F-1], [F-3,F-2] (no expansion), then [F-4,F-3] → restart * ... * Round k: scan k files before finding new expansion * Total comparisons: 2 + 3 + ... + F-1 = O(F²) */ static List buildWorstCaseChain(int F, int[] queryOut) { // Files: [0,1],[1,2],...,[F-2,F-1] // Listed in reverse: [F-2,F-1],[F-3,F-2],...,[0,1] List files = new ArrayList<>(); for (int i = F - 2; i >= 0; i--) { files.add(new FileMetaData(i, i + 1)); } queryOut[0] = F - 2; // begin = start of last file queryOut[1] = F - 2; // end = same (single point) return files; } // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- static List buildNonOverlappingFiles(int f) { List files = new ArrayList<>(); for (int i = 0; i < f; i++) { files.add(new FileMetaData(i * 10, i * 10 + 9)); } return files; } // ------------------------------------------------------------------------- // Tests // ------------------------------------------------------------------------- static int passed = 0, failed = 0; static void expect(String label, boolean condition) { if (condition) { System.out.println(" PASS: " + label); passed++; } else { System.out.println(" FAIL: " + label); failed++; } } static void testBasicOverlap() { System.out.println("[testBasicOverlap]"); List files = new ArrayList<>(); files.add(new FileMetaData(10, 20)); files.add(new FileMetaData(15, 25)); files.add(new FileMetaData(50, 60)); Result slow = DefectiveFinder.find(files, 18, 22); Result fast = FixedFinder.find(files, 18, 22); expect("defective finds 2 files", slow.files.size() == 2); expect("fixed finds 2 files", fast.files.size() == 2); expect("same file set", new HashSet<>(slow.files).equals(new HashSet<>(fast.files))); } static void testNoOverlap() { System.out.println("[testNoOverlap]"); List files = buildNonOverlappingFiles(20); Result slow = DefectiveFinder.find(files, 15, 25); Result fast = FixedFinder.find(files, 15, 25); expect("defective finds files in range", slow.files.size() >= 1); expect("fixed finds same count", slow.files.size() == fast.files.size()); expect("same file set", new HashSet<>(slow.files).equals(new HashSet<>(fast.files))); } static void testSingleFile() { System.out.println("[testSingleFile]"); List files = new ArrayList<>(); files.add(new FileMetaData(5, 15)); Result slow = DefectiveFinder.find(files, 10, 20); Result fast = FixedFinder.find(files, 10, 20); expect("defective finds 1", slow.files.size() == 1); expect("fixed finds 1", fast.files.size() == 1); } static void testEmptyLevel() { System.out.println("[testEmptyLevel]"); List files = new ArrayList<>(); Result slow = DefectiveFinder.find(files, 0, 100); Result fast = FixedFinder.find(files, 0, 100); expect("defective: 0 files", slow.files.isEmpty()); expect("fixed: 0 files", fast.files.isEmpty()); } static void testWorstCaseChain_F20() { System.out.println("[testWorstCaseChain F=20]"); int F = 20; int[] query = new int[2]; List files = buildWorstCaseChain(F, query); Result slow = DefectiveFinder.find(files, query[0], query[1]); Result fast = FixedFinder.find(files, query[0], query[1]); System.out.println(" defective comparisons: " + slow.comparisons); System.out.println(" fixed comparisons: " + fast.comparisons); System.out.println(" defective files found: " + slow.files.size()); System.out.println(" fixed files found: " + fast.files.size()); expect("same file set", new HashSet<>(slow.files).equals(new HashSet<>(fast.files))); // O(F²): for F=20 chain has F-1=19 files; expected ~(F-1)²/2 ≈ 180 comparisons expect("defective is super-linear (> F comparisons)", slow.comparisons > F); expect("fixed is linear (<= 3*F comparisons)", fast.comparisons <= 3 * F); expect("defective uses more comparisons than fixed", slow.comparisons >= fast.comparisons); } static void testWorstCaseChain_F50() { System.out.println("[testWorstCaseChain F=50]"); int F = 50; int[] query = new int[2]; List files = buildWorstCaseChain(F, query); Result slow = DefectiveFinder.find(files, query[0], query[1]); Result fast = FixedFinder.find(files, query[0], query[1]); System.out.println(" defective comparisons: " + slow.comparisons); System.out.println(" fixed comparisons: " + fast.comparisons); expect("same file set", new HashSet<>(slow.files).equals(new HashSet<>(fast.files))); // F=50: chain has 49 files; expected quadratic ~ 49*50/2 ≈ 1225 comparisons expect("defective is quadratic (>= F*F/4 comparisons)", slow.comparisons >= F * F / 4); expect("fixed is linear (<= 3*F comparisons)", fast.comparisons <= 3 * F); double ratio = (double) slow.comparisons / Math.max(1, fast.comparisons); System.out.printf(" speedup ratio: %.1fx%n", ratio); expect("speedup >= 5x", ratio >= 5.0); } static void testWorstCaseChain_F100() { System.out.println("[testWorstCaseChain F=100]"); int F = 100; int[] query = new int[2]; List files = buildWorstCaseChain(F, query); Result slow = DefectiveFinder.find(files, query[0], query[1]); Result fast = FixedFinder.find(files, query[0], query[1]); System.out.println(" defective comparisons: " + slow.comparisons); System.out.println(" fixed comparisons: " + fast.comparisons); expect("same file set", new HashSet<>(slow.files).equals(new HashSet<>(fast.files))); // F=100: expected ~99*100/2 ≈ 4950 comparisons expect("defective is quadratic (>= F*F/4)", slow.comparisons >= F * F / 4); expect("fixed is linear (<= 3*F)", fast.comparisons <= 3 * F); double ratio = (double) slow.comparisons / Math.max(1, fast.comparisons); System.out.printf(" speedup ratio: %.1fx%n", ratio); expect("speedup >= 10x at F=100", ratio >= 10.0); } static void testRangeExpansionBothDirections() { System.out.println("[testRangeExpansionBothDirections]"); // 5 files that all need to be included: // [5,6], [4,5], [6,7], [3,4], [7,8] // Listed in this order. Query [5,6]. // Each new file expands the range in alternating directions. List files = new ArrayList<>(); files.add(new FileMetaData(5, 6)); files.add(new FileMetaData(4, 5)); files.add(new FileMetaData(6, 7)); files.add(new FileMetaData(3, 4)); files.add(new FileMetaData(7, 8)); Result slow = DefectiveFinder.find(files, 5, 6); Result fast = FixedFinder.find(files, 5, 6); expect("defective finds all 5 files", slow.files.size() == 5); expect("fixed finds all 5 files", fast.files.size() == 5); expect("same file set", new HashSet<>(slow.files).equals(new HashSet<>(fast.files))); System.out.println(" defective comparisons: " + slow.comparisons); System.out.println(" fixed comparisons: " + fast.comparisons); } // ------------------------------------------------------------------------- // Main // ------------------------------------------------------------------------- public static void main(String[] args) { System.out.println("=== leveldb-001: GetOverlappingInputs Level-0 quadratic restart ==="); testBasicOverlap(); testNoOverlap(); testSingleFile(); testEmptyLevel(); testRangeExpansionBothDirections(); testWorstCaseChain_F20(); testWorstCaseChain_F50(); testWorstCaseChain_F100(); System.out.println(); System.out.println("Results: " + passed + " passed, " + failed + " failed"); if (failed > 0) System.exit(1); } }