use crate::libradicl::chunk::Chunk;
use crate::libradicl::codec::{CHUNK_CODEC_TAG, ChunkCodec, decompress_payload};
use crate::libradicl::header::RadPrelude;
use crate::libradicl::rad_types::{TagMap, TagValue};
use crate::libradicl::record::{MappedRecord, RecordContext};
use crate::libradicl::utils;
use anyhow::Context;
use crossbeam_queue::ArrayQueue;
use crossbeam_utils::Backoff;
use scroll::Pwrite;
use std::io::{BufRead, Cursor, Seek};
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
pub const EMPTY_METACHUNK_CALLBACK: Option<Box<dyn FnMut(u64, u64)>> = None;
fn codec_from_tag_map(file_tag_map: &TagMap) -> anyhow::Result<ChunkCodec> {
match file_tag_map.get(CHUNK_CODEC_TAG) {
None => Ok(ChunkCodec::None),
Some(TagValue::U8(v)) => ChunkCodec::from_u8(*v),
Some(_) => anyhow::bail!("'{CHUNK_CODEC_TAG}' file tag must be a U8"),
}
}
pub struct MetaChunk<R: MappedRecord> {
pub first_chunk_index: usize,
pub num_sub_chunks: usize,
pub num_bytes: u32,
pub num_records: u32,
chunk_blob: Vec<u8>,
record_context: <R as MappedRecord>::ParsingContext,
}
pub struct MetaChunkIterator<'a, 'b, R: MappedRecord> {
curr_sub_chunk: usize,
num_sub_chunks: usize,
data: Cursor<&'a [u8]>,
record_context: &'b <R as MappedRecord>::ParsingContext,
}
impl<'a, 'b, R: MappedRecord> Iterator for MetaChunkIterator<'a, 'b, R> {
type Item = Chunk<R>;
fn next(&mut self) -> Option<Self::Item> {
if self.curr_sub_chunk < self.num_sub_chunks {
self.curr_sub_chunk += 1;
let c = Chunk::<R>::from_bytes(&mut self.data, self.record_context);
Some(c)
} else {
None
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
let rem = self.num_sub_chunks - self.curr_sub_chunk;
(rem, Some(rem))
}
}
impl<'a, 'b, R: MappedRecord> ExactSizeIterator for MetaChunkIterator<'a, 'b, R> {}
impl<R: MappedRecord> MetaChunk<R>
where
<R as MappedRecord>::ParsingContext: RecordContext,
{
pub fn new(
first_chunk_index: usize,
num_sub_chunks: usize,
num_bytes: u32,
num_records: u32,
record_context: <R as MappedRecord>::ParsingContext,
chunk_blob: Vec<u8>,
) -> Self {
Self {
first_chunk_index,
num_sub_chunks,
num_bytes,
num_records,
chunk_blob,
record_context,
}
}
pub fn iter(&self) -> MetaChunkIterator<'_, '_, R> {
MetaChunkIterator {
curr_sub_chunk: 0,
num_sub_chunks: self.num_sub_chunks,
data: Cursor::new(self.chunk_blob.as_slice()),
record_context: &self.record_context,
}
}
pub fn num_records(&self) -> u32 {
self.num_records
}
pub fn num_bytes(&self) -> u32 {
self.num_bytes
}
pub fn first_chunk_index(&self) -> usize {
self.first_chunk_index
}
}
fn fill_work_queue_filtered<
R: MappedRecord,
T: BufRead,
ChunkIt: Iterator<Item = usize> + BufReadProvider<T> + LastChunkSignaler,
FilterF,
F: FnMut(u64, u64),
>(
mut chunk_iter: ChunkIt,
filter_fn: FilterF,
mut callback: Option<F>,
prelude: &RadPrelude,
codec: ChunkCodec,
meta_chunk_queue: Arc<ArrayQueue<MetaChunk<R>>>,
done_var: Arc<AtomicBool>,
) -> anyhow::Result<()>
where
<R as MappedRecord>::ParsingContext: RecordContext,
<R as MappedRecord>::ParsingContext: Clone,
FilterF: Fn(&[u8], &<R as MappedRecord>::ParsingContext) -> bool,
{
const BUFSIZE: usize = 524208;
let mut buf = vec![0u8; BUFSIZE];
let mut scratch: Vec<u8> = Vec::new();
let mut cbytes = 0u32;
let mut crec = 0u32;
let mut chunks_in_meta_chunk = 0usize;
let mut first_chunk = 0usize;
let mut force_push = false;
let mut nbytes_chunk = 0u32;
let mut nrec_chunk = 0u32;
let record_context = prelude
.get_record_context::<<R as MappedRecord>::ParsingContext>()
.unwrap();
while let Some(chunk_num) = chunk_iter.next() {
if chunk_num > 0 {
let boffset = cbytes as usize;
let eff_nbytes = if codec == ChunkCodec::None {
if nbytes_chunk as usize > buf.len() {
force_push = true;
let chunk_resize = nbytes_chunk as usize + cbytes as usize;
buf.resize(chunk_resize, 0);
}
let br = chunk_iter.get_mut_buf_read();
buf.pwrite::<u32>(nbytes_chunk, boffset)?;
buf.pwrite::<u32>(nrec_chunk, boffset + 4)?;
br.read_exact(&mut buf[(boffset + 8)..(boffset + nbytes_chunk as usize)])
.context("failed to read from work queue.")?;
nbytes_chunk
} else {
let br = chunk_iter.get_mut_buf_read();
scratch.resize(nbytes_chunk as usize - 8, 0);
br.read_exact(&mut scratch)
.context("failed to read compressed chunk from work queue.")?;
let decoded = decompress_payload(codec, &scratch)?;
let eff = decoded.len() as u32 + 8;
if boffset + eff as usize > buf.len() {
force_push = true;
buf.resize(boffset + eff as usize, 0);
}
buf.pwrite::<u32>(eff, boffset)?;
buf.pwrite::<u32>(nrec_chunk, boffset + 4)?;
buf[(boffset + 8)..(boffset + eff as usize)].copy_from_slice(&decoded);
eff
};
if filter_fn(&buf[boffset..], &record_context) {
chunks_in_meta_chunk += 1;
cbytes += eff_nbytes;
crec += nrec_chunk;
} else {
force_push = false;
}
}
let last_chunk = chunk_iter.is_last_chunk();
if !last_chunk {
let (nc, nr) = Chunk::<R>::read_header(chunk_iter.get_mut_buf_read());
nbytes_chunk = nc;
nrec_chunk = nr;
}
if force_push || ((cbytes + nbytes_chunk) as usize > buf.len() && chunks_in_meta_chunk > 0)
|| last_chunk
{
let mut bclone = MetaChunk::<R>::new(
first_chunk,
chunks_in_meta_chunk,
cbytes,
crec,
record_context.clone(),
buf.clone(),
);
while let Err(t) = meta_chunk_queue.push(bclone) {
bclone = t;
while meta_chunk_queue.is_full() {}
}
callback
.iter_mut()
.for_each(|f| f(cbytes as u64, chunks_in_meta_chunk as u64));
first_chunk += chunks_in_meta_chunk;
chunks_in_meta_chunk = 0;
cbytes = 0;
crec = 0;
buf.resize(BUFSIZE, 0);
force_push = false;
}
}
done_var.store(true, Ordering::SeqCst);
Ok(())
}
fn fill_work_queue<
R: MappedRecord,
T: BufRead,
ChunkIt: Iterator<Item = usize> + BufReadProvider<T> + LastChunkSignaler,
F: FnMut(u64, u64),
>(
mut chunk_iter: ChunkIt,
mut callback: Option<F>,
prelude: &RadPrelude,
codec: ChunkCodec,
meta_chunk_queue: Arc<ArrayQueue<MetaChunk<R>>>,
done_var: Arc<AtomicBool>,
) -> anyhow::Result<()>
where
<R as MappedRecord>::ParsingContext: RecordContext,
<R as MappedRecord>::ParsingContext: Clone,
{
const BUFSIZE: usize = 524208;
let mut buf = vec![0u8; BUFSIZE];
let mut scratch: Vec<u8> = Vec::new();
let mut cbytes = 0u32;
let mut crec = 0u32;
let mut chunks_in_meta_chunk = 0usize;
let mut first_chunk = 0usize;
let mut force_push = false;
let mut nbytes_chunk = 0u32;
let mut nrec_chunk = 0u32;
let record_context = prelude
.get_record_context::<<R as MappedRecord>::ParsingContext>()
.unwrap();
while let Some(chunk_num) = chunk_iter.next() {
if chunk_num > 0 {
if codec == ChunkCodec::None {
if nbytes_chunk as usize > buf.len() {
force_push = true;
let chunk_resize = nbytes_chunk as usize + cbytes as usize;
buf.resize(chunk_resize, 0);
}
let br = chunk_iter.get_mut_buf_read();
let boffset = cbytes as usize;
buf.pwrite::<u32>(nbytes_chunk, boffset)?;
buf.pwrite::<u32>(nrec_chunk, boffset + 4)?;
br.read_exact(&mut buf[(boffset + 8)..(boffset + nbytes_chunk as usize)])
.context("failed to read from work queue.")?;
chunks_in_meta_chunk += 1;
cbytes += nbytes_chunk;
crec += nrec_chunk;
} else {
let br = chunk_iter.get_mut_buf_read();
scratch.resize(nbytes_chunk as usize - 8, 0);
br.read_exact(&mut scratch)
.context("failed to read compressed chunk from work queue.")?;
let decoded = decompress_payload(codec, &scratch)?;
let eff_nbytes = decoded.len() as u32 + 8;
let boffset = cbytes as usize;
if boffset + eff_nbytes as usize > buf.len() {
force_push = true;
buf.resize(boffset + eff_nbytes as usize, 0);
}
buf.pwrite::<u32>(eff_nbytes, boffset)?;
buf.pwrite::<u32>(nrec_chunk, boffset + 4)?;
buf[(boffset + 8)..(boffset + eff_nbytes as usize)].copy_from_slice(&decoded);
chunks_in_meta_chunk += 1;
cbytes += eff_nbytes;
crec += nrec_chunk;
}
}
let last_chunk = chunk_iter.is_last_chunk();
if !last_chunk {
let (nc, nr) = Chunk::<R>::read_header(chunk_iter.get_mut_buf_read());
nbytes_chunk = nc;
nrec_chunk = nr;
}
if force_push || ((cbytes + nbytes_chunk) as usize > buf.len() && chunks_in_meta_chunk > 0)
|| last_chunk
{
let mut bclone = MetaChunk::<R>::new(
first_chunk,
chunks_in_meta_chunk,
cbytes,
crec,
record_context.clone(),
buf.clone(),
);
while let Err(t) = meta_chunk_queue.push(bclone) {
bclone = t;
while meta_chunk_queue.is_full() {}
}
callback
.iter_mut()
.for_each(|f| f(cbytes as u64, chunks_in_meta_chunk as u64));
first_chunk += chunks_in_meta_chunk;
chunks_in_meta_chunk = 0;
cbytes = 0;
crec = 0;
buf.resize(BUFSIZE, 0);
force_push = false;
}
}
done_var.store(true, Ordering::SeqCst);
Ok(())
}
#[derive(Debug)]
pub struct ParallelRadReader<R: MappedRecord, T: BufRead + Seek> {
pub prelude: RadPrelude,
pub file_tag_map: TagMap,
reader: T,
pub meta_chunk_queue: Arc<ArrayQueue<MetaChunk<R>>>,
done_var: Arc<AtomicBool>,
}
pub struct MetaChunkStream<R: MappedRecord> {
queue: Arc<ArrayQueue<MetaChunk<R>>>,
done: Arc<AtomicBool>,
}
impl<R: MappedRecord> MetaChunkStream<R> {
pub fn new(queue: Arc<ArrayQueue<MetaChunk<R>>>, done: Arc<AtomicBool>) -> Self {
Self { queue, done }
}
}
impl<R: MappedRecord> Iterator for MetaChunkStream<R> {
type Item = MetaChunk<R>;
fn next(&mut self) -> Option<Self::Item> {
const IDLE_SLEEP: std::time::Duration = std::time::Duration::from_micros(50);
let backoff = Backoff::new();
loop {
if let Some(meta_chunk) = self.queue.pop() {
return Some(meta_chunk);
}
if self.done.load(Ordering::Acquire) {
return self.queue.pop();
}
if backoff.is_completed() {
std::thread::sleep(IDLE_SLEEP);
} else {
backoff.snooze();
}
}
}
}
impl<R: MappedRecord, T: BufRead + Seek> ParallelRadReader<R, T> {
pub fn try_new(mut reader: T, num_consumers: std::num::NonZeroUsize) -> anyhow::Result<Self> {
let prelude = RadPrelude::from_bytes(&mut reader).context(
"could not parse the RAD prelude; the input may be truncated or not a RAD file",
)?;
let file_tag_map = prelude
.file_tags
.parse_tags_from_bytes(&mut reader)
.context("could not parse the file-level tag map from the RAD prelude")?;
Ok(Self {
prelude,
file_tag_map,
reader,
meta_chunk_queue: Arc::new(ArrayQueue::<MetaChunk<R>>::new(num_consumers.get() * 4)),
done_var: Arc::new(AtomicBool::new(false)),
})
}
pub fn new(reader: T, num_consumers: std::num::NonZeroUsize) -> Self {
Self::try_new(reader, num_consumers).expect("could not create ParallelRadReader")
}
pub fn from_prelude(
reader: T,
prelude: RadPrelude,
num_consumers: std::num::NonZeroUsize,
) -> Self {
Self::try_from_prelude(reader, prelude, num_consumers)
.expect("could not create ParallelRadReader from prelude")
}
pub fn try_from_prelude(
mut reader: T,
prelude: RadPrelude,
num_consumers: std::num::NonZeroUsize,
) -> anyhow::Result<Self> {
let file_tag_map = prelude
.file_tags
.parse_tags_from_bytes(&mut reader)
.context("could not parse the file-level tag map from the RAD prelude")?;
Ok(Self {
prelude,
file_tag_map,
reader,
meta_chunk_queue: Arc::new(ArrayQueue::<MetaChunk<R>>::new(num_consumers.get() * 4)),
done_var: Arc::new(AtomicBool::new(false)),
})
}
pub fn from_prelude_and_file_tag_map(
reader: T,
prelude: RadPrelude,
file_tag_map: TagMap,
num_consumers: std::num::NonZeroUsize,
) -> Self {
Self {
prelude,
file_tag_map,
reader,
meta_chunk_queue: Arc::new(ArrayQueue::<MetaChunk<R>>::new(num_consumers.get() * 4)),
done_var: Arc::new(AtomicBool::new(false)),
}
}
pub fn get_queue(&self) -> Arc<ArrayQueue<MetaChunk<R>>> {
self.meta_chunk_queue.clone()
}
pub fn is_done(&self) -> Arc<AtomicBool> {
self.done_var.clone()
}
pub fn chunk_iter(&self) -> MetaChunkStream<R> {
MetaChunkStream::new(self.meta_chunk_queue.clone(), self.done_var.clone())
}
pub fn get_byte_offset(&mut self) -> u64 {
self.reader.stream_position().unwrap()
}
pub fn process_parallel<P>(
&mut self,
num_workers: std::num::NonZeroUsize,
process: P,
) -> anyhow::Result<()>
where
P: Fn(MetaChunk<R>) + Sync,
R: Send,
<R as MappedRecord>::ParsingContext: RecordContext,
<R as MappedRecord>::ParsingContext: Clone + Send,
{
let queue = self.meta_chunk_queue.clone();
let done = self.done_var.clone();
let process = &process;
std::thread::scope(|s| -> anyhow::Result<()> {
for _ in 0..num_workers.get() {
let chunks = MetaChunkStream::new(queue.clone(), done.clone());
s.spawn(move || {
for meta_chunk in chunks {
process(meta_chunk);
}
});
}
self.start_chunk_parsing(None::<fn(u64, u64)>)
})
}
pub fn start_chunk_parsing<F: FnMut(u64, u64)>(
&mut self,
callback: Option<F>,
) -> anyhow::Result<()>
where
<R as MappedRecord>::ParsingContext: RecordContext,
<R as MappedRecord>::ParsingContext: Clone,
{
let mut pcr = ParallelChunkReader::<R> {
prelude: &self.prelude,
meta_chunk_queue: self.meta_chunk_queue.clone(),
done_var: self.done_var.clone(),
codec: codec_from_tag_map(&self.file_tag_map)?,
};
pcr.start(&mut self.reader, callback)
}
pub fn start_chunk_parsing_filtered<FilterFn, F: FnMut(u64, u64)>(
&mut self,
filter_fn: FilterFn,
callback: Option<F>,
) -> anyhow::Result<()>
where
<R as MappedRecord>::ParsingContext: RecordContext,
<R as MappedRecord>::ParsingContext: Clone,
FilterFn: Fn(&[u8], &<R as MappedRecord>::ParsingContext) -> bool,
{
let mut pcr = ParallelChunkReader::<R> {
prelude: &self.prelude,
meta_chunk_queue: self.meta_chunk_queue.clone(),
done_var: self.done_var.clone(),
codec: codec_from_tag_map(&self.file_tag_map)?,
};
pcr.start_filtered(&mut self.reader, filter_fn, callback)
}
}
trait LastChunkSignaler {
fn is_last_chunk(&mut self) -> bool;
}
trait BufReadProvider<T: BufRead> {
#[allow(dead_code)]
fn get_buf_read(&self) -> &T;
fn get_mut_buf_read(&mut self) -> &mut T;
}
struct ChunkCountIterator<T: BufRead> {
num_chunks: usize,
current_chunk: usize,
buf_reader: T,
}
impl<T: BufRead> Iterator for ChunkCountIterator<T> {
type Item = usize;
#[inline(always)]
fn next(&mut self) -> Option<Self::Item> {
let c = self.current_chunk;
self.current_chunk += 1;
if c <= self.num_chunks { Some(c) } else { None }
}
#[inline(always)]
fn size_hint(&self) -> (usize, Option<usize>) {
let rem = self.num_chunks + 1 - self.current_chunk;
(rem, Some(rem))
}
}
impl<T: BufRead> ExactSizeIterator for ChunkCountIterator<T> {}
impl<T: BufRead> LastChunkSignaler for ChunkCountIterator<T> {
fn is_last_chunk(&mut self) -> bool {
self.current_chunk > self.num_chunks
}
}
impl<T: BufRead> BufReadProvider<T> for ChunkCountIterator<T> {
fn get_buf_read(&self) -> &T {
&self.buf_reader
}
fn get_mut_buf_read(&mut self) -> &mut T {
&mut self.buf_reader
}
}
struct ReadUntilEOFIter<T: BufRead> {
current_chunk: usize,
buf_reader: T,
}
impl<T: BufRead> Iterator for ReadUntilEOFIter<T> {
type Item = usize;
#[inline(always)]
fn next(&mut self) -> Option<Self::Item> {
let c = self.current_chunk;
self.current_chunk += 1;
if utils::has_data_left(&mut self.buf_reader).expect("encountered error reading input file")
{
Some(c)
} else {
None
}
}
}
impl<T: BufRead> BufReadProvider<T> for ReadUntilEOFIter<T> {
fn get_buf_read(&self) -> &T {
&self.buf_reader
}
fn get_mut_buf_read(&mut self) -> &mut T {
&mut self.buf_reader
}
}
impl<T: BufRead> LastChunkSignaler for ReadUntilEOFIter<T> {
fn is_last_chunk(&mut self) -> bool {
!utils::has_data_left(&mut self.buf_reader).expect("encountered error reading input file")
}
}
#[derive(Debug)]
pub struct ParallelChunkReader<'a, R: MappedRecord> {
pub prelude: &'a RadPrelude,
pub meta_chunk_queue: Arc<ArrayQueue<MetaChunk<R>>>,
pub done_var: Arc<AtomicBool>,
codec: ChunkCodec,
}
impl<'a, R: MappedRecord> ParallelChunkReader<'a, R> {
pub fn new(prelude: &'a RadPrelude, num_consumers: std::num::NonZeroUsize) -> Self {
Self {
prelude,
meta_chunk_queue: Arc::new(ArrayQueue::<MetaChunk<R>>::new(num_consumers.get() * 4)),
done_var: Arc::new(AtomicBool::new(false)),
codec: ChunkCodec::None,
}
}
pub fn get_queue(&self) -> Arc<ArrayQueue<MetaChunk<R>>> {
self.meta_chunk_queue.clone()
}
pub fn is_done(&self) -> Arc<AtomicBool> {
self.done_var.clone()
}
pub fn chunk_iter(&self) -> MetaChunkStream<R> {
MetaChunkStream::new(self.meta_chunk_queue.clone(), self.done_var.clone())
}
}
impl<'a, R: MappedRecord> ParallelChunkReader<'a, R> {
pub fn process_parallel<T: BufRead, P>(
&mut self,
br: T,
num_workers: std::num::NonZeroUsize,
process: P,
) -> anyhow::Result<()>
where
P: Fn(MetaChunk<R>) + Sync,
R: Send,
<R as MappedRecord>::ParsingContext: RecordContext,
<R as MappedRecord>::ParsingContext: Clone + Send,
{
let queue = self.meta_chunk_queue.clone();
let done = self.done_var.clone();
let process = &process;
std::thread::scope(|s| -> anyhow::Result<()> {
for _ in 0..num_workers.get() {
let chunks = MetaChunkStream::new(queue.clone(), done.clone());
s.spawn(move || {
for meta_chunk in chunks {
process(meta_chunk);
}
});
}
self.start(br, None::<fn(u64, u64)>)
})
}
pub fn start<T: BufRead, F: FnMut(u64, u64)>(
&mut self,
br: T,
callback: Option<F>,
) -> anyhow::Result<()>
where
<R as MappedRecord>::ParsingContext: RecordContext,
<R as MappedRecord>::ParsingContext: Clone,
{
if let Some(nchunks) = self.prelude.hdr.num_chunks() {
let num_chunks: usize = nchunks.into();
let chunk_iter = ChunkCountIterator::<T> {
num_chunks,
current_chunk: 0,
buf_reader: br,
};
fill_work_queue(
chunk_iter,
callback,
self.prelude,
self.codec,
self.meta_chunk_queue.clone(),
self.done_var.clone(),
)?;
} else {
let chunk_iter = ReadUntilEOFIter::<T> {
current_chunk: 0,
buf_reader: br,
};
fill_work_queue(
chunk_iter,
callback,
self.prelude,
self.codec,
self.meta_chunk_queue.clone(),
self.done_var.clone(),
)?;
}
Ok(())
}
pub fn start_filtered<T: BufRead, FilterF, F: FnMut(u64, u64)>(
&mut self,
br: T,
filter_fn: FilterF,
callback: Option<F>,
) -> anyhow::Result<()>
where
<R as MappedRecord>::ParsingContext: RecordContext,
<R as MappedRecord>::ParsingContext: Clone,
FilterF: Fn(&[u8], &<R as MappedRecord>::ParsingContext) -> bool,
{
if let Some(nchunks) = self.prelude.hdr.num_chunks() {
let num_chunks: usize = nchunks.into();
let chunk_iter = ChunkCountIterator::<T> {
num_chunks,
current_chunk: 0,
buf_reader: br,
};
fill_work_queue_filtered(
chunk_iter,
filter_fn,
callback,
self.prelude,
self.codec,
self.meta_chunk_queue.clone(),
self.done_var.clone(),
)?;
} else {
let chunk_iter = ReadUntilEOFIter::<T> {
current_chunk: 0,
buf_reader: br,
};
fill_work_queue_filtered(
chunk_iter,
filter_fn,
callback,
self.prelude,
self.codec,
self.meta_chunk_queue.clone(),
self.done_var.clone(),
)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rad_types::RadIntId;
use crate::record::{PiscemBulkReadRecord, PiscemBulkRecordContext};
use std::sync::atomic::AtomicUsize;
fn dummy_meta_chunk(index: usize) -> MetaChunk<PiscemBulkReadRecord> {
MetaChunk {
first_chunk_index: index,
num_sub_chunks: 0,
num_bytes: 0,
num_records: 0,
chunk_blob: Vec::new(),
record_context: PiscemBulkRecordContext {
frag_map_t: RadIntId::U8,
},
}
}
#[test]
fn chunk_iter_drains_a_queue_that_is_already_done() {
const NCHUNKS: usize = 500;
for nconsumers in [1_usize, 4, 8] {
let queue = Arc::new(ArrayQueue::<MetaChunk<PiscemBulkReadRecord>>::new(NCHUNKS));
let done = Arc::new(AtomicBool::new(false));
let seen = AtomicUsize::new(0);
for i in 0..NCHUNKS {
queue.push(dummy_meta_chunk(i)).ok().unwrap();
}
done.store(true, Ordering::SeqCst);
std::thread::scope(|s| {
for _ in 0..nconsumers {
let chunks = MetaChunkStream::new(queue.clone(), done.clone());
let seen = &seen;
s.spawn(move || {
for _meta_chunk in chunks {
seen.fetch_add(1, Ordering::SeqCst);
}
});
}
});
assert_eq!(
seen.load(Ordering::SeqCst),
NCHUNKS,
"{nconsumers} consumer(s) stopped at the done-flag with chunks still queued"
);
assert!(queue.is_empty(), "queue not fully drained");
}
}
#[test]
fn chunk_iter_loses_nothing_racing_a_live_producer() {
const NCHUNKS: usize = 500;
for nconsumers in [1_usize, 4, 8] {
let queue = Arc::new(ArrayQueue::<MetaChunk<PiscemBulkReadRecord>>::new(NCHUNKS));
let done = Arc::new(AtomicBool::new(false));
let seen = AtomicUsize::new(0);
let started = Arc::new(AtomicUsize::new(0));
std::thread::scope(|s| {
for _ in 0..nconsumers {
let chunks = MetaChunkStream::new(queue.clone(), done.clone());
let started = started.clone();
let seen = &seen;
s.spawn(move || {
started.fetch_add(1, Ordering::SeqCst);
for _meta_chunk in chunks {
seen.fetch_add(1, Ordering::SeqCst);
}
});
}
while started.load(Ordering::SeqCst) < nconsumers {
std::hint::spin_loop();
}
for i in 0..NCHUNKS {
queue.push(dummy_meta_chunk(i)).ok().unwrap();
}
done.store(true, Ordering::SeqCst);
});
assert_eq!(
seen.load(Ordering::SeqCst),
NCHUNKS,
"{nconsumers} consumer(s)"
);
}
}
#[test]
fn try_new_rejects_malformed_input() {
let n = std::num::NonZeroUsize::new(2).unwrap();
for (what, bytes) in [
("truncated", vec![0_u8; 12]),
("empty", Vec::new()),
("not a rad file", b"@HD\tVN:1.6\nnot rad at all".to_vec()),
] {
let res = ParallelRadReader::<PiscemBulkReadRecord, _>::try_new(
std::io::BufReader::new(Cursor::new(bytes)),
n,
);
assert!(
res.is_err(),
"{what} input was accepted as a valid RAD stream"
);
}
}
#[test]
fn process_parallel_visits_every_record() {
use crate::chunk::Chunk;
use crate::header::RadPrelude;
use crate::rad_types::{RadType, TagDesc, TagSection, TagSectionLabel};
use crate::record::{AlevinFryReadRecord, AlevinFryRecordContext};
use crate::writers::RadFileWriter;
use std::io::Cursor;
const NCHUNKS: usize = 64;
const RECS_PER_CHUNK: u32 = 3;
let hdr = crate::header::RadHeader {
is_paired: 0,
ref_count: 3,
ref_names: vec!["tgt1".into(), "tgt2".into(), "tgt3".into()],
num_chunks: 0,
};
let mut file_tags = TagSection::new_with_label(TagSectionLabel::FileTags);
for name in ["bclen", "umilen"] {
file_tags.add_tag_desc(TagDesc {
name: name.to_string(),
typeid: RadType::Int(RadIntId::U16),
});
}
let mut read_tags = TagSection::new_with_label(TagSectionLabel::ReadTags);
for name in ["b", "u"] {
read_tags.add_tag_desc(TagDesc {
name: name.to_string(),
typeid: RadType::Int(RadIntId::U32),
});
}
let mut aln_tags = TagSection::new_with_label(TagSectionLabel::AlignmentTags);
aln_tags.add_tag_desc(TagDesc {
name: "compressed_ori_refid".to_string(),
typeid: RadType::Int(RadIntId::U32),
});
let prelude = RadPrelude {
hdr,
file_tags,
read_tags,
aln_tags,
};
let mut file_tag_map = crate::rad_types::TagMap::with_keyset(&prelude.file_tags.tags);
file_tag_map.add(crate::rad_types::TagValue::U16(16));
file_tag_map.add(crate::rad_types::TagValue::U16(12));
let ctx = AlevinFryRecordContext::get_context_from_tag_section(
&prelude.file_tags,
&prelude.read_tags,
&prelude.aln_tags,
)
.unwrap();
let rec = AlevinFryReadRecord {
bc: 12345,
umi: 6789,
dirs: vec![true, false, true],
refs: vec![0, 1, 2],
};
let chunk = Chunk::<AlevinFryReadRecord> {
nbytes: 0,
nrec: RECS_PER_CHUNK,
reads: vec![rec.clone(), rec.clone(), rec],
};
let mut fw = RadFileWriter::new(Cursor::new(Vec::new()), &prelude, &file_tag_map).unwrap();
for _ in 0..NCHUNKS {
fw.write_chunk(&chunk, &ctx).unwrap();
}
let bytes = fw.finalize().unwrap().into_inner();
let expected = NCHUNKS * RECS_PER_CHUNK as usize;
for nworkers in [1_usize, 2, 8] {
let n = std::num::NonZeroUsize::new(nworkers).unwrap();
let mut reader = ParallelRadReader::<AlevinFryReadRecord, _>::new(
std::io::BufReader::new(Cursor::new(bytes.clone())),
n,
);
let seen = AtomicUsize::new(0);
reader
.process_parallel(n, |meta_chunk| {
for c in meta_chunk.iter() {
seen.fetch_add(c.reads.len(), Ordering::SeqCst);
}
})
.unwrap();
assert_eq!(
seen.load(Ordering::SeqCst),
expected,
"process_parallel with {nworkers} worker(s) did not visit every record"
);
}
}
}