use std::collections::VecDeque;
use std::io::{self, Write};
use std::sync::mpsc::{Receiver, sync_channel};
use super::lzma::{Lzma2Encoder, Lzma2EncoderOptions};
use super::{Encoder, method};
pub(crate) const BLOCKS_PER_WORKER: usize = 2;
const BLOCKS_PER_STEP: u64 = 12;
const MAX_DICTIONARIES: u64 = 4;
fn dictionaries_at(index: u64) -> u64 {
const LAST_STEP: u64 = MAX_DICTIONARIES.trailing_zeros() as u64;
1 << (index / BLOCKS_PER_STEP).min(LAST_STEP)
}
pub(crate) fn shortest_split_stream(dictionary: u64) -> u64 {
dictionary.saturating_add(dictionary / 4)
}
#[cfg(test)]
const PANIC_SENTINEL: &[u8] = b"zesven: panic in this block";
fn compress_block(
data: &[u8],
context: Option<Vec<u8>>,
options: &Lzma2EncoderOptions,
) -> io::Result<Vec<u8>> {
#[cfg(test)]
if data.starts_with(PANIC_SENTINEL) {
panic!("worker asked to blow up");
}
let mut out = Vec::new();
let mut encoder = Lzma2Encoder::with_preset_dict(&mut out, options, context);
encoder.write_all(data)?;
encoder.flush()?;
Ok(out)
}
pub(crate) struct ChunkedLzma2Encoder<W: Write> {
sink: W,
options: Lzma2EncoderOptions,
emitted: u64,
staging: Vec<u8>,
in_flight: VecDeque<(Receiver<io::Result<Vec<u8>>>, u64)>,
held: std::sync::Arc<std::sync::atomic::AtomicU64>,
window_ceiling: usize,
budget: u64,
spoken_for: Option<std::sync::Arc<std::sync::atomic::AtomicU64>>,
pool: rayon::ThreadPool,
window_bytes: usize,
context: Vec<u8>,
}
impl<W: Write> ChunkedLzma2Encoder<W> {
pub(crate) fn new(
sink: W,
options: &Lzma2EncoderOptions,
workers: usize,
budget: u64,
spoken_for: Option<std::sync::Arc<std::sync::atomic::AtomicU64>>,
) -> crate::Result<Self> {
let window_bytes = options
.dict_size
.filter(|size| *size > 0)
.and_then(|size| usize::try_from(size).ok())
.ok_or_else(|| {
crate::Error::InvalidFormat(
"a split LZMA2 stream needs a stated dictionary size".into(),
)
})?;
let workers = workers.max(1);
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(workers)
.build()
.map_err(|e| crate::Error::Io(io::Error::other(e)))?;
Ok(Self {
sink,
options: options.clone(),
emitted: 0,
staging: Vec::new(),
in_flight: VecDeque::with_capacity(workers * BLOCKS_PER_WORKER),
held: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
window_ceiling: workers * BLOCKS_PER_WORKER,
budget,
spoken_for,
pool,
window_bytes,
context: Vec::new(),
})
}
pub(crate) fn held(&self) -> std::sync::Arc<std::sync::atomic::AtomicU64> {
std::sync::Arc::clone(&self.held)
}
fn block_cost(&self, len: usize) -> u64 {
crate::codec::lzma::encoder_memory_usage(
self.options.preset,
self.options.dict_size.unwrap_or(0),
)
.saturating_add(2 * len as u64)
.saturating_add(self.window_bytes as u64)
}
fn block_size(&self) -> usize {
let block = dictionaries_at(self.emitted).saturating_mul(self.window_bytes as u64);
usize::try_from(block).unwrap_or(usize::MAX)
}
fn dispatch_threshold(&self) -> usize {
let block = self.block_size();
block.saturating_add(block / 4)
}
fn window(&self) -> usize {
let per_block = self.block_cost(self.block_size());
let available = self.budget.saturating_sub(
self.spoken_for
.as_ref()
.map_or(0, |held| held.load(std::sync::atomic::Ordering::Relaxed)),
);
let affordable = available
.checked_div(per_block)
.and_then(|n| usize::try_from(n).ok())
.unwrap_or(self.window_ceiling);
affordable.clamp(1, self.window_ceiling)
}
fn dispatch(&mut self, len: usize) -> io::Result<()> {
let len = len.min(self.staging.len());
if len == 0 {
return Ok(());
}
let window = self.window();
while self.in_flight.len() >= window {
self.collect_one()?;
}
let rest = self.staging.split_off(len);
let block = std::mem::replace(&mut self.staging, rest);
self.emitted += 1;
let options = self.options.clone();
let (tx, rx) = sync_channel(1);
let context = (!self.context.is_empty()).then(|| std::mem::take(&mut self.context));
let keep = self.window_bytes.min(block.len());
self.context = block[block.len() - keep..].to_vec();
let cost = self.block_cost(block.len());
self.pool.spawn(move || {
let compressed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
compress_block(&block, context, &options)
}))
.unwrap_or_else(|_| {
Err(io::Error::other(
"LZMA2 worker panicked compressing a block",
))
});
let _ = tx.send(compressed);
});
self.held
.fetch_add(cost, std::sync::atomic::Ordering::Relaxed);
self.in_flight.push_back((rx, cost));
Ok(())
}
fn collect_one(&mut self) -> io::Result<()> {
let Some((rx, cost)) = self.in_flight.pop_front() else {
return Ok(());
};
self.held
.fetch_sub(cost, std::sync::atomic::Ordering::Relaxed);
let block = rx.recv().map_err(|_| {
io::Error::other("LZMA2 worker thread stopped before finishing a block")
})?;
self.sink.write_all(&block?)
}
pub(crate) fn finish(mut self) -> io::Result<W> {
self.dispatch(self.staging.len())?;
while !self.in_flight.is_empty() {
self.collect_one()?;
}
self.sink.write_all(&[0x00])?;
self.sink.flush()?;
Ok(self.sink)
}
}
impl<W: Write> Write for ChunkedLzma2Encoder<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let mut rest = buf;
while !rest.is_empty() {
let threshold = self.dispatch_threshold();
let take = (threshold - self.staging.len()).min(rest.len());
self.staging.extend_from_slice(&rest[..take]);
rest = &rest[take..];
if self.staging.len() >= threshold {
self.dispatch(self.block_size())?;
}
}
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
while let Some((rx, cost)) = self.in_flight.front() {
let cost = *cost;
match rx.try_recv() {
Ok(block) => {
self.in_flight.pop_front();
self.held
.fetch_sub(cost, std::sync::atomic::Ordering::Relaxed);
self.sink.write_all(&block?)?;
}
Err(_) => break,
}
}
self.sink.flush()
}
}
impl<W: Write + Send> Encoder for ChunkedLzma2Encoder<W> {
fn method_id(&self) -> &'static [u8] {
method::LZMA2
}
fn finish(self: Box<Self>) -> io::Result<()> {
(*self).finish().map(|_| ())
}
fn drain_one_block(&mut self) -> io::Result<bool> {
if self.in_flight.is_empty() {
return Ok(false);
}
self.collect_one()?;
Ok(true)
}
}
#[cfg(test)]
fn boundaries_for(size: u64, dictionary: u64) -> Vec<u64> {
let mut boundaries = Vec::new();
let mut staged = size;
let mut index = 0;
loop {
let block = dictionaries_at(index) * dictionary;
if staged < block + block / 4 {
break;
}
boundaries.push(block);
staged -= block;
index += 1;
}
if staged > 0 {
boundaries.push(staged);
}
boundaries
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codec::lzma::Lzma2Decoder;
use std::io::Read;
const BUDGET: u64 = 1 << 40;
fn options() -> Lzma2EncoderOptions {
Lzma2EncoderOptions::with_preset(1).with_dict_size(1 << 16)
}
fn decode(compressed: &[u8], dict_size: u32) -> Vec<u8> {
let properties = [crate::codec::lzma::encode_lzma2_dict_size(dict_size)];
let mut decoder = Lzma2Decoder::new(compressed, &properties).expect("builds");
let mut out = Vec::new();
decoder.read_to_end(&mut out).expect("decodes");
out
}
fn compressible(len: usize) -> Vec<u8> {
let mut out = Vec::with_capacity(len);
let mut n = 0u64;
while out.len() < len {
n = n.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
out.extend_from_slice(
format!("record {n}: status=active payload=abcdefghijklmnopqrstuvwxyz\n")
.as_bytes(),
);
}
out.truncate(len);
out
}
fn data(len: usize) -> Vec<u8> {
(0..len)
.map(|i| {
let x = (i as u64).wrapping_mul(2_654_435_761);
(x >> ((i % 8) * 8)) as u8
})
.collect()
}
fn incompressible(len: usize) -> Vec<u8> {
let mut out = Vec::with_capacity(len);
let mut state = 0x2545_F491_4F6C_DD1Du64;
while out.len() < len {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
out.extend_from_slice(&state.to_le_bytes());
}
out.truncate(len);
out
}
fn roundtrip(len: usize, workers: usize) {
let input = data(len);
let mut out = Vec::new();
let mut encoder =
ChunkedLzma2Encoder::new(&mut out, &options(), workers, BUDGET, None).expect("builds");
encoder.write_all(&input).expect("writes");
encoder.finish().expect("finishes");
assert_eq!(decode(&out, 1 << 16), input, "len={len} workers={workers}");
}
#[test]
fn test_roundtrip_across_block_counts() {
for len in [0, 1, 1000, (1 << 16) - 1, 1 << 16, (1 << 16) + 1, 500_000] {
roundtrip(len, 4);
}
}
#[test]
fn test_roundtrip_with_one_worker() {
roundtrip(500_000, 1);
}
#[test]
fn test_incompressible_data_round_trips() {
let input = incompressible(500_000);
let mut out = Vec::new();
let mut encoder =
ChunkedLzma2Encoder::new(&mut out, &options(), 4, BUDGET, None).expect("builds");
encoder.write_all(&input).expect("writes");
encoder.finish().expect("finishes");
assert_eq!(decode(&out, 1 << 16), input);
}
#[test]
fn test_bytes_do_not_depend_on_worker_count() {
let input = data(400_000);
let encode = |workers: usize| {
let mut out = Vec::new();
let mut encoder = ChunkedLzma2Encoder::new(&mut out, &options(), workers, BUDGET, None)
.expect("builds");
encoder.write_all(&input).expect("writes");
encoder.finish().expect("finishes");
out
};
let one = encode(1);
for workers in [2, 3, 8, 16] {
assert_eq!(encode(workers), one, "worker count changed the output");
}
}
#[test]
fn test_bytes_do_not_depend_on_write_size() {
let input = data(300_000);
let mut whole = Vec::new();
let mut encoder =
ChunkedLzma2Encoder::new(&mut whole, &options(), 4, BUDGET, None).expect("builds");
encoder.write_all(&input).expect("writes");
encoder.finish().expect("finishes");
for piece in [1, 7, 4096, 100_000] {
let mut split = Vec::new();
let mut encoder =
ChunkedLzma2Encoder::new(&mut split, &options(), 4, BUDGET, None).expect("builds");
for part in input.chunks(piece) {
encoder.write_all(part).expect("writes");
encoder.flush().expect("flushes");
}
encoder.finish().expect("finishes");
assert_eq!(split, whole, "write size {piece} changed the output");
}
}
#[test]
fn test_one_block_matches_the_ordinary_encoder() {
let input = data(40_000);
let mut chunked = Vec::new();
let mut encoder =
ChunkedLzma2Encoder::new(&mut chunked, &options(), 4, BUDGET, None).expect("builds");
encoder.write_all(&input).expect("writes");
encoder.finish().expect("finishes");
let mut plain = Vec::new();
let mut ordinary = Lzma2Encoder::new(&mut plain, &options());
ordinary.write_all(&input).expect("writes");
ordinary.try_finish().expect("finishes");
assert_eq!(chunked, plain);
}
#[test]
fn test_compressible_data_round_trips_and_is_stable() {
let input = compressible(400_000);
let encode = |workers: usize| {
let mut out = Vec::new();
let mut encoder = ChunkedLzma2Encoder::new(&mut out, &options(), workers, BUDGET, None)
.expect("builds");
encoder.write_all(&input).expect("writes");
encoder.finish().expect("finishes");
out
};
let one = encode(1);
assert_eq!(decode(&one, 1 << 16), input);
for workers in [2, 4, 8] {
assert_eq!(encode(workers), one, "worker count changed the output");
}
}
#[test]
fn test_carrying_the_window_pays_for_itself() {
let input = compressible(600_000);
let mut chunked = Vec::new();
let mut encoder =
ChunkedLzma2Encoder::new(&mut chunked, &options(), 4, BUDGET, None).expect("builds");
encoder.write_all(&input).expect("writes");
encoder.finish().expect("finishes");
let mut isolated = Vec::new();
for block in input.chunks(1 << 16) {
isolated.extend_from_slice(&compress_block(block, None, &options()).expect("encodes"));
}
isolated.push(0x00);
assert!(
chunked.len() < isolated.len(),
"carried window produced {} bytes against {} without it",
chunked.len(),
isolated.len(),
);
assert_eq!(decode(&chunked, 1 << 16), input);
}
#[test]
fn test_matches_reaching_past_a_block_survive() {
let period: Vec<u8> = (0..(1 << 16) - 4096)
.map(|i| {
let mut x = (i as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
x ^= x >> 29;
x = x.wrapping_mul(0xBF58_476D_1CE4_E5B9);
(x >> 32) as u8
})
.collect();
let input: Vec<u8> = period
.iter()
.cycle()
.take(period.len() * 8)
.copied()
.collect();
let mut out = Vec::new();
let mut encoder =
ChunkedLzma2Encoder::new(&mut out, &options(), 4, BUDGET, None).expect("builds");
encoder.write_all(&input).expect("writes");
encoder.finish().expect("finishes");
assert_eq!(decode(&out, 1 << 16), input);
assert!(
out.len() < input.len() / 4,
"{} bytes from {} of eightfold-repeated data: the split is losing \
matches that reach past a block",
out.len(),
input.len(),
);
}
#[test]
fn test_a_panicking_worker_becomes_an_error() {
let mut input = PANIC_SENTINEL.to_vec();
input.extend_from_slice(&data(200_000));
let mut out = Vec::new();
let mut encoder =
ChunkedLzma2Encoder::new(&mut out, &options(), 2, BUDGET, None).expect("builds");
let error = encoder
.write_all(&input)
.and_then(|()| encoder.finish().map(|_| ()))
.expect_err("a panicking worker has to fail the stream");
assert!(
error.to_string().contains("panicked"),
"a panic has to arrive as an error saying so, got {error}"
);
}
#[test]
fn test_blocks_fall_where_the_schedule_says() {
let input = compressible(900_000);
let mut chunked = Vec::new();
let mut encoder =
ChunkedLzma2Encoder::new(&mut chunked, &options(), 4, BUDGET, None).expect("builds");
encoder.write_all(&input).expect("writes");
encoder.finish().expect("finishes");
let mut expected = Vec::new();
let mut start = 0usize;
for length in boundaries_for(input.len() as u64, 1 << 16) {
let end = start + length as usize;
let context = (start > 0).then(|| {
let from = start.saturating_sub(1 << 16);
input[from..start].to_vec()
});
expected.extend_from_slice(
&compress_block(&input[start..end], context, &options()).expect("encodes"),
);
start = end;
}
expected.push(0x00);
assert_eq!(start, input.len(), "the schedule has to cover the input");
assert_eq!(chunked, expected, "the encoder cut somewhere unscheduled");
}
#[test]
fn test_a_short_tail_stays_on_the_block_before_it() {
let dictionary = 1u64 << 16;
assert_eq!(
boundaries_for(dictionary + 1, dictionary),
vec![dictionary + 1]
);
assert_eq!(
boundaries_for(dictionary * 2, dictionary),
vec![dictionary, dictionary]
);
for size in [1u64, 100, dictionary - 1, dictionary * 3 + 7, 1_000_000] {
let blocks = boundaries_for(size, dictionary);
assert_eq!(blocks.iter().sum::<u64>(), size, "size {size}");
if let Some(last) = blocks.last() {
assert!(*last >= dictionary / 4 || blocks.len() == 1, "size {size}");
}
}
}
#[test]
fn test_blocks_grow_and_then_stop() {
assert_eq!(dictionaries_at(0), 1);
assert_eq!(dictionaries_at(BLOCKS_PER_STEP - 1), 1);
assert_eq!(dictionaries_at(BLOCKS_PER_STEP), 2);
assert_eq!(dictionaries_at(BLOCKS_PER_STEP * 2), 4);
assert_eq!(dictionaries_at(BLOCKS_PER_STEP * 3), MAX_DICTIONARIES);
assert_eq!(dictionaries_at(u64::MAX / 2), MAX_DICTIONARIES);
}
#[test]
fn test_a_prefix_is_cut_like_the_stream_it_starts() {
let long = boundaries_for(5_000_000, 1 << 16);
let short = boundaries_for(1_000_000, 1 << 16);
let shared = short.len() - 1;
assert_eq!(&long[..shared], &short[..shared]);
}
}