1use crate::buckets::{BucketEmitStats, SharedBucketEmitter, SharedBucketSink};
8use crate::dna::{ascii_base_bits, valid_ascii_base_bits};
9use crate::hash::{hash_u64, wyhash_u64};
10use crate::input::{
11 BorrowedSequenceFragment, InputError, expand_input_paths, parse_fragments,
12 parse_fragments_borrowed, parse_fragments_borrowed_with,
13};
14use crate::params::{BuildParams, ParamError};
15use std::path::{Path, PathBuf};
16use std::sync::atomic::{AtomicUsize, Ordering};
17use std::sync::{Arc, mpsc};
18use std::time::{Duration, Instant};
19
20const PARTITION_FRAGMENT_BATCH: usize = 16 * 1024;
21const PARTITION_BATCH_BASES: usize = 8 * 1024 * 1024;
22
23const STREAM_BATCH_BASES: usize = 1024 * 1024;
28const STREAM_BATCH_FRAGMENTS: usize = 8 * 1024;
29const STREAM_POOL_BYTES: usize = 256 * 1024 * 1024;
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct WeakSuperKmer {
37 pub graph_id: usize,
38 pub offset: usize,
39 pub len: usize,
40 pub source_id: Option<u32>,
41 pub left_discontinuous: bool,
42 pub right_discontinuous: bool,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct PartitionStats {
48 pub input_files: usize,
49 pub records: u64,
50 pub fragments: u64,
51 pub fragment_bases: u64,
52 pub weak_superkmers: u64,
53 pub weak_superkmer_bases: u64,
54 pub graph_histogram: Vec<u64>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct PartitionEmissionStats {
60 pub partition: PartitionStats,
61 pub buckets: BucketEmitStats,
62 pub parse_elapsed: Duration,
63 pub worker_elapsed: Duration,
64 pub bucket_flush_elapsed: Duration,
65 pub bucket_flushes: u64,
66 pub bucket_finish_elapsed: Duration,
67}
68
69struct PartitionWorkerOutput {
70 stats: PartitionStats,
71 elapsed: Duration,
72}
73
74struct PartitionFragmentBatch {
75 fragments: Vec<PartitionBatchFragment>,
76 bases: Vec<u8>,
77}
78
79struct PartitionBatchFragment {
80 source_id: u32,
81 offset: usize,
82 len: usize,
83}
84
85impl PartitionStats {
86 pub fn new(graph_count: usize) -> Self {
87 Self {
88 input_files: 0,
89 records: 0,
90 fragments: 0,
91 fragment_bases: 0,
92 weak_superkmers: 0,
93 weak_superkmer_bases: 0,
94 graph_histogram: vec![0; graph_count],
95 }
96 }
97
98 #[inline]
99 pub fn non_empty_graphs(&self) -> usize {
100 self.graph_histogram
101 .iter()
102 .filter(|&&count| count > 0)
103 .count()
104 }
105
106 #[inline]
107 pub fn max_graph_superkmers(&self) -> u64 {
108 self.graph_histogram.iter().copied().max().unwrap_or(0)
109 }
110
111 fn merge_from(&mut self, other: &Self) {
112 self.input_files += other.input_files;
113 self.records += other.records;
114 self.fragments += other.fragments;
115 self.fragment_bases += other.fragment_bases;
116 self.weak_superkmers += other.weak_superkmers;
117 self.weak_superkmer_bases += other.weak_superkmer_bases;
118 for (dst, src) in self
119 .graph_histogram
120 .iter_mut()
121 .zip(other.graph_histogram.iter())
122 {
123 *dst += *src;
124 }
125 }
126}
127
128pub fn emit_weak_superkmer_buckets<const K: usize>(
129 params: &BuildParams,
130 graph_count: usize,
131) -> Result<PartitionEmissionStats, PartitionRunError> {
132 params.validate()?;
133
134 let paths = expand_input_paths(params)?;
135 if params.color {
136 if paths.len() > crate::state::VertexState::MAX_SOURCE_ID as usize {
140 return Err(PartitionRunError::TooManySources);
141 }
142 return emit_colored_weak_superkmer_buckets::<K>(params, graph_count, &paths);
143 }
144 if std::env::var_os("CF3_RS_LEGACY_UNCOLORED_PARTITION").is_none() {
145 return emit_uncolored_windowed_weak_superkmer_buckets::<K>(params, graph_count, &paths);
146 }
147 let mut stats = PartitionStats::new(graph_count);
148 let workers = params.threads.max(1);
149 let mut batch_txs = Vec::with_capacity(workers);
150 let mut batch_rxs = Vec::with_capacity(workers);
151 for _ in 0..workers {
152 let (tx, rx) = mpsc::sync_channel::<PartitionFragmentBatch>(4);
153 batch_txs.push(tx);
154 batch_rxs.push(rx);
155 }
156 let sink = SharedBucketSink::create(params, graph_count)?;
157 let mut worker_elapsed = Duration::ZERO;
158 let parse_started = Instant::now();
159 let mut parse_elapsed = Duration::ZERO;
160
161 std::thread::scope(|scope| {
162 let mut handles = Vec::new();
163 for rx in batch_rxs {
164 let sink = Arc::clone(&sink);
165 handles.push(scope.spawn(move || {
166 let mut worker_stats = PartitionStats::new(graph_count);
167 let mut worker_elapsed = Duration::ZERO;
168 let mut buckets = sink.emitter();
169 loop {
170 let batch = rx.recv();
171 let Ok(batch) = batch else {
172 break;
173 };
174 for fragment in &batch.fragments {
175 let seq = &batch.bases[fragment.offset..fragment.offset + fragment.len];
176 let fragment_started = Instant::now();
177 emit_fragment_seq_weak_superkmer_buckets::<K, PartitionRunError>(
178 params,
179 graph_count,
180 fragment.source_id,
181 seq,
182 &mut buckets,
183 &mut worker_stats,
184 )?;
185 worker_elapsed += fragment_started.elapsed();
186 }
187 }
188 buckets.finish()?;
189 Ok::<_, PartitionRunError>(PartitionWorkerOutput {
190 stats: worker_stats,
191 elapsed: worker_elapsed,
192 })
193 }));
194 }
195
196 let mut producer_handles = Vec::new();
197 for (path_idx, path) in paths.iter().enumerate() {
198 let source_id =
199 u32::try_from(path_idx + 1).map_err(|_| PartitionRunError::TooManySources)?;
200 let txs = batch_txs.clone();
201 producer_handles.push(scope.spawn(move || {
202 let mut batch = PartitionFragmentBatch::new();
203 let mut next_worker = path_idx % txs.len();
204 let records = parse_fragments_borrowed(path, source_id, K + 1, |fragment| {
205 batch.push(fragment);
206 if batch.fragments.len() >= PARTITION_FRAGMENT_BATCH
207 || batch.bases.len() >= PARTITION_BATCH_BASES
208 {
209 txs[next_worker]
210 .send(std::mem::take(&mut batch))
211 .map_err(|_| {
212 InputError::Partition(PartitionError::WorkerDisconnected)
213 })?;
214 next_worker = (next_worker + 1) % txs.len();
215 }
216 Ok(())
217 })?;
218 if !batch.fragments.is_empty() {
219 txs[next_worker]
220 .send(batch)
221 .map_err(|_| InputError::Partition(PartitionError::WorkerDisconnected))?;
222 }
223 Ok::<_, InputError>(records)
224 }));
225 }
226 drop(batch_txs);
227 for handle in producer_handles {
228 let records = handle
229 .join()
230 .map_err(|_| PartitionRunError::WorkerPanic)??;
231 stats.input_files += 1;
232 stats.records += records;
233 }
234 parse_elapsed = parse_started.elapsed();
235
236 for handle in handles {
237 let worker_stats = handle
238 .join()
239 .map_err(|_| PartitionRunError::WorkerPanic)??;
240 worker_elapsed += worker_stats.elapsed;
241 stats.merge_from(&worker_stats.stats);
242 }
243 Ok::<(), PartitionRunError>(())
244 })?;
245
246 let (bucket_flushes, bucket_flush_elapsed) = sink.flush_stats();
247 let bucket_finish_started = Instant::now();
248 let bucket_stats = sink.finish()?;
249 let bucket_finish_elapsed = bucket_finish_started.elapsed();
250 populate_emitted_graph_histogram(&mut stats, &bucket_stats)?;
251
252 Ok(PartitionEmissionStats {
253 partition: stats,
254 buckets: bucket_stats,
255 parse_elapsed,
256 worker_elapsed,
257 bucket_flush_elapsed,
258 bucket_flushes,
259 bucket_finish_elapsed,
260 })
261}
262
263fn streamed_reader_count(params: &BuildParams, files: usize) -> Option<usize> {
271 if std::env::var_os("CF3_RS_DIRECT_PARTITION").is_some() {
272 return None;
273 }
274 let workers = params.partition_workers(usize::MAX);
277 if files == 0 || files >= workers {
278 return None;
279 }
280 Some(files)
281}
282
283fn emit_uncolored_windowed_weak_superkmer_buckets<const K: usize>(
284 params: &BuildParams,
285 graph_count: usize,
286 paths: &[PathBuf],
287) -> Result<PartitionEmissionStats, PartitionRunError> {
288 if let Some(readers) = streamed_reader_count(params, paths.len()) {
289 return emit_uncolored_streamed_weak_superkmer_buckets::<K>(
290 params,
291 graph_count,
292 paths,
293 readers,
294 );
295 }
296 emit_uncolored_direct_weak_superkmer_buckets::<K>(params, graph_count, paths)
297}
298
299fn emit_uncolored_streamed_weak_superkmer_buckets<const K: usize>(
306 params: &BuildParams,
307 graph_count: usize,
308 paths: &[PathBuf],
309 readers: usize,
310) -> Result<PartitionEmissionStats, PartitionRunError> {
311 let sink = SharedBucketSink::create(params, graph_count)?;
312 let mut stats = PartitionStats::new(graph_count);
313 let mut worker_elapsed = Duration::ZERO;
314 let workers = params.partition_workers(usize::MAX);
315 let buffers = (2 * (workers + readers)).min(STREAM_POOL_BYTES / STREAM_BATCH_BASES);
316 eprintln!(
317 "cuttlefish: uncolored partition streaming {readers} reader(s) into {workers} worker(s), {buffers} batch buffer(s)"
318 );
319
320 let inflate_workers = (workers / readers.max(1)).max(1);
323 let pool = BatchPool::new(buffers, readers);
324 let next_source = AtomicUsize::new(0);
325 let mut work_order = (0..paths.len()).collect::<Vec<_>>();
326 work_order.sort_unstable_by_key(|&offset| {
327 std::cmp::Reverse(paths[offset].metadata().map_or(0, |meta| meta.len()))
328 });
329
330 let started = Instant::now();
331 let mut emitters = Vec::with_capacity(workers);
332 std::thread::scope(|scope| {
333 let mut worker_handles = Vec::with_capacity(workers);
334 for _ in 0..workers {
335 let sink = Arc::clone(&sink);
336 let pool = &pool;
337 worker_handles.push(scope.spawn(move || {
338 let mut worker_stats = PartitionStats::new(graph_count);
339 let mut elapsed = Duration::ZERO;
340 let mut buckets = sink.deferred_uncolored_emitter();
341 let result = (|| -> Result<(), PartitionRunError> {
342 while let Some(batch) = pool.take_ready() {
343 let fragment_started = Instant::now();
344 let outcome = (|| -> Result<(), PartitionRunError> {
345 for fragment in &batch.fragments {
346 let seq =
347 &batch.bases[fragment.offset..fragment.offset + fragment.len];
348 emit_fragment_seq_weak_superkmer_buckets::<K, PartitionRunError>(
349 params,
350 graph_count,
351 fragment.source_id,
352 seq,
353 &mut buckets,
354 &mut worker_stats,
355 )?;
356 }
357 Ok(())
358 })();
359 elapsed += fragment_started.elapsed();
360 pool.release(batch);
361 outcome?;
362 }
363 Ok(())
364 })();
365 if result.is_err() {
366 pool.fail();
367 }
368 result.map(|()| {
369 (
370 PartitionWorkerOutput {
371 stats: worker_stats,
372 elapsed,
373 },
374 buckets,
375 )
376 })
377 }));
378 }
379
380 let mut reader_handles = Vec::with_capacity(readers);
381 for _ in 0..readers {
382 let pool = &pool;
383 let next_source = &next_source;
384 let work_order = &work_order;
385 reader_handles.push(scope.spawn(move || {
386 let result = (|| -> Result<(u64, usize), PartitionRunError> {
387 let mut records = 0u64;
388 let mut input_files = 0usize;
389 loop {
390 let order_idx = next_source.fetch_add(1, Ordering::Relaxed);
391 let Some(&offset) = work_order.get(order_idx) else {
392 break;
393 };
394 let source_id = u32::try_from(offset + 1)
395 .map_err(|_| PartitionRunError::TooManySources)?;
396 let mut batch = pool.take_free().ok_or(PartitionRunError::Partition(
397 PartitionError::WorkerDisconnected,
398 ))?;
399 let parsed = parse_fragments_borrowed_with(
400 &paths[offset],
401 source_id,
402 K + 1,
403 inflate_workers,
404 |fragment| {
405 batch.push(fragment);
406 if batch.is_full() {
407 let replacement = pool.take_free().ok_or(
408 InputError::Partition(PartitionError::WorkerDisconnected),
409 )?;
410 pool.publish(std::mem::replace(&mut batch, replacement));
411 }
412 Ok(())
413 },
414 );
415 if batch.fragments.is_empty() {
418 pool.release(batch);
419 } else {
420 pool.publish(batch);
421 }
422 match parsed {
423 Ok(parsed) => {
424 records += parsed;
425 input_files += 1;
426 }
427 Err(error) => {
428 handle_source_failure(params, &paths[offset], error)?;
429 }
430 }
431 }
432 Ok((records, input_files))
433 })();
434 pool.reader_done();
435 if result.is_err() {
436 pool.fail();
437 }
438 result
439 }));
440 }
441
442 for handle in reader_handles {
443 let (records, input_files) = handle
444 .join()
445 .map_err(|_| PartitionRunError::WorkerPanic)??;
446 stats.records += records;
447 stats.input_files += input_files;
448 }
449 for handle in worker_handles {
450 let (output, buckets) = handle
451 .join()
452 .map_err(|_| PartitionRunError::WorkerPanic)??;
453 worker_elapsed += output.elapsed;
454 stats.merge_from(&output.stats);
455 emitters.push(buckets);
456 }
457 Ok::<(), PartitionRunError>(())
458 })?;
459 let parse_elapsed = started.elapsed();
460 sink.flush_uncolored_emitters(emitters)?;
461
462 let (bucket_flushes, bucket_flush_elapsed) = sink.flush_stats();
463 let bucket_finish_started = Instant::now();
464 let bucket_stats = sink.finish()?;
465 let bucket_finish_elapsed = bucket_finish_started.elapsed();
466 populate_emitted_graph_histogram(&mut stats, &bucket_stats)?;
467 Ok(PartitionEmissionStats {
468 partition: stats,
469 buckets: bucket_stats,
470 parse_elapsed,
471 worker_elapsed,
472 bucket_flush_elapsed,
473 bucket_flushes,
474 bucket_finish_elapsed,
475 })
476}
477
478fn emit_uncolored_direct_weak_superkmer_buckets<const K: usize>(
479 params: &BuildParams,
480 graph_count: usize,
481 paths: &[PathBuf],
482) -> Result<PartitionEmissionStats, PartitionRunError> {
483 let sink = SharedBucketSink::create(params, graph_count)?;
484 let mut stats = PartitionStats::new(graph_count);
485 let mut worker_elapsed = Duration::ZERO;
486 let mut parse_elapsed = Duration::ZERO;
487
488 {
489 let (source_start, window) = (0, paths);
490 let started = Instant::now();
491 let mut work_order = (0..window.len()).collect::<Vec<_>>();
492 work_order.sort_unstable_by_key(|&offset| {
493 std::cmp::Reverse(window[offset].metadata().map_or(0, |meta| meta.len()))
494 });
495 let next_source = AtomicUsize::new(0);
496 let workers = params.partition_workers(window.len());
497 eprintln!("cuttlefish: uncolored partition using {workers} worker(s)");
498 let mut emitters = Vec::with_capacity(workers);
499
500 std::thread::scope(|scope| {
501 let mut handles = Vec::with_capacity(workers);
502 for _ in 0..workers {
503 let sink = Arc::clone(&sink);
504 let next_source = &next_source;
505 let work_order = &work_order;
506 handles.push(scope.spawn(move || {
507 let mut worker_stats = PartitionStats::new(graph_count);
508 let mut elapsed = Duration::ZERO;
509 let mut records = 0u64;
510 let mut input_files = 0usize;
511 let mut buckets = sink.deferred_uncolored_emitter();
512 loop {
513 let order_idx = next_source.fetch_add(1, Ordering::Relaxed);
514 let Some(&offset) = work_order.get(order_idx) else {
515 break;
516 };
517 let source_id = u32::try_from(source_start + offset + 1)
518 .map_err(|_| PartitionRunError::TooManySources)?;
519 match parse_fragments_borrowed(
520 &window[offset],
521 source_id,
522 K + 1,
523 |fragment| {
524 let fragment_started = Instant::now();
525 emit_fragment_seq_weak_superkmer_buckets::<K, InputError>(
526 params,
527 graph_count,
528 fragment.source_id,
529 fragment.seq,
530 &mut buckets,
531 &mut worker_stats,
532 )?;
533 elapsed += fragment_started.elapsed();
534 Ok(())
535 },
536 ) {
537 Ok(parsed) => {
538 records += parsed;
539 input_files += 1;
540 }
541 Err(error) => {
542 handle_source_failure(params, &window[offset], error)?;
543 }
544 }
545 }
546 Ok::<_, PartitionRunError>((
547 records,
548 input_files,
549 PartitionWorkerOutput {
550 stats: worker_stats,
551 elapsed,
552 },
553 buckets,
554 ))
555 }));
556 }
557 for handle in handles {
558 let (records, input_files, output, buckets) = handle
559 .join()
560 .map_err(|_| PartitionRunError::WorkerPanic)??;
561 stats.records += records;
562 stats.input_files += input_files;
563 worker_elapsed += output.elapsed;
564 stats.merge_from(&output.stats);
565 emitters.push(buckets);
566 }
567 Ok::<_, PartitionRunError>(())
568 })?;
569 parse_elapsed += started.elapsed();
570 sink.flush_uncolored_emitters(emitters)?;
571 }
572
573 let (bucket_flushes, bucket_flush_elapsed) = sink.flush_stats();
574 let bucket_finish_started = Instant::now();
575 let bucket_stats = sink.finish()?;
576 let bucket_finish_elapsed = bucket_finish_started.elapsed();
577 populate_emitted_graph_histogram(&mut stats, &bucket_stats)?;
578 Ok(PartitionEmissionStats {
579 partition: stats,
580 buckets: bucket_stats,
581 parse_elapsed,
582 worker_elapsed,
583 bucket_flush_elapsed,
584 bucket_flushes,
585 bucket_finish_elapsed,
586 })
587}
588
589fn emit_colored_weak_superkmer_buckets<const K: usize>(
590 params: &BuildParams,
591 graph_count: usize,
592 paths: &[PathBuf],
593) -> Result<PartitionEmissionStats, PartitionRunError> {
594 let sink = SharedBucketSink::create(params, graph_count)?;
595 let mut stats = PartitionStats::new(graph_count);
596 let mut worker_elapsed = Duration::ZERO;
597 let mut parse_elapsed = Duration::ZERO;
598
599 {
602 let (source_start, window) = (0, paths);
603 let parse_started = Instant::now();
604 let mut work_order = (0..window.len()).collect::<Vec<_>>();
605 work_order.sort_unstable_by_key(|&offset| {
606 std::cmp::Reverse(window[offset].metadata().map_or(0, |meta| meta.len()))
607 });
608 let next_source = AtomicUsize::new(0);
609 let workers = params.partition_workers(window.len());
610 eprintln!("cuttlefish: colored partition using {workers} worker(s)");
611 let mut emitters = Vec::with_capacity(workers);
612 std::thread::scope(|scope| {
613 let mut handles = Vec::new();
614 for _ in 0..workers {
615 let sink = Arc::clone(&sink);
616 let next_source = &next_source;
617 let work_order = &work_order;
618 handles.push(scope.spawn(move || {
619 let mut worker_stats = PartitionStats::new(graph_count);
620 let mut elapsed = Duration::ZERO;
621 let mut input_files = 0usize;
622 let mut records = 0u64;
623 let mut buckets = sink.emitter();
624 loop {
625 let order_idx = next_source.fetch_add(1, Ordering::Relaxed);
626 let Some(&offset) = work_order.get(order_idx) else {
627 break;
628 };
629 let source_id = u32::try_from(source_start + offset + 1)
630 .map_err(|_| PartitionRunError::TooManySources)?;
631 match parse_fragments_borrowed(
632 &window[offset],
633 source_id,
634 K + 1,
635 |fragment| {
636 let started = Instant::now();
637 emit_fragment_seq_weak_superkmer_buckets::<K, InputError>(
638 params,
639 graph_count,
640 fragment.source_id,
641 fragment.seq,
642 &mut buckets,
643 &mut worker_stats,
644 )?;
645 elapsed += started.elapsed();
646 Ok(())
647 },
648 ) {
649 Ok(parsed) => {
650 records += parsed;
651 input_files += 1;
652 }
653 Err(error) => {
654 handle_source_failure(params, &window[offset], error)?;
655 }
656 }
657 buckets.flush_colored_worker_if_required()?;
658 }
659 Ok::<_, PartitionRunError>((
660 records,
661 input_files,
662 PartitionWorkerOutput {
663 stats: worker_stats,
664 elapsed,
665 },
666 buckets,
667 ))
668 }));
669 }
670 for handle in handles {
671 let (records, input_files, output, buckets) = handle
672 .join()
673 .map_err(|_| PartitionRunError::WorkerPanic)??;
674 stats.records += records;
675 stats.input_files += input_files;
676 worker_elapsed += output.elapsed;
677 stats.merge_from(&output.stats);
678 emitters.push(buckets);
679 }
680 Ok::<_, PartitionRunError>(())
681 })?;
682 parse_elapsed += parse_started.elapsed();
683
684 sink.flush_colored_emitters(emitters)?;
685 }
686
687 let (bucket_flushes, bucket_flush_elapsed) = sink.flush_stats();
688 let bucket_finish_started = Instant::now();
689 let bucket_stats = sink.finish()?;
690 let bucket_finish_elapsed = bucket_finish_started.elapsed();
691 populate_emitted_graph_histogram(&mut stats, &bucket_stats)?;
692 Ok(PartitionEmissionStats {
693 partition: stats,
694 buckets: bucket_stats,
695 parse_elapsed,
696 worker_elapsed,
697 bucket_flush_elapsed,
698 bucket_flushes,
699 bucket_finish_elapsed,
700 })
701}
702
703impl PartitionFragmentBatch {
704 fn new() -> Self {
705 Self {
706 fragments: Vec::with_capacity(PARTITION_FRAGMENT_BATCH),
707 bases: Vec::with_capacity(PARTITION_BATCH_BASES.min(PARTITION_FRAGMENT_BATCH * 256)),
708 }
709 }
710
711 fn with_capacity(fragments: usize, bases: usize) -> Self {
712 Self {
713 fragments: Vec::with_capacity(fragments),
714 bases: Vec::with_capacity(bases),
715 }
716 }
717
718 fn push(&mut self, fragment: BorrowedSequenceFragment<'_>) {
719 let offset = self.bases.len();
720 self.bases.extend_from_slice(fragment.seq);
721 self.fragments.push(PartitionBatchFragment {
722 source_id: fragment.source_id,
723 offset,
724 len: fragment.seq.len(),
725 });
726 }
727
728 #[inline]
729 fn is_full(&self) -> bool {
730 self.fragments.len() >= STREAM_BATCH_FRAGMENTS || self.bases.len() >= STREAM_BATCH_BASES
731 }
732
733 fn clear(&mut self) {
734 self.fragments.clear();
735 self.bases.clear();
736 }
737}
738
739struct BatchPool {
746 inner: std::sync::Mutex<BatchPoolInner>,
747 ready_available: std::sync::Condvar,
748 free_available: std::sync::Condvar,
749}
750
751struct BatchPoolInner {
752 ready: std::collections::VecDeque<PartitionFragmentBatch>,
753 free: Vec<PartitionFragmentBatch>,
754 readers: usize,
755 failed: bool,
756}
757
758impl BatchPool {
759 fn new(buffers: usize, readers: usize) -> Self {
760 let buffers = buffers.max(1);
761 let mut free = Vec::with_capacity(buffers);
762 for _ in 0..buffers {
763 free.push(PartitionFragmentBatch::with_capacity(
764 STREAM_BATCH_FRAGMENTS,
765 STREAM_BATCH_BASES,
766 ));
767 }
768 Self {
769 inner: std::sync::Mutex::new(BatchPoolInner {
770 ready: std::collections::VecDeque::with_capacity(buffers),
771 free,
772 readers,
773 failed: false,
774 }),
775 ready_available: std::sync::Condvar::new(),
776 free_available: std::sync::Condvar::new(),
777 }
778 }
779
780 fn take_free(&self) -> Option<PartitionFragmentBatch> {
785 let mut inner = self.inner.lock().ok()?;
786 loop {
787 if inner.failed {
788 return None;
789 }
790 if let Some(batch) = inner.free.pop() {
791 return Some(batch);
792 }
793 inner = self.free_available.wait(inner).ok()?;
794 }
795 }
796
797 fn publish(&self, batch: PartitionFragmentBatch) {
798 if let Ok(mut inner) = self.inner.lock() {
799 inner.ready.push_back(batch);
800 self.ready_available.notify_one();
801 }
802 }
803
804 fn take_ready(&self) -> Option<PartitionFragmentBatch> {
808 let mut inner = self.inner.lock().ok()?;
809 loop {
810 if let Some(batch) = inner.ready.pop_front() {
811 return Some(batch);
812 }
813 if inner.readers == 0 || inner.failed {
814 return None;
815 }
816 inner = self.ready_available.wait(inner).ok()?;
817 }
818 }
819
820 fn release(&self, mut batch: PartitionFragmentBatch) {
821 batch.clear();
822 if let Ok(mut inner) = self.inner.lock() {
823 inner.free.push(batch);
824 self.free_available.notify_one();
825 }
826 }
827
828 fn reader_done(&self) {
829 if let Ok(mut inner) = self.inner.lock() {
830 inner.readers -= 1;
831 if inner.readers == 0 {
832 self.ready_available.notify_all();
833 }
834 }
835 }
836
837 fn fail(&self) {
839 if let Ok(mut inner) = self.inner.lock() {
840 inner.failed = true;
841 }
842 self.ready_available.notify_all();
843 self.free_available.notify_all();
844 }
845}
846
847impl Default for PartitionFragmentBatch {
848 fn default() -> Self {
849 Self::new()
850 }
851}
852
853fn handle_source_failure(
860 params: &BuildParams,
861 path: &Path,
862 error: InputError,
863) -> Result<(), PartitionRunError> {
864 if !params.skip_unreadable {
865 return Err(error.into());
866 }
867 eprintln!(
868 "cuttlefish: skipping unreadable input {}: {error}",
869 path.display()
870 );
871 Ok(())
872}
873
874fn parse_only_diagnostic() -> bool {
876 static PARSE_ONLY: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
877 *PARSE_ONLY.get_or_init(|| std::env::var_os("CF3_RS_PARSE_ONLY").is_some())
878}
879
880fn scan_only_diagnostic() -> bool {
882 static SCAN_ONLY: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
883 *SCAN_ONLY.get_or_init(|| std::env::var_os("CF3_RS_SCAN_ONLY").is_some())
884}
885
886fn emit_fragment_seq_weak_superkmer_buckets<const K: usize, E>(
887 params: &BuildParams,
888 graph_count: usize,
889 source_id: u32,
890 seq: &[u8],
891 buckets: &mut SharedBucketEmitter,
892 stats: &mut PartitionStats,
893) -> Result<(), E>
894where
895 E: From<PartitionError> + From<crate::buckets::BucketError>,
896{
897 stats.fragments += 1;
898 stats.fragment_bases += seq.len() as u64;
899 let scan_only = scan_only_diagnostic();
902 if parse_only_diagnostic() {
905 return Ok(());
906 }
907 for_each_valid_weak_superkmer::<K, E, _>(
908 seq,
909 params.minimizer_len as usize,
910 graph_count,
911 params.color.then_some(source_id),
912 |sk| {
913 if !scan_only {
914 buckets.add_valid(&sk, sk.sequence(seq))?;
915 }
916 stats.weak_superkmers += 1;
917 stats.weak_superkmer_bases += sk.len as u64;
918 Ok(())
919 },
920 )?;
921 Ok(())
922}
923
924fn populate_emitted_graph_histogram(
925 stats: &mut PartitionStats,
926 buckets: &BucketEmitStats,
927) -> Result<(), PartitionRunError> {
928 stats.graph_histogram.fill(0);
929 let (_, entries) = crate::buckets::BucketStore::open_dir(&buckets.bucket_dir)?;
930 for entry in entries {
931 stats.graph_histogram[entry.graph_id] = entry.records;
932 }
933 Ok(())
934}
935
936pub fn partition_inputs<const K: usize>(
937 params: &BuildParams,
938 graph_count: usize,
939) -> Result<PartitionStats, PartitionRunError> {
940 params.validate()?;
941
942 let paths = expand_input_paths(params)?;
943 let mut stats = PartitionStats::new(graph_count);
944 let workers = params.partition_workers(paths.len());
945 let chunk_len = paths.len().div_ceil(workers);
946
947 std::thread::scope(|scope| {
948 let mut handles = Vec::new();
949 for (chunk_idx, chunk) in paths.chunks(chunk_len).enumerate() {
950 let start_idx = chunk_idx * chunk_len;
951 handles.push(scope.spawn(move || {
952 let mut worker_stats = PartitionStats::new(graph_count);
953 for (offset, path) in chunk.iter().enumerate() {
954 let source_id = u32::try_from(start_idx + offset + 1)
955 .map_err(|_| PartitionRunError::TooManySources)?;
956 let path_stats = partition_path::<K>(params, graph_count, source_id, path)?;
957 worker_stats.merge_from(&path_stats);
958 }
959 Ok::<PartitionStats, PartitionRunError>(worker_stats)
960 }));
961 }
962
963 for handle in handles {
964 let worker_stats = handle
965 .join()
966 .map_err(|_| PartitionRunError::WorkerPanic)??;
967 stats.merge_from(&worker_stats);
968 }
969 Ok::<(), PartitionRunError>(())
970 })?;
971
972 Ok(stats)
973}
974
975fn partition_path<const K: usize>(
976 params: &BuildParams,
977 graph_count: usize,
978 source_id: u32,
979 path: &Path,
980) -> Result<PartitionStats, PartitionRunError> {
981 let mut stats = PartitionStats::new(graph_count);
982 stats.input_files = 1;
983
984 let records = parse_fragments(path, source_id, K + 1, |fragment| {
985 stats.fragments += 1;
986 stats.fragment_bases += fragment.seq.len() as u64;
987 for_each_valid_weak_superkmer::<K, InputError, _>(
988 &fragment.seq,
989 params.minimizer_len as usize,
990 graph_count,
991 params.color.then_some(fragment.source_id),
992 |sk| {
993 stats.weak_superkmers += 1;
994 stats.weak_superkmer_bases += sk.len as u64;
995 stats.graph_histogram[sk.graph_id] += 1;
996 Ok(())
997 },
998 )?;
999 Ok(())
1000 })?;
1001 stats.records = records;
1002
1003 Ok(stats)
1004}
1005
1006impl WeakSuperKmer {
1007 #[inline]
1008 pub fn sequence<'a>(&self, fragment: &'a [u8]) -> &'a [u8] {
1009 &fragment[self.offset..self.offset + self.len]
1010 }
1011}
1012
1013pub fn partition_fragment<const K: usize>(
1014 fragment: &[u8],
1015 minimizer_len: usize,
1016 graph_count: usize,
1017 source_id: Option<u32>,
1018) -> Result<Vec<WeakSuperKmer>, PartitionError> {
1019 let mut out = Vec::new();
1020 for_each_weak_superkmer::<K, PartitionError, _>(
1021 fragment,
1022 minimizer_len,
1023 graph_count,
1024 source_id,
1025 |sk| {
1026 out.push(sk);
1027 Ok(())
1028 },
1029 )?;
1030 Ok(out)
1031}
1032
1033fn for_each_weak_superkmer<const K: usize, E, F>(
1034 fragment: &[u8],
1035 minimizer_len: usize,
1036 graph_count: usize,
1037 source_id: Option<u32>,
1038 mut visit: F,
1039) -> Result<(), E>
1040where
1041 E: From<PartitionError>,
1042 F: FnMut(WeakSuperKmer) -> Result<(), E>,
1043{
1044 for_each_weak_superkmer_impl::<K, E, _, true>(
1045 fragment,
1046 minimizer_len,
1047 graph_count,
1048 source_id,
1049 &mut visit,
1050 )
1051}
1052
1053fn for_each_valid_weak_superkmer<const K: usize, E, F>(
1054 fragment: &[u8],
1055 minimizer_len: usize,
1056 graph_count: usize,
1057 source_id: Option<u32>,
1058 mut visit: F,
1059) -> Result<(), E>
1060where
1061 E: From<PartitionError>,
1062 F: FnMut(WeakSuperKmer) -> Result<(), E>,
1063{
1064 for_each_weak_superkmer_impl::<K, E, _, false>(
1065 fragment,
1066 minimizer_len,
1067 graph_count,
1068 source_id,
1069 &mut visit,
1070 )
1071}
1072
1073#[inline]
1081fn for_each_weak_superkmer_impl<const K: usize, E, F, const CHECK_BASES: bool>(
1082 fragment: &[u8],
1083 minimizer_len: usize,
1084 graph_count: usize,
1085 source_id: Option<u32>,
1086 visit: &mut F,
1087) -> Result<(), E>
1088where
1089 E: From<PartitionError>,
1090 F: FnMut(WeakSuperKmer) -> Result<(), E>,
1091{
1092 if K < 3 || K % 2 == 0 || K > 63 {
1093 return Err(PartitionError::InvalidK(K).into());
1094 }
1095 if minimizer_len == 0 || minimizer_len >= K || minimizer_len > 32 {
1096 return Err(PartitionError::InvalidMinimizerLen(minimizer_len).into());
1097 }
1098 if !graph_count.is_power_of_two() {
1099 return Err(PartitionError::GraphCountNotPowerOfTwo(graph_count).into());
1100 }
1101 if fragment.len() < K + 1 {
1102 return Ok(());
1103 }
1104
1105 let window_k = K - 1;
1106 let max_super_km1_len = 2 * (K - 1) - minimizer_len;
1107 let lmers_per_window = window_k - minimizer_len + 1;
1108 let ring_size = lmers_per_window - 1;
1109 let mut hashes = [0u64; K];
1110 let mut prefix_min = [0u64; K];
1111
1112 let mut fwd = 0u64;
1113 let mut rev = 0u64;
1114 #[allow(clippy::needless_range_loop)]
1122 for idx in 0..minimizer_len {
1123 let base_bits = partition_base_bits::<CHECK_BASES, E>(fragment[idx])?;
1124 fwd = (fwd << 2) | base_bits as u64;
1125 rev |= ((base_bits ^ 0b11) as u64) << (2 * idx);
1126 }
1127 let mask = if minimizer_len == 32 {
1128 u64::MAX
1129 } else {
1130 (1u64 << (2 * minimizer_len)) - 1
1131 };
1132 let rev_high_shift = 2 * (minimizer_len - 1);
1133
1134 let mut pivot = ring_size;
1135 hashes[pivot] = canonical_lmer_hash(fwd, rev);
1136 #[allow(clippy::needless_range_loop)]
1137 for idx in minimizer_len..window_k {
1138 let next_bits = partition_base_bits::<CHECK_BASES, E>(fragment[idx])?;
1139 fwd = ((fwd << 2) | next_bits as u64) & mask;
1140 rev = (rev >> 2) | (((next_bits ^ 0b11) as u64) << rev_high_shift);
1141 pivot -= 1;
1142 hashes[pivot] = canonical_lmer_hash(fwd, rev);
1143 }
1144
1145 reset_min_window(&hashes, &mut prefix_min, ring_size, &mut pivot);
1146 let mut suffix_min = u64::MAX;
1147
1148 let mut cur_off = 0usize;
1150 let mut km1_idx = 0usize;
1151 let mut cur_g = graph_id(prefix_min[pivot].min(suffix_min), graph_count);
1152 let mut prev_g = graph_count;
1153
1154 for &base in &fragment[window_k..] {
1155 let next_bits = partition_base_bits::<CHECK_BASES, E>(base)?;
1156 fwd = ((fwd << 2) | next_bits as u64) & mask;
1157 rev = (rev >> 2) | (((next_bits ^ 0b11) as u64) << rev_high_shift);
1158
1159 hashes[pivot] = canonical_lmer_hash(fwd, rev);
1160 suffix_min = suffix_min.min(hashes[pivot]);
1161 if pivot > 0 {
1162 pivot -= 1;
1163 } else {
1164 reset_min_window(&hashes, &mut prefix_min, ring_size, &mut pivot);
1165 suffix_min = u64::MAX;
1166 }
1167
1168 let len = km1_idx + window_k;
1169 let next_g = graph_id(prefix_min[pivot].min(suffix_min), graph_count);
1170 km1_idx += 1;
1171 if next_g != cur_g || len == max_super_km1_len {
1172 let next_off = cur_off + km1_idx;
1173 let left_joined = cur_off > 0;
1174 visit(WeakSuperKmer {
1175 graph_id: cur_g,
1176 offset: cur_off - usize::from(left_joined),
1177 len: usize::from(left_joined) + len + 1,
1178 source_id,
1179 left_discontinuous: left_joined && prev_g != cur_g,
1180 right_discontinuous: next_g != cur_g,
1181 })?;
1182
1183 cur_off = next_off;
1184 prev_g = cur_g;
1185 cur_g = next_g;
1186 km1_idx = 0;
1187 }
1188 }
1189
1190 let len = fragment.len() - cur_off;
1191 let left_joined = cur_off > 0;
1192 visit(WeakSuperKmer {
1193 graph_id: cur_g,
1194 offset: cur_off - usize::from(left_joined),
1195 len: usize::from(left_joined) + len,
1196 source_id,
1197 left_discontinuous: left_joined && prev_g != cur_g,
1198 right_discontinuous: false,
1199 })?;
1200
1201 Ok(())
1202}
1203
1204fn reset_min_window(hashes: &[u64], prefix_min: &mut [u64], ring_size: usize, pivot: &mut usize) {
1205 prefix_min[0] = hashes[0];
1206 for idx in 1..=ring_size {
1207 prefix_min[idx] = prefix_min[idx - 1].min(hashes[idx]);
1208 }
1209 *pivot = ring_size;
1210}
1211
1212#[inline(always)]
1213fn canonical_lmer_hash(fwd: u64, rev: u64) -> u64 {
1214 wyhash_u64(fwd, 0).min(wyhash_u64(rev, 0))
1215}
1216
1217#[inline(always)]
1218fn partition_base_bits<const CHECK_BASES: bool, E>(base: u8) -> Result<u8, E>
1219where
1220 E: From<PartitionError>,
1221{
1222 if CHECK_BASES {
1223 ascii_base_bits(base)
1224 .ok_or(PartitionError::InvalidBase(base))
1225 .map_err(E::from)
1226 } else {
1227 debug_assert!(ascii_base_bits(base).is_some());
1228 Ok(valid_ascii_base_bits(base))
1229 }
1230}
1231
1232#[inline]
1233fn graph_id(hash: u64, graph_count: usize) -> usize {
1234 hash as usize & (graph_count - 1)
1235}
1236
1237#[inline]
1238pub fn source_hash(source_id: u32) -> u64 {
1239 hash_u64(source_id as u64, 0)
1240}
1241
1242#[derive(Debug)]
1243pub enum PartitionError {
1244 InvalidK(usize),
1245 InvalidMinimizerLen(usize),
1246 GraphCountNotPowerOfTwo(usize),
1247 InvalidBase(u8),
1248 Kmer(crate::kmer::KmerError),
1249 WorkerDisconnected,
1250}
1251
1252impl From<crate::kmer::KmerError> for PartitionError {
1253 fn from(value: crate::kmer::KmerError) -> Self {
1254 Self::Kmer(value)
1255 }
1256}
1257
1258impl std::fmt::Display for PartitionError {
1259 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1260 match self {
1261 Self::InvalidK(k) => write!(f, "invalid k for partitioning: {k}"),
1262 Self::InvalidMinimizerLen(l) => write!(f, "invalid minimizer length: {l}"),
1263 Self::GraphCountNotPowerOfTwo(c) => {
1264 write!(f, "graph count must be a power of two: {c}")
1265 }
1266 Self::InvalidBase(b) => {
1267 write!(f, "invalid base in partition fragment: '{}'", *b as char)
1268 }
1269 Self::Kmer(err) => write!(f, "{err}"),
1270 Self::WorkerDisconnected => write!(f, "partition worker disconnected"),
1271 }
1272 }
1273}
1274
1275impl std::error::Error for PartitionError {}
1276
1277impl From<PartitionError> for InputError {
1278 fn from(value: PartitionError) -> Self {
1279 Self::Partition(value)
1280 }
1281}
1282
1283#[derive(Debug)]
1284pub enum PartitionRunError {
1285 Params(ParamError),
1286 Input(InputError),
1287 Partition(PartitionError),
1288 Bucket(crate::buckets::BucketError),
1289 TooManySources,
1290 WorkerPanic,
1291}
1292
1293impl From<ParamError> for PartitionRunError {
1294 fn from(value: ParamError) -> Self {
1295 Self::Params(value)
1296 }
1297}
1298
1299impl From<InputError> for PartitionRunError {
1300 fn from(value: InputError) -> Self {
1301 Self::Input(value)
1302 }
1303}
1304
1305impl From<PartitionError> for PartitionRunError {
1306 fn from(value: PartitionError) -> Self {
1307 Self::Partition(value)
1308 }
1309}
1310
1311impl From<crate::buckets::BucketError> for PartitionRunError {
1312 fn from(value: crate::buckets::BucketError) -> Self {
1313 Self::Bucket(value)
1314 }
1315}
1316
1317impl std::fmt::Display for PartitionRunError {
1318 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1319 match self {
1320 Self::Params(err) => write!(f, "{err}"),
1321 Self::Input(err) => write!(f, "{err}"),
1322 Self::Partition(err) => write!(f, "{err}"),
1323 Self::Bucket(err) => write!(f, "{err}"),
1324 Self::TooManySources => write!(f, "too many input sources for 32-bit source IDs"),
1325 Self::WorkerPanic => write!(f, "partition worker thread panicked"),
1326 }
1327 }
1328}
1329
1330impl std::error::Error for PartitionRunError {}
1331
1332#[cfg(test)]
1333mod tests {
1334 use super::*;
1335
1336 #[test]
1337 fn emits_covering_weak_superkmers() {
1338 let seq = b"ACGTACGTACGTACGTACGTACGT";
1339 let parts = partition_fragment::<7>(seq, 3, 128, Some(1)).unwrap();
1340 assert!(!parts.is_empty());
1341 assert!(parts.iter().all(|p| p.len >= 7));
1342 assert_eq!(parts[0].offset, 0);
1343 assert_eq!(
1344 parts.last().unwrap().offset + parts.last().unwrap().len,
1345 seq.len()
1346 );
1347 assert!(parts.iter().all(|p| p.source_id == Some(1)));
1348 }
1349
1350 #[test]
1351 fn rejects_non_power_of_two_graph_count() {
1352 let err = partition_fragment::<7>(b"ACGTACGT", 3, 127, None).unwrap_err();
1353 assert!(matches!(err, PartitionError::GraphCountNotPowerOfTwo(127)));
1354 }
1355
1356 #[test]
1357 fn unreadable_input_aborts_unless_skipping_is_requested() {
1358 let base = std::env::temp_dir().join(format!("cf3rs-skip-{}", std::process::id()));
1359 let _ = std::fs::remove_dir_all(&base);
1360 std::fs::create_dir_all(&base).unwrap();
1361 let good = base.join("good.fa");
1362 std::fs::write(&good, b">r1\nACGTACGTACGTACGTACGTACGT\n").unwrap();
1363 let bad = base.join("bad.fa.gz");
1366 std::fs::write(&bad, b"").unwrap();
1367
1368 let build = |skip: bool, threads: usize| {
1369 let work = base.join(format!("work-{skip}-{threads}"));
1370 let _ = std::fs::remove_dir_all(&work);
1371 std::fs::create_dir_all(&work).unwrap();
1372 let mut params = BuildParams::new(crate::GraphInput::References, "unused".to_string());
1373 params.seqs.push(good.to_string_lossy().into_owned());
1374 params.seqs.push(bad.to_string_lossy().into_owned());
1375 params.k = 7;
1376 params.minimizer_len = 3;
1377 params.threads = threads;
1378 params.skip_unreadable = skip;
1379 params.work_dir = work.to_string_lossy().into_owned();
1380 emit_weak_superkmer_buckets::<7>(¶ms, 128)
1381 };
1382
1383 for threads in [1usize, 8] {
1386 assert!(
1387 build(false, threads).is_err(),
1388 "unreadable input must abort by default at {threads} thread(s)"
1389 );
1390 let stats = build(true, threads)
1391 .unwrap_or_else(|e| panic!("skipping should succeed at {threads} thread(s): {e}"));
1392 assert_eq!(
1393 stats.partition.input_files, 1,
1394 "only the good source counts"
1395 );
1396 assert!(stats.partition.weak_superkmers > 0);
1397 }
1398
1399 let _ = std::fs::remove_dir_all(&base);
1400 }
1401
1402 #[test]
1403 fn streamed_partition_matches_direct_partition() {
1404 let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
1407 let input = manifest.join("../../data/refs2.fa");
1408 assert!(input.exists(), "missing fixture {}", input.display());
1409
1410 let run = |dir: &std::path::Path| {
1411 let _ = std::fs::remove_dir_all(dir);
1412 std::fs::create_dir_all(dir).unwrap();
1413 let mut params = BuildParams::new(crate::GraphInput::References, "unused".to_string());
1414 params.seqs.push(input.to_string_lossy().into_owned());
1415 params.k = 7;
1416 params.minimizer_len = 3;
1417 params.threads = 4;
1418 params.work_dir = dir.to_string_lossy().into_owned();
1419 let paths = expand_input_paths(¶ms).unwrap();
1420 (params, paths)
1421 };
1422
1423 let base = std::env::temp_dir().join(format!("cf3rs-stream-{}", std::process::id()));
1424 let streamed_dir = base.join("streamed");
1425 let direct_dir = base.join("direct");
1426
1427 let (params, paths) = run(&streamed_dir);
1428 assert_eq!(streamed_reader_count(¶ms, paths.len()), Some(1));
1429 let streamed =
1430 emit_uncolored_streamed_weak_superkmer_buckets::<7>(¶ms, 128, &paths, 1).unwrap();
1431
1432 let (params, paths) = run(&direct_dir);
1433 let direct =
1434 emit_uncolored_direct_weak_superkmer_buckets::<7>(¶ms, 128, &paths).unwrap();
1435
1436 assert_eq!(streamed.partition.records, direct.partition.records);
1437 assert_eq!(streamed.partition.fragments, direct.partition.fragments);
1438 assert_eq!(
1439 streamed.partition.fragment_bases,
1440 direct.partition.fragment_bases
1441 );
1442 assert_eq!(
1443 streamed.partition.weak_superkmers,
1444 direct.partition.weak_superkmers
1445 );
1446 assert_eq!(
1447 streamed.partition.weak_superkmer_bases,
1448 direct.partition.weak_superkmer_bases
1449 );
1450 assert_eq!(
1453 streamed.partition.graph_histogram,
1454 direct.partition.graph_histogram
1455 );
1456 assert_eq!(streamed.buckets.bucket_files, direct.buckets.bucket_files);
1457 assert_eq!(streamed.buckets.bytes_written, direct.buckets.bytes_written);
1458
1459 let _ = std::fs::remove_dir_all(&base);
1460 }
1461}