Skip to content

feat(sort): add per-phase --sort-threads / --merge-threads - #608

Merged
nh13 merged 1 commit into
mainfrom
nh/feat-sort-merge-threads
Jul 22, 2026
Merged

nh13 merged 1 commit into
mainfrom
nh/feat-sort-merge-threads

Conversation

@nh13

@nh13 nh13 commented Jul 21, 2026 •

Copy link
Copy Markdown
Member

fgumi sort had a single -@/--threads sizing both Phase 1 (accumulate/sort/spill) and Phase 2 (merge/write). Those phases contend with different things: Phase 1 competes with whatever feeds the sort, while Phase 2 runs after the producer is finished. A user running bwa mem -t 32 ... | fgumi sort -@ 8 had no way to cede cores to the aligner during ingest while keeping the merge wide.

Engine

SortWorkerPool gains an active_worker_limit and set_active_workers. A single pool still spans both phases — sized to the wider of the two counts — and the driver flips the active cap at the ingest/merge boundary. Workers above the cap idle rather than taking new work and re-check the limit within MAX_BACKOFF_US, so raising it reactivates them without an explicit wake.

The invariant that makes this safe: a capped worker still drains items it already holds and only declines to acquire new work. Held items are per-worker, so a worker that froze while holding work would strand that output with no other worker able to advance it.

RawExternalSorter gains sort_threads/merge_threads builders plus phase1_threads()/phase2_threads(), each falling back to threads independently. The CLI exposes --sort-threads / --merge-threads with the same defaulting.

This is purely a scheduling knob: output is byte-identical for any split.

On testing a knob that by definition changes nothing observable

An end-to-end run cannot tell a correctly wired flag from one that was parsed and then silently dropped — both produce identical bytes. So the sorter construction is extracted from execute into Sort::build_sorter, and the wiring is asserted directly on the resolved per-phase counts. I verified that test is non-vacuous by unwiring the flag and confirming it fails; the output-identity test, as expected, does not.

Both properties are covered separately:

  • Wiring — build_sorter resolves each override, and each falls back to --threads independently.
  • Output identity — four asymmetric splits produce byte-identical output to a plain --threads run over a spilling multi-chunk merge, exercising both the wider-Phase-1 and wider-Phase-2 pool sizings.
  • Pool behavior — clamping of the active count, and (on a single pool, the only way to show it) that workers idled by a cap come back online when the cap is raised.

Runall-only halves of the upstream change — SortSpillDecompress::with_max_concurrency and the chains builder — are deliberately not ported; they depend on crates that do not exist on main.

Verification

cargo ci-test (5557 tests), cargo ci-fmt, cargo ci-lint, and RUSTDOCFLAGS="-D warnings" cargo ci-doc pass. Patch coverage 96%.

Summary by CodeRabbit

  • New Features
    • Added independent thread controls for the sorter’s phase 1 (sort/accumulate/spill) and phase 2 (merge/output).
    • Introduced --sort-threads and --merge-threads for fgumi sort, with safe clamping.
  • Bug Fixes
    • Ensured varying phase thread settings do not change output bytes.
  • Tests
    • Added coverage for CLI parsing, phase override fallback/clamping, worker cap enforcement/reactivation, and output consistency.

@nh13
nh13 temporarily deployed to github-actions July 21, 2026 01:06 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown

Review Change Stack

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: 9babede2-ce76-490d-8936-b1f80fa992d3

📥 Commits

Reviewing files that changed from the base of the PR and between 5c1b587 and 6585365.

📒 Files selected for processing (3)
  • crates/fgumi-sort/src/external.rs
  • crates/fgumi-sort/src/worker_pool.rs
  • src/lib/commands/sort.rs

Walkthrough

The sorter now accepts separate Phase 1 and Phase 2 thread counts through the CLI and builder API. Worker pools cap active workers per phase, while sort paths switch caps at the merge boundary and tests verify clamping and byte-identical output.

Changes

Per-phase sorter concurrency

Layer / File(s) Summary
Phase thread configuration and CLI wiring
crates/fgumi-sort/src/external.rs, src/lib/commands/sort.rs
RawExternalSorter and fgumi sort accept optional phase overrides, resolve effective counts, and test parsing, defaults, clamping, configuration, and output identity.
Phase-aware worker-pool control
crates/fgumi-sort/src/worker_pool.rs
SortWorkerPool enforces a clamped active-worker limit, supports later reactivation, and tests cap behavior.
Phase scheduling across sort implementations
crates/fgumi-sort/src/external.rs
Phase 1 resource sizing uses phase1_threads(), shared pools are sized for both phases, and sort paths raise the active worker cap to phase2_threads() before merge/write. Partitioning and indexing writer budgets use the corresponding phase counts.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant RawExternalSorter
  participant SortWorkerPool
  participant Phase1
  participant Phase2
  CLI->>RawExternalSorter: configure sort and merge thread counts
  RawExternalSorter->>SortWorkerPool: create pool and activate Phase 1 workers
  RawExternalSorter->>Phase1: accumulate, sort, and spill
  RawExternalSorter->>SortWorkerPool: activate Phase 2 workers
  RawExternalSorter->>Phase2: merge and write output
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: per-phase thread controls for fgumi sort.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch nh/feat-sort-merge-threads

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

@codecov

codecov Bot commented Jul 21, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.01325% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.52%. Comparing base (4bd2824) to head (6585365).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
crates/fgumi-sort/src/external.rs 95.91% 2 Missing ⚠️
crates/fgumi-sort/src/worker_pool.rs 98.59% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main     #608    +/-   ##
========================================
  Coverage   93.52%   93.52%            
========================================
  Files         175      175            
  Lines      105404   105530   +126     
========================================
+ Hits        98574    98699   +125     
- Misses       6830     6831     +1     

