use std::cell::OnceCell;
use std::rc::Rc;
use super::bloom::BloomFilter;
use gnitz_wire::PkBuf;
use gnitz_zset::repr::{merge_consolidated, Batch, MemBatch};
use gnitz_zset::schema::key::{pk_bytes_eq, pk_in_range, pk_ranges_overlap, probe_key};
use gnitz_zset::schema::SchemaDescriptor;
const FOLD_THRESHOLD: usize = 16;
const ROOM_SHARE: usize = 16;
fn pk_min(run: &Batch) -> &[u8] {
run.get_pk_bytes(0)
}
fn pk_max(run: &Batch) -> &[u8] {
run.get_pk_bytes(run.len() - 1)
}
pub(super) struct RunSet {
runs: Vec<Rc<Batch>>,
bloom: OnceCell<BloomFilter>,
budget: usize,
bytes: usize,
}
impl RunSet {
pub(super) fn new(budget: usize) -> Self {
RunSet {
runs: Vec::with_capacity(FOLD_THRESHOLD),
bloom: OnceCell::new(),
budget,
bytes: 0,
}
}
pub(super) fn push(&mut self, run: Batch, schema: &SchemaDescriptor) {
debug_assert!(run.is_consolidated(), "RunSet::push requires a consolidated run",);
self.push_run(Rc::new(run.trimmed()), schema);
}
fn push_run(&mut self, run: Rc<Batch>, schema: &SchemaDescriptor) {
if run.is_empty() {
return;
}
if let Some(bloom) = self.bloom.get_mut() {
bloom_add_batch(bloom, &run);
}
self.bytes += run.total_bytes();
self.runs.push(run);
if self.runs.len() >= FOLD_THRESHOLD {
self.fold(schema);
}
}
pub(super) fn runs_overlapping(&self, bound: Option<(PkBuf, PkBuf)>) -> impl Iterator<Item = &Rc<Batch>> {
self.runs.iter().filter(move |run| {
bound.is_none_or(|(lo, hi)| pk_ranges_overlap(pk_min(run), pk_max(run), lo.pk_bytes(), hi.pk_bytes()))
})
}
pub(super) fn find_pk_bytes(&self, key: &[u8], fingerprint: u64, mut visitor: impl FnMut(&Rc<Batch>, usize)) {
if !self.may_contain(fingerprint) {
return;
}
for run in self.runs.iter().rev() {
if !pk_in_range(pk_min(run), pk_max(run), key) {
continue;
}
let start = run.find_lower_bound_bytes(key);
if start < run.len() && pk_bytes_eq(run.get_pk_bytes(start), key) {
visitor(run, start);
}
}
}
pub(super) fn len(&self) -> usize {
self.runs.len()
}
pub(super) fn is_full(&self) -> bool {
self.bytes > self.budget
}
pub(super) fn is_crowded(&self) -> bool {
self.bytes > self.budget - self.budget / ROOM_SHARE
}
pub(super) fn row_count(&self) -> usize {
self.runs.iter().map(|r| r.len()).sum()
}
pub(super) fn clear(&mut self) {
self.runs.clear();
self.bytes = 0;
self.bloom.take();
}
pub(super) fn widen_runs(&mut self, schema: &SchemaDescriptor) {
let npc = schema.num_payload_cols();
let mut bytes = 0;
for run in &mut self.runs {
if run.num_payload_cols() < npc {
let widened = run.widened_with_nulls(schema, false);
debug_assert!(
widened.is_consolidated(),
"widen_runs: the widened run must still be consolidated",
);
*run = Rc::new(widened.trimmed());
}
bytes += run.total_bytes();
}
self.bytes = bytes;
}
pub(super) fn fold(&mut self, schema: &SchemaDescriptor) {
if self.runs.len() <= 1 {
return;
}
let input_rows = self.row_count();
let big = (0..self.runs.len()).max_by_key(|&i| self.runs[i].len()).unwrap();
let merged = if self.runs[big].len() * 2 >= input_rows {
let dominant = self.runs.remove(big);
match &self.runs[..] {
[one] => dominant.merged_consolidated(one, schema),
_ => dominant.merged_consolidated(&self.consolidate_all(schema), schema),
}
} else {
self.consolidate_all(schema)
};
self.runs.clear();
if self.bloom.get().is_some_and(|b| b.stale(merged.len())) {
self.bloom.take();
}
self.bytes = 0;
if !merged.is_empty() {
let run = Rc::new(merged.trimmed());
self.bytes = run.total_bytes();
self.runs.push(run);
}
}
fn consolidate_all(&self, schema: &SchemaDescriptor) -> Batch {
let views: Vec<MemBatch> = self.runs.iter().map(|r| r.as_mem_batch()).collect();
merge_consolidated(&views, schema)
}
pub(super) fn spill<E>(
&mut self,
schema: &SchemaDescriptor,
write: impl FnOnce(&Batch) -> Result<(), E>,
) -> Result<bool, E> {
self.fold(schema);
let Some(run) = self.runs.first() else {
return Ok(false);
};
write(run)?;
self.clear();
Ok(true)
}
pub(super) fn drain_into(&mut self, dst: &mut RunSet, schema: &SchemaDescriptor) -> Option<Rc<Batch>> {
self.fold(schema);
let run = self.runs.pop();
self.clear();
if let Some(run) = &run {
dst.push_run(Rc::clone(run), schema);
}
run
}
fn may_contain(&self, probe_key: u64) -> bool {
if self.runs.is_empty() {
return false;
}
let bloom = self.bloom.get_or_init(|| {
let row_width = self.runs[0].schema().row_width();
let mut bloom = BloomFilter::new((self.budget / row_width).max(16));
for run in &self.runs {
bloom_add_batch(&mut bloom, run);
}
bloom
});
bloom.may_contain(probe_key)
}
}
fn bloom_add_batch(bloom: &mut BloomFilter, batch: &Batch) {
for i in 0..batch.len() {
bloom.add(probe_key(batch.get_pk_bytes(i)));
}
}
#[cfg(test)]
#[path = "tests/run_set.rs"]
mod tests;
#[cfg(test)]
#[path = "benches/run_set.rs"]
mod bench;