Repository navigation
perf: single-pass aux-tag rebuild (RawTagsEditor::rebuild_with) for zipper merge and NM/UQ/MD strip - #962
Conversation
Applying K tag operations to a record's aux block by calling remove_tag then append_raw_tag per tag is O(K*M): each remove_tag is a fresh O(M) linear scan of the aux data. rebuild_with does it in one pass -- walk the existing aux once, drop the tags in a remove-set, keep the rest in order, and append pre-encoded adds -- for O(M + adds). - TagKeySet trait: membership over two-byte tag keys, implemented for [[u8;2]] slices/arrays (small fixed sets) and TagBitset (large per-run sets). - TagBitset (256x256 bit table) moved into fgumi-raw-bam as the shared membership type; it previously lived privately in the zipper command. - Upsert semantics: an added key that also survives the remove-set is dropped from the survivors so the appended value wins with no duplicate, matching the remove_tag+append idiom it replaces. - Does not reverse/revcomp; callers transform negative-strand tags in a separate pass over the rebuilt record. Tests pin the contract (drop, keep-order, append-order, upsert, remove+readd, empty aux, TagBitset) including an oracle test asserting equality with the naive remove+append loop.
merge_raw_with removed and copied tags with per-tag remove_tag+append, each a fresh O(aux) scan: Step 2 removed the configured set tag-by-tag, and Steps 3-4 did remove_tag+append per unmapped tag per mapped record. Replace both with a single rebuild_with aux pass per record: - Step 2: one rebuild_with(&remove_set, &[]) per mapped record. - Steps 3-4: pre-filter the unmapped tags once (drop the remove-set; a PG-less variant for destinations that already carry PG), then one rebuild_with per mapped record that upserts the copied set. Negative-strand reverse/revcomp now runs as a separate pass over the appended tags — byte-identical to the old interleaved form, since each transform locates its tag by key. Also replaces zipper's private TagBitset with the promoted fgumi_raw_bam TagBitset and drops the now-unused remove_list field and append_raw_tag_entry helper. Output is unchanged: zipper + align + runall + duplex + simulate integration suites and the merge_raw unit/oracle tests all pass.
The unmapped and no-reference branches dropped the three alignment tags with three separate editor.remove() calls, each a fresh O(aux) scan. Replace with a single rebuild_with(&[NM, UQ, MD], &[]) pass. The NM/UQ/MD re-emit branches use update_int/update_string (value updates, not verbatim adds) and are unchanged. Not converting retag apply_op: its per-op metrics (dst_overwritten, src_missing) are counted per operation against the record state at that op, and ops apply left-to-right so an earlier op's write is observed by a later op. Batching into one rebuild_with would lose that per-op ordering and mis-count overwrites; each retag op also touches only 1-2 tags (one scan), so there is no K-scan blowup to collapse. Left on the current path per the design's metrics-safety gate. alignment_tags tests pass unchanged.
|
Note Reviews pausedUse the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (5)
Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour. WalkthroughThe PR adds raw-byte tag membership and single-pass auxiliary-field rebuilding. Alignment cleanup and zipper tag transfer use the new API, with ChangesRaw tag rebuild and integration
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~45 minutes Change: Refactor Sequence Diagram(s)sequenceDiagram
participant ZipperTags
participant UnmappedRecord
participant RawTagsEditor
participant MappedRecord
ZipperTags->>UnmappedRecord: Filter transferable tags
ZipperTags->>RawTagsEditor: Rebuild mapped auxiliary fields
RawTagsEditor->>MappedRecord: Preserve PG and append deduplicated tags
ZipperTags->>MappedRecord: Synchronize QC flag
Merge Risk: ⚪ Minimal · up to No confirmed merge-blocking issue remains in this change. 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
Comment |
|
@coderabbitai pause |
✅ Action performedReviews paused. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #962 +/- ##
==========================================
+ Coverage 96.03% 96.04% +0.01%
==========================================
Files 291 291
Lines 143872 144087 +215
==========================================
+ Hits 138167 138393 +226
+ Misses 5705 5694 -11 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
Summary
Several BAM tag transforms edited a record's auxiliary block with one
remove_tag+append_raw_tagper tag, in a loop — and eachremove_tagis a fresh O(aux) scan, so K tag operations on a record with M tags cost O(K·M) plus K separatedrain/extendsplices. This addsRawTagsEditor::rebuild_with(remove, adds): walk the aux once, drop a remove-set, keep the rest in order, append pre-encoded adds, and splice back with a single allocation.The flagship consumer is
fgumi zipper's tag merge, where profiling put the per-tagfind_tag_positionscans at ~6% of CPU. Because zipper runs concurrently with the aligner (upstream) and sort (downstream) inbwa-mem3 | fgumi zipper | fgumi sort, every CPU-second it doesn't burn is one those neighbors get.What changed
RawTagsEditor::rebuild_with+TagKeySet+TagBitset(crates/fgumi-raw-bam/src/tags.rs) — the single-pass primitive, plus aTagKeySetmembership trait implemented for[[u8; 2]]slices/arrays (small fixed sets) andTagBitset(large per-run sets).TagBitset(a 256×256 bit table) is promoted out of the zipper command intofgumi-raw-bamas the shared membership type. Upsert semantics match theremove_tag(tag); append(tag, ..)idiom exactly, including last-wins de-duplication withinadds.zipper::merge_raw_with(src/lib/commands/zipper.rs) — Step 2 (drop the configured remove-set) and Steps 3–4 (copy unmapped tags) each become onerebuild_withper mapped record instead of a per-tagremove_tag+appendloop. Negative-strand reverse/revcomp runs as a separate pass over the appended tags. The remove pass is skipped entirely when no tags are configured for removal (the common case), and the copy pass is skipped when there is nothing to copy — restoring the old no-op behavior for those cases.regenerate_alignment_tags_raw(crates/fgumi-sam/src/alignment_tags.rs) — the two branches that stripNM/UQ/MDnow do onerebuild_with(&[NM, UQ, MD], &[])instead of threeremovecalls.Byte-identity
Output is byte-identical to the old per-tag idiom for all valid input. The zipper rewrite reorders two things, both output-neutral:
rebuild_with's contract is pinned by unit tests including an oracle test asserting equality with the naiveremove_tag+appendloop. An EC2 check (c8g, 20M-record mapped SAM + unmapped BAM) reports 20,000,000 records, 0 content diffs, IDENTICAL before vs after (raw bytes differ only in BGZF block framing).Not converted (deliberate)
retag apply_opstays on the per-op path: its per-operation metrics (dst_overwritten,src_missing) are counted against record state at each op and ops apply left-to-right, so batching would mis-count overwrites; each op also touches only 1–2 tags, so there is no K-scan blowup. Value-update families (copy_umi, correct, dedup MI),template.rsMQ/MC/MS (mixed update/remove + signed/unsigned encodings), clipper per-base truncation, pure in-place reverse/revcomp, and freshSamBuilderbuilds are different primitives and left alone.Verification
cargo ci-test(full workspace): 10,204 passed, 0 failed, 31 skipped.cargo ci-fmt/cargo ci-lint(pedantic,-D warnings) /cargo ci-doc(-D warnings) / tag-literals / publish-order: green.rebuild_withcontract (drop / keep-order / append-order / upsert / remove+readd / empty-aux / TagBitset / dup-key last-wins / malformed-aux truncation / aux-offset-past-end clamp), and the zipperhas_pg → adds_no_pgbranch.Reading order
crates/fgumi-raw-bam/src/tags.rs—rebuild_with,TagKeySet,TagBitset, and the contract/oracle tests.src/lib/commands/zipper.rs—merge_raw_with(the reorder, the empty-set fast paths, and the transform post-pass).crates/fgumi-sam/src/alignment_tags.rs— the NM/UQ/MD strip.Follow-ups (deferred)
Re-profile
fgumi zipperat the realistic 1–2 threads to size the tag-merge slice's real weight. The zipper profile also flagged a separate, larger win — the SAM→BAMRecordBufre-encode of the mapped input — which is its own PR.Output risk: yes—zipper tag fields and NM/UQ/MD cleanup can change; contract, oracle, and byte-identical tests pin the intended output. Unsafe: none. CLAUDE.md allowlist: unchanged. Memory bounds, queue capacity, and thread/backpressure policy: none.
RawTagsEditor::rebuild_withfor single-pass tag rebuilding.TagKeySetandTagBitsetAPIs.PGbehavior, and strand transforms.