use std::mem::size_of;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::{Mutex, OnceLock};
use rudb_common::{Error, LogicalType, Memory, Reservation, Result, Stage, Value, stage};
use rudb_pipeline::Lease;
use rudb_vector::{Chunk, Vector};
use crate::group::{largest, signed_value};
use crate::key::{mix, spread};
use crate::pairs::{PARTITIONS, in_parallel};
use crate::rows;
use crate::signed::SignedBlock;
const SPLIT_ROWS: usize = 16_384;
const SHIFT: u32 = u64::BITS - PARTITIONS.ilog2();
const EMPTY: u32 = u32::MAX;
#[inline]
fn hash(key: i64) -> u64 {
spread(mix(0, key as u64))
}
#[inline]
fn split(hash: u64, splits: usize) -> usize {
(hash >> 40) as usize & (splits - 1)
}
#[derive(Debug, Default)]
struct Run {
keys: Vec<i64>,
weights: Vec<u32>,
}
impl Run {
fn push(&mut self, key: i64, weight: u32) {
self.keys.push(key);
self.weights.push(weight);
}
fn len(&self) -> usize {
self.keys.len()
}
fn footprint(&self) -> usize {
self.keys.capacity() * size_of::<i64>() + self.weights.capacity() * size_of::<u32>()
}
}
#[derive(Debug)]
pub(crate) struct Exchange {
key: LogicalType,
partitions: Vec<Mutex<Vec<Run>>>,
nulls: AtomicI64,
held: Mutex<Vec<Reservation>>,
}
#[derive(Debug)]
pub(crate) struct Local {
used: bool,
runs: Vec<Run>,
pending: Option<(i64, u32)>,
nulls: i64,
block: SignedBlock,
memory: Reservation,
}
impl Local {
pub(crate) fn new(memory: &Memory) -> Self {
Self {
used: false,
runs: (0..PARTITIONS).map(|_| Run::default()).collect(),
pending: None,
nulls: 0,
block: SignedBlock::default(),
memory: memory.reservation(),
}
}
pub(crate) fn used(&self) -> bool {
self.used
}
fn footprint(&self) -> usize {
self.runs.iter().map(Run::footprint).sum()
}
#[inline]
fn scatter(&mut self, key: i64, weight: u32) {
self.runs[(hash(key) >> SHIFT) as usize].push(key, weight);
}
}
impl Exchange {
pub(crate) fn buffer(
slot: &OnceLock<Self>,
key_type: &LogicalType,
key: &Vector,
rows: usize,
local: &mut Local,
) -> Result<()> {
slot.get_or_init(|| Self {
key: key_type.clone(),
partitions: (0..PARTITIONS).map(|_| Mutex::new(Vec::new())).collect(),
nulls: AtomicI64::new(0),
held: Mutex::new(Vec::new()),
});
let timing = stage::Timing::start(Stage::Scatter);
let before = local.footprint();
let mut block = std::mem::take(&mut local.block);
block.read(rows, key)?;
let nulled = block.nulled();
let values = block.cut(rows)?;
let mut pending = local.pending.take();
for (row, &value) in values.iter().enumerate() {
if nulled && key.is_null_at(row) {
local.nulls += 1;
continue;
}
pending = match pending {
Some((held, weight)) if held == value && weight < u32::MAX => {
Some((held, weight + 1))
}
Some((held, weight)) => {
local.scatter(held, weight);
Some((value, 1))
}
None => Some((value, 1)),
};
}
local.pending = pending;
local.block = block;
local.memory.grow(width(local.footprint().saturating_sub(before)))?;
local.used = true;
timing.stop(0);
Ok(())
}
pub(crate) fn combine(&self, mut local: Local) -> Result<()> {
if let Some((key, weight)) = local.pending.take() {
local.scatter(key, weight);
}
self.nulls.fetch_add(local.nulls, Ordering::Relaxed);
for (at, run) in local.runs.iter_mut().enumerate() {
if run.len() != 0 {
let run = std::mem::take(run);
self.partitions[at].lock().map_err(poisoned)?.push(run);
}
}
self.held.lock().map_err(poisoned)?.push(local.memory);
Ok(())
}
pub(crate) fn records(&self) -> Result<usize> {
self.partitions
.iter()
.map(|runs| {
runs.lock().map(|runs| runs.iter().map(Run::len).sum::<usize>()).map_err(poisoned)
})
.sum()
}
pub(crate) fn finish(
&self,
threads: &Lease<'_>,
degree: usize,
bound: usize,
memory: &Memory,
) -> Result<Vec<Chunk>> {
let parts = in_parallel(threads, PARTITIONS, degree, "counted radix partition", |at| {
let runs = std::mem::take(&mut *self.partitions[at].lock().map_err(poisoned)?);
count_partition(runs, bound, memory)
})?;
let timing = stage::Timing::start(Stage::Emit);
let nulls = self.nulls.load(Ordering::Relaxed);
let mut output = Vec::new();
if nulls > 0 {
output.push(vec![Value::Null, Value::BigInt(nulls)]);
}
for part in parts {
for (key, count) in part {
output.push(vec![signed_value(&self.key, key)?, Value::BigInt(count)]);
}
}
let mut held = self.held.lock().map_err(poisoned)?;
held.clear();
let mut charge = memory.reservation();
let chunks = rows::chunks(&[self.key.clone(), LogicalType::BigInt], &output, &mut charge)?;
held.push(charge);
timing.stop(0);
Ok(chunks)
}
}
fn count_partition(runs: Vec<Run>, bound: usize, memory: &Memory) -> Result<Vec<(i64, i64)>> {
let total: usize = runs.iter().map(Run::len).sum();
if total == 0 {
return Ok(Vec::new());
}
let reserving = stage::Timing::start(Stage::Reserve);
let splits = (total / SPLIT_ROWS).max(1).next_power_of_two();
let share = total.div_ceil(splits);
let share = (share + share.isqrt() * 4).min(total);
let capacity = share.saturating_mul(2).max(64).next_power_of_two();
let mut working = memory.reservation();
working.grow(width(
capacity * size_of::<u32>()
+ share * (size_of::<i64>() * 2)
+ if splits > 1 { total * (size_of::<i64>() + size_of::<u32>()) } else { 0 },
))?;
reserving.stop(0);
let parts = if splits == 1 {
runs
} else {
let timing = stage::Timing::start(Stage::Merge);
let mut parts: Vec<Run> = (0..splits)
.map(|_| Run { keys: Vec::with_capacity(share), weights: Vec::with_capacity(share) })
.collect();
for run in runs {
for (&key, &weight) in run.keys.iter().zip(&run.weights) {
parts[split(hash(key), splits)].push(key, weight);
}
}
timing.stop(0);
parts
};
let timing = stage::Timing::start(Stage::Fold);
let mut buckets: Vec<u32> = Vec::with_capacity(capacity);
let mut keys: Vec<i64> = Vec::with_capacity(share);
let mut counts: Vec<i64> = Vec::with_capacity(share);
let mut output = Vec::new();
let groups_of = |parts: &[Run]| parts.iter().map(Run::len).sum::<usize>();
let batches: Vec<Vec<Run>> =
if splits == 1 { vec![parts] } else { parts.into_iter().map(|part| vec![part]).collect() };
for batch in batches {
let rows = groups_of(&batch);
let size = rows.saturating_mul(2).max(64).next_power_of_two();
let mask = size - 1;
buckets.clear();
buckets.resize(size, EMPTY);
keys.clear();
counts.clear();
for run in &batch {
for (&key, &weight) in run.keys.iter().zip(&run.weights) {
let mut at = hash(key) as usize & mask;
loop {
let slot = buckets[at];
if slot == EMPTY {
buckets[at] = u32::try_from(keys.len())
.map_err(|_| Error::internal("a counted radix split is too large"))?;
keys.push(key);
counts.push(i64::from(weight));
break;
}
if keys[slot as usize] == key {
counts[slot as usize] += i64::from(weight);
break;
}
at = (at + 1) & mask;
}
}
}
for slot in largest(keys.len(), bound, |slot| counts[slot]) {
output.push((keys[slot], counts[slot]));
}
}
timing.stop(0);
Ok(output)
}
fn width(value: usize) -> u64 {
u64::try_from(value).unwrap_or(u64::MAX)
}
fn poisoned<T>(_: T) -> Error {
Error::internal("a counted radix exchange lock was poisoned")
}
#[cfg(test)]
mod tests {
use super::{Run, count_partition};
use rudb_common::Memory;
#[test]
fn a_counted_partition_adds_weights_and_keeps_the_largest() {
let memory = Memory::unlimited();
let mut first = Run::default();
let mut second = Run::default();
for key in 0..40_000_i64 {
first.push(key, 1);
}
first.push(7, 5);
second.push(7, 2);
second.push(-3, 9);
second.push(40_001, 3);
let mut kept = count_partition(vec![first, second], 3, &memory).expect("counted");
kept.sort_by_key(|&(key, count)| (std::cmp::Reverse(count), key));
assert_eq!(&kept[..3], &[(-3, 9), (7, 8), (40_001, 3)]);
}
}