Skip to content

perf(zipper): probe tag membership with a 256×256 bitset - #830

Merged
nh13 merged 1 commit into
mainfrom
nh/perf-zipper-tag-bitset
Aug 21, 2026
Merged

nh13 merged 1 commit into
mainfrom
nh/perf-zipper-tag-bitset

Conversation

@nh13

@nh13 nh13 commented Aug 19, 2026 •

Copy link
Copy Markdown
Member

Summary

The per-record tag-copy loop in merge_raw (zipper) probed three HashSet<String> sets (remove/reverse/revcomp) once per aux tag per mapped read — each lookup paying a SipHash of the tag name plus a UTF-8 conversion of the two raw tag bytes. This replaces the hot-path probe with a direct bit test on the two tag bytes via a new private TagBitset (256×256 bit table), built once per run into a ZipperTags struct and reused for every template.

Approach

  • TagBitset — a 256×256 bit table (1024 u64 words) keyed by the two raw tag bytes; contains is a single bit test, no hash and no UTF-8 conversion. Only two-byte tag names are representable (all SAM tags are two bytes), so longer/shorter filter strings are dropped at construction — mirroring the prior tag_str.len() == 2 guard.
  • ZipperTags — precomputes the three bitsets plus the two-byte remove_list (for the Step-2 tag removal) and has_transforms, once per run.
  • merge_raw(&TagInfo) stays as a thin public wrapper that builds ZipperTags and delegates to a new internal merge_raw_with; the hot per-template path in process_raw builds the lookups once and calls merge_raw_with directly. The public API and the fgumi-umi TagInfo type are unchanged.

Measured impact (M3 Ultra, aarch64)

  • Isolated probe: 29.6 ns/tag → 1.1 ns/tag (populated Consensus sets), ~27× faster. The bitset also decisively beats a HashSet<[u8;2]> (which still hashes).
  • Real per-record tag-copy loop (18-tag consensus record, ~3.5 KB aux, real remove_tag+append, neg strand): −23% (three runs, 20–26%). The loop is otherwise dominated by remove_tag+append over the consensus aux, so the probe is ~a quarter of it.
  • The win scales with tags/record on the single-threaded tag-copy path; it is a Consensus-config / many-tag win. On the light default path (empty tag sets, few tags) the loop is BGZF-I/O-bound and the change is neutral (see below) — not a regression.

Behavior

Tags are matched by their raw two bytes. For all spec-conforming SAM tags (ASCII [A-Za-z][A-Za-z0-9]) this is identical to the old from_utf8-based probe. The only divergence is for a non-UTF-8 tag byte pair (reachable only from a malformed BAM) combined with an empty-string filter: the old path aliased it to "" and could match an empty filter, whereas here the raw bytes are matched directly. This is the intended behavior; it is documented on TagBitset and pinned by a test.

Validation

  • Full suite (7710 tests) + clippy --pedantic -D warnings + the bare-tag-literal check all green. Added unit tests for TagBitset membership (case-sensitivity, full 0..=255 byte range, two-byte filtering, empty-filter semantics) and ZipperTags::from_tag_info.
  • Real-data parity: old vs new binaries on a 721,681-record real zipper run (SRR6109273.aligned.bam + .extract.fgbio.bam, -r hs38DH, -t 1) produce byte-identical records — the decompressed streams differ in exactly 46 of 393,397,066 bytes, reproducibly, and those 46 bytes are entirely the @PG VN: build commit hash. (Zipper output is deterministic: two runs of the same binary to the same output path are byte-identical — an earlier "1-byte per-run wobble" note was a measurement artifact, the differing byte being the -o filename recorded verbatim in the @PG CL: field.)
  • Real-data wall (default config, I/O-bound): old 2.32/2.32/2.43 s vs new 2.32/2.35/2.34 s — neutral, within noise, as expected for empty tag sets and ~2 tags/record.

Risk: command output changes — none; unsafe changes — none, and the CLAUDE.md allowlist is unchanged; memory bounds, queue capacity, and thread/backpressure policy changes — none. The raw-byte TagBitset preserves existing two-byte tag filtering and transformation behavior. Tests cover membership, byte ranges, invalid-length filters, and lookup construction.

