wave10 complete: 472/219
This commit is contained in:
parent
70702dff5c
commit
6276aea2e5
28 changed files with 929 additions and 5 deletions
402
defects/leveldb/unit/GetOverlappingInputsAlgorithm.java
Normal file
402
defects/leveldb/unit/GetOverlappingInputsAlgorithm.java
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue