use alloc::collections::VecDeque;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::mem::{self, MaybeUninit};
use std::sync::{Condvar, Mutex, PoisonError};
use super::{EnginePool, FramePlan, compress_segment, compress_serial, lock};
use crate::compressor::core::cctx::RequestedParams;
use crate::compressor::error::EncodeError;
use crate::compressor::parallel::{
ParallelEncodeError, ParallelEncoderStatus, ParallelProgress, TaskCount,
};
use crate::compressor::session::Operation;
use crate::shared::xxhash::Xxh64;
#[derive(Debug)]
struct Shared {
state: Mutex<QueueState>,
ready: Condvar,
completed: Condvar,
}
type Done = (u64, Result<Vec<u8>, ParallelEncodeError>);
#[derive(Debug)]
struct QueueState {
tasks: VecDeque<StreamTask>,
closed: bool,
cancelled: bool,
handles: usize,
done: Vec<Done>,
}
impl Shared {
fn push(&self, task: StreamTask) -> bool {
let mut state = lock(&self.state);
if state.closed {
drop(state);
drop(task);
return false;
}
state.tasks.push_back(task);
drop(state);
self.ready.notify_one();
true
}
fn close(&self, cancel: bool) {
let mut state = lock(&self.state);
state.closed = true;
state.cancelled |= cancel;
let tasks = mem::take(&mut state.tasks);
drop(state);
self.ready.notify_all();
drop(tasks);
}
}
#[derive(Debug)]
pub(crate) struct QueueHandle(Arc<Shared>);
impl QueueHandle {
pub(crate) fn recv(&self) -> Option<StreamTask> {
let mut state = lock(&self.0.state);
loop {
if let Some(task) = state.tasks.pop_front() {
return Some(task);
}
if state.closed {
return None;
}
state = self
.0
.ready
.wait(state)
.unwrap_or_else(PoisonError::into_inner);
}
}
pub(crate) fn try_recv(&self) -> Option<StreamTask> {
lock(&self.0.state).tasks.pop_front()
}
pub(crate) fn queued(&self) -> usize {
lock(&self.0.state).tasks.len()
}
}
impl Clone for QueueHandle {
fn clone(&self) -> Self {
lock(&self.0.state).handles += 1;
Self(Arc::clone(&self.0))
}
}
impl Drop for QueueHandle {
fn drop(&mut self) {
let mut state = lock(&self.0.state);
state.handles -= 1;
if state.handles != 0 {
return;
}
drop(state);
self.0.close(false);
}
}
#[derive(Debug)]
enum Work {
Fragment { plan: FramePlan, overlap: usize },
Serial { requested: RequestedParams },
}
#[derive(Debug)]
pub(crate) struct StreamTask {
work: Work,
data: Vec<u8>,
segment: u64,
shared: Arc<Shared>,
pool: Arc<EnginePool>,
running: bool,
published: bool,
}
impl StreamTask {
pub(crate) fn run(mut self) {
if lock(&self.shared.state).cancelled {
self.publish(Err(ParallelEncodeError::TaskAbandoned {
segment: self.segment,
}));
return;
}
self.running = true;
let mut engine = self.pool.take();
let result = match &self.work {
Work::Fragment { plan, overlap } => {
let mut blocks = Vec::new();
match compress_segment(&mut engine, plan, &self.data, *overlap, &mut blocks) {
Ok(()) => Ok(blocks),
Err(error) => Err(EncodeError::from_core(error, 0, None)),
}
}
Work::Serial { requested } => compress_serial(&mut engine, requested, &self.data),
};
self.pool.give(engine);
self.publish(result.map_err(ParallelEncodeError::from));
}
fn publish(&mut self, result: Result<Vec<u8>, ParallelEncodeError>) {
self.published = true;
lock(&self.shared.state).done.push((self.segment, result));
self.shared.completed.notify_all();
}
}
impl Drop for StreamTask {
fn drop(&mut self) {
if !self.published {
let segment = self.segment;
self.publish(Err(if self.running {
ParallelEncodeError::TaskPanicked { segment }
} else {
ParallelEncodeError::TaskAbandoned { segment }
}));
}
}
}
pub(crate) trait Sink {
fn capacity(&self) -> usize;
fn put(&mut self, at: usize, bytes: &[u8]);
}
impl Sink for [u8] {
fn capacity(&self) -> usize {
self.len()
}
fn put(&mut self, at: usize, bytes: &[u8]) {
self[at..at + bytes.len()].copy_from_slice(bytes);
}
}
impl Sink for [MaybeUninit<u8>] {
fn capacity(&self) -> usize {
self.len()
}
fn put(&mut self, at: usize, bytes: &[u8]) {
for (slot, &byte) in self[at..at + bytes.len()].iter_mut().zip(bytes) {
slot.write(byte);
}
}
}
#[derive(Debug)]
enum Phase {
Open,
Finishing,
Finished,
Failed(Arc<ParallelEncodeError>),
}
#[derive(Debug)]
enum State {
Running,
Done { bytes: Vec<u8>, cursor: usize },
Failed(ParallelEncodeError),
}
#[derive(Debug)]
struct Entry {
segment: Option<u64>,
state: State,
}
#[derive(Debug)]
pub(crate) struct Stream {
shared: Arc<Shared>,
pool: Arc<EnginePool>,
requested: RequestedParams,
plan: FramePlan,
declared: Option<u64>,
segment_size: usize,
window: usize,
in_flight: usize,
flight: VecDeque<Entry>,
filling: Vec<u8>,
history: usize,
received: u64,
hasher: Xxh64,
next_segment: u64,
fragments: bool,
phase: Phase,
}
impl Stream {
pub(crate) fn new(
requested: RequestedParams,
declared: Option<u64>,
segment_size: usize,
window: TaskCount,
pool: Arc<EnginePool>,
) -> (Self, QueueHandle) {
let shared = Arc::new(Shared {
state: Mutex::new(QueueState {
tasks: VecDeque::new(),
closed: false,
cancelled: false,
handles: 1,
done: Vec::new(),
}),
ready: Condvar::new(),
completed: Condvar::new(),
});
let stream = Self {
shared: Arc::clone(&shared),
pool,
requested,
plan: FramePlan::new(&requested, declared),
declared,
segment_size,
window: window.get(),
in_flight: 0,
flight: VecDeque::new(),
filling: Vec::new(),
history: 0,
received: 0,
hasher: Xxh64::new(0),
next_segment: 0,
fragments: false,
phase: Phase::Open,
};
(stream, QueueHandle(shared))
}
pub(crate) const fn is_finished(&self) -> bool {
matches!(self.phase, Phase::Finished)
}
pub(crate) fn process<O: Sink + ?Sized>(
&mut self,
input: &[u8],
output: &mut O,
operation: Operation,
) -> Result<ParallelProgress, ParallelEncodeError> {
self.check()?;
if self.is_finished() {
return Ok(ParallelProgress {
consumed: 0,
produced: 0,
status: ParallelEncoderStatus::Finished,
});
}
self.collect()?;
let mut produced = self.drain(output, 0);
let mut consumed = 0;
if matches!(self.phase, Phase::Open) {
consumed = self.accept(input)?;
if consumed == input.len() {
match operation {
Operation::Continue => {}
Operation::Flush => {
if self.pending() != 0 {
self.dispatch_fragment()?;
}
}
Operation::End => self.begin_finish()?,
}
}
produced = self.drain(output, produced);
}
let status = self.status(consumed < input.len(), operation);
Ok(ParallelProgress {
consumed,
produced,
status,
})
}
pub(crate) fn wait(&mut self) -> Result<(), ParallelEncodeError> {
self.check()?;
if self.collect()? != 0
|| !self
.flight
.iter()
.any(|entry| matches!(entry.state, State::Running))
{
return Ok(());
}
{
let mut state = lock(&self.shared.state);
while state.done.is_empty() {
state = self
.shared
.completed
.wait(state)
.unwrap_or_else(PoisonError::into_inner);
}
}
self.collect().map(drop)
}
const fn pending(&self) -> usize {
self.filling.len() - self.history
}
fn check(&self) -> Result<(), ParallelEncodeError> {
match &self.phase {
Phase::Failed(cause) => Err(ParallelEncodeError::StreamFailed(Arc::clone(cause))),
_ => Ok(()),
}
}
fn fail(&mut self, error: ParallelEncodeError) -> ParallelEncodeError {
let cause = Arc::new(error);
self.phase = Phase::Failed(Arc::clone(&cause));
self.shared.close(true);
ParallelEncodeError::StreamFailed(cause)
}
fn collect(&mut self) -> Result<usize, ParallelEncodeError> {
let done = mem::take(&mut lock(&self.shared.state).done);
let count = done.len();
for (segment, result) in done {
if let Some(entry) = self
.flight
.iter_mut()
.find(|entry| entry.segment == Some(segment))
{
entry.state = match result {
Ok(bytes) => State::Done { bytes, cursor: 0 },
Err(error) => State::Failed(error),
};
}
}
let first = self
.flight
.iter()
.find(|entry| !matches!(entry.state, State::Done { .. }));
if let Some(Entry {
state: State::Failed(error),
..
}) = first
{
let error = error.clone();
return Err(self.fail(error));
}
Ok(count)
}
fn drain<O: Sink + ?Sized>(&mut self, output: &mut O, mut produced: usize) -> usize {
while let Some(entry) = self.flight.front_mut() {
let State::Done { bytes, cursor } = &mut entry.state else {
break;
};
let take = (bytes.len() - *cursor).min(output.capacity() - produced);
output.put(produced, &bytes[*cursor..*cursor + take]);
produced += take;
*cursor += take;
if *cursor < bytes.len() {
break;
}
if entry.segment.is_some() {
self.in_flight -= 1;
}
self.flight.pop_front();
}
produced
}
fn accept(&mut self, input: &[u8]) -> Result<usize, ParallelEncodeError> {
if let Some(declared) = self.declared {
let actual = self.received + input.len() as u64;
if actual > declared {
return Err(self.fail(ParallelEncodeError::InputSizeMismatch { declared, actual }));
}
}
let mut consumed = 0;
loop {
if self.pending() == self.segment_size {
let undecided = !self.fragments && consumed == input.len();
if undecided || !self.dispatch_fragment()? {
break;
}
}
let take = (self.segment_size - self.pending()).min(input.len() - consumed);
if take == 0 {
break;
}
let chunk = &input[consumed..consumed + take];
if self.filling.capacity() < self.history + self.segment_size {
self.filling
.reserve_exact(self.history + self.segment_size - self.filling.len());
}
self.filling.extend_from_slice(chunk);
if self.plan.applied.fparams.checksum {
self.hasher.update(chunk);
}
consumed += take;
}
self.received += consumed as u64;
Ok(consumed)
}
fn dispatch_fragment(&mut self) -> Result<bool, ParallelEncodeError> {
if self.in_flight == self.window {
return Ok(false);
}
if !self.fragments {
self.fragments = true;
self.push_inline(self.plan.header());
}
let keep = self.plan.overlap().min(self.filling.len());
let mut next = Vec::with_capacity(keep + self.segment_size);
next.extend_from_slice(&self.filling[self.filling.len() - keep..]);
let data = mem::replace(&mut self.filling, next);
let overlap = mem::replace(&mut self.history, keep);
self.queue(
Work::Fragment {
plan: self.plan,
overlap,
},
data,
)?;
Ok(true)
}
fn queue(&mut self, work: Work, data: Vec<u8>) -> Result<(), ParallelEncodeError> {
let segment = self.next_segment;
self.next_segment += 1;
self.in_flight += 1;
self.flight.push_back(Entry {
segment: Some(segment),
state: State::Running,
});
let task = StreamTask {
work,
data,
segment,
shared: Arc::clone(&self.shared),
pool: Arc::clone(&self.pool),
running: false,
published: false,
};
if self.shared.push(task) {
Ok(())
} else {
Err(self.fail(ParallelEncodeError::TaskQueueClosed))
}
}
fn begin_finish(&mut self) -> Result<(), ParallelEncodeError> {
if let Some(declared) = self.declared
&& self.received != declared
{
return Err(self.fail(ParallelEncodeError::InputSizeMismatch {
declared,
actual: self.received,
}));
}
if !self.fragments {
let data = mem::take(&mut self.filling);
if data.is_empty() {
let mut engine = self.pool.take();
let frame = compress_serial(&mut engine, &self.requested, &data);
self.pool.give(engine);
match frame {
Ok(frame) => self.push_inline(frame),
Err(error) => return Err(self.fail(error.into())),
}
} else {
self.queue(
Work::Serial {
requested: self.requested,
},
data,
)?;
}
self.phase = Phase::Finishing;
return Ok(());
}
if self.pending() != 0 && !self.dispatch_fragment()? {
return Ok(());
}
self.push_inline(self.plan.epilogue(&self.hasher));
self.phase = Phase::Finishing;
Ok(())
}
fn push_inline(&mut self, bytes: Vec<u8>) {
self.flight.push_back(Entry {
segment: None,
state: State::Done { bytes, cursor: 0 },
});
}
fn status(&mut self, unconsumed: bool, operation: Operation) -> ParallelEncoderStatus {
if self
.flight
.front()
.is_some_and(|entry| matches!(entry.state, State::Done { .. }))
{
return ParallelEncoderStatus::NeedsOutput;
}
match self.phase {
Phase::Finishing if self.flight.is_empty() => {
self.phase = Phase::Finished;
self.shared.close(true);
ParallelEncoderStatus::Finished
}
Phase::Open
if !unconsumed
&& match operation {
Operation::Continue => true,
Operation::Flush => self.pending() == 0 && self.flight.is_empty(),
Operation::End => false,
} =>
{
ParallelEncoderStatus::NeedsInput
}
_ => ParallelEncoderStatus::Pending,
}
}
}
impl Drop for Stream {
fn drop(&mut self) {
self.shared.close(true);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compressor::parallel::SegmentSize;
fn stream(window: usize) -> (Stream, QueueHandle) {
Stream::new(
RequestedParams::default(),
None,
SegmentSize::MIN.get(),
TaskCount::try_from(window).unwrap(),
Arc::new(EnginePool::default()),
)
}
#[test]
fn results_for_unknown_segments_are_ignored() {
let (mut stream, _queue) = stream(1);
lock(&stream.shared.state).done.push((9, Ok(Vec::new())));
assert_eq!(stream.collect().unwrap(), 1);
assert!(stream.flight.is_empty());
}
#[test]
fn the_first_failure_in_input_order_fails_the_stream() {
let (mut stream, _queue) = stream(4);
for segment in 0..3 {
stream.flight.push_back(Entry {
segment: Some(segment),
state: State::Running,
});
}
lock(&stream.shared.state)
.done
.push((2, Err(ParallelEncodeError::TaskAbandoned { segment: 2 })));
stream.collect().unwrap();
lock(&stream.shared.state).done.extend([
(0, Ok(vec![1])),
(1, Err(ParallelEncodeError::TaskPanicked { segment: 1 })),
]);
let error = stream.collect().unwrap_err();
assert_eq!(
error,
ParallelEncodeError::StreamFailed(Arc::new(ParallelEncodeError::TaskPanicked {
segment: 1
}))
);
assert_eq!(stream.wait(), Err(error));
}
#[test]
fn cancelled_tasks_report_abandonment_without_compressing() {
let (mut stream, queue) = stream(1);
stream.filling.extend_from_slice(b"abc");
assert!(stream.dispatch_fragment().unwrap());
let task = queue.try_recv().unwrap();
lock(&stream.shared.state).cancelled = true;
task.run();
let done = mem::take(&mut lock(&stream.shared.state).done);
assert_eq!(
done,
[(0, Err(ParallelEncodeError::TaskAbandoned { segment: 0 }))]
);
}
#[test]
fn a_task_that_unwinds_reports_a_panic() {
let (mut stream, queue) = stream(1);
stream.filling.extend_from_slice(b"abc");
assert!(stream.dispatch_fragment().unwrap());
let mut task = queue.try_recv().unwrap();
task.running = true;
drop(task);
assert_eq!(
stream.wait(),
Err(ParallelEncodeError::StreamFailed(Arc::new(
ParallelEncodeError::TaskPanicked { segment: 0 }
)))
);
}
#[test]
fn the_queue_closes_when_its_last_handle_drops() {
let (stream, queue) = stream(1);
let other = queue.clone();
drop(queue);
assert!(!lock(&stream.shared.state).closed);
drop(other);
let state = lock(&stream.shared.state);
assert!(state.closed);
assert!(!state.cancelled);
assert_eq!(state.handles, 0);
}
#[test]
fn uninitialized_sinks_write_only_the_delivered_range() {
let mut output = [MaybeUninit::new(0xAAu8); 6];
let sink: &mut [MaybeUninit<u8>] = &mut output;
assert_eq!(sink.capacity(), 6);
sink.put(1, b"abcd");
let bytes = output.map(|slot| unsafe { slot.assume_init() });
assert_eq!(&bytes, b"\xAAabcd\xAA");
}
}