use std::io::{self, Write};
use std::num::NonZero;
use std::thread::{self, JoinHandle};
use bytes::{Bytes, BytesMut};
use crossbeam_channel::{bounded, Receiver, Sender};
use crate::{BgzfError, CompressionLevel, Compressor, BGZF_BLOCK_SIZE, BGZF_EOF};
const MIN_BUFFERS: usize = 8;
type Compressed = io::Result<Vec<u8>>;
type CompressedTx = oneshot::Sender<Compressed>;
type CompressedRx = oneshot::Receiver<Compressed>;
type DeflateTx = Sender<(Bytes, CompressedTx)>;
type DeflateRx = Receiver<(Bytes, CompressedTx)>;
type OrderTx = Sender<CompressedRx>;
type OrderRx = Receiver<CompressedRx>;
enum State<W> {
Running {
writer_handle: JoinHandle<io::Result<W>>,
deflater_handles: Vec<JoinHandle<()>>,
deflate_tx: DeflateTx,
order_tx: OrderTx,
},
Done,
}
pub struct MultithreadedWriter<W>
where
W: Write + Send + 'static,
{
state: State<W>,
buf: BytesMut,
blocksize: usize,
}
impl<W> MultithreadedWriter<W>
where
W: Write + Send + 'static,
{
pub fn new(inner: W, compression_level: CompressionLevel) -> Self {
let workers = thread::available_parallelism().map_or(1, NonZero::get);
Self::with_worker_count(
NonZero::new(workers).unwrap_or(NonZero::<usize>::MIN),
inner,
compression_level,
)
}
pub fn with_worker_count(
worker_count: NonZero<usize>,
inner: W,
compression_level: CompressionLevel,
) -> Self {
Self::with_capacity(worker_count, inner, compression_level, BGZF_BLOCK_SIZE)
}
pub fn with_capacity(
worker_count: NonZero<usize>,
inner: W,
compression_level: CompressionLevel,
blocksize: usize,
) -> Self {
assert!(
(1..=BGZF_BLOCK_SIZE).contains(&blocksize),
"blocksize must be in 1..={BGZF_BLOCK_SIZE}"
);
let capacity = worker_count.get().max(MIN_BUFFERS);
let (deflate_tx, deflate_rx) = bounded::<(Bytes, CompressedTx)>(capacity);
let (order_tx, order_rx) = bounded::<CompressedRx>(capacity);
let deflater_handles = spawn_deflaters(compression_level, worker_count.get(), deflate_rx);
let writer_handle = spawn_writer(inner, order_rx);
Self {
state: State::Running { writer_handle, deflater_handles, deflate_tx, order_tx },
buf: BytesMut::with_capacity(blocksize),
blocksize,
}
}
pub fn finish(&mut self) -> io::Result<W> {
if !self.buf.is_empty() {
self.send()?;
}
match std::mem::replace(&mut self.state, State::Done) {
State::Running { writer_handle, mut deflater_handles, deflate_tx, order_tx } => {
drop(deflate_tx);
for handle in deflater_handles.drain(..) {
handle.join().map_err(|_| thread_panicked("deflater"))?;
}
drop(order_tx);
writer_handle.join().map_err(|_| thread_panicked("writer"))?
}
State::Done => Err(io::Error::new(io::ErrorKind::Other, "writer already finished")),
}
}
fn send(&mut self) -> io::Result<()> {
if self.buf.is_empty() {
return Ok(());
}
let State::Running { deflate_tx, order_tx, .. } = &self.state else {
return Err(io::Error::new(io::ErrorKind::Other, "writer already finished"));
};
let data = self.buf.split().freeze();
let (compressed_tx, compressed_rx) = oneshot::channel::<Compressed>();
deflate_tx
.send((data, compressed_tx))
.map_err(|_| io::Error::new(io::ErrorKind::Other, "bgzf writer pipeline stopped"))?;
order_tx
.send(compressed_rx)
.map_err(|_| io::Error::new(io::ErrorKind::Other, "bgzf writer pipeline stopped"))?;
Ok(())
}
}
impl MultithreadedWriter<std::fs::File> {
pub fn from_path<P: AsRef<std::path::Path>>(
path: P,
compression_level: CompressionLevel,
) -> io::Result<Self> {
std::fs::File::create(path).map(|f| Self::new(f, compression_level))
}
}
impl<W> Write for MultithreadedWriter<W>
where
W: Write + Send + 'static,
{
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let n = (self.blocksize - self.buf.len()).min(buf.len());
self.buf.extend_from_slice(&buf[..n]);
if self.buf.len() >= self.blocksize {
self.send()?;
}
Ok(n)
}
fn flush(&mut self) -> io::Result<()> {
self.send()
}
}
impl<W> Drop for MultithreadedWriter<W>
where
W: Write + Send + 'static,
{
fn drop(&mut self) {
if matches!(self.state, State::Running { .. }) {
let _ = self.finish();
}
}
}
fn thread_panicked(which: &str) -> io::Error {
io::Error::new(io::ErrorKind::Other, format!("bgzf {which} thread panicked"))
}
fn spawn_deflaters(
compression_level: CompressionLevel,
worker_count: usize,
deflate_rx: DeflateRx,
) -> Vec<JoinHandle<()>> {
(0..worker_count)
.map(|_| {
let deflate_rx = deflate_rx.clone();
thread::spawn(move || {
let mut compressor = Compressor::new(compression_level);
while let Ok((data, compressed_tx)) = deflate_rx.recv() {
let mut out = Vec::new();
let result = compressor.compress(&data, &mut out).map(|()| out).map_err(to_io);
compressed_tx.send(result).ok();
}
})
})
.collect()
}
fn spawn_writer<W>(mut writer: W, order_rx: OrderRx) -> JoinHandle<io::Result<W>>
where
W: Write + Send + 'static,
{
thread::spawn(move || {
while let Ok(compressed_rx) = order_rx.recv() {
let block = compressed_rx.recv().map_err(|_| {
io::Error::new(io::ErrorKind::Other, "bgzf deflater thread stopped")
})??;
writer.write_all(&block)?;
}
writer.write_all(BGZF_EOF)?;
writer.flush()?;
Ok(writer)
})
}
fn to_io(e: BgzfError) -> io::Error {
io::Error::new(io::ErrorKind::Other, e)
}
#[cfg(test)]
mod tests {
use std::io::Read;
use std::sync::{Arc, Mutex};
use crate::{CompressionLevel, Reader};
use super::*;
fn level(n: u8) -> CompressionLevel {
CompressionLevel::new(n).unwrap()
}
fn sample(len: usize) -> Vec<u8> {
(0..len as u32).map(|i| i.wrapping_mul(2_654_435_761).rotate_left(13) as u8).collect()
}
#[derive(Clone)]
struct SharedBuf(Arc<Mutex<Vec<u8>>>);
impl SharedBuf {
fn new() -> Self {
Self(Arc::new(Mutex::new(vec![])))
}
fn bytes(&self) -> Vec<u8> {
self.0.lock().unwrap().clone()
}
}
impl Write for SharedBuf {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
fn decode(blob: &[u8]) -> Vec<u8> {
let mut out = vec![];
Reader::new(blob).read_to_end(&mut out).unwrap();
out
}
fn write_mt(input: &[u8], workers: usize, comp_level: u8) -> Vec<u8> {
let mut writer = MultithreadedWriter::with_worker_count(
NonZero::new(workers).unwrap(),
Vec::new(),
level(comp_level),
);
writer.write_all(input).unwrap();
writer.finish().unwrap()
}
#[test]
fn round_trips_through_serial_reader() {
let input = sample(300_000);
for comp_level in [0u8, 1, 6] {
for workers in [1usize, 2, 4, 8] {
assert_eq!(
decode(&write_mt(&input, workers, comp_level)),
input,
"mt writer diverged at level {comp_level}, {workers} workers"
);
}
}
}
#[test]
fn output_independent_of_write_chunking() {
let input = sample(200_000);
let one_shot = write_mt(&input, 4, 6);
let chunked = {
let mut w = MultithreadedWriter::with_worker_count(
NonZero::new(4).unwrap(),
Vec::new(),
level(6),
);
for chunk in input.chunks(7) {
w.write_all(chunk).unwrap();
}
w.finish().unwrap()
};
assert_eq!(one_shot, chunked);
}
#[test]
fn empty_writes_only_eof() {
let out = MultithreadedWriter::new(Vec::new(), level(6)).finish().unwrap();
assert_eq!(out.as_slice(), BGZF_EOF);
}
#[test]
fn eof_written_once() {
for use_finish in [true, false] {
let sink = SharedBuf::new();
{
let mut w = MultithreadedWriter::with_worker_count(
NonZero::new(3).unwrap(),
sink.clone(),
level(6),
);
w.write_all(b"some data to compress").unwrap();
if use_finish {
w.finish().unwrap();
}
}
let out = sink.bytes();
assert!(
out.ends_with(BGZF_EOF),
"output must end with the EOF marker (finish={use_finish})"
);
let eof_count = out.windows(BGZF_EOF.len()).filter(|w| *w == BGZF_EOF).count();
assert_eq!(eof_count, 1, "EOF marker should appear exactly once (finish={use_finish})");
}
}
#[test]
fn level_zero_round_trips() {
let input = sample(250_000);
assert_eq!(decode(&write_mt(&input, 4, 0)), input);
}
#[test]
fn matches_single_threaded_writer_byte_for_byte() {
use crate::Writer;
let input = sample(200_000);
for comp_level in [0u8, 1, 6, 9] {
let serial = {
let mut out = vec![];
let mut w = Writer::new(&mut out, level(comp_level));
w.write_all(&input).unwrap();
w.finish().unwrap();
out
};
let mt = write_mt(&input, 4, comp_level);
assert_eq!(mt, serial, "mt vs serial writer framing differs at level {comp_level}");
}
}
#[test]
fn mt_writer_to_mt_reader_round_trips() {
use std::io::Cursor;
use crate::MultithreadedReader;
let input = sample(400_000);
for comp_level in [0u8, 6] {
let compressed = write_mt(&input, 4, comp_level);
let mut out = vec![];
MultithreadedReader::with_worker_count(
NonZero::new(3).unwrap(),
Cursor::new(compressed),
)
.read_to_end(&mut out)
.unwrap();
assert_eq!(out, input, "mt->mt round trip failed at level {comp_level}");
}
}
#[test]
fn flush_midstream_round_trips() {
let input = sample(100_000);
let mut w =
MultithreadedWriter::with_worker_count(NonZero::new(4).unwrap(), Vec::new(), level(6));
w.write_all(&input[..40_000]).unwrap();
w.flush().unwrap();
w.write_all(&input[40_000..]).unwrap();
let compressed = w.finish().unwrap();
assert_eq!(decode(&compressed), input);
}
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(48))]
#[test]
fn proptest_mt_writer_round_trips(
input in prop::collection::vec(any::<u8>(), 1..100_000usize),
comp_level in 0..=12u8,
workers in 1usize..=4,
) {
let blob = write_mt(&input, workers, comp_level);
prop_assert_eq!(decode(&blob), input);
}
}
}