use std::collections::{BTreeSet, BinaryHeap};
use std::io::{BufReader, BufWriter, Read, Write};
use std::path::PathBuf;
use std::rc::Rc;
use crate::row::Val;
const MIN_CHUNK: usize = 1 << 20;
const STR_OVERHEAD: u64 = 64;
pub(super) struct DistinctSet {
ids: Vec<i64>,
compacted: usize,
strs: BTreeSet<Rc<str>>,
str_bytes: u64,
threshold: u64,
id_runs: Vec<Run>,
str_runs: Vec<Run>,
}
impl DistinctSet {
pub(super) fn new(threshold: u64) -> Self {
DistinctSet {
ids: Vec::new(),
compacted: 0,
strs: BTreeSet::new(),
str_bytes: 0,
threshold,
id_runs: Vec::new(),
str_runs: Vec::new(),
}
}
pub(super) fn insert(&mut self, v: &Val) {
match v {
Val::Id(id) => {
self.ids.push(*id);
if self.ids.len() >= self.compacted.saturating_mul(2).max(MIN_CHUNK) {
self.compact_ids();
}
}
Val::Str(s) => {
if self.strs.insert(s.clone()) {
self.str_bytes += s.len() as u64 + STR_OVERHEAD;
if self.str_bytes > self.threshold / 2 {
self.spill_strs();
}
}
}
}
}
fn compact_ids(&mut self) {
self.ids.sort_unstable();
self.ids.dedup();
self.compacted = self.ids.len();
if (self.ids.len() as u64).saturating_mul(8) > self.threshold / 2 {
if let Some(run) = Run::write_ids(&self.ids) {
self.id_runs.push(run);
self.ids = Vec::new();
self.compacted = 0;
}
}
}
fn spill_strs(&mut self) {
if let Some(run) = Run::write_strs(&self.strs) {
self.str_runs.push(run);
self.strs.clear();
self.str_bytes = 0;
}
}
#[cfg(test)]
pub(super) fn runs(&self) -> usize {
self.id_runs.len() + self.str_runs.len()
}
pub(super) fn count(mut self) -> u64 {
self.ids.sort_unstable();
self.ids.dedup();
let ids = if self.id_runs.is_empty() {
self.ids.len() as u64
} else {
let mut sources: Vec<Box<dyn Iterator<Item = i64>>> = Vec::new();
for run in &self.id_runs {
sources.push(Box::new(run.read_ids()));
}
sources.push(Box::new(std::mem::take(&mut self.ids).into_iter()));
count_merged(sources)
};
let strs = if self.str_runs.is_empty() {
self.strs.len() as u64
} else {
let mut sources: Vec<Box<dyn Iterator<Item = Vec<u8>>>> = Vec::new();
for run in &self.str_runs {
sources.push(Box::new(run.read_strs()));
}
let mem: Vec<Vec<u8>> = std::mem::take(&mut self.strs)
.into_iter()
.map(|s| s.as_bytes().to_vec())
.collect();
sources.push(Box::new(mem.into_iter()));
count_merged(sources)
};
ids + strs
}
}
fn count_merged<T: Ord>(mut sources: Vec<Box<dyn Iterator<Item = T> + '_>>) -> u64 {
let mut heap: BinaryHeap<std::cmp::Reverse<(T, usize)>> = BinaryHeap::new();
for (i, s) in sources.iter_mut().enumerate() {
if let Some(v) = s.next() {
heap.push(std::cmp::Reverse((v, i)));
}
}
let mut n = 0u64;
let mut last: Option<T> = None;
while let Some(std::cmp::Reverse((v, i))) = heap.pop() {
if let Some(next) = sources[i].next() {
heap.push(std::cmp::Reverse((next, i)));
}
if last.as_ref() != Some(&v) {
n += 1;
last = Some(v);
}
}
n
}
struct Run {
path: PathBuf,
}
impl Drop for Run {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
impl Run {
fn create() -> Option<(Run, std::fs::File)> {
if cfg!(target_arch = "wasm32") {
return None;
}
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
let n = SEQ.fetch_add(1, Ordering::Relaxed);
let path =
std::env::temp_dir().join(format!("rete-distinct-{}-{n}.run", std::process::id()));
let file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
.ok()?;
Some((Run { path }, file))
}
fn write_ids(ids: &[i64]) -> Option<Run> {
let (run, file) = Run::create()?;
let mut w = BufWriter::with_capacity(1 << 20, file);
let mut buf = Vec::with_capacity(10);
let mut prev: Option<i64> = None;
for &v in ids {
buf.clear();
match prev {
None => crate::varint::write_uvarint(&mut buf, ((v << 1) ^ (v >> 63)) as u64),
Some(p) => crate::varint::write_uvarint(&mut buf, v.wrapping_sub(p) as u64),
}
w.write_all(&buf).ok()?;
prev = Some(v);
}
w.flush().ok()?;
Some(run)
}
fn write_strs(strs: &BTreeSet<Rc<str>>) -> Option<Run> {
let (run, file) = Run::create()?;
let mut w = BufWriter::with_capacity(1 << 20, file);
let mut buf = Vec::with_capacity(10);
for s in strs {
buf.clear();
crate::varint::write_uvarint(&mut buf, s.len() as u64);
w.write_all(&buf).ok()?;
w.write_all(s.as_bytes()).ok()?;
}
w.flush().ok()?;
Some(run)
}
fn reader(&self) -> Option<BufReader<std::fs::File>> {
std::fs::File::open(&self.path)
.ok()
.map(|f| BufReader::with_capacity(1 << 20, f))
}
fn read_ids(&self) -> impl Iterator<Item = i64> {
let mut r = self.reader();
let mut prev: Option<i64> = None;
std::iter::from_fn(move || {
let u = read_varint(r.as_mut()?)?;
let v = match prev {
None => ((u >> 1) as i64) ^ -((u & 1) as i64),
Some(p) => p.wrapping_add(u as i64),
};
prev = Some(v);
Some(v)
})
}
fn read_strs(&self) -> impl Iterator<Item = Vec<u8>> {
let mut r = self.reader();
std::iter::from_fn(move || {
let r = r.as_mut()?;
let n = read_varint(r)? as usize;
let mut s = vec![0u8; n];
r.read_exact(&mut s).ok()?;
Some(s)
})
}
}
fn read_varint(r: &mut impl Read) -> Option<u64> {
let mut v = 0u64;
let mut shift = 0u32;
loop {
let mut b = [0u8; 1];
r.read_exact(&mut b).ok()?;
v |= u64::from(b[0] & 0x7f) << shift;
if b[0] & 0x80 == 0 {
return Some(v);
}
shift += 7;
if shift >= 64 {
return None;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn exact(vals: &[Val]) -> u64 {
vals.iter().collect::<BTreeSet<_>>().len() as u64
}
#[test]
fn counts_exactly_at_any_threshold() {
let mut vals: Vec<Val> = Vec::new();
let mut x: u64 = 1;
for i in 0..3_000_000u64 {
x = x.wrapping_mul(6364136223846793005).wrapping_add(1);
let id = ((x >> 20) % 900_000) as i64 - 1000; vals.push(Val::Id(id));
if i % 1000 == 0 {
vals.push(Val::Str(Rc::from(format!("\"v{}\"", (x >> 40) % 1500))));
}
}
vals.extend(std::iter::repeat_n(Val::Id(42), 2_500_000));
let want = exact(&vals);
for threshold in [u64::MAX, 4 << 20, 64 << 10, 1] {
let mut set = DistinctSet::new(threshold);
for v in &vals {
set.insert(v);
}
let spilled = set.runs();
if threshold == u64::MAX {
assert_eq!(spilled, 0);
} else if threshold <= 64 << 10 {
assert!(spilled > 1, "threshold {threshold}: {spilled} runs");
}
assert_eq!(set.count(), want, "threshold {threshold}");
}
}
#[test]
fn empty_and_single_values() {
assert_eq!(DistinctSet::new(1).count(), 0);
let mut s = DistinctSet::new(1);
s.insert(&Val::Id(-5));
s.insert(&Val::Id(-5));
s.insert(&Val::Str(Rc::from("x")));
assert_eq!(s.count(), 2);
}
#[test]
fn runs_are_removed_when_done() {
let mut s = DistinctSet::new(1);
for i in 0..(MIN_CHUNK as i64 * 3) {
s.insert(&Val::Id(i));
}
let paths: Vec<PathBuf> = s.id_runs.iter().map(|r| r.path.clone()).collect();
assert!(!paths.is_empty());
assert!(paths.iter().all(|p| p.exists()));
assert_eq!(s.count(), MIN_CHUNK as u64 * 3);
assert!(paths.iter().all(|p| !p.exists()));
}
}