#![cfg(not(target_arch = "wasm32"))]
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use crate::build_trace as trace;
use crate::extbuild::ExtBuildError;
use crate::extbuild_spill::{put_block, spill_level, take_block};
use crate::varint::write_uvarint;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Seg {
Shared = 0,
SubjOnly = 1,
ObjOnly = 2,
Pred = 3,
Graph = 4,
}
const SEGS: usize = 5;
const SEG_FILE: [&str; SEGS] = ["map.shared", "map.subj", "map.obj", "map.pred", "map.graph"];
pub(crate) const ROLE_ONLY: u32 = 1 << 31;
pub(crate) type SegCounts = [u32; SEGS];
pub(crate) fn remap_buf_bytes(budget: u64, chunks: usize) -> usize {
let per = budget / 16 / (3 * chunks.max(1) as u64);
(per.clamp(4 << 10, 256 << 10) as usize) & !3
}
pub(crate) fn shared_id(rank: u32) -> Result<u32, ExtBuildError> {
if rank >= ROLE_ONLY - 1 {
return Err(ExtBuildError::Internal(
"more than 2^31 shared terms: the dictionary is too large for one file",
));
}
Ok(rank + 1)
}
pub(crate) fn role_only_id(rank: u32) -> Result<u32, ExtBuildError> {
if rank >= ROLE_ONLY {
return Err(ExtBuildError::Internal(
"more than 2^31 subject-only or object-only terms: the dictionary is \
too large for one file",
));
}
Ok(ROLE_ONLY | rank)
}
fn seg_path(dir: &Path, ci: usize, seg: usize) -> PathBuf {
dir.join(format!("c{ci}.{}", SEG_FILE[seg]))
}
fn table_bytes(c: &SegCounts) -> u64 {
4 * (2 * c[0] as u64 + c[1] as u64 + c[2] as u64 + c[3] as u64 + c[4] as u64)
}
#[derive(Default)]
struct SegState {
expected: u32,
written: u32,
buf: Vec<u8>,
prev: [u32; 2],
}
fn put_id(out: &mut Vec<u8>, prev: &mut [u32; 2], v: u32) {
let class = (v >> 31) as usize;
let x = v & !ROLE_ONLY;
let d = x.wrapping_sub(prev[class]) & !ROLE_ONLY;
prev[class] = x;
write_uvarint(out, ((d as u64) << 1) | class as u64);
}
fn get_id(raw: &[u8], pos: &mut usize, prev: &mut [u32; 2]) -> Result<u32, ExtBuildError> {
let (w, n) = crate::varint::read_uvarint(&raw[*pos..])
.ok_or(ExtBuildError::Internal("a remap spill block is truncated"))?;
*pos += n;
let class = (w & 1) as usize;
let x = prev[class].wrapping_add((w >> 1) as u32) & !ROLE_ONLY;
prev[class] = x;
Ok(if class == 1 { x | ROLE_ONLY } else { x })
}
pub(crate) struct RemapWriter {
dir: PathBuf,
chunks: Vec<[SegState; SEGS]>,
buf_bytes: usize,
}
impl RemapWriter {
pub(crate) fn new(dir: &Path, counts: &[SegCounts], buf_bytes: usize) -> Self {
RemapWriter {
dir: dir.to_path_buf(),
chunks: counts
.iter()
.map(|c| {
std::array::from_fn(|s| SegState {
expected: c[s],
..Default::default()
})
})
.collect(),
buf_bytes: buf_bytes.max(4),
}
}
pub(crate) fn push(
&mut self,
ci: usize,
seg: Seg,
local: u32,
id: u32,
) -> Result<(), ExtBuildError> {
let st = self
.chunks
.get_mut(ci)
.map(|c| &mut c[seg as usize])
.ok_or(ExtBuildError::Internal("remap for an unknown chunk"))?;
if local != st.written + 1 || local > st.expected {
return Err(ExtBuildError::Internal(
"dictionary merge reached a chunk's local ids out of order",
));
}
st.written = local;
if st.buf.capacity() == 0 {
st.buf
.reserve_exact(self.buf_bytes.min(4 * st.expected as usize));
}
st.buf.extend_from_slice(&id.to_le_bytes());
if st.buf.len() >= self.buf_bytes {
Self::append(&self.dir, ci, seg as usize, st)?;
}
Ok(())
}
fn append(dir: &Path, ci: usize, seg: usize, st: &mut SegState) -> Result<(), ExtBuildError> {
if st.buf.is_empty() {
return Ok(());
}
let mut raw = Vec::with_capacity(st.buf.len() / 2);
for w in st.buf.chunks_exact(4) {
put_id(
&mut raw,
&mut st.prev,
u32::from_le_bytes([w[0], w[1], w[2], w[3]]),
);
}
let mut block = Vec::with_capacity(raw.len() / 4 + 16);
put_block(&mut block, &raw, (st.buf.len() / 4) as u32, spill_level())?;
let mut f = OpenOptions::new()
.create(true)
.append(true)
.open(seg_path(dir, ci, seg))?;
f.write_all(&block)?;
st.buf.clear();
Ok(())
}
pub(crate) fn flush(&mut self, seg: Seg) -> Result<(), ExtBuildError> {
for (ci, c) in self.chunks.iter_mut().enumerate() {
let st = &mut c[seg as usize];
Self::append(&self.dir, ci, seg as usize, st)?;
st.buf = Vec::new();
}
Ok(())
}
pub(crate) fn finish(mut self, n_shared: u32) -> Result<RemapStore, ExtBuildError> {
for seg in [
Seg::Shared,
Seg::SubjOnly,
Seg::ObjOnly,
Seg::Pred,
Seg::Graph,
] {
self.flush(seg)?;
}
let mut counts = Vec::with_capacity(self.chunks.len());
for c in &self.chunks {
if c.iter().any(|s| s.written != s.expected) {
return Err(ExtBuildError::Internal(
"dictionary merge left a chunk-local id without a global id",
));
}
counts.push(std::array::from_fn(|s| c[s].expected));
}
Ok(RemapStore {
dir: self.dir,
counts,
n_shared,
})
}
}
pub(crate) struct ChunkRemap {
pub(crate) subj: Vec<u32>,
pub(crate) obj: Vec<u32>,
pub(crate) pred: Vec<u32>,
pub(crate) graph: Vec<u32>,
}
pub(crate) struct RemapStore {
dir: PathBuf,
counts: Vec<SegCounts>,
n_shared: u32,
}
impl RemapStore {
pub(crate) fn total_table_bytes(&self) -> u64 {
self.counts.iter().map(table_bytes).sum()
}
pub(crate) fn largest_table_bytes(&self) -> u64 {
self.counts.iter().map(table_bytes).max().unwrap_or(0)
}
pub(crate) fn load(&self, ci: usize) -> Result<ChunkRemap, ExtBuildError> {
let c = self
.counts
.get(ci)
.ok_or(ExtBuildError::Internal("remap for an unknown chunk"))?;
let n_shared = self.n_shared;
let fix = |v: u32| -> Result<u32, ExtBuildError> {
if v & ROLE_ONLY == 0 {
return Ok(v);
}
n_shared
.checked_add(v & !ROLE_ONLY)
.and_then(|x| x.checked_add(1))
.ok_or(ExtBuildError::Internal(
"global term ids overflow 32 bits: the dictionary is too large for one file",
))
};
let (s, a, b) = (c[0] as usize, c[1] as usize, c[2] as usize);
let mut subj = Vec::with_capacity(s + a);
self.read(ci, Seg::Shared, &mut subj, &Ok)?;
let mut obj = Vec::with_capacity(s + b);
obj.extend_from_slice(&subj);
self.read(ci, Seg::SubjOnly, &mut subj, &fix)?;
self.read(ci, Seg::ObjOnly, &mut obj, &fix)?;
let mut pred = Vec::with_capacity(c[3] as usize);
self.read(ci, Seg::Pred, &mut pred, &Ok)?;
let mut graph = Vec::with_capacity(c[4] as usize);
self.read(ci, Seg::Graph, &mut graph, &Ok)?;
for seg in 0..SEGS {
let _ = std::fs::remove_file(seg_path(&self.dir, ci, seg));
}
trace::max("remap_peak_bytes", table_bytes(c));
Ok(ChunkRemap {
subj,
obj,
pred,
graph,
})
}
fn read(
&self,
ci: usize,
seg: Seg,
out: &mut Vec<u32>,
f: &dyn Fn(u32) -> Result<u32, ExtBuildError>,
) -> Result<(), ExtBuildError> {
let expected = self.counts[ci][seg as usize] as u64;
if expected == 0 {
return Ok(());
}
let path = seg_path(&self.dir, ci, seg as usize);
let mut file = File::open(&path)?;
let (mut payload, mut raw) = (Vec::new(), Vec::new());
let mut prev = [0u32; 2];
let mut got = 0u64;
while let Some(count) = take_block(&mut file, &mut payload, &mut raw)? {
let mut pos = 0;
for _ in 0..count {
out.push(f(get_id(&raw, &mut pos, &mut prev)?)?);
}
if pos != raw.len() {
return Err(ExtBuildError::Internal(
"a remap spill block has trailing bytes",
));
}
got += count as u64;
}
if got != expected {
return Err(ExtBuildError::Internal(
"a remap spill file has the wrong length",
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tmpdir(tag: &str) -> PathBuf {
static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let seq = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let d = std::env::temp_dir().join(format!("rete-remap-{tag}-{}-{seq}", std::process::id()));
std::fs::create_dir_all(&d).unwrap();
d
}
#[test]
fn round_trip_fixes_up_provisional_ids() {
let dir = tmpdir("rt");
let counts = [[3, 2, 1, 2, 0], [0, 0, 0, 0, 1]];
let mut w = RemapWriter::new(&dir, &counts, 4); w.push(0, Seg::Shared, 1, shared_id(0).unwrap()).unwrap();
w.push(0, Seg::SubjOnly, 1, role_only_id(0).unwrap())
.unwrap();
w.push(0, Seg::Shared, 2, shared_id(4).unwrap()).unwrap();
w.push(0, Seg::ObjOnly, 1, shared_id(5).unwrap()).unwrap();
w.push(0, Seg::SubjOnly, 2, role_only_id(7).unwrap())
.unwrap();
w.push(0, Seg::Shared, 3, shared_id(6).unwrap()).unwrap();
w.flush(Seg::Shared).unwrap();
w.push(0, Seg::Pred, 1, 1).unwrap();
w.push(0, Seg::Pred, 2, 9).unwrap();
w.push(1, Seg::Graph, 1, 4).unwrap();
let store = w.finish(10).unwrap();
assert_eq!(store.total_table_bytes(), 4 * (6 + 2 + 1 + 2 + 1));
let m = store.load(0).unwrap();
assert_eq!(m.subj, [1, 5, 7, 11, 18]);
assert_eq!(m.obj, [1, 5, 7, 6]);
assert_eq!(m.pred, [1, 9]);
assert!(m.graph.is_empty());
assert_eq!(store.load(1).unwrap().graph, [4]);
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 0);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn out_of_order_and_missing_ids_are_errors() {
let dir = tmpdir("order");
let mut w = RemapWriter::new(&dir, &[[2, 0, 0, 0, 0]], 64);
assert!(w.push(0, Seg::Shared, 2, 1).is_err(), "skipped local id 1");
w.push(0, Seg::Shared, 1, 1).unwrap();
assert!(w.push(0, Seg::Shared, 1, 2).is_err(), "repeated local id");
assert!(w.push(0, Seg::Pred, 1, 1).is_err(), "beyond the segment");
assert!(w.finish(0).is_err(), "local id 2 never got a global id");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn class_limits_are_refused_not_wrapped() {
assert!(shared_id((1 << 31) - 2).is_ok());
assert!(shared_id((1 << 31) - 1).is_err());
assert!(role_only_id((1 << 31) - 1).is_ok());
assert!(role_only_id(1 << 31).is_err());
let dir = tmpdir("ovf");
let mut w = RemapWriter::new(&dir, &[[0, 1, 0, 0, 0]], 64);
w.push(0, Seg::SubjOnly, 1, role_only_id(10).unwrap())
.unwrap();
let store = w.finish(u32::MAX - 5).unwrap();
assert!(store.load(0).is_err(), "id past u32::MAX");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn buffers_share_a_sixteenth_of_the_budget() {
assert_eq!(remap_buf_bytes(12_000 << 20, 200), 256 << 10);
assert_eq!(remap_buf_bytes(64 << 20, 1_000), 4 << 10);
let b = remap_buf_bytes(1_000 << 20, 80);
assert!(b * 3 * 80 <= (1_000 << 20) / 16 && b.is_multiple_of(4));
}
}