@nh13
nh13 deployed to github-actions August 19, 2026 18:06 — with GitHub Actions Active
@coderabbitai

coderabbitai Bot commented Aug 19, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0a2c67c5-9135-4317-af1c-2762b4870d7b

📥 Commits

Reviewing files that changed from the base of the PR and between 3ff9306 and bc5b9b7.

📒 Files selected for processing (1)
  • src/lib/commands/zipper.rs

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.


Walkthrough

Raw SAM tag filtering now uses cached two-byte lookups instead of repeated UTF-8 conversions and string searches. process_raw constructs the lookup state once and reuses it during template merging.

Changes

Raw SAM tag lookup caching

Layer / File(s) Summary
Tag lookup state and validation
src/lib/commands/zipper.rs
Adds TagBitset and ZipperTags for raw tag membership, removal, transformation, and cached transform state. Tests cover byte ranges, case sensitivity, invalid names, empty filters, and construction.
Raw merge filtering
src/lib/commands/zipper.rs
Updates merge_raw and negative-strand tag copying to use raw-byte lookups for removal and transformation checks.
Zipper execution integration
src/lib/commands/zipper.rs
Constructs ZipperTags once per zipper execution and passes it through raw processing and template merging.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to bc5b9

This localized performance change preserves the public API and documented behavior, with comprehensive tests and real-data parity validation; no actionable merge-blocking risk remains beyond normal checks and review.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title follows Conventional Commit format and accurately describes the zipper tag-membership bitset optimization.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@nh13

nh13 commented Aug 19, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai pause

@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews paused.

The per-record tag-copy loop in merge_raw probed three HashSet<String> sets
(remove/reverse/revcomp) once per aux tag per mapped read, each lookup paying
a SipHash of the tag name plus a UTF-8 conversion of the two raw tag bytes.
Replace the hot-path probe with a direct bit test on the two tag bytes via a
new TagBitset (256x256 bit table), built once per run into a ZipperTags struct
and reused for every template.

Measured on an M3 Ultra (aarch64): the bitset probe is ~27x faster per tag
than the HashSet<String> lookup and removes ~23% of the per-record tag-copy
loop on a consensus-config zipper (--tags-to-reverse Consensus
--tags-to-revcomp Consensus), where the loop is otherwise dominated by
remove_tag+append over the ~3.5 KB consensus aux. The bitset also decisively
beats a HashSet<[u8;2]>, which still hashes.

The public merge_raw(&TagInfo) API is unchanged: it now builds the lookups and
delegates to an internal merge_raw_with. Only two-byte tag names are
representable (all SAM tags are two bytes); longer/shorter filter strings are
dropped at construction, matching the prior len() == 2 guard.
@nh13
nh13 force-pushed the nh/perf-zipper-tag-bitset branch from db22c10 to bc5b9b7 Compare August 19, 2026 18:09
@nh13
nh13 deployed to github-actions August 19, 2026 18:10 — with GitHub Actions Active
@codecov

codecov Bot commented Aug 19, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.41%. Comparing base (3ff9306) to head (bc5b9b7).
⚠️ Report is 8 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #830   +/-   ##
=======================================
  Coverage   94.40%   94.41%           
=======================================
  Files         186      186           
  Lines      114369   114458   +89     
=======================================
+ Hits       107974   108061   +87     
- Misses       6395     6397    +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@nh13

nh13 commented Aug 20, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 20, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nh13
nh13 added this pull request to the merge queue Aug 21, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 21, 2026
@nh13
nh13 added this pull request to the merge queue Aug 21, 2026
Merged via the queue into main with commit 2c32aa5 Aug 21, 2026
16 checks passed
@nh13
nh13 deleted the nh/perf-zipper-tag-bitset branch August 21, 2026 03:13
@nh13 nh13 mentioned this pull request Aug 21, 2026

This branch was successfully deployed

1 active deployment
github-actions — bc5b9b79 Deployed Aug 19, 2026 by nh13 via coverage #3719
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant