diff --git a/src/lib/commands/zipper.rs b/src/lib/commands/zipper.rs index 371f79bf1..835f76be5 100644 --- a/src/lib/commands/zipper.rs +++ b/src/lib/commands/zipper.rs @@ -532,7 +532,13 @@ impl TagBitset { /// Precomputed tag lookups for `merge_raw`, built once per zipper run from the /// user's `TagInfo` and reused for every template. Building the bitsets once /// rather than per template is the entire reason this type exists. -struct ZipperTags { +/// +/// `pub(crate)` so callers that merge many templates per `TagInfo` (e.g. +/// `AlignAndMergeStep` and `ZipperMergeStep`) can build one `ZipperTags` for +/// the whole step and drive [`merge_one_template_with`] directly, instead of +/// paying the three-`TagBitset`-allocation cost on every template. Fields +/// stay private — callers hold this opaquely. +pub(crate) struct ZipperTags { /// Two-byte tag names to remove from mapped reads (Step 2), pre-filtered to /// exactly the two-byte names (mirrors the old `len() == 2` guard). remove_list: Vec<[u8; 2]>, @@ -545,7 +551,7 @@ struct ZipperTags { } impl ZipperTags { - fn from_tag_info(tag_info: &TagInfo) -> Self { + pub(crate) fn from_tag_info(tag_info: &TagInfo) -> Self { let remove_list = tag_info .remove .iter() @@ -1353,26 +1359,30 @@ impl Command for Zipper { /// with the log site and cannot drift. pub const NEW_PIPELINE_START_LOG: &str = "Starting zipper (new pipeline)"; -/// Apply the full zipper merge body to a single (unmapped, mapped) -/// template pair. Runs `merge_raw`, then (when `reference` is `Some`) -/// `restore_unconverted_bases_in_raw_template` for the bisulfite path. +/// Apply the full zipper merge body to a single (unmapped, mapped) template +/// pair, given precomputed [`ZipperTags`]. Runs `merge_raw_with`, then (when +/// `reference` is `Some`) `restore_unconverted_bases_in_raw_template` for the +/// bisulfite path. /// -/// Returned errors are bare — callers add their own context (e.g. the -/// caller's step name) via `.map_err`/`?` at the call site so the -/// surfaced error is attributable to the dispatching context. +/// Both callers that merge many templates against the same `TagInfo` — +/// `ZipperMergeStep::emit_merged` (typed-step zipper) and +/// `AlignAndMergeStep::merge_zipper_batch` (AAM dispatcher) — build the +/// `ZipperTags` once, outside their per-template loop, and hold it on the +/// step for the step's whole lifetime rather than rebuilding it (three +/// `TagBitset` allocations) on every call. /// -/// Used by: -/// - `ZipperMergeStep::emit_merged` (typed-step zipper) -/// - `AlignAndMergeStep::merge_zipper_batch` (AAM dispatcher) -pub(crate) fn merge_one_template( +/// Returned errors are bare — callers add their own context (e.g. the +/// caller's step name) via `.map_err`/`?` at the call site so the surfaced +/// error is attributable to the dispatching context. +pub(crate) fn merge_one_template_with( unmapped: &Template, mapped: &mut Template, - tag_info: &TagInfo, + tags: &ZipperTags, skip_tc_tags: bool, reference: Option<&ReferenceReader>, output_header: &Header, ) -> Result<()> { - merge_raw(unmapped, mapped, tag_info, skip_tc_tags)?; + merge_raw_with(unmapped, mapped, tags, skip_tc_tags)?; if let Some(ref_reader) = reference { restore_unconverted_bases_in_raw_template(mapped, ref_reader, output_header)?; } @@ -1474,6 +1484,12 @@ pub(crate) mod merge_step { /// and emits merged [`BamTemplateBatch`]es. pub struct ZipperMergeStep { cfg: ZipperMergeConfig, + /// Precomputed tag-merge bitsets, built once in [`ZipperMergeStep::new`] + /// from `cfg.tag_info` and reused for every template `emit_merged` + /// processes over the step's lifetime — `cfg.tag_info` is immutable for + /// the whole step, so there is no reason to rebuild + /// [`super::ZipperTags`] (three `TagBitset` allocations) per template. + tags: super::ZipperTags, pending_a: Option, pending_b: Option, accumulator: Vec