#![cfg(not(target_arch = "wasm32"))]
use std::collections::BinaryHeap;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use crate::extbuild_spill::{ZReader, ZWriter};
use crate::varint::write_uvarint;
pub(crate) trait SortRecord: Ord + Sized {
fn write_to(&self, out: &mut Vec<u8>);
fn read_from<R: Read>(r: &mut R) -> std::io::Result<Option<Self>>;
fn mem_size(&self) -> usize;
}
fn read_varint<R: Read>(r: &mut R) -> std::io::Result<Option<u64>> {
let mut v = 0u64;
let mut shift = 0u32;
let mut first = true;
loop {
let mut b = [0u8; 1];
match r.read(&mut b)? {
0 if first => return Ok(None),
0 => return Err(std::io::ErrorKind::UnexpectedEof.into()),
_ => {}
}
first = false;
if shift > 63 {
return Err(std::io::Error::other("varint overflow"));
}
v |= ((b[0] & 0x7f) as u64) << shift;
if b[0] & 0x80 == 0 {
return Ok(Some(v));
}
shift += 7;
}
}
fn need<T>(v: Option<T>) -> std::io::Result<T> {
v.ok_or_else(|| std::io::ErrorKind::UnexpectedEof.into())
}
fn write_str(out: &mut Vec<u8>, s: &str) {
write_uvarint(out, s.len() as u64);
out.extend_from_slice(s.as_bytes());
}
fn read_str<R: Read>(r: &mut R, first: Option<u64>) -> std::io::Result<Option<String>> {
let len = match first {
Some(l) => l,
None => match read_varint(r)? {
Some(l) => l,
None => return Ok(None),
},
};
let mut buf = vec![0u8; len as usize];
r.read_exact(&mut buf)?;
String::from_utf8(buf)
.map(Some)
.map_err(|_| std::io::Error::other("bad utf8 in spill"))
}
impl SortRecord for (String, String, String) {
fn write_to(&self, out: &mut Vec<u8>) {
write_str(out, &self.0);
write_str(out, &self.1);
write_str(out, &self.2);
}
fn read_from<R: Read>(r: &mut R) -> std::io::Result<Option<Self>> {
let Some(a) = read_str(r, None)? else {
return Ok(None);
};
let b = need(read_str(r, None)?)?;
let c = need(read_str(r, None)?)?;
Ok(Some((a, b, c)))
}
fn mem_size(&self) -> usize {
self.0.len() + self.1.len() + self.2.len() + 3 * 24
}
}
impl SortRecord for (u32, u32) {
fn write_to(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&self.0.to_le_bytes());
out.extend_from_slice(&self.1.to_le_bytes());
}
fn read_from<R: Read>(r: &mut R) -> std::io::Result<Option<Self>> {
let mut b = [0u8; 8];
match read_exact_or_eof(r, &mut b)? {
false => Ok(None),
true => Ok(Some((
u32::from_le_bytes(b[0..4].try_into().unwrap()),
u32::from_le_bytes(b[4..8].try_into().unwrap()),
))),
}
}
fn mem_size(&self) -> usize {
8
}
}
impl SortRecord for (u32, u32, u32) {
fn write_to(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&self.0.to_le_bytes());
out.extend_from_slice(&self.1.to_le_bytes());
out.extend_from_slice(&self.2.to_le_bytes());
}
fn read_from<R: Read>(r: &mut R) -> std::io::Result<Option<Self>> {
let mut b = [0u8; 12];
match read_exact_or_eof(r, &mut b)? {
false => Ok(None),
true => Ok(Some((
u32::from_le_bytes(b[0..4].try_into().unwrap()),
u32::from_le_bytes(b[4..8].try_into().unwrap()),
u32::from_le_bytes(b[8..12].try_into().unwrap()),
))),
}
}
fn mem_size(&self) -> usize {
12
}
}
impl SortRecord for (u32, u32, String) {
fn write_to(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&self.0.to_le_bytes());
out.extend_from_slice(&self.1.to_le_bytes());
write_str(out, &self.2);
}
fn read_from<R: Read>(r: &mut R) -> std::io::Result<Option<Self>> {
let mut b = [0u8; 8];
if !read_exact_or_eof(r, &mut b)? {
return Ok(None);
}
let c = need(read_str(r, None)?)?;
Ok(Some((
u32::from_le_bytes(b[0..4].try_into().unwrap()),
u32::from_le_bytes(b[4..8].try_into().unwrap()),
c,
)))
}
fn mem_size(&self) -> usize {
8 + self.2.len() + 24
}
}
fn read_exact_or_eof<R: Read>(r: &mut R, buf: &mut [u8]) -> std::io::Result<bool> {
let mut got = 0;
while got < buf.len() {
match r.read(&mut buf[got..])? {
0 if got == 0 => return Ok(false),
0 => return Err(std::io::ErrorKind::UnexpectedEof.into()),
n => got += n,
}
}
Ok(true)
}
#[cfg(test)]
thread_local! {
pub(crate) static TEST_RUN_BYTES: std::cell::Cell<Option<usize>> = const { std::cell::Cell::new(None) };
}
pub(crate) struct ExtSorter<T: SortRecord> {
dir: PathBuf,
name: String,
run_bytes: usize,
buf: Vec<T>,
buf_bytes: usize,
runs: Vec<PathBuf>,
pushed: u64,
dedup: bool,
}
impl<T: SortRecord> ExtSorter<T> {
pub(crate) fn new(dir: &Path, name: &str, run_bytes: usize) -> Self {
#[cfg(test)]
let run_bytes = TEST_RUN_BYTES
.with(|c| c.get())
.unwrap_or(run_bytes.max(1 << 20));
#[cfg(not(test))]
let run_bytes = run_bytes.max(1 << 20);
ExtSorter {
dir: dir.to_path_buf(),
name: name.to_string(),
run_bytes,
buf: Vec::new(),
buf_bytes: 0,
runs: Vec::new(),
pushed: 0,
dedup: true,
}
}
pub(crate) fn new_keep_duplicates(dir: &Path, name: &str, run_bytes: usize) -> Self {
ExtSorter {
dedup: false,
..Self::new(dir, name, run_bytes)
}
}
pub(crate) fn push(&mut self, r: T) -> std::io::Result<()> {
self.buf_bytes += r.mem_size() + std::mem::size_of::<T>();
self.buf.push(r);
self.pushed += 1;
if self.buf_bytes >= self.run_bytes {
self.spill()?;
}
Ok(())
}
pub(crate) fn pushed(&self) -> u64 {
self.pushed
}
fn spill(&mut self) -> std::io::Result<()> {
if self.buf.is_empty() {
return Ok(());
}
self.buf.sort_unstable();
if self.dedup {
self.buf.dedup();
}
let path = self
.dir
.join(format!("{}.run{}", self.name, self.runs.len()));
let mut w = ZWriter::create(&path)?;
let mut enc = Vec::new();
for r in self.buf.drain(..) {
enc.clear();
r.write_to(&mut enc);
w.write_all(&enc)?;
}
w.finish()?;
self.runs.push(path);
self.buf_bytes = 0;
self.buf.shrink_to_fit();
Ok(())
}
pub(crate) fn finish(mut self) -> std::io::Result<Sorted<T>> {
if self.runs.is_empty() {
self.buf.sort_unstable();
if self.dedup {
self.buf.dedup();
}
let v = std::mem::take(&mut self.buf);
return Ok(Sorted::Memory(v.into_iter()));
}
self.spill()?;
crate::build_trace::note_merge(self.runs.len());
let mut readers = Vec::with_capacity(self.runs.len());
let mut heap = BinaryHeap::new();
for (i, p) in self.runs.iter().enumerate() {
let mut rd = ZReader::open(p)?;
if let Some(r) = T::read_from(&mut rd)? {
heap.push(std::cmp::Reverse((r, i)));
}
readers.push(rd);
}
Ok(Sorted::Runs {
readers,
heap,
runs: std::mem::take(&mut self.runs),
last: None,
dedup: self.dedup,
})
}
}
pub(crate) enum Sorted<T: SortRecord> {
Memory(std::vec::IntoIter<T>),
Runs {
readers: Vec<ZReader>,
heap: BinaryHeap<std::cmp::Reverse<(T, usize)>>,
runs: Vec<PathBuf>,
last: Option<T>,
dedup: bool,
},
}
impl<T: SortRecord + Clone> Sorted<T> {
pub(crate) fn next_record(&mut self) -> std::io::Result<Option<T>> {
match self {
Sorted::Memory(it) => Ok(it.next()),
Sorted::Runs {
readers,
heap,
last,
dedup,
..
} => loop {
let Some(std::cmp::Reverse((r, i))) = heap.pop() else {
return Ok(None);
};
if let Some(n) = T::read_from(&mut readers[i])? {
heap.push(std::cmp::Reverse((n, i)));
}
if !*dedup || last.as_ref() != Some(&r) {
*last = Some(r.clone());
return Ok(Some(r));
}
},
}
}
}
impl<T: SortRecord> Drop for Sorted<T> {
fn drop(&mut self) {
if let Sorted::Runs { runs, .. } = self {
for p in runs.iter() {
let _ = std::fs::remove_file(p);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tmpdir(tag: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("rete-extsort-{tag}-{}", std::process::id()));
std::fs::create_dir_all(&d).unwrap();
d
}
#[test]
fn sorts_and_dedups_across_spilled_runs() {
let dir = tmpdir("strings");
let mut s: ExtSorter<(String, String, String)> = ExtSorter::new(&dir, "t", 1);
let n = 60_000u32;
for i in (0..n).rev() {
let r = (
format!("k{:06}", i % 20_000),
"s".to_string(),
"v".repeat(16),
);
s.push(r.clone()).unwrap();
}
assert!(s.runs.len() >= 2, "spilled {} runs", s.runs.len());
let mut out = s.finish().unwrap();
let mut got = Vec::new();
while let Some(r) = out.next_record().unwrap() {
got.push(r.0);
}
assert_eq!(got.len(), 20_000);
assert!(got.windows(2).all(|w| w[0] < w[1]));
drop(out);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn small_input_stays_in_memory() {
let dir = tmpdir("ids");
let mut s: ExtSorter<(u32, u32)> = ExtSorter::new(&dir, "t", 1 << 20);
for (a, b) in [(3, 1), (1, 2), (3, 1), (2, 9)] {
s.push((a, b)).unwrap();
}
let mut out = s.finish().unwrap();
let mut got = Vec::new();
while let Some(r) = out.next_record().unwrap() {
got.push(r);
}
assert_eq!(got, vec![(1, 2), (2, 9), (3, 1)]);
let _ = std::fs::remove_dir_all(&dir);
}
}