use std::collections::HashMap;
use std::fs::File;
use std::path::Path;
use crate::backend::{default_backend, IoBackend};
use engramdb_core::layout::Layout;
#[derive(Debug, Default, Clone)]
pub struct PrefetchPlan {
pub shard_badges: HashMap<u64, Vec<u64>>,
pub n_badges: usize,
}
impl PrefetchPlan {
pub fn build(keys: &[u64], layout: &Layout) -> Self {
let mut shard_badges: HashMap<u64, Vec<u64>> = HashMap::new();
for &k in keys {
let (shard, badge, _) = layout.locate(k);
shard_badges.entry(shard).or_default().push(badge);
}
for v in shard_badges.values_mut() {
v.sort_unstable();
v.dedup();
}
let n_badges = shard_badges.values().map(|v| v.len()).sum();
Self {
shard_badges,
n_badges,
}
}
pub fn badges(&self, shard: u64) -> &[u64] {
self.shard_badges
.get(&shard)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn entry(&mut self, shard: u64, badge: u64) {
self.shard_badges.entry(shard).or_default().push(badge);
self.n_badges += 1;
}
pub fn settle(&mut self) {
let mut n = 0usize;
for v in self.shard_badges.values_mut() {
v.sort_unstable();
v.dedup();
n += v.len();
}
self.n_badges = n;
}
}
pub struct BadgeGather<'a> {
pub layout: &'a Layout,
files: Vec<File>,
backend: Box<dyn IoBackend>,
}
impl<'a> BadgeGather<'a> {
pub fn open(dir: &Path, layout: &'a Layout) -> std::io::Result<Self> {
Self::open_with_backend(dir, layout, default_backend())
}
pub fn open_with_backend(
dir: &Path,
layout: &'a Layout,
backend: Box<dyn IoBackend>,
) -> std::io::Result<Self> {
let n = layout.shards as usize;
let mut files = Vec::with_capacity(n);
for i in 0..n {
let s = dir.join(format!("shard_{:03}.bin", i));
let b = dir.join(format!("badge_{:03}.bin", i));
let p = if s.exists() { s } else { b };
files.push(File::open(p)?);
}
Ok(Self {
layout,
files,
backend,
})
}
pub fn into_files(self) -> Vec<File> {
self.files
}
pub fn gather_parallel(
&self,
keys: &[u64],
out: &mut [u8],
threads: usize,
) -> std::io::Result<()> {
if threads <= 1 || keys.len() <= 1024 {
return self.gather_naive(keys, out);
}
let w = self.layout.width as usize;
let chunk_keys = keys.len().div_ceil(threads);
std::thread::scope(|s| {
for (kc, oc) in keys.chunks(chunk_keys).zip(out.chunks_mut(chunk_keys * w)) {
s.spawn(|| {
let _ = self.gather_naive(kc, oc);
});
}
});
Ok(())
}
pub fn gather_naive(&self, keys: &[u64], out: &mut [u8]) -> std::io::Result<()> {
let w = self.layout.width as usize;
let rb = self.layout.row_bytes as usize;
let mut badge_buf = vec![0u8; self.layout.badge_bytes() as usize];
let mut last: Option<(u64, u64)> = None;
for (i, &k) in keys.iter().enumerate() {
let (shard, badge, in_badge) = self.layout.locate(k);
if last != Some((shard, badge)) {
let off = badge * self.layout.badge_bytes();
self.backend
.read_exact_at(&self.files[shard as usize], &mut badge_buf, off)?;
last = Some((shard, badge));
}
let src = in_badge as usize * rb;
out[i * w..(i + 1) * w].copy_from_slice(&badge_buf[src..src + w]);
}
Ok(())
}
pub fn gather_pp(&self, keys: &[u64], out: &mut [u8], threads: usize) -> std::io::Result<()> {
const PAGE: u64 = 4096;
let w = self.layout.width as usize;
let rb = self.layout.row_bytes as usize;
let mut groups: HashMap<u64, Vec<(u64, usize)>> = HashMap::new();
for (i, &k) in keys.iter().enumerate() {
let (shard, _, _) = self.layout.locate(k);
groups.entry(shard).or_default().push((k, i));
}
let mut tasks: Vec<(u64, Vec<(u64, usize)>)> = groups.into_iter().collect();
tasks.sort_unstable_by_key(|&(s, _)| s);
let nt = threads.max(1).min(tasks.len());
let chunk = tasks.len().div_ceil(nt);
let mut results: Vec<(Vec<usize>, Vec<u8>)> = Vec::new();
std::thread::scope(|s| {
let mut handles = Vec::new();
let mut task_iter = tasks.into_iter();
while task_iter.len() > 0 {
let t: Vec<(u64, Vec<(u64, usize)>)> = task_iter.by_ref().take(chunk).collect();
handles.push(s.spawn(move || {
let mut out_rows: Vec<u8> = Vec::new();
let mut out_idxs: Vec<usize> = Vec::new();
for (shard, mut pairs) in t {
pairs.sort_unstable();
let f = &self.files[shard as usize];
let mut last_page: Option<u64> = None;
let mut page = vec![0u8; (PAGE + 2 * (rb as u64)) as usize];
let mut prev_key: Option<u64> = None;
for (k, oi) in pairs {
let (_, _, in_b) = self.layout.locate(k);
let byte_off = k * rb as u64;
let page_id = byte_off & !(PAGE - 1);
if last_page != Some(page_id) {
let want = (PAGE + rb as u64) as usize;
let n = self
.backend
.read_at(f, &mut page[..want], page_id)
.unwrap_or(0);
let _ = n;
last_page = Some(page_id);
}
let in_page = (byte_off - page_id) as usize;
if in_page + rb <= PAGE as usize {
out_rows.extend_from_slice(&page[in_page..in_page + rb]);
} else {
let mut tmp = vec![0u8; rb];
let _ = self.backend.read_exact_at(f, &mut tmp, byte_off);
out_rows.extend_from_slice(&tmp);
}
out_idxs.push(oi);
let _ = (in_b, prev_key);
prev_key = Some(k);
}
}
(out_idxs, out_rows)
}));
}
for h in handles {
if let Ok(r) = h.join() {
results.push(r);
}
}
});
for (idxs, rows) in results {
for (j, &oi) in idxs.iter().enumerate() {
let slice = &rows[j * w..(j + 1) * w];
out[oi * w..(oi + 1) * w].copy_from_slice(slice);
}
}
Ok(())
}
pub fn gather_planned(&self, keys: &[u64], out: &mut [u8]) -> std::io::Result<()> {
let plan = PrefetchPlan::build(keys, self.layout);
let rb = self.layout.row_bytes as usize;
let w = self.layout.width as usize;
let mut cache: HashMap<(u64, u64), Vec<u8>> = HashMap::new();
let mut groups: HashMap<(u64, u64), Vec<usize>> = HashMap::new();
for (i, &k) in keys.iter().enumerate() {
let (s, b, _) = self.layout.locate(k);
groups.entry((s, b)).or_default().push(i);
}
for (&(s, b), idxs) in &groups {
let buf = cache.entry((s, b)).or_insert_with(|| {
let mut buf = vec![0u8; self.layout.badge_bytes() as usize];
let off = b * self.layout.badge_bytes();
let _ = self
.backend
.read_exact_at(&self.files[s as usize], &mut buf, off);
buf
});
for &i in idxs {
let (_, _, in_badge) = self.layout.locate(keys[i]);
let src = in_badge as usize * rb;
out[i * w..(i + 1) * w].copy_from_slice(&buf[src..src + w]);
}
}
let _ = plan;
Ok(())
}
pub fn gather_plan(
&self,
keys: &[u64],
plan: &PrefetchPlan,
out: &mut [u8],
) -> std::io::Result<()> {
let rb = self.layout.row_bytes as usize;
let w = self.layout.width as usize;
let mut groups: HashMap<(u64, u64), Vec<usize>> = HashMap::new();
for (i, &k) in keys.iter().enumerate() {
let (s, b, _) = self.layout.locate(k);
if plan.badges(s).contains(&b) {
groups.entry((s, b)).or_default().push(i);
} else {
let mut tmp = vec![0u8; rb];
self.backend
.read_exact_at(&self.files[s as usize], &mut tmp, k * rb as u64)?;
out[i * w..(i + 1) * w].copy_from_slice(&tmp);
}
}
for (&(s, b), idxs) in &groups {
let mut buf = vec![0u8; self.layout.badge_bytes() as usize];
let off = b * self.layout.badge_bytes();
self.backend
.read_exact_at(&self.files[s as usize], &mut buf, off)?;
for &i in idxs {
let (_, _, in_badge) = self.layout.locate(keys[i]);
let src = in_badge as usize * rb;
out[i * w..(i + 1) * w].copy_from_slice(&buf[src..src + w]);
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use engramdb_core::layout::Layout;
#[test]
fn plan_dedup_sort() {
let layout = Layout::new(1, 10_000, 160, 1); let keys = vec![9999, 0, 5, 9999, 250, 250];
let p = PrefetchPlan::build(&keys, &layout);
assert_eq!(p.badges(0), &[0, 10, 399]);
}
#[test]
fn gather_plan_end_to_end() {
use std::io::Write;
let dir = std::env::temp_dir().join("engramdb-gather-plan-test");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let layout = Layout::new(1, 100, 8, 1);
let mut f = std::fs::File::create(dir.join("shard_000.bin")).unwrap();
for i in 0..512u64 {
f.write_all(&i.to_le_bytes()).unwrap();
}
drop(f);
let bg = BadgeGather::open(&dir, &layout).unwrap();
let keys = vec![3u64, 77, 3, 99, 60];
let plan = PrefetchPlan::build(&keys, &layout);
let mut out = vec![0u8; keys.len() * 8];
bg.gather_plan(&keys, &plan, &mut out).unwrap();
for (j, &want) in keys.iter().enumerate() {
let got = u64::from_le_bytes(out[j * 8..(j + 1) * 8].try_into().unwrap());
assert_eq!(got, want, "rowid {want} at out[{j}]");
}
let _ = std::fs::remove_dir_all(&dir);
}
}