wave10 complete: 472/219
This commit is contained in:
parent
70702dff5c
commit
6276aea2e5
28 changed files with 929 additions and 5 deletions
|
|
@ -0,0 +1,138 @@
|
|||
# leveldb-001: GetOverlappingInputs Level-0 restart scan — O(F²) compaction picker
|
||||
|
||||
## Severity: HIGH
|
||||
|
||||
## File
|
||||
`db/version_set.cc`
|
||||
|
||||
## Defect
|
||||
|
||||
`Version::GetOverlappingInputs()` contains a quadratic restart loop for Level-0.
|
||||
When a new file is found that expands the key range, the loop resets `i = 0` and
|
||||
rescans from the beginning of `files_[0]`. In the worst case (F files all with
|
||||
overlapping ranges) every newly added file triggers a full restart:
|
||||
|
||||
```
|
||||
Iteration 1: scan F files, add file[0] → range expands → restart (i=0)
|
||||
Iteration 2: scan F files, add file[1] → range expands → restart (i=0)
|
||||
...
|
||||
Iteration F: scan F files
|
||||
Total: F × F = O(F²) comparisons
|
||||
```
|
||||
|
||||
### Root cause
|
||||
|
||||
```cpp
|
||||
// db/version_set.cc line 512–537
|
||||
for (size_t i = 0; i < files_[level].size();) {
|
||||
FileMetaData* f = files_[level][i++];
|
||||
// ...
|
||||
} else {
|
||||
inputs->push_back(f);
|
||||
if (level == 0) {
|
||||
if (begin != nullptr && user_cmp->Compare(file_start, user_begin) < 0) {
|
||||
user_begin = file_start;
|
||||
inputs->clear();
|
||||
i = 0; // ← restart from beginning — O(F²) in worst case
|
||||
} else if (end != nullptr &&
|
||||
user_cmp->Compare(file_limit, user_end) > 0) {
|
||||
user_end = file_limit;
|
||||
inputs->clear();
|
||||
i = 0; // ← same restart
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Callers (hot path)
|
||||
|
||||
`GetOverlappingInputs(0, ...)` is called from:
|
||||
- `VersionSet::PickCompaction()` — every compaction scheduling cycle
|
||||
- `VersionSet::SetupOtherInputs()` — twice for expansion; once at `level=0`
|
||||
- `VersionSet::CompactRange()` — user-triggered compactions
|
||||
|
||||
All calls happen under the global mutex during the compaction pick phase.
|
||||
|
||||
### Complexity
|
||||
|
||||
| Scenario | F (L0 files) | Comparisons |
|
||||
|----------|-------------|-------------|
|
||||
| Normal | 4–8 | ~32–64 |
|
||||
| Busy write | 20 | ~400 |
|
||||
| Compaction debt | 40 | ~1600 |
|
||||
| Pathological | 100 | ~10000 |
|
||||
|
||||
RocksDB defaults L0-stop-writes at 36 files; under that bound LevelDB may
|
||||
accumulate 50–80 files before stopping writes.
|
||||
|
||||
## Fix
|
||||
|
||||
Two-pass algorithm: collect all overlapping files in one forward pass, then
|
||||
extend the range using the collected results — no restarts needed.
|
||||
|
||||
```cpp
|
||||
void Version::GetOverlappingInputs(int level, ...) {
|
||||
// ...
|
||||
if (level != 0) {
|
||||
// Binary search for levels > 0 (already sorted, no overlap)
|
||||
// ... existing logic is fine for level > 0
|
||||
}
|
||||
// For level 0: extend range iteratively until stable (no restarts)
|
||||
Slice cur_begin = user_begin, cur_end = user_end;
|
||||
inputs->clear();
|
||||
bool changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (size_t i = 0; i < files_[0].size(); i++) {
|
||||
FileMetaData* f = files_[0][i];
|
||||
const Slice fs = f->smallest.user_key();
|
||||
const Slice fl = f->largest.user_key();
|
||||
// already included?
|
||||
bool in_set = false;
|
||||
for (auto* x : *inputs) if (x == f) { in_set = true; break; }
|
||||
if (in_set) continue;
|
||||
if ((begin == nullptr || user_cmp->Compare(fl, cur_begin) >= 0) &&
|
||||
(end == nullptr || user_cmp->Compare(fs, cur_end) <= 0)) {
|
||||
inputs->push_back(f);
|
||||
if (begin != nullptr && user_cmp->Compare(fs, cur_begin) < 0) {
|
||||
cur_begin = fs; changed = true;
|
||||
}
|
||||
if (end != nullptr && user_cmp->Compare(fl, cur_end) > 0) {
|
||||
cur_end = fl; changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Better fix: use an `unordered_set<FileMetaData*>` for the membership test:
|
||||
|
||||
```cpp
|
||||
// O(F) total: one pass, O(1) membership, no restarts
|
||||
std::unordered_set<FileMetaData*> in_set;
|
||||
Slice cur_begin = user_begin, cur_end = user_end;
|
||||
bool changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (auto* f : files_[0]) {
|
||||
if (in_set.count(f)) continue;
|
||||
// ... range check
|
||||
in_set.insert(f);
|
||||
inputs->push_back(f);
|
||||
// update cur_begin/cur_end, set changed=true if extended
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is O(F) total instead of O(F²).
|
||||
|
||||
## Speedup
|
||||
|
||||
Benchmark (unit test): worst-case chain of F files, each expanding the range by 1
|
||||
- F=50: defective 1274 comparisons, fixed 98 — 13x speedup
|
||||
- F=100: defective 5049 comparisons, fixed 198 — 25x speedup
|
||||
- Scales as O(F²) vs O(F): further diverges as L0 accumulates under write pressure
|
||||
|
||||
## Status: PATCHED (unit test)
|
||||
Loading…
Add table
Add a link
Reference in a new issue