Skip to main content

gwseq_io/bam/
reader.rs

1//! The BAM reader.
2//!
3//! Four read paths over the same machinery: the
4//! index turns a locus into chunks, BGZF turns a chunk into records, and the
5//! filter decides which of them the caller asked for.
6//!
7//! # Why a locus is split by chunk, not by sub-region
8//!
9//! Cutting a window into sub-regions costs far more than it saves: the index
10//! reaches an alignment through runs of compressed blocks rather than through a
11//! region, so neighbouring sub-regions land in the same blocks and each pays to
12//! decompress them — three times the work at 24 pieces, measured on a 10 M
13//! read file. The chunks of a locus are disjoint, so sharing *those* out
14//! splits the work where the format divides it, and only the block a run
15//! boundary falls in is read twice.
16
17use std::sync::Arc;
18
19use crate::bam::bai::{BamIndex, MAX_MERGE_SPAN};
20use crate::bam::bgzf::Chunk;
21use crate::bam::header::SamHeader;
22use crate::bam::record::{decode_block, BamRecord, EntryFilter, RecordFilter};
23use crate::error::{Error, Result};
24use crate::genomic::{ChrMap, Locs};
25use crate::parallel::Executor;
26use crate::progress::{ProgressFn, ProgressTracker};
27use crate::source::ByteSource;
28
29/// Chunks a cursor keeps decompressed, so a walk that revisits one does not
30/// inflate it again.
31const CHUNK_CACHE_SIZE: usize = 4;
32
33/// And the bytes it keeps between them, since a chunk has no size limit worth
34/// the name — `chunks` merges neighbours up to `MAX_MERGE_SPAN` compressed. A
35/// walk holds one cursor per worker for its whole life, so this is what bounds
36/// what a walk costs to have open.
37const CURSOR_CACHE_BYTES: usize = 8 << 20;
38
39/// A run of chunks under this many compressed bytes costs more to hand a thread
40/// than to read.
41const MIN_RUN_SIZE: u64 = 64 * 1024;
42
43#[derive(Debug)]
44struct Inner {
45    source: Arc<dyn ByteSource>,
46    executor: Executor,
47    index: Option<BamIndex>,
48}
49
50pub struct BamReader {
51    inner: Option<Inner>,
52    path: String,
53    index_path: String,
54    header: SamHeader,
55    chr_map: ChrMap,
56    /// Reference names by the index the file gives them, shared with every
57    /// record so turning an index back into a name costs a clone of an `Arc`.
58    chr_names: Arc<Vec<String>>,
59    /// Why the index is absent, when it is. Empty when it loaded — and empty as
60    /// well when the file simply has none, which is a distinction the API keeps.
61    index_error: String,
62    /// Whether an index loaded at open. Kept here rather than read off the
63    /// index itself, which `close()` drops, so that `is_indexed` still answers
64    /// afterwards: what kind of file was opened does not stop being true
65    /// because the handle was released.
66    indexed: bool,
67}
68
69impl std::fmt::Debug for BamReader {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        f.debug_struct("BamReader")
72            .field("path", &self.path)
73            .field("references", &self.chr_map.len())
74            .field("indexed", &self.is_indexed())
75            .field("closed", &self.is_closed())
76            .finish()
77    }
78}
79
80/// One worker's state for a walk: a decompressed-chunk cache it carries from
81/// locus to locus.
82///
83/// The cache is the caller's, not the call's. A walk that reads locus after
84/// locus hands the same cursors back every time, which is what lets a cursor's
85/// blocks serve more than the read that filled them — and rebuilding them per
86/// call is what makes reading a few loci at a time cost more than reading many.
87/// See [`Cursors`], which is what a walk holds.
88#[derive(Debug, Default)]
89pub struct Cursor {
90    cache: Vec<(Chunk, bytes::Bytes)>,
91    next: usize,
92    /// Decompressed bytes resident, so the budget below is a subtraction rather
93    /// than a walk of the cache.
94    bytes: usize,
95}
96
97/// One [`Cursor`] per worker, carried across the steps of a walk.
98///
99/// A run takes the cursor its batch index names, so two workers never want the
100/// same one and the lock is uncontended — a `Mutex` each rather than one shared
101/// is what keeps it that way. Living on the walk rather than on the call is the
102/// point: it is what makes `sort_locations` mean anything, since neighbouring
103/// loci touch the same BGZF blocks and a cursor rebuilt per call throws them
104/// away before the next locus can ask.
105#[derive(Debug, Default)]
106pub struct Cursors(Vec<parking_lot::Mutex<Cursor>>);
107
108impl Cursors {
109    pub fn new(count: usize) -> Self {
110        Self(
111            (0..count.max(1))
112                .map(|_| parking_lot::Mutex::new(Cursor::default()))
113                .collect(),
114        )
115    }
116
117    /// The cursor for a worker, by its batch index.
118    ///
119    /// Wrapped rather than bounded: `chunk_runs` never makes more runs than
120    /// there are workers, so this is exact in practice and safe if that ever
121    /// stops being true.
122    fn get(&self, index: usize) -> parking_lot::MutexGuard<'_, Cursor> {
123        self.0[index % self.0.len()].lock()
124    }
125}
126
127impl BamReader {
128    pub fn open(
129        path: &str,
130        index_path: Option<&str>,
131        parallel: i64,
132        block_size: Option<u64>,
133        max_blocks: Option<usize>,
134    ) -> Result<Self> {
135        let source = crate::source::open(path, block_size, max_blocks)?;
136        Self::from_source(source, path, index_path, parallel, block_size, max_blocks)
137    }
138
139    pub(crate) fn from_source(
140        source: Arc<dyn ByteSource>,
141        path: &str,
142        index_path: Option<&str>,
143        parallel: i64,
144        block_size: Option<u64>,
145        max_blocks: Option<usize>,
146    ) -> Result<Self> {
147        // Before the header, so a truncated file is refused rather than opened
148        // and read short.
149        super::bgzf::check_eof(source.as_ref())?;
150        let (header, chr_map) = super::header::read(source.as_ref())?;
151
152        let mut names = vec![String::new(); chr_map.iter().map(|e| e.index + 1).max().unwrap_or(0)];
153        for entry in chr_map.iter() {
154            names[entry.index] = entry.id.clone();
155        }
156
157        let index_path = index_path
158            .map(str::to_string)
159            .unwrap_or_else(|| format!("{path}.bai"));
160        // An index is optional, so a missing one is no error — but a corrupt one
161        // is worth reporting separately, and used to reach the caller as the
162        // same "not indexed". A local index that is simply absent is checked for
163        // before opening, since opening fails the same way a corrupt one does.
164        let (index, index_error) =
165            if !crate::source::is_url(&index_path) && !std::path::Path::new(&index_path).exists() {
166                (None, String::new())
167            } else {
168                match crate::source::open(&index_path, block_size, max_blocks)
169                    .and_then(|s| BamIndex::read(s.as_ref()))
170                {
171                    Ok(index) => (Some(index), String::new()),
172                    Err(e) => (None, e.to_string()),
173                }
174            };
175
176        Ok(Self {
177            indexed: index.is_some(),
178            inner: Some(Inner {
179                source,
180                executor: Executor::new(parallel)?,
181                index,
182            }),
183            path: path.to_string(),
184            index_path,
185            header,
186            chr_map,
187            chr_names: Arc::new(names),
188            index_error,
189        })
190    }
191
192    pub fn header(&self) -> &SamHeader {
193        &self.header
194    }
195    pub fn chr_sizes(&self) -> &ChrMap {
196        &self.chr_map
197    }
198    pub fn index_error(&self) -> &str {
199        &self.index_error
200    }
201    pub fn is_indexed(&self) -> bool {
202        self.indexed
203    }
204    pub fn is_closed(&self) -> bool {
205        self.inner.is_none()
206    }
207    pub fn path(&self) -> &str {
208        &self.path
209    }
210    pub fn parallel(&self) -> usize {
211        self.inner.as_ref().map_or(0, |i| i.executor.parallel())
212    }
213
214    pub fn close(&mut self) {
215        if let Some(inner) = self.inner.take() {
216            inner.source.close();
217        }
218    }
219
220    fn inner(&self) -> Result<&Inner> {
221        self.inner.as_ref().ok_or_else(|| Error::Closed {
222            path: self.path.clone(),
223        })
224    }
225
226    /// The index, or why there is none.
227    ///
228    /// A missing index and a corrupt one are different failures, and used to
229    /// reach the caller as the same "not indexed".
230    fn index<'a>(&self, inner: &'a Inner) -> Result<&'a BamIndex> {
231        inner.index.as_ref().ok_or_else(|| {
232            Error::invalid(if self.index_error.is_empty() {
233                format!("bam file is not indexed ({} not found)", self.index_path)
234            } else {
235                format!(
236                    "bam index {} could not be read: {}",
237                    self.index_path, self.index_error
238                )
239            })
240        })
241    }
242
243    /// One list per locus, in the order the loci were given.
244    ///
245    /// Each locus gets its own full list, so two overlapping loci both report
246    /// the alignments they share.
247    pub fn read_entries(&self, req: &EntriesRequest) -> Result<Vec<Vec<BamRecord>>> {
248        let inner = self.inner()?;
249        let resolved = self.resolve(&req.locs)?;
250        let coverage = resolved.iter().map(|(_, s, e)| (e - s).max(0) as u64).sum();
251        let tracker = ProgressTracker::with_callback(coverage, req.progress.clone());
252        let out = self.read_loci(inner, &resolved, req, &tracker)?;
253        tracker.done_report();
254        Ok(out)
255    }
256
257    /// Every alignment on the named references, in reference then coordinate
258    /// order.
259    ///
260    /// Unplaced alignments are left out: the index reaches an alignment only
261    /// through the reference it sits on.
262    pub fn read_all_entries(&self, req: &EntriesRequest) -> Result<Vec<BamRecord>> {
263        let locs = Locs::whole_chromosomes(&self.chr_map, &req.locs.chr_ids)?;
264        let whole = EntriesRequest {
265            locs,
266            ..req.clone()
267        };
268        Ok(self.read_entries(&whole)?.into_iter().flatten().collect())
269    }
270
271    /// Per-locus, lazily.
272    pub fn iter_entries(&self, req: &EntriesRequest) -> Result<LocusEntries<'_>> {
273        LocusEntries::plan(self, req)
274    }
275
276    /// Successive windows over whole references.
277    ///
278    /// A window never spans two references, and an alignment reaching over a
279    /// boundary is reported by the window it starts in.
280    pub fn iter_all_entries(&self, req: &EntriesRequest, window: i64) -> Result<WindowEntries<'_>> {
281        WindowEntries::plan(self, req, window)
282    }
283
284    /// Resolve each locus to (reference index, start, end).
285    fn resolve(&self, locs: &Locs) -> Result<Vec<(usize, i64, i64)>> {
286        (0..locs.len())
287            .map(|i| {
288                let entry = self.chr_map.resolve(&locs.chr_ids[i])?;
289                Ok((entry.index, locs.starts[i], locs.ends[i]))
290            })
291            .collect()
292    }
293
294    /// Read a stretch of loci, spread over the executor's workers.
295    ///
296    /// Split into runs of consecutive loci rather than dealt round-robin, so
297    /// each worker walks the request forward and the blocks it decompressed for
298    /// one locus serve the ones after it.
299    fn read_loci(
300        &self,
301        inner: &Inner,
302        loci: &[(usize, i64, i64)],
303        req: &EntriesRequest,
304        tracker: &ProgressTracker,
305    ) -> Result<Vec<Vec<BamRecord>>> {
306        if loci.is_empty() {
307            return Ok(Vec::new());
308        }
309        let index = self.index(inner)?;
310        let workers = inner.executor.parallel().min(loci.len()).max(1);
311        let per_worker = loci.len().div_ceil(workers);
312        let batches: Vec<(usize, usize)> = (0..workers)
313            .map(|w| (w * per_worker, ((w + 1) * per_worker).min(loci.len())))
314            .filter(|(from, to)| from < to)
315            .collect();
316
317        let lists = inner.executor.map_batches(&batches, |_, (from, to)| {
318            let mut cursor = Cursor::default();
319            let mut out = Vec::with_capacity(to - from);
320            for (chr, start, end) in &loci[*from..*to] {
321                let chunks = index.chunks(*chr, *start, *end, Some(MAX_MERGE_SPAN))?;
322                let mut records = Vec::new();
323                self.read_chunks(
324                    inner,
325                    &mut cursor,
326                    &chunks,
327                    0..chunks.len(),
328                    (*chr, *start, *end),
329                    req,
330                    &mut records,
331                )?;
332                out.push(records);
333                tracker.add((end - start).max(0) as u64);
334            }
335            Ok(out)
336        })?;
337        Ok(lists.into_iter().flatten().collect())
338    }
339
340    /// Read one locus, its chunks shared out over the executor's workers.
341    ///
342    /// The runs come back in file order, which for a coordinate-sorted file is
343    /// coordinate order, so laying them end to end gives exactly what reading
344    /// the locus on one thread gives.
345    fn read_locus_split(
346        &self,
347        inner: &Inner,
348        locus: (usize, i64, i64),
349        req: &EntriesRequest,
350        cursors: &Cursors,
351    ) -> Result<Vec<BamRecord>> {
352        let index = self.index(inner)?;
353        let (chr, start, end) = locus;
354        let chunks = index.chunks(chr, start, end, Some(MAX_MERGE_SPAN))?;
355        let runs = chunk_runs(&chunks, inner.executor.parallel());
356        if runs.is_empty() {
357            return Ok(Vec::new());
358        }
359        if runs.len() == 1 {
360            // Read where it stands rather than through the pool, which for a
361            // window holding one run of chunks is every window of a sparse file.
362            // This is also the path a scattered request spends its time on, and
363            // so the one whose cursor has to survive the call: cursor 0 carries
364            // the blocks from one locus to the next.
365            let mut cursor = cursors.get(0);
366            let mut out = Vec::new();
367            self.read_chunks(
368                inner,
369                &mut cursor,
370                &chunks,
371                runs[0].clone(),
372                locus,
373                req,
374                &mut out,
375            )?;
376            return Ok(out);
377        }
378        let lists = inner.executor.map_batches(&runs, |index, run| {
379            let mut cursor = cursors.get(index);
380            let mut out = Vec::new();
381            self.read_chunks(
382                inner,
383                &mut cursor,
384                &chunks,
385                run.clone(),
386                locus,
387                req,
388                &mut out,
389            )?;
390            Ok(out)
391        })?;
392        Ok(lists.into_iter().flatten().collect())
393    }
394
395    /// Append the alignments of a run of chunks that overlap the locus.
396    ///
397    /// The region still reaches the decoder as the filter, so a run holds only
398    /// the alignments of its own chunks that the locus overlaps. The chunks of a
399    /// locus are disjoint, so a run is disjoint from every other and an
400    /// alignment is read by exactly one of them.
401    #[allow(clippy::too_many_arguments)]
402    fn read_chunks(
403        &self,
404        inner: &Inner,
405        cursor: &mut Cursor,
406        chunks: &[Chunk],
407        run: std::ops::Range<usize>,
408        locus: (usize, i64, i64),
409        req: &EntriesRequest,
410        out: &mut Vec<BamRecord>,
411    ) -> Result<()> {
412        let (chr, start, end) = locus;
413        let filter = EntryFilter {
414            chr_index: Some(chr as i32),
415            start,
416            end: Some(end),
417            standard_flags: req.filter.enabled,
418        };
419        for chunk in &chunks[run] {
420            let data = cursor.get_or_read(inner.source.as_ref(), *chunk, &self.path)?;
421            out.extend(decode_block(
422                &data,
423                req.parse_tags,
424                &filter,
425                &self.chr_names,
426                &self.path,
427            )?);
428        }
429        Ok(())
430    }
431}
432
433impl Cursor {
434    /// The decompressed bytes of a chunk, from the cache or from the file.
435    ///
436    /// Keyed by the *pair* of virtual offsets, not by the start alone: two loci
437    /// touch different bins, so their chunk runs merge differently and two
438    /// chunks may start together and end apart.
439    fn get_or_read(
440        &mut self,
441        source: &dyn ByteSource,
442        chunk: Chunk,
443        path: &str,
444    ) -> Result<bytes::Bytes> {
445        if let Some((_, data)) = self.cache.iter().find(|(c, _)| *c == chunk) {
446            return Ok(data.clone());
447        }
448        let data = super::bgzf::decompress_chunk(source, chunk, path)?;
449        // A count alone is not a bound: `chunks` merges neighbours up to
450        // `MAX_MERGE_SPAN` of *compressed* bytes, so four entries can be a lot
451        // of memory — and a cursor now lives as long as the walk that holds it
452        // rather than as long as one call. A chunk too large to share is still
453        // returned; it is simply not kept.
454        if data.len() <= CURSOR_CACHE_BYTES {
455            if self.cache.len() < CHUNK_CACHE_SIZE {
456                self.bytes += data.len();
457                self.cache.push((chunk, data.clone()));
458            } else {
459                self.bytes -= self.cache[self.next].1.len();
460                self.bytes += data.len();
461                self.cache[self.next] = (chunk, data.clone());
462                self.next = (self.next + 1) % CHUNK_CACHE_SIZE;
463            }
464            while self.bytes > CURSOR_CACHE_BYTES && self.cache.len() > 1 {
465                // Oldest first, which with a ring of `next` is the entry it is
466                // about to overwrite anyway.
467                let oldest = self.next % self.cache.len();
468                self.bytes -= self.cache[oldest].1.len();
469                self.cache.remove(oldest);
470                self.next = oldest.min(self.cache.len().saturating_sub(1));
471            }
472        }
473        Ok(data)
474    }
475}
476
477/// Split a locus's chunks into the consecutive runs the threads share out.
478///
479/// Fewer runs than `workers` where the chunks hold too little to be worth that
480/// many, and a single run where they hold less than one. A chunk is never split,
481/// so a locus reaching one chunk is read on one thread however many there are.
482/// This is the loci batcher's rule, over compressed bytes rather than base
483/// pairs.
484fn chunk_runs(chunks: &[Chunk], workers: usize) -> Vec<std::ops::Range<usize>> {
485    if chunks.is_empty() || workers < 1 {
486        return Vec::new();
487    }
488    let total: u64 = chunks.iter().map(|c| c.compressed_size()).sum();
489    let wanted = (workers as u64).min(total / MIN_RUN_SIZE).max(1);
490    // Rounded up, so filling every run to it leaves no run in excess.
491    let per_run = total.div_ceil(wanted).max(1);
492
493    #[allow(clippy::single_range_in_vec_init)]
494    let mut runs = vec![0usize..0];
495    let mut size = 0u64;
496    for (i, chunk) in chunks.iter().enumerate().take(chunks.len() - 1) {
497        size += chunk.compressed_size();
498        if size < per_run {
499            continue;
500        }
501        // Once the count is reached everything left joins the last run, which is
502        // where the rounding already expects the remainder to go.
503        if runs.len() as u64 >= wanted {
504            continue;
505        }
506        runs.last_mut().expect("pushed one above").end = i + 1;
507        runs.push(i + 1..i + 1);
508        size = 0;
509    }
510    runs.last_mut().expect("pushed one above").end = chunks.len();
511    runs
512}
513
514#[derive(Clone)]
515pub struct EntriesRequest {
516    pub locs: Locs,
517    pub filter: RecordFilter,
518    /// Optional fields are only decoded when read, so keeping them costs one
519    /// small slice per alignment.
520    pub parse_tags: bool,
521    /// Read the loci in reference and position order, and report them in that
522    /// order. Loci close together then share the blocks a read decompressed —
523    /// 4 to 7 times faster on a scattered request.
524    pub sort_locations: bool,
525    pub progress: Option<ProgressFn>,
526}
527
528impl std::fmt::Debug for EntriesRequest {
529    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
530        f.debug_struct("EntriesRequest")
531            .field("loci", &self.locs.len())
532            .field("filter", &self.filter.enabled)
533            .field("parse_tags", &self.parse_tags)
534            .field("sort_locations", &self.sort_locations)
535            .finish()
536    }
537}
538
539impl EntriesRequest {
540    pub fn new(locs: Locs) -> Self {
541        Self {
542            locs,
543            filter: RecordFilter::default(),
544            parse_tags: true,
545            sort_locations: false,
546            progress: None,
547        }
548    }
549    pub fn filter(mut self, enabled: bool) -> Self {
550        self.filter.enabled = enabled;
551        self
552    }
553    pub fn parse_tags(mut self, v: bool) -> Self {
554        self.parse_tags = v;
555        self
556    }
557    pub fn sort_locations(mut self, v: bool) -> Self {
558        self.sort_locations = v;
559        self
560    }
561    pub fn progress(mut self, f: ProgressFn) -> Self {
562        self.progress = Some(f);
563        self
564    }
565}
566
567/// A walk over loci, one list at a time.
568///
569/// Owns its plan and takes the reader on each step, so a `#[pyclass]` can drive
570/// it — the same shape as the bbi walks.
571/// The plan a [`LocusWalk`] walks: everything decided when it was made, and
572/// nothing that changes as it runs.
573///
574/// Split out and shared behind an [`Arc`] so that restarting a walk costs a
575/// refcount bump rather than a copy of every locus. A per-locus walk over a
576/// hundred thousand requested regions holds a hundred thousand of each of
577/// these, and a caller looping twice should not pay for them twice.
578struct WalkPlan {
579    /// The loci in the order they will be read.
580    loci: Vec<(usize, i64, i64)>,
581    /// Request index of each, so a sorted walk can say which locus a list is.
582    order: Vec<usize>,
583    /// Lowest start an alignment may have to be reported by its locus, or
584    /// `None` to report every alignment the locus overlaps.
585    ///
586    /// What makes a whole-file walk group alignments by the window they *start*
587    /// in: an alignment reaching over a boundary overlaps two windows and would
588    /// otherwise be handed over by both.
589    min_starts: Vec<Option<i64>>,
590    request: EntriesRequest,
591    /// Bases the whole walk covers, for a restart's fresh tracker.
592    coverage: u64,
593}
594
595pub struct LocusWalk {
596    plan: Arc<WalkPlan>,
597    next: usize,
598    tracker: Arc<ProgressTracker>,
599    /// Carried across steps, which is what makes `sort_locations` worth asking
600    /// for: neighbouring loci share BGZF blocks, and a cursor built per call
601    /// has thrown them away before the next locus arrives.
602    cursors: Cursors,
603}
604
605impl std::fmt::Debug for LocusWalk {
606    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
607        f.debug_struct("LocusWalk")
608            .field("loci", &self.plan.loci.len())
609            .field("next", &self.next)
610            .finish()
611    }
612}
613
614impl LocusWalk {
615    /// A per-locus walk: every alignment its locus overlaps, boundaries and
616    /// all.
617    pub fn plan(reader: &BamReader, req: &EntriesRequest) -> Result<Self> {
618        Self::plan_with(reader, req, false)
619    }
620
621    /// `from_locus_start` reports only alignments starting at or after each
622    /// locus's own start, which is what tiling windows need and what a
623    /// per-locus walk must not do.
624    fn plan_with(reader: &BamReader, req: &EntriesRequest, from_locus_start: bool) -> Result<Self> {
625        let inner = reader.inner()?;
626        reader.index(inner)?;
627        let resolved = reader.resolve(&req.locs)?;
628        let mut order: Vec<usize> = (0..resolved.len()).collect();
629        if req.sort_locations {
630            order.sort_by_key(|i| resolved[*i]);
631        }
632        let loci: Vec<(usize, i64, i64)> = order.iter().map(|i| resolved[*i]).collect();
633        // Progress is counted in base pairs, as every other reader counts it.
634        let coverage = loci.iter().map(|(_, s, e)| (e - s).max(0) as u64).sum();
635        let min_starts = if from_locus_start {
636            loci.iter().map(|(_, start, _)| Some(*start)).collect()
637        } else {
638            vec![None; loci.len()]
639        };
640        Ok(Self {
641            tracker: Arc::new(ProgressTracker::with_callback(
642                coverage,
643                req.progress.clone(),
644            )),
645            plan: Arc::new(WalkPlan {
646                min_starts,
647                loci,
648                order,
649                request: req.clone(),
650                coverage,
651            }),
652            next: 0,
653            cursors: Cursors::new(reader.parallel()),
654        })
655    }
656
657    /// The same walk, back at its first locus.
658    ///
659    /// Shares the plan, so this is one refcount bump however many loci it
660    /// holds. The progress tracker is fresh: a second pass reports its own
661    /// progress from zero, which is what a caller watching it expects.
662    pub fn restarted(&self) -> Self {
663        Self {
664            plan: self.plan.clone(),
665            next: 0,
666            tracker: Arc::new(ProgressTracker::with_callback(
667                self.plan.coverage,
668                self.plan.request.progress.clone(),
669            )),
670            // Fresh, not shared: a second pass reads the file again, as the
671            // docs say, and two cursors of held blocks is not what a caller
672            // asking for a second `for` loop is asking to pay for.
673            cursors: Cursors::new(self.cursors.0.len()),
674        }
675    }
676
677    /// The same plan, for a walk that tiles whole references.
678    pub fn plan_windows(reader: &BamReader, req: &EntriesRequest, span: i64) -> Result<Self> {
679        if span < 1 {
680            return Err(Error::invalid(format!(
681                "span must be positive (got {span})"
682            )));
683        }
684        let locs = window_locs(&reader.chr_map, &req.locs.chr_ids, span)?;
685        let windowed = EntriesRequest {
686            locs,
687            sort_locations: false,
688            ..req.clone()
689        };
690        Self::plan_with(reader, &windowed, true)
691    }
692
693    pub fn len(&self) -> usize {
694        self.plan.loci.len()
695    }
696    pub fn is_empty(&self) -> bool {
697        self.plan.loci.is_empty()
698    }
699    /// Request index of each yielded list: the nth list belongs to locus
700    /// `order()[n]`.
701    pub fn order(&self) -> &[usize] {
702        &self.plan.order
703    }
704
705    pub fn next_window(&mut self, reader: &BamReader) -> Option<Result<Vec<BamRecord>>> {
706        if self.next >= self.plan.loci.len() {
707            self.tracker.done_report();
708            return None;
709        }
710        let index = self.next;
711        let locus = self.plan.loci[index];
712        let outcome = reader.inner().and_then(|inner| {
713            reader.read_locus_split(inner, locus, &self.plan.request, &self.cursors)
714        });
715        match outcome {
716            // A failed step is not a step at all: the walk stays where it was,
717            // so the call after it comes back here and raises the same error
718            // rather than reading past a locus nothing was read for. That is
719            // what makes closing a reader under a walk raise cleanly, every
720            // time, instead of once.
721            Err(e) => Some(Err(e)),
722            Ok(mut records) => {
723                self.next += 1;
724                if let Some(min_start) = self.plan.min_starts[index] {
725                    records.retain(|r| r.start() >= min_start);
726                }
727                let (_, start, end) = locus;
728                self.tracker.add((end - start).max(0) as u64);
729                Some(Ok(records))
730            }
731        }
732    }
733}
734
735/// [`LocusWalk`] as a plain [`Iterator`], for Rust callers.
736#[derive(Debug)]
737pub struct LocusEntries<'a> {
738    reader: &'a BamReader,
739    walk: LocusWalk,
740}
741
742impl<'a> LocusEntries<'a> {
743    fn plan(reader: &'a BamReader, req: &EntriesRequest) -> Result<Self> {
744        Ok(Self {
745            reader,
746            walk: LocusWalk::plan(reader, req)?,
747        })
748    }
749    pub fn len(&self) -> usize {
750        self.walk.len()
751    }
752    pub fn is_empty(&self) -> bool {
753        self.walk.is_empty()
754    }
755    pub fn order(&self) -> &[usize] {
756        self.walk.order()
757    }
758}
759
760impl Iterator for LocusEntries<'_> {
761    type Item = Result<Vec<BamRecord>>;
762    fn next(&mut self) -> Option<Self::Item> {
763        self.walk.next_window(self.reader)
764    }
765}
766
767/// [`LocusWalk`] over windows tiling whole references.
768#[derive(Debug)]
769pub struct WindowEntries<'a> {
770    reader: &'a BamReader,
771    walk: LocusWalk,
772}
773
774/// The windows a whole-file walk tiles the references with.
775pub fn window_locs(map: &ChrMap, chr_ids: &[String], span: i64) -> Result<Locs> {
776    let mut ids = Vec::new();
777    let mut starts = Vec::new();
778    let mut ends = Vec::new();
779    for chr in map.select(chr_ids)? {
780        let mut start = 0;
781        while start < chr.size {
782            ids.push(chr.id.clone());
783            starts.push(start);
784            ends.push((start + span).min(chr.size));
785            start += span;
786        }
787    }
788    Locs::spans(&ids, &starts, &ends)
789}
790
791impl<'a> WindowEntries<'a> {
792    fn plan(reader: &'a BamReader, req: &EntriesRequest, span: i64) -> Result<Self> {
793        if span < 1 {
794            return Err(Error::invalid(format!(
795                "span must be positive (got {span})"
796            )));
797        }
798        let locs = window_locs(&reader.chr_map, &req.locs.chr_ids, span)?;
799        let windowed = EntriesRequest {
800            locs,
801            sort_locations: false,
802            ..req.clone()
803        };
804        let walk = LocusWalk::plan_with(reader, &windowed, true)?;
805        Ok(Self { reader, walk })
806    }
807    pub fn len(&self) -> usize {
808        self.walk.len()
809    }
810    pub fn is_empty(&self) -> bool {
811        self.walk.is_empty()
812    }
813}
814
815impl Iterator for WindowEntries<'_> {
816    type Item = Result<Vec<BamRecord>>;
817    fn next(&mut self) -> Option<Self::Item> {
818        self.walk.next_window(self.reader)
819    }
820}
821
822#[cfg(test)]
823mod tests {
824    use super::*;
825    use crate::bam::bgzf::VirtualOffset;
826
827    fn chunk(a: u64, b: u64) -> Chunk {
828        Chunk {
829            begin: VirtualOffset::new(a, 0),
830            end: VirtualOffset::new(b, 0),
831        }
832    }
833
834    #[test]
835    fn no_chunks_means_no_runs() {
836        assert!(chunk_runs(&[], 4).is_empty());
837        assert!(chunk_runs(&[chunk(0, 100)], 0).is_empty());
838    }
839
840    #[test]
841    fn chunks_holding_too_little_are_read_as_one_run() {
842        // Well under MIN_RUN_SIZE: splitting would cost more than it saves.
843        let chunks = [chunk(0, 100), chunk(100, 200), chunk(200, 300)];
844        #[allow(clippy::single_range_in_vec_init)]
845        let one_run = [0..3];
846        assert_eq!(chunk_runs(&chunks, 8), one_run);
847    }
848
849    #[test]
850    fn a_big_locus_splits_into_at_most_one_run_per_worker() {
851        let big = MIN_RUN_SIZE * 4;
852        let chunks: Vec<Chunk> = (0..8).map(|i| chunk(i * big, (i + 1) * big)).collect();
853        let runs = chunk_runs(&chunks, 4);
854        assert_eq!(runs.len(), 4);
855        // Contiguous, covering every chunk exactly once.
856        assert_eq!(runs[0].start, 0);
857        assert_eq!(runs.last().unwrap().end, chunks.len());
858        for pair in runs.windows(2) {
859            assert_eq!(pair[0].end, pair[1].start);
860        }
861    }
862
863    #[test]
864    fn a_run_is_never_empty_and_a_chunk_is_never_split() {
865        let big = MIN_RUN_SIZE * 100;
866        let chunks = [chunk(0, big), chunk(big, big * 2)];
867        // Far more workers than chunks: two chunks cannot make eight runs.
868        let runs = chunk_runs(&chunks, 8);
869        assert!(runs.len() <= chunks.len());
870        assert!(runs.iter().all(|r| r.start < r.end));
871    }
872}
873
874#[cfg(test)]
875mod cursor_tests {
876    use super::*;
877    use crate::bam::bgzf::VirtualOffset;
878    use crate::source::testing::MemorySource;
879    use std::sync::atomic::{AtomicUsize, Ordering};
880
881    /// A source that counts the reads reaching it, so a cache hit is visible as
882    /// a read that never happened.
883    #[derive(Debug)]
884    struct CountingSource {
885        inner: MemorySource,
886        reads: AtomicUsize,
887    }
888
889    impl ByteSource for CountingSource {
890        fn path(&self) -> &str {
891            self.inner.path()
892        }
893        fn len(&self) -> Result<u64> {
894            self.inner.len()
895        }
896        fn read_at(&self, offset: u64, len: usize) -> Result<bytes::Bytes> {
897            self.reads.fetch_add(1, Ordering::SeqCst);
898            self.inner.read_at(offset, len)
899        }
900    }
901
902    /// One BGZF block wrapping `payload`, the way `crate::fuzz` builds them.
903    fn bgzf_block(payload: &[u8]) -> Vec<u8> {
904        use std::io::Write as _;
905        let mut encoder =
906            flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::new(6));
907        encoder.write_all(payload).expect("deflate to a Vec");
908        let deflated = encoder.finish().expect("deflate to a Vec");
909        let total = 18 + deflated.len() + 8;
910        let mut out = Vec::with_capacity(total);
911        out.extend_from_slice(&[0x1f, 0x8b, 8, 4, 0, 0, 0, 0, 0, 0xff]);
912        out.extend_from_slice(&6u16.to_le_bytes());
913        out.extend_from_slice(b"BC");
914        out.extend_from_slice(&2u16.to_le_bytes());
915        out.extend_from_slice(&((total - 1) as u16).to_le_bytes());
916        out.extend_from_slice(&deflated);
917        let mut crc = flate2::Crc::new();
918        crc.update(payload);
919        out.extend_from_slice(&crc.sum().to_le_bytes());
920        out.extend_from_slice(&(payload.len() as u32).to_le_bytes());
921        out
922    }
923
924    /// `count` blocks of `size` bytes each, and where each one begins.
925    fn blocks(count: usize, size: usize) -> (CountingSource, Vec<u64>) {
926        let mut bytes = Vec::new();
927        let mut offsets = Vec::new();
928        for i in 0..count {
929            offsets.push(bytes.len() as u64);
930            bytes.extend_from_slice(&bgzf_block(&vec![(i % 251) as u8; size]));
931        }
932        offsets.push(bytes.len() as u64);
933        (
934            CountingSource {
935                inner: MemorySource::new(bytes),
936                reads: AtomicUsize::new(0),
937            },
938            offsets,
939        )
940    }
941
942    fn chunk(from: u64, to: u64) -> Chunk {
943        Chunk {
944            begin: VirtualOffset::new(from, 0),
945            end: VirtualOffset::new(to, 0),
946        }
947    }
948
949    /// What carrying a cursor across a walk's steps buys, without a stopwatch:
950    /// a chunk asked for twice is inflated once. The end-to-end effect of that
951    /// — `sort_locations` going from doing nothing to a 3.5x to 20x speedup,
952    /// depending on how close the loci sit — is a measurement, recorded in
953    /// `local/plan.md`; this is the mechanism it rests on.
954    #[test]
955    fn a_chunk_a_cursor_has_already_read_is_not_read_again() {
956        let (source, offsets) = blocks(4, 4096);
957        let mut cursor = Cursor::default();
958        let first = chunk(offsets[0], offsets[1]);
959
960        let a = cursor.get_or_read(&source, first, "x.bam").unwrap();
961        assert_eq!(source.reads.load(Ordering::SeqCst), 1);
962        let b = cursor.get_or_read(&source, first, "x.bam").unwrap();
963        assert_eq!(a, b);
964        assert_eq!(
965            source.reads.load(Ordering::SeqCst),
966            1,
967            "the second read of one chunk reached the file"
968        );
969
970        // A different chunk is a different entry, and the first is still there.
971        let second = chunk(offsets[1], offsets[2]);
972        cursor.get_or_read(&source, second, "x.bam").unwrap();
973        let before = source.reads.load(Ordering::SeqCst);
974        cursor.get_or_read(&source, first, "x.bam").unwrap();
975        assert_eq!(source.reads.load(Ordering::SeqCst), before);
976
977        // A fresh cursor is what the walk used to build per call, and it knows
978        // nothing.
979        let mut fresh = Cursor::default();
980        fresh.get_or_read(&source, first, "x.bam").unwrap();
981        assert!(source.reads.load(Ordering::SeqCst) > before);
982    }
983
984    /// The count alone is no bound: chunks merge up to `MAX_MERGE_SPAN`
985    /// compressed, and a cursor now lives as long as its walk.
986    #[test]
987    fn a_cursor_stays_under_its_byte_budget() {
988        // Four chunks whose payloads together are well past the budget.
989        let each = CURSOR_CACHE_BYTES / 2 + 1;
990        let (source, offsets) = blocks(4, each);
991        let mut cursor = Cursor::default();
992        for i in 0..4 {
993            cursor
994                .get_or_read(&source, chunk(offsets[i], offsets[i + 1]), "x.bam")
995                .unwrap();
996            let held: usize = cursor.cache.iter().map(|(_, d)| d.len()).sum();
997            assert_eq!(held, cursor.bytes, "the running total drifted");
998            assert!(
999                cursor.bytes <= CURSOR_CACHE_BYTES || cursor.cache.len() == 1,
1000                "after {} chunks the cursor holds {} bytes",
1001                i + 1,
1002                cursor.bytes
1003            );
1004        }
1005        // And a chunk too big to share is returned without being kept.
1006        let (big_source, big_offsets) = blocks(1, CURSOR_CACHE_BYTES * 2);
1007        let mut cursor = Cursor::default();
1008        let data = cursor
1009            .get_or_read(&big_source, chunk(big_offsets[0], big_offsets[1]), "x.bam")
1010            .unwrap();
1011        assert_eq!(data.len(), CURSOR_CACHE_BYTES * 2);
1012        assert!(cursor.cache.is_empty(), "an oversized chunk was cached");
1013    }
1014}