java-topology/defects/leveldb/unit/GetOverlappingInputsAlgorithm.java

402 lines
17 KiB
Java

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<FileMetaData> level0, int beginKey, int endKey) {
int comparisons = 0;
int userBegin = beginKey;
int userEnd = endKey;
List<FileMetaData> 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<FileMetaData> level0, int beginKey, int endKey) {
int comparisons = 0;
int curBegin = beginKey;
int curEnd = endKey;
Set<FileMetaData> inSet = new HashSet<>();
List<FileMetaData> 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<FileMetaData> files;
final int comparisons;
Result(List<FileMetaData> 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<FileMetaData> 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<FileMetaData> 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<FileMetaData> 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<FileMetaData> 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<FileMetaData> buildNonOverlappingFiles(int f) {
List<FileMetaData> 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<FileMetaData> 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<FileMetaData> 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<FileMetaData> 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<FileMetaData> 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<FileMetaData> 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<FileMetaData> 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<FileMetaData> 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<FileMetaData> 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);
}
}