1use std::collections::HashMap;
4use std::fs::File;
5use std::path::Path;
6
7use crate::backend::{default_backend, IoBackend};
8use engramdb_core::layout::Layout;
9
10#[derive(Debug, Default, Clone)]
12pub struct PrefetchPlan {
13 pub shard_badges: HashMap<u64, Vec<u64>>,
15 pub n_badges: usize,
16}
17
18impl PrefetchPlan {
19 pub fn build(keys: &[u64], layout: &Layout) -> Self {
20 let mut shard_badges: HashMap<u64, Vec<u64>> = HashMap::new();
21 for &k in keys {
22 let (shard, badge, _) = layout.locate(k);
23 shard_badges.entry(shard).or_default().push(badge);
24 }
25 for v in shard_badges.values_mut() {
26 v.sort_unstable();
27 v.dedup();
28 }
29 let n_badges = shard_badges.values().map(|v| v.len()).sum();
30 Self {
31 shard_badges,
32 n_badges,
33 }
34 }
35
36 pub fn badges(&self, shard: u64) -> &[u64] {
37 self.shard_badges
38 .get(&shard)
39 .map(|v| v.as_slice())
40 .unwrap_or(&[])
41 }
42
43 pub fn entry(&mut self, shard: u64, badge: u64) {
45 self.shard_badges.entry(shard).or_default().push(badge);
46 self.n_badges += 1;
47 }
48
49 pub fn settle(&mut self) {
51 let mut n = 0usize;
52 for v in self.shard_badges.values_mut() {
53 v.sort_unstable();
54 v.dedup();
55 n += v.len();
56 }
57 self.n_badges = n;
58 }
59}
60
61pub struct BadgeGather<'a> {
63 pub layout: &'a Layout,
64 files: Vec<File>,
65 backend: Box<dyn IoBackend>,
66}
67
68impl<'a> BadgeGather<'a> {
69 pub fn open(dir: &Path, layout: &'a Layout) -> std::io::Result<Self> {
70 Self::open_with_backend(dir, layout, default_backend())
71 }
72
73 pub fn open_with_backend(
75 dir: &Path,
76 layout: &'a Layout,
77 backend: Box<dyn IoBackend>,
78 ) -> std::io::Result<Self> {
79 let n = layout.shards as usize;
80 let mut files = Vec::with_capacity(n);
81 for i in 0..n {
82 let s = dir.join(format!("shard_{:03}.bin", i));
83 let b = dir.join(format!("badge_{:03}.bin", i));
84 let p = if s.exists() { s } else { b };
85 files.push(File::open(p)?);
86 }
87 Ok(Self {
88 layout,
89 files,
90 backend,
91 })
92 }
93
94 pub fn into_files(self) -> Vec<File> {
95 self.files
96 }
97
98 pub fn gather_parallel(
100 &self,
101 keys: &[u64],
102 out: &mut [u8],
103 threads: usize,
104 ) -> std::io::Result<()> {
105 if threads <= 1 || keys.len() <= 1024 {
106 return self.gather_naive(keys, out);
107 }
108 let w = self.layout.width as usize;
109 let chunk_keys = keys.len().div_ceil(threads);
110 std::thread::scope(|s| {
111 for (kc, oc) in keys.chunks(chunk_keys).zip(out.chunks_mut(chunk_keys * w)) {
112 s.spawn(|| {
113 let _ = self.gather_naive(kc, oc);
114 });
115 }
116 });
117 Ok(())
118 }
119
120 pub fn gather_naive(&self, keys: &[u64], out: &mut [u8]) -> std::io::Result<()> {
122 let w = self.layout.width as usize;
123 let rb = self.layout.row_bytes as usize;
124 let mut badge_buf = vec![0u8; self.layout.badge_bytes() as usize];
125 let mut last: Option<(u64, u64)> = None;
126 for (i, &k) in keys.iter().enumerate() {
127 let (shard, badge, in_badge) = self.layout.locate(k);
128 if last != Some((shard, badge)) {
129 let off = badge * self.layout.badge_bytes();
130 self.backend
131 .read_exact_at(&self.files[shard as usize], &mut badge_buf, off)?;
132 last = Some((shard, badge));
133 }
134 let src = in_badge as usize * rb;
135 out[i * w..(i + 1) * w].copy_from_slice(&badge_buf[src..src + w]);
136 }
137 Ok(())
138 }
139
140 pub fn gather_pp(&self, keys: &[u64], out: &mut [u8], threads: usize) -> std::io::Result<()> {
143 const PAGE: u64 = 4096;
144 let w = self.layout.width as usize;
145 let rb = self.layout.row_bytes as usize;
146 let mut groups: HashMap<u64, Vec<(u64, usize)>> = HashMap::new();
147 for (i, &k) in keys.iter().enumerate() {
148 let (shard, _, _) = self.layout.locate(k);
149 groups.entry(shard).or_default().push((k, i));
150 }
151 let mut tasks: Vec<(u64, Vec<(u64, usize)>)> = groups.into_iter().collect();
152 tasks.sort_unstable_by_key(|&(s, _)| s);
153
154 let nt = threads.max(1).min(tasks.len());
156 let chunk = tasks.len().div_ceil(nt);
157 let mut results: Vec<(Vec<usize>, Vec<u8>)> = Vec::new();
158
159 std::thread::scope(|s| {
160 let mut handles = Vec::new();
161 let mut task_iter = tasks.into_iter();
162 while task_iter.len() > 0 {
163 let t: Vec<(u64, Vec<(u64, usize)>)> = task_iter.by_ref().take(chunk).collect();
164 handles.push(s.spawn(move || {
165 let mut out_rows: Vec<u8> = Vec::new();
166 let mut out_idxs: Vec<usize> = Vec::new();
167 for (shard, mut pairs) in t {
168 pairs.sort_unstable();
169 let f = &self.files[shard as usize];
170 let mut last_page: Option<u64> = None;
171 let mut page = vec![0u8; (PAGE + 2 * (rb as u64)) as usize];
172 let mut prev_key: Option<u64> = None;
173 for (k, oi) in pairs {
174 let (_, _, in_b) = self.layout.locate(k);
175 let byte_off = k * rb as u64;
176 let page_id = byte_off & !(PAGE - 1);
177 if last_page != Some(page_id) {
178 let want = (PAGE + rb as u64) as usize;
179 let n = self
180 .backend
181 .read_at(f, &mut page[..want], page_id)
182 .unwrap_or(0);
183 let _ = n;
184 last_page = Some(page_id);
185 }
186 let in_page = (byte_off - page_id) as usize;
187 if in_page + rb <= PAGE as usize {
188 out_rows.extend_from_slice(&page[in_page..in_page + rb]);
189 } else {
190 let mut tmp = vec![0u8; rb];
191 let _ = self.backend.read_exact_at(f, &mut tmp, byte_off);
192 out_rows.extend_from_slice(&tmp);
193 }
194 out_idxs.push(oi);
195 let _ = (in_b, prev_key);
196 prev_key = Some(k);
197 }
198 }
199 (out_idxs, out_rows)
200 }));
201 }
202 for h in handles {
203 if let Ok(r) = h.join() {
204 results.push(r);
205 }
206 }
207 });
208
209 for (idxs, rows) in results {
210 for (j, &oi) in idxs.iter().enumerate() {
211 let slice = &rows[j * w..(j + 1) * w];
212 out[oi * w..(oi + 1) * w].copy_from_slice(slice);
213 }
214 }
215 Ok(())
216 }
217
218 pub fn gather_planned(&self, keys: &[u64], out: &mut [u8]) -> std::io::Result<()> {
221 let plan = PrefetchPlan::build(keys, self.layout);
223 let rb = self.layout.row_bytes as usize;
224 let w = self.layout.width as usize;
225 let mut cache: HashMap<(u64, u64), Vec<u8>> = HashMap::new();
226 let mut groups: HashMap<(u64, u64), Vec<usize>> = HashMap::new();
227 for (i, &k) in keys.iter().enumerate() {
228 let (s, b, _) = self.layout.locate(k);
229 groups.entry((s, b)).or_default().push(i);
230 }
231 for (&(s, b), idxs) in &groups {
232 let buf = cache.entry((s, b)).or_insert_with(|| {
233 let mut buf = vec![0u8; self.layout.badge_bytes() as usize];
234 let off = b * self.layout.badge_bytes();
235 let _ = self
236 .backend
237 .read_exact_at(&self.files[s as usize], &mut buf, off);
238 buf
239 });
240 for &i in idxs {
241 let (_, _, in_badge) = self.layout.locate(keys[i]);
242 let src = in_badge as usize * rb;
243 out[i * w..(i + 1) * w].copy_from_slice(&buf[src..src + w]);
244 }
245 }
246 let _ = plan;
247 Ok(())
248 }
249 pub fn gather_plan(
253 &self,
254 keys: &[u64],
255 plan: &PrefetchPlan,
256 out: &mut [u8],
257 ) -> std::io::Result<()> {
258 let rb = self.layout.row_bytes as usize;
259 let w = self.layout.width as usize;
260 let mut groups: HashMap<(u64, u64), Vec<usize>> = HashMap::new();
261 for (i, &k) in keys.iter().enumerate() {
262 let (s, b, _) = self.layout.locate(k);
263 if plan.badges(s).contains(&b) {
264 groups.entry((s, b)).or_default().push(i);
265 } else {
266 let mut tmp = vec![0u8; rb];
268 self.backend
269 .read_exact_at(&self.files[s as usize], &mut tmp, k * rb as u64)?;
270 out[i * w..(i + 1) * w].copy_from_slice(&tmp);
271 }
272 }
273 for (&(s, b), idxs) in &groups {
274 let mut buf = vec![0u8; self.layout.badge_bytes() as usize];
275 let off = b * self.layout.badge_bytes();
276 self.backend
277 .read_exact_at(&self.files[s as usize], &mut buf, off)?;
278 for &i in idxs {
279 let (_, _, in_badge) = self.layout.locate(keys[i]);
280 let src = in_badge as usize * rb;
281 out[i * w..(i + 1) * w].copy_from_slice(&buf[src..src + w]);
282 }
283 }
284 Ok(())
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use engramdb_core::layout::Layout;
292
293 #[test]
294 fn plan_dedup_sort() {
295 let layout = Layout::new(1, 10_000, 160, 1); let keys = vec![9999, 0, 5, 9999, 250, 250];
297 let p = PrefetchPlan::build(&keys, &layout);
298 assert_eq!(p.badges(0), &[0, 10, 399]);
299 }
300
301 #[test]
302 fn gather_plan_end_to_end() {
303 use std::io::Write;
304 let dir = std::env::temp_dir().join("engramdb-gather-plan-test");
305 let _ = std::fs::remove_dir_all(&dir);
306 std::fs::create_dir_all(&dir).unwrap();
307 let layout = Layout::new(1, 100, 8, 1);
309 let mut f = std::fs::File::create(dir.join("shard_000.bin")).unwrap();
310 for i in 0..512u64 {
311 f.write_all(&i.to_le_bytes()).unwrap();
312 }
313 drop(f);
314 let bg = BadgeGather::open(&dir, &layout).unwrap();
315 let keys = vec![3u64, 77, 3, 99, 60];
316 let plan = PrefetchPlan::build(&keys, &layout);
317 let mut out = vec![0u8; keys.len() * 8];
318 bg.gather_plan(&keys, &plan, &mut out).unwrap();
319 for (j, &want) in keys.iter().enumerate() {
320 let got = u64::from_le_bytes(out[j * 8..(j + 1) * 8].try_into().unwrap());
321 assert_eq!(got, want, "rowid {want} at out[{j}]");
322 }
323 let _ = std::fs::remove_dir_all(&dir);
324 }
325}