Skip to content

refactor(pipeline): PerThreadAccumulator for all per-batch metric collection - #290

Merged
nh13 merged 8 commits into
mainfrom
nh/pipeline-metrics-per-thread
Apr 18, 2026
Merged

nh13 merged 8 commits into
mainfrom
nh/pipeline-metrics-per-thread

Conversation

@nh13

@nh13 nh13 commented Apr 17, 2026

Copy link
Copy Markdown
Member

Summary

Stacked on #287. Generalizes the group-specific accumulator into a reusable
`PerThreadAccumulator` helper and rolls it out across the remaining
pipeline commands that previously used `Arc<SegQueue>`
to buffer one metrics struct per batch for the whole run.

Commits:

  1. `refactor(pipeline)` — adds `crate::per_thread_accumulator::PerThreadAccumulator` with unit tests; migrates `group` off its inline implementation.
  2. `fix(filter)` — drops the `SegQueue` in filter's setup; 4×u64 metrics merged per-thread.
  3. `fix(clip)` — same pattern, 3×u64 metrics.
  4. `fix(correct)` — per-batch template counts, per-UMI match map, and reject bytes merged into per-thread slots.
  5. `fix(simplex)` — per-batch consensus stats, overlapping stats, groups, and reject bytes.
  6. `fix(duplex)` — mirrors simplex.
  7. `fix(codec)` — mirrors simplex/duplex.
  8. `docs(changelog)` — rollout note.

Why

Issue #285 showed that the `SegQueue` pattern retained one metrics struct per batch for the life of the run, scaling memory with input size. Group was the acute case (~10 GB at 180 M records via eager `AHashMap` allocations) and is the sole known OOM so far, but the other six commands share the same unbounded-growth shape and should be audited together — `filter` and `clip` leak tens-of-MB per billion records, `correct` can retain a per-UMI hashmap per batch, and the consensus commands retain reject bytes that scale with both batches and rejection rate.

