use std::io::Write;
use crate::plan::Config;
use crate::unit::{DeflateError, EncoderPool};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Placed {
pub offset: u64,
pub len: u64,
}
pub fn compute_workers() -> usize {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(2)
.saturating_sub(1)
.max(1)
}
pub fn compress_objects_to_writer<W: Write>(
objects: &[&[u8]],
cfg: &Config,
out: &mut W,
) -> Result<Vec<Placed>, DeflateError> {
let workers = if cfg.workers == 0 {
compute_workers()
} else {
cfg.workers
};
let cap = (workers * 4).max(8);
let level = cfg.level;
let wb = cfg.window_bits;
let ct = cfg.container;
let mut next = 0usize;
let producer = move || -> Option<(usize, &[u8])> {
if next < objects.len() {
let i = next;
next += 1;
Some((i, objects[i]))
} else {
None
}
};
let pool = EncoderPool::new(level, ct.encoder_is_zlib_wrapped(), wb);
let mut placed: Vec<Placed> = Vec::with_capacity(objects.len());
let mut offset: u64 = 0;
let mut sink_err: Option<DeflateError> = None;
let mut sink = |_seq: u64, r: Result<Vec<u8>, DeflateError>| -> anyhow::Result<()> {
match r {
Ok(bytes) => {
out.write_all(&bytes)
.map_err(|e| anyhow::anyhow!("ldeflate: write failed: {e}"))?;
placed.push(Placed {
offset,
len: bytes.len() as u64,
});
offset += bytes.len() as u64;
Ok(())
}
Err(e) => {
sink_err.get_or_insert(e.clone());
Err(anyhow::anyhow!("{e}"))
}
}
};
let run = gatling::gatling::ordered::run_ordered_sink(
producer,
workers,
cap,
|_label: usize, input: &[u8]| -> Result<Vec<u8>, DeflateError> {
let mut enc = pool.checkout()?;
let mut o = Vec::new();
enc.compress_into(input, &mut o)?;
if ct.encoder_is_zlib_wrapped() {
Ok(o)
} else {
Ok(ct.wrap(level, &o, input))
}
},
&mut sink,
);
if let Some(e) = sink_err {
return Err(e);
}
run.map_err(|_| DeflateError::Encode("ordered sink aborted"))?;
gatling::functional_status(
"ldeflate",
"compress_objects_to_writer",
true,
&"N-1 compressors + 1 dedicated writer on the calling thread",
);
Ok(placed)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
fn objs(n: usize) -> Vec<Vec<u8>> {
(0..n)
.map(|i| {
let mut v = Vec::new();
let tag = format!("obj{i}:");
let target = if i % 97 == 0 {
400_000
} else {
40 + (i * 7) % 3_000
};
while v.len() < target {
v.extend_from_slice(tag.as_bytes());
v.extend_from_slice(b"the quick brown fox ");
}
v.truncate(target);
v
})
.collect()
}
#[test]
fn streamed_output_is_ordered_and_offsets_are_right() {
let data = objs(600);
let r: Vec<&[u8]> = data.iter().map(|v| v.as_slice()).collect();
for w in [1usize, 2, 7, 31] {
let mut buf: Vec<u8> = Vec::new();
let placed =
compress_objects_to_writer(&r, &Config::default().workers(w), &mut buf).unwrap();
assert_eq!(placed.len(), data.len(), "workers={w}");
assert_eq!(
placed.last().unwrap().offset + placed.last().unwrap().len,
buf.len() as u64,
"workers={w}: recorded extents must cover the whole output"
);
for (i, p) in placed.iter().enumerate() {
let slice = &buf[p.offset as usize..(p.offset + p.len) as usize];
let mut got = Vec::new();
flate2::read::ZlibDecoder::new(slice)
.read_to_end(&mut got)
.unwrap_or_else(|e| panic!("workers={w} obj {i}: {e}"));
assert_eq!(
got, data[i],
"workers={w}: object {i} landed at the wrong offset"
);
}
}
}
#[test]
fn the_ordering_check_can_actually_fail() {
let data = objs(50);
let r: Vec<&[u8]> = data.iter().map(|v| v.as_slice()).collect();
let mut buf: Vec<u8> = Vec::new();
let mut placed = compress_objects_to_writer(&r, &Config::default(), &mut buf).unwrap();
placed.swap(3, 17);
let mut mismatches = 0;
for (i, p) in placed.iter().enumerate() {
let slice = &buf[p.offset as usize..(p.offset + p.len) as usize];
let mut got = Vec::new();
if flate2::read::ZlibDecoder::new(slice)
.read_to_end(&mut got)
.is_err()
|| got != data[i]
{
mismatches += 1;
}
}
assert!(
mismatches > 0,
"a deliberately shuffled extent table still passed the per-object decode \
check — that check cannot detect mis-ordering and proves nothing"
);
}
#[test]
fn default_worker_count_reserves_one_core() {
let cores = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(2);
let w = compute_workers();
assert_eq!(
w,
(cores - 1).max(1),
"expected cores-1 workers, got {w} on {cores} cores"
);
assert!(
w < cores || cores == 1,
"the writer must not be sharing a core with a compressor"
);
}
}