☔ 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 Jul 21, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 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 commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 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 commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 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 commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 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 commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 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.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/fgumi-sort/src/external.rs (1)

2581-2664: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

--write-index path doesn't honor merge_threads.

pool.set_active_workers(self.phase2_threads()) is raised here, but the two writers this function actually uses ignore it: the in-memory branch's create_indexing_bam_writer(..., self.threads) (~line 2603) and merge_chunks_with_index's let writer_threads = self.threads; (~line 3493) both hardcode the base threads, not phase2_threads(). Contrast with the non-indexed merge (merge_chunks_generic), whose PooledBamWriter correctly inherits the raised cap via the shared pool. So fgumi sort --order coordinate --write-index --merge-threads N silently caps output compression at --threads instead of N — output stays byte-identical (compression thread count doesn't change block content), but the documented scheduling contract ("output write" phase) is broken for this path. No test combines write_index with sort_threads/merge_threads either, which is how this slipped through.

🐛 Proposed fix
@@ sort_coordinate_with_index (in-memory branch)
             timer.time_write_output(|| {
                 let mut writer = create_indexing_bam_writer(
                     output,
                     &output_header,
                     self.output_compression,
-                    self.threads,
+                    self.phase2_threads(),
                 )?;
@@ merge_chunks_with_index
-        let writer_threads = self.threads;
+        let writer_threads = self.phase2_threads();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fgumi-sort/src/external.rs` around lines 2581 - 2664, Update the
write-index paths to use the Phase 2 merge-thread count rather than the base
thread count: pass self.phase2_threads() to create_indexing_bam_writer in the
in-memory branch and replace the self.threads-based writer_threads value in
merge_chunks_with_index. Preserve the existing output and indexing behavior
while ensuring --merge-threads controls compression during indexed output.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/fgumi-sort/src/external.rs`:
- Around line 2581-2664: Update the write-index paths to use the Phase 2
merge-thread count rather than the base thread count: pass self.phase2_threads()
to create_indexing_bam_writer in the in-memory branch and replace the
self.threads-based writer_threads value in merge_chunks_with_index. Preserve the
existing output and indexing behavior while ensuring --merge-threads controls
compression during indexed output.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c5fd5733-f75e-4170-b328-1bc40427ef76

📥 Commits

Reviewing files that changed from the base of the PR and between 3da9945 and 5c1b587.

📒 Files selected for processing (3)
  • crates/fgumi-sort/src/external.rs
  • crates/fgumi-sort/src/worker_pool.rs
  • src/lib/commands/sort.rs

@nh13
nh13 force-pushed the nh/feat-sort-merge-threads branch from 5c1b587 to 2fdf559 Compare July 21, 2026 17:56
@nh13
nh13 temporarily deployed to github-actions July 21, 2026 17:56 — with GitHub Actions Inactive
@nh13

nh13 commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

Addressed the outside-diff finding: the --write-index path now honors --merge-threads. Both indexed writers use phase2_threads() instead of the base threads — create_indexing_bam_writer in the in-memory branch of sort_coordinate_with_index, and writer_threads in merge_chunks_with_index (which feeds all three create_indexing_bam_writer calls there). This matches the non-indexed merge_chunks_generic, whose PooledBamWriter already inherits the cap via the shared pool.

Also closed the coverage gap: test_per_phase_threads_produce_identical_output now crosses in write_index (false/true) and memory_limit (spill/in-memory) via #[values], so both indexed writer sites are exercised across every thread split. Output stays byte-identical (compression thread count does not change block content), so the test guards the path against regression rather than asserting the thread count.

@nh13

nh13 commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 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 commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 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.

`fgumi sort` had a single `-@/--threads` sizing both Phase 1
(accumulate/sort/spill) and Phase 2 (merge/write). Those phases contend with
different things: Phase 1 competes with whatever feeds the sort, while Phase 2
runs after the producer is done. A user running
`bwa mem -t 32 ... | fgumi sort -@ 8` had no way to cede cores to the aligner
during ingest while keeping the merge wide.

`SortWorkerPool` gains an `active_worker_limit` and `set_active_workers`. One
pool still spans both phases -- sized to the wider of the two counts -- and the
driver flips the active cap at the ingest/merge boundary. Workers above the cap
idle instead of taking new work, and re-check the limit within `MAX_BACKOFF_US`,
so raising it reactivates them without an explicit wake.

Crucially, a capped worker still drains items it already holds and only declines
to acquire *new* work. Held items are per-worker, so a worker that froze while
holding work would strand that output with no other worker able to advance it.

`RawExternalSorter` gains `sort_threads`/`merge_threads` builders and
`phase1_threads()`/`phase2_threads()`, each falling back to `threads`
independently, and the CLI exposes `--sort-threads` / `--merge-threads` with the
same defaulting. This is purely a scheduling knob: output is byte-identical for
any split.

Tests cover byte-identical output against a plain `--threads` run across four
asymmetric splits (exercising both the wider-Phase-1 and wider-Phase-2 pool
sizings over a spilling multi-chunk merge), independent fallback of each
override, clamping of the active-worker count, and -- on a single pool, which is
the only way to show it -- that workers idled by a cap come back online when the
cap is raised.
@nh13
nh13 force-pushed the nh/feat-sort-merge-threads branch from 2fdf559 to 6585365 Compare July 21, 2026 20:49
@nh13
nh13 had a problem deploying to github-actions July 21, 2026 20:49 — with GitHub Actions Failure
@nh13

nh13 commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 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 commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 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 commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 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 commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 22, 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.

This branch was previously deployed

1 inactive deployment
github-actions — 65853651 Deployed Jul 22, 2026 by nh13 via coverage #2862
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