This PR does not fix reject-buffering in `simplex`/`duplex`/`codec` — those still collect rejected raw BAM bytes into per-thread `Vec<Vec>`s and write them after the pipeline. Streaming rejects to the rejects BAM writer during the pipeline is a separate follow-up (PR #3, to be stacked on this branch).

What's next

Test plan

  • `cargo ci-fmt`, `cargo ci-lint`, `cargo ci-test` — 2545 tests pass.
  • Memory microbench on a real run of each migrated command to confirm flat peak RSS w.r.t. input size.
  • Byte-identity output check on a shared input for each command (unchanged behavior expected).

@nh13
nh13 temporarily deployed to github-actions April 17, 2026 20:28 — with GitHub Actions Inactive
@codecov

codecov Bot commented Apr 17, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.84393% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.04%. Comparing base (9bb0dd6) to head (ee7ac26).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/lib/commands/duplex.rs 94.44% 1 Missing ⚠️
src/lib/per_thread_accumulator.rs 98.41% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #290   +/-   ##
=======================================
  Coverage   90.03%   90.04%           
=======================================
  Files         122      123    +1     
  Lines       61811    61894   +83     
=======================================
+ Hits        55652    55732   +80     
- Misses       6159     6162    +3     

☔ View full report in Codecov by Sentry.
📢 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 marked this pull request as ready for review April 17, 2026 21:04
nh13 added a commit that referenced this pull request Apr 17, 2026
Rejects are now written to the rejects BAM directly from serialize_fn via
a shared BGZF writer guarded by a parking_lot mutex. The mutex only
serializes the byte append; BGZF compression runs on the writer's own
thread pool. This removes the last unbounded Vec<Vec<u8>> growth path
from simplex, complementing the metric-memory fix in #285/#290.

Peak reject memory drops from proportional to the reject count to a
small fixed working set of the BGZF block buffer.
@nh13

nh13 commented Apr 17, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 17, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

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.

@coderabbitai

coderabbitai Bot commented Apr 17, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@nh13 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 18 minutes and 21 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 18 minutes and 21 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 606d726d-25b8-460f-b8a2-3d58af8dfe24

📥 Commits

Reviewing files that changed from the base of the PR and between cadc96c and ee7ac26.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • src/lib/commands/codec.rs
  • src/lib/commands/correct.rs
  • src/lib/commands/duplex.rs
  • src/lib/commands/simplex.rs
📝 Walkthrough

Walkthrough

Added a new public module per_thread_accumulator exposing PerThreadAccumulator<A>. Replaced unbounded SegQueue<...>-based multithreaded metrics collection with PerThreadAccumulator in the filter, clip, correct, simplex, duplex, and codec pipeline commands. Refactored group to use PerThreadAccumulator instead of manual mutex/TLS slot management. Updated CHANGELOG.md to document the change; consensus commands’ buffering behavior remains unchanged and is tracked separately.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed Title accurately describes the main change: introducing PerThreadAccumulator for metric collection across pipeline commands, replacing the unbounded SegQueue pattern.
Description check ✅ Passed Description is well-related to the changeset, explaining the motivation (unbounded memory growth), what's being changed (SegQueue to PerThreadAccumulator across six commands), and known limitations.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch nh/pipeline-metrics-per-thread

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/lib/per_thread_accumulator.rs (2)

28-31: Process-wide TLS index is intentional — worth a sharper doc.

SLOT_COUNTER and THREAD_SLOT are shared across every PerThreadAccumulator in the process. The module docs call this out, but the practical consequence is: for a long-lived thread pool that services multiple accumulators with mismatched num_slots, the idx % slot_count mapping can cluster several threads onto the same slot deterministically (not random). Correctness is fine (mutex handles it), throughput isn't — just lock contention.

Not blocking; a one-liner noting "distribution is only uniform when num_slots ≥ the count of distinct global indices ever assigned" would save a future reader the analysis.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/per_thread_accumulator.rs` around lines 28 - 31, Update the
module-level documentation for PerThreadAccumulator to explicitly state that
SLOT_COUNTER and THREAD_SLOT are process-global and that thread-to-slot mapping
uses a global index (SLOT_COUNTER -> THREAD_SLOT) so that idx % num_slots can
lead to deterministic clustering when num_slots is smaller than the total number
of distinct global indices assigned; add a one-line note near the
PerThreadAccumulator docs mentioning "distribution is only uniform when
num_slots ≥ the count of distinct global indices ever assigned" and reference
SLOT_COUNTER and THREAD_SLOT to clarify the behaviour and performance
implication.

55-70: with_slot body is tight — nit on readability.

The c.get().unwrap_or_else(...) + trailing % slot_count is correct but reads as two unrelated operations glued together. Extracting the index acquisition into a private helper (e.g., fn global_thread_index() -> usize) would make with_slot a two-liner and make the global-state dependency explicit at the call site. Optional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/per_thread_accumulator.rs` around lines 55 - 70, Extract the thread
index acquisition logic from with_slot into a small private helper fn
global_thread_index() -> usize that encapsulates THREAD_SLOT and SLOT_COUNTER
access (using c.get().unwrap_or_else(...) and SLOT_COUNTER.fetch_add(...)), then
simplify with_slot to compute slot_count, call global_thread_index() %
slot_count to pick the slot, lock self.slots[slot], and call f(&mut guard); keep
all existing ordering/unwrap behavior and names (with_slot, THREAD_SLOT,
SLOT_COUNTER) unchanged so behavior is identical but readability improves.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/per_thread_accumulator.rs`:
- Around line 87-92: The fallback branch in into_slots can silently drop
concurrent writes because mem::take replaces slot values with A::default(), so
either make the behavior explicit in the docs and mark it lossy or enforce
unique ownership; specifically, update the doc comment on into_slots to state
the fallback is lossy under concurrent use, and add a
debug_assert!(Arc::strong_count(&self) == 1) (or similar unique-ownership
assert) before attempting Arc::try_unwrap in into_slots (referencing the slots
field and with_slot callers) so during development you catch misuse rather than
silently losing data.

---

Nitpick comments:
In `@src/lib/per_thread_accumulator.rs`:
- Around line 28-31: Update the module-level documentation for
PerThreadAccumulator to explicitly state that SLOT_COUNTER and THREAD_SLOT are
process-global and that thread-to-slot mapping uses a global index (SLOT_COUNTER
-> THREAD_SLOT) so that idx % num_slots can lead to deterministic clustering
when num_slots is smaller than the total number of distinct global indices
assigned; add a one-line note near the PerThreadAccumulator docs mentioning
"distribution is only uniform when num_slots ≥ the count of distinct global
indices ever assigned" and reference SLOT_COUNTER and THREAD_SLOT to clarify the
behaviour and performance implication.
- Around line 55-70: Extract the thread index acquisition logic from with_slot
into a small private helper fn global_thread_index() -> usize that encapsulates
THREAD_SLOT and SLOT_COUNTER access (using c.get().unwrap_or_else(...) and
SLOT_COUNTER.fetch_add(...)), then simplify with_slot to compute slot_count,
call global_thread_index() % slot_count to pick the slot, lock self.slots[slot],
and call f(&mut guard); keep all existing ordering/unwrap behavior and names
(with_slot, THREAD_SLOT, SLOT_COUNTER) unchanged so behavior is identical but
readability improves.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: db69432a-f124-4929-bf4e-7a96a2b734da

📥 Commits

Reviewing files that changed from the base of the PR and between a2c7b90 and 3072739.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • src/lib/commands/clip.rs
  • src/lib/commands/codec.rs
  • src/lib/commands/correct.rs
  • src/lib/commands/duplex.rs
  • src/lib/commands/filter.rs
  • src/lib/commands/group.rs
  • src/lib/commands/simplex.rs
  • src/lib/mod.rs
  • src/lib/per_thread_accumulator.rs

Comment thread src/lib/per_thread_accumulator.rs
@nh13 nh13 added the enhancement New feature or request label Apr 18, 2026
@nh13
nh13 force-pushed the nh/group-oom branch 2 times, most recently from 7b092d8 to 68c17b7 Compare April 18, 2026 02:15
Base automatically changed from nh/group-oom to main April 18, 2026 02:19
An error occurred while trying to automatically change base from nh/group-oom to main April 18, 2026 02:19
@nh13
nh13 force-pushed the nh/pipeline-metrics-per-thread branch from 3072739 to e7ed89c Compare April 18, 2026 02:27
@nh13
nh13 temporarily deployed to github-actions April 18, 2026 02:27 — with GitHub Actions Inactive
@nh13

nh13 commented Apr 18, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/per_thread_accumulator.rs`:
- Line 1: Add the crate-level attribute to deny unsafe code in this module by
inserting #![deny(unsafe_code)] at the very top of
src/lib/per_thread_accumulator.rs (place it before the existing module doc
comment `//! Sharded per-thread accumulator...`). Ensure the attribute is the
first line so the compiler enforces no unsafe code for this file (no other code
changes required).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 01001777-07d1-4778-abc5-9f01162206c6

📥 Commits

Reviewing files that changed from the base of the PR and between 3072739 and e7ed89c.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • src/lib/commands/clip.rs
  • src/lib/commands/codec.rs
  • src/lib/commands/correct.rs
  • src/lib/commands/duplex.rs
  • src/lib/commands/filter.rs
  • src/lib/commands/group.rs
  • src/lib/commands/simplex.rs
  • src/lib/mod.rs
  • src/lib/per_thread_accumulator.rs
✅ Files skipped from review due to trivial changes (2)
  • src/lib/mod.rs
  • src/lib/commands/simplex.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • CHANGELOG.md
  • src/lib/commands/duplex.rs
  • src/lib/commands/correct.rs
  • src/lib/commands/clip.rs

Comment thread src/lib/per_thread_accumulator.rs
nh13 added 3 commits April 17, 2026 20:08
…group

Introduces `crate::per_thread_accumulator::PerThreadAccumulator<A>`, a
sharded accumulator backed by a Vec of mutex-protected slots. Workers claim
a slot lazily via TLS-cached indices from a process-wide atomic counter,
then merge per-group results into the slot immediately. After the pipeline
completes, callers fold the slots into the final aggregate.

This generalizes the inline accumulator introduced for `fgumi group` (#285,
#287) so the same pattern can replace `SegQueue<CollectedXxxMetrics>` in
the other pipeline commands without re-implementing the slot-assignment
machinery each time.

Migrates `fgumi group` to the shared helper.
Replaces `Arc<SegQueue<CollectedFilterMetrics>>` with a
`PerThreadAccumulator` sized to the pipeline's worker count. Each worker
merges per-batch counts into its slot directly, eliminating the unbounded
queue that previously retained one metrics struct per batch for the life
of the run (see issue #285).
Replaces `Arc<SegQueue<CollectedClipMetrics>>` with a
`PerThreadAccumulator`. See issue #285 for the shared pattern.
@nh13
nh13 force-pushed the nh/pipeline-metrics-per-thread branch from e7ed89c to cadc96c Compare April 18, 2026 03:09
@nh13
nh13 temporarily deployed to github-actions April 18, 2026 03:09 — with GitHub Actions Inactive
@nh13

nh13 commented Apr 18, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/lib/commands/correct.rs (1)

975-985: Minor: avoid key clone and duplicated merge logic.

entry(umi.clone()).or_insert_with(|| UmiCorrectionMetrics::new(umi)) clones the key even on hit. Swap to or_insert_with_key to clone only on insert. The same merge shape is repeated verbatim at lines 1031–1039; a tiny helper would deduplicate.

♻️ Suggested tweak
-                for (umi, counts) in umi_matches {
-                    let entry = m
-                        .umi_matches
-                        .entry(umi.clone())
-                        .or_insert_with(|| UmiCorrectionMetrics::new(umi));
-                    entry.total_matches += counts.total_matches;
-                    entry.perfect_matches += counts.perfect_matches;
-                    entry.one_mismatch_matches += counts.one_mismatch_matches;
-                    entry.two_mismatch_matches += counts.two_mismatch_matches;
-                    entry.other_matches += counts.other_matches;
-                }
+                for (umi, counts) in umi_matches {
+                    merge_umi_counts(&mut m.umi_matches, umi, &counts);
+                }

With a free function:

fn merge_umi_counts(
    dst: &mut AHashMap<String, UmiCorrectionMetrics>,
    umi: String,
    counts: &UmiCorrectionMetrics,
) {
    let entry = dst.entry(umi).or_insert_with_key(|k| UmiCorrectionMetrics::new(k.clone()));
    entry.total_matches += counts.total_matches;
    entry.perfect_matches += counts.perfect_matches;
    entry.one_mismatch_matches += counts.one_mismatch_matches;
    entry.two_mismatch_matches += counts.two_mismatch_matches;
    entry.other_matches += counts.other_matches;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/commands/correct.rs` around lines 975 - 985, The loop clones keys
even on entry hits and duplicates merge logic; replace the current
entry(umi.clone()).or_insert_with(...) pattern by using
entry(umi).or_insert_with_key(|k| UmiCorrectionMetrics::new(k.clone())) (or call
a tiny helper) so the key is only cloned on insert, and factor the repeated
accumulation into a small function such as merge_umi_counts(dst: &mut
AHashMap<String, UmiCorrectionMetrics>, umi: String, counts:
&UmiCorrectionMetrics) that performs the entry lookup with or_insert_with_key
and increments total_matches, perfect_matches, one_mismatch_matches,
two_mismatch_matches, and other_matches to deduplicate the logic used at both
sites.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/lib/commands/correct.rs`:
- Around line 975-985: The loop clones keys even on entry hits and duplicates
merge logic; replace the current entry(umi.clone()).or_insert_with(...) pattern
by using entry(umi).or_insert_with_key(|k| UmiCorrectionMetrics::new(k.clone()))
(or call a tiny helper) so the key is only cloned on insert, and factor the
repeated accumulation into a small function such as merge_umi_counts(dst: &mut
AHashMap<String, UmiCorrectionMetrics>, umi: String, counts:
&UmiCorrectionMetrics) that performs the entry lookup with or_insert_with_key
and increments total_matches, perfect_matches, one_mismatch_matches,
two_mismatch_matches, and other_matches to deduplicate the logic used at both
sites.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b6c0ec55-b33e-4133-b9da-df1092b217ae

📥 Commits

Reviewing files that changed from the base of the PR and between e7ed89c and cadc96c.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • src/lib/commands/clip.rs
  • src/lib/commands/codec.rs
  • src/lib/commands/correct.rs
  • src/lib/commands/duplex.rs
  • src/lib/commands/filter.rs
  • src/lib/commands/group.rs
  • src/lib/commands/simplex.rs
  • src/lib/mod.rs
  • src/lib/per_thread_accumulator.rs
✅ Files skipped from review due to trivial changes (2)
  • src/lib/mod.rs
  • src/lib/per_thread_accumulator.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • CHANGELOG.md
  • src/lib/commands/simplex.rs
  • src/lib/commands/clip.rs
  • src/lib/commands/filter.rs

Replaces `Arc<SegQueue<CollectedCorrectMetrics>>` with a
`PerThreadAccumulator`. Each worker merges per-batch template counts,
per-UMI match counts, and rejected records into its own slot. Rejected
records are drained by-move from the slot at pipeline end, preserving the
existing write-all-rejects-after-pipeline semantics. See issue #285.
nh13 added 4 commits April 18, 2026 00:39
Replaces `Arc<SegQueue<CollectedSimplexMetrics>>` with a
`PerThreadAccumulator`. Each worker merges per-batch consensus stats,
overlapping-correction stats, group counts, and raw reject bytes into its
own slot. Rejects buffering semantics are preserved; streaming rejects to
disk during the pipeline is tracked as a separate follow-up. See #285.
Mirrors the simplex change: replaces `Arc<SegQueue<CollectedDuplexMetrics>>`
with a `PerThreadAccumulator`. Rejects buffering semantics preserved.
See #285.
Mirrors the simplex/duplex changes: replaces
`Arc<SegQueue<CollectedCodecMetrics>>` with a `PerThreadAccumulator`.
Rejects buffering semantics preserved. See #285.
@nh13
nh13 force-pushed the nh/pipeline-metrics-per-thread branch from cadc96c to ee7ac26 Compare April 18, 2026 07:40
@nh13
nh13 temporarily deployed to github-actions April 18, 2026 07:40 — with GitHub Actions Inactive
@nh13
nh13 merged commit a129ed7 into main Apr 18, 2026
8 of 9 checks passed
@nh13
nh13 deleted the nh/pipeline-metrics-per-thread branch April 18, 2026 07:42
@nh13 nh13 mentioned this pull request Apr 18, 2026
nh13 added a commit that referenced this pull request Apr 18, 2026
Rejects are now written to the rejects BAM directly from serialize_fn via
a shared BGZF writer guarded by a parking_lot mutex. The mutex only
serializes the byte append; BGZF compression runs on the writer's own
thread pool. This removes the last unbounded Vec<Vec<u8>> growth path
from simplex, complementing the metric-memory fix in #285/#290.

Peak reject memory drops from proportional to the reject count to a
small fixed working set of the BGZF block buffer.
nh13 added a commit that referenced this pull request Apr 18, 2026
Rejects are now written to the rejects BAM directly from serialize_fn via
a shared BGZF writer guarded by a parking_lot mutex. The mutex only
serializes the byte append; BGZF compression runs on the writer's own
thread pool. This removes the last unbounded Vec<Vec<u8>> growth path
from simplex, complementing the metric-memory fix in #285/#290.

Peak reject memory drops from proportional to the reject count to a
small fixed working set of the BGZF block buffer.
nh13 added a commit that referenced this pull request Apr 20, 2026
Rejects are now written to the rejects BAM directly from serialize_fn via
a shared BGZF writer guarded by a parking_lot mutex. The mutex only
serializes the byte append; BGZF compression runs on the writer's own
thread pool. This removes the last unbounded Vec<Vec<u8>> growth path
from simplex, complementing the metric-memory fix in #285/#290.

Peak reject memory drops from proportional to the reject count to a
small fixed working set of the BGZF block buffer.
nh13 added a commit that referenced this pull request Apr 20, 2026
…#293)

* fix(simplex): stream rejects to disk instead of buffering in memory

Rejects are now written to the rejects BAM directly from serialize_fn via
a shared BGZF writer guarded by a parking_lot mutex. The mutex only
serializes the byte append; BGZF compression runs on the writer's own
thread pool. This removes the last unbounded Vec<Vec<u8>> growth path
from simplex, complementing the metric-memory fix in #285/#290.

Peak reject memory drops from proportional to the reject count to a
small fixed working set of the BGZF block buffer.

* fix(duplex): stream rejects to disk instead of buffering in memory

Same pattern as the simplex change: a shared BGZF writer guarded by a
parking_lot mutex receives reject bytes directly from serialize_fn, so
rejected raw BAM records no longer accumulate in per-thread Vecs.

* fix(codec): stream rejects to disk instead of buffering in memory

Same pattern as the simplex/duplex changes: shared BGZF writer guarded
by a parking_lot mutex receives reject bytes directly from serialize_fn.

* fix(correct): stream rejects to disk instead of buffering in memory

Rejected raw records are now written to the rejects BAM directly from
serialize_fn via a shared BGZF writer guarded by a parking_lot mutex,
completing the rejects-streaming work started in simplex/duplex/codec.
Removes the last per-thread Vec<Vec<u8>> accumulator in the pipeline
commands.
@nh13 nh13 mentioned this pull request Apr 22, 2026

This branch was previously deployed

1 inactive deployment
github-actions — ee7ac264 Deployed Apr 18, 2026 by nh13 via coverage #1198
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant