1use std::fs::File;
24use std::io::{self, BufReader, Read, Seek, SeekFrom, Take};
25use std::ops::Range;
26use std::path::Path;
27
28use rayon::prelude::*;
29
30use crate::error::{Error, Result};
31use crate::format::Format;
32use crate::reader::FastxReader;
33use crate::record::Sequence;
34use crate::stats::SeqStats;
35
36pub const DEFAULT_CHUNK_SIZE: usize = 4096;
38
39pub type ChunkReader = FastxReader<Take<File>>;
41
42struct Batch {
44 records: Vec<Sequence>,
45 filled: usize,
46}
47
48impl Batch {
49 fn new(capacity: usize) -> Batch {
50 Batch {
51 records: Vec::with_capacity(capacity),
52 filled: 0,
53 }
54 }
55
56 fn refill<R: Read>(&mut self, reader: &mut FastxReader<R>, capacity: usize) -> Result<usize> {
58 self.filled = 0;
59 while self.filled < capacity {
60 if self.records.len() == self.filled {
61 self.records.push(Sequence::default());
62 }
63 if !reader.read_into(&mut self.records[self.filled])? {
64 break;
65 }
66 self.filled += 1;
67 }
68 Ok(self.filled)
69 }
70
71 fn as_slice(&self) -> &[Sequence] {
72 &self.records[..self.filled]
73 }
74}
75
76pub fn par_for_each<R, F>(reader: &mut FastxReader<R>, chunk_size: usize, f: F) -> Result<()>
82where
83 R: Read,
84 F: Fn(&Sequence) -> Result<()> + Send + Sync,
85{
86 let chunk_size = chunk_size.max(1);
87 let mut batch = Batch::new(chunk_size);
88 while batch.refill(reader, chunk_size)? > 0 {
89 batch.as_slice().par_iter().try_for_each(&f)?;
90 }
91 Ok(())
92}
93
94pub fn par_map<R, T, F>(reader: &mut FastxReader<R>, chunk_size: usize, f: F) -> Result<Vec<T>>
96where
97 R: Read,
98 T: Send,
99 F: Fn(&Sequence) -> T + Send + Sync,
100{
101 let chunk_size = chunk_size.max(1);
102 let mut batch = Batch::new(chunk_size);
103 let mut out = Vec::new();
104 while batch.refill(reader, chunk_size)? > 0 {
105 let mut mapped: Vec<T> = Vec::new();
106 batch
107 .as_slice()
108 .par_iter()
109 .map(&f)
110 .collect_into_vec(&mut mapped);
111 out.append(&mut mapped);
112 }
113 Ok(out)
114}
115
116pub fn par_fold<R, T, I, F, M>(
137 reader: &mut FastxReader<R>,
138 chunk_size: usize,
139 identity: I,
140 fold: F,
141 merge: M,
142) -> Result<T>
143where
144 R: Read,
145 T: Send,
146 I: Fn() -> T + Send + Sync,
147 F: Fn(T, &Sequence) -> T + Send + Sync,
148 M: Fn(T, T) -> T + Send + Sync,
149{
150 let chunk_size = chunk_size.max(1);
151 let mut batch = Batch::new(chunk_size);
152 let mut accumulator = identity();
153 while batch.refill(reader, chunk_size)? > 0 {
154 let folded = batch
155 .as_slice()
156 .par_iter()
157 .fold(&identity, &fold)
158 .reduce(&identity, &merge);
159 accumulator = merge(accumulator, folded);
160 }
161 Ok(accumulator)
162}
163
164pub fn par_stats<R: Read>(reader: &mut FastxReader<R>, chunk_size: usize) -> Result<SeqStats> {
166 par_fold(
167 reader,
168 chunk_size,
169 SeqStats::new,
170 |mut stats, record| {
171 stats.push(record);
172 stats
173 },
174 |mut a, b| {
175 a.merge(&b);
176 a
177 },
178 )
179}
180
181pub fn split_file<P: AsRef<Path>>(path: P, parts: usize) -> Result<Vec<Range<u64>>> {
194 let path = path.as_ref();
195 if crate::format::Compression::from_path(path) != crate::format::Compression::None {
196 return Err(Error::Unsupported(
197 "compressed files cannot be split by byte range; use par_for_each instead",
198 ));
199 }
200 let mut file = File::open(path)
201 .map_err(|e| Error::Io(io::Error::new(e.kind(), format!("{}: {e}", path.display()))))?;
202 let size = file.metadata()?.len();
203 if size == 0 {
204 return Ok(Vec::new());
205 }
206 let format = sniff_format(&mut file)?;
207 let parts = parts.max(1);
208
209 let mut boundaries = vec![0u64];
210 for i in 1..parts {
211 let candidate = size * i as u64 / parts as u64;
212 if candidate <= *boundaries.last().unwrap() {
213 continue;
214 }
215 if let Some(start) = find_record_start(&mut file, candidate, size, format)? {
216 if start > *boundaries.last().unwrap() && start < size {
217 boundaries.push(start);
218 }
219 }
220 }
221 boundaries.push(size);
222 Ok(boundaries.windows(2).map(|w| w[0]..w[1]).collect())
223}
224
225pub fn par_map_chunks_file<P, T, F>(path: P, parts: usize, f: F) -> Result<Vec<T>>
239where
240 P: AsRef<Path>,
241 T: Send,
242 F: Fn(&mut ChunkReader) -> Result<T> + Send + Sync,
243{
244 let path = path.as_ref();
245 let ranges = split_file(path, parts)?;
246 if ranges.is_empty() {
247 return Ok(Vec::new());
248 }
249 let format = sniff_format(&mut File::open(path)?)?;
250 ranges
251 .into_par_iter()
252 .map(|range| {
253 let mut file = File::open(path)?;
254 file.seek(SeekFrom::Start(range.start))?;
255 let mut reader = FastxReader::with_format(file.take(range.end - range.start), format);
256 f(&mut reader)
257 })
258 .collect()
259}
260
261pub fn par_for_each_file<P, F>(path: P, parts: usize, f: F) -> Result<()>
263where
264 P: AsRef<Path>,
265 F: Fn(&Sequence) -> Result<()> + Send + Sync,
266{
267 par_map_chunks_file(path, parts, |reader| reader.for_each_record(&f))?;
268 Ok(())
269}
270
271pub fn par_stats_file<P: AsRef<Path>>(path: P, parts: usize) -> Result<SeqStats> {
273 let per_chunk = par_map_chunks_file(path, parts, |reader| {
274 let mut stats = SeqStats::new();
275 reader.for_each_record(|record| {
276 stats.push(record);
277 Ok(())
278 })?;
279 Ok(stats)
280 })?;
281 let mut total = SeqStats::new();
282 for stats in &per_chunk {
283 total.merge(stats);
284 }
285 Ok(total)
286}
287
288fn sniff_format(file: &mut File) -> Result<Format> {
290 file.seek(SeekFrom::Start(0))?;
291 let mut byte = [0u8; 1];
292 loop {
293 let read = file.read(&mut byte)?;
294 if read == 0 {
295 return Err(Error::UnknownFormat {
296 hint: "file is empty".to_string(),
297 });
298 }
299 if byte[0].is_ascii_whitespace() {
300 continue;
301 }
302 return Format::from_first_byte(byte[0]).ok_or_else(|| Error::UnknownFormat {
303 hint: format!("first byte is {:?}", byte[0]),
304 });
305 }
306}
307
308fn find_record_start(file: &mut File, from: u64, size: u64, format: Format) -> Result<Option<u64>> {
314 if from >= size {
315 return Ok(None);
316 }
317 file.seek(SeekFrom::Start(from))?;
318 let mut reader = BufReader::with_capacity(64 * 1024, file);
319 let mut offset = from;
320 let mut line = Vec::new();
321
322 if from > 0 {
324 let read = read_line(&mut reader, &mut line)?;
325 if read == 0 {
326 return Ok(None);
327 }
328 offset += read as u64;
329 }
330
331 match format {
332 Format::Fasta => loop {
333 let read = read_line(&mut reader, &mut line)?;
334 if read == 0 {
335 return Ok(None);
336 }
337 if line.first() == Some(&b'>') {
338 return Ok(Some(offset));
339 }
340 offset += read as u64;
341 },
342 Format::Fastq => {
343 let mut window: Vec<(u64, Vec<u8>)> = Vec::with_capacity(4);
345 loop {
346 let read = read_line(&mut reader, &mut line)?;
347 if read == 0 {
348 return Ok(None);
349 }
350 if window.len() == 4 {
351 window.remove(0);
352 }
353 window.push((offset, trim_newline(&line).to_vec()));
354 offset += read as u64;
355 if window.len() == 4
356 && window[0].1.first() == Some(&b'@')
357 && window[2].1.first() == Some(&b'+')
358 && window[1].1.len() == window[3].1.len()
359 {
360 return Ok(Some(window[0].0));
361 }
362 }
363 }
364 }
365}
366
367fn read_line<R: Read>(reader: &mut BufReader<R>, line: &mut Vec<u8>) -> Result<usize> {
368 use std::io::BufRead;
369 line.clear();
370 Ok(reader.read_until(b'\n', line)?)
371}
372
373fn trim_newline(line: &[u8]) -> &[u8] {
374 let mut end = line.len();
375 if end > 0 && line[end - 1] == b'\n' {
376 end -= 1;
377 }
378 if end > 0 && line[end - 1] == b'\r' {
379 end -= 1;
380 }
381 &line[..end]
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use std::io::Write;
388
389 fn temp_dir(tag: &str) -> std::path::PathBuf {
390 let dir = std::env::temp_dir().join(format!("fastx-par-{}-{tag}", std::process::id()));
391 std::fs::create_dir_all(&dir).unwrap();
392 dir
393 }
394
395 fn write_fastq(path: &Path, records: usize) {
396 let mut file = std::fs::File::create(path).unwrap();
397 for i in 0..records {
398 writeln!(file, "@read{i} sample\nACGTACGTAC\n+\n@@++IIIIII").unwrap();
400 }
401 }
402
403 fn write_fasta(path: &Path, records: usize) {
404 let mut file = std::fs::File::create(path).unwrap();
405 for i in 0..records {
406 writeln!(file, ">contig{i}\nACGTACGTAC\nGGGG").unwrap();
407 }
408 }
409
410 #[test]
411 fn parallel_batches_preserve_order() {
412 let data = b">a\nAC\n>b\nACGT\n>c\nACGTAC\n";
413 let lengths = par_map(&mut FastxReader::new(&data[..]), 2, |r| r.len()).unwrap();
414 assert_eq!(lengths, vec![2, 4, 6]);
415 }
416
417 #[test]
418 fn parallel_fold_counts_bases() {
419 let data = b"@a\nACGT\n+\nIIII\n@b\nAC\n+\nII\n";
420 let total = par_fold(
421 &mut FastxReader::new(&data[..]),
422 1,
423 || 0usize,
424 |acc, r| acc + r.len(),
425 |a, b| a + b,
426 )
427 .unwrap();
428 assert_eq!(total, 6);
429 }
430
431 #[test]
432 fn parallel_for_each_propagates_errors() {
433 let data = b">a\nACGT\n";
434 let err = par_for_each(&mut FastxReader::new(&data[..]), 4, |_| {
435 Err(Error::Index("boom".into()))
436 });
437 assert!(err.is_err());
438 }
439
440 #[test]
441 fn parallel_stats_match_sequential() {
442 let data = b">a\nACGTACGTAC\n>b\nGGCC\n>c\nNNNN\n";
443 let parallel = par_stats(&mut FastxReader::new(&data[..]), 2).unwrap();
444 let mut sequential = SeqStats::new();
445 FastxReader::new(&data[..])
446 .for_each_record(|r| {
447 sequential.push(r);
448 Ok(())
449 })
450 .unwrap();
451 assert_eq!(parallel.count, sequential.count);
452 assert_eq!(parallel.total_length, sequential.total_length);
453 assert_eq!(parallel.n50(), sequential.n50());
454 assert_eq!(parallel.gc_content(), sequential.gc_content());
455 }
456
457 #[test]
458 fn splits_fastq_on_record_boundaries() {
459 let dir = temp_dir("fq");
460 let path = dir.join("reads.fq");
461 write_fastq(&path, 500);
462
463 for parts in [1, 2, 3, 7, 64, 4096] {
464 let ranges = split_file(&path, parts).unwrap();
465 assert_eq!(ranges.first().unwrap().start, 0);
466 assert_eq!(
467 ranges.last().unwrap().end,
468 std::fs::metadata(&path).unwrap().len()
469 );
470 assert!(
471 ranges.windows(2).all(|w| w[0].end == w[1].start),
472 "{ranges:?}"
473 );
474
475 let counts =
476 par_map_chunks_file(&path, parts, |reader| reader.count_records()).unwrap();
477 assert_eq!(counts.iter().sum::<u64>(), 500, "parts={parts}");
478 }
479 std::fs::remove_dir_all(&dir).ok();
480 }
481
482 #[test]
483 fn splits_fasta_on_record_boundaries() {
484 let dir = temp_dir("fa");
485 let path = dir.join("contigs.fa");
486 write_fasta(&path, 300);
487
488 for parts in [1, 4, 9, 128] {
489 let ids = par_map_chunks_file(&path, parts, |reader| {
490 let mut ids = Vec::new();
491 reader.for_each_record(|r| {
492 ids.push(r.id.clone());
493 Ok(())
494 })?;
495 Ok(ids)
496 })
497 .unwrap();
498 let flat: Vec<String> = ids.into_iter().flatten().collect();
499 assert_eq!(flat.len(), 300, "parts={parts}");
500 assert_eq!(flat[0], "contig0");
502 assert_eq!(flat[299], "contig299");
503 }
504
505 let stats = par_stats_file(&path, 8).unwrap();
506 assert_eq!(stats.count, 300);
507 assert_eq!(stats.total_length, 300 * 14);
508 std::fs::remove_dir_all(&dir).ok();
509 }
510
511 #[test]
512 fn parallel_file_processing_sees_every_record() {
513 let dir = temp_dir("each");
514 let path = dir.join("reads.fq");
515 write_fastq(&path, 200);
516 let count = std::sync::atomic::AtomicUsize::new(0);
517 par_for_each_file(&path, 8, |_| {
518 count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
519 Ok(())
520 })
521 .unwrap();
522 assert_eq!(count.load(std::sync::atomic::Ordering::Relaxed), 200);
523 std::fs::remove_dir_all(&dir).ok();
524 }
525
526 #[test]
527 fn empty_file_splits_into_nothing() {
528 let dir = temp_dir("empty");
529 let path = dir.join("empty.fa");
530 std::fs::write(&path, b"").unwrap();
531 assert!(split_file(&path, 4).unwrap().is_empty());
532 assert!(par_map_chunks_file(&path, 4, |r| r.count_records())
533 .unwrap()
534 .is_empty());
535 std::fs::remove_dir_all(&dir).ok();
536 }
537
538 #[test]
539 fn refuses_to_split_gzip() {
540 assert!(split_file("x.fq.gz", 4).is_err());
541 }
542}