Pace and count the mapper's writes, not just the sort's - #52
Merged
Conversation
…n their own items A four-angle review of the realignment surface, prompted by two bugs that both traced to the same rule existing in several copies. **The duplication that caused those bugs was still there.** `DEFAULT_TARGET_BUILD` was exported so the UI could name the build it offers, and then only the Simple mode modal used it: both Advanced cards still carried the literal, and one compared builds with `eq_ignore_ascii_case` where the job uses `builds_match`, which trims. A stored " chm13v2.0" was offered by the card and refused by the job. `is_target_build` is now `pub` and the UI asks it instead of spelling out a comparison. The per-alignment card was also missing the `bam_path` condition, so it offered to re-map rows with no file. **Four doc comments documented the wrong item** — a new item inserted between an existing doc block and its function, four times, twice on public API. The worst had one sentence severed across two items and its tail orphaned twenty lines below. **Measured wins, benchmarked rather than guessed.** The revert's FASTQ writer issued seven `write_all` calls per read into the gzip encoder, paying the encoder's per-call overhead seven times: 1,882 ns/record against 539 for the same bytes assembled once, ~13 minutes of CPU per realignment. `realignable_in_project` was an N+1 over members where the grouped query already existed: 2.7 ms against 17.7 ms on a 2,504-member project, twice per batch. **And one bug, fixed at its own altitude.** Simple mode's running card matched on alignment id, and once the offer was gone it had nothing to match against — so it claimed *any* running job, and a page open on one person announced another person's realignment as theirs. `RealignState` now carries the owning subject, resolved once per job by `App::subject_of_alignment`, and the card matches on that. Tightening the old predicate would have broken the Done card instead; the ownership question needed answering where the mapping lives. Also: `preflight`/`resume_preflight` were the same function twice, `discard` had two implementations, the project card cloned a Vec per frame to read its length, finished states carried progress fields nobody reads, and a user-visible string held an 18-space run. 983 tests, clippy clean, fmt clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The write pacing and byte accounting that came out of the 2026-08-13 WindowServer teardown shipped as a module inside navigator-analysis. That crate holds the revert, sort, markdup and CRAM stages — but not the mapper, which lives in navigator-align, a leaf crate that cannot depend on it and should not. So half the pipeline was covered. The mapper's ~60 GB mapped.bam, the 8.93 GB minimizer index, and the final CRAM all went straight to the page cache, unpaced and uncounted. The phase-5 run log reads 0 MB/s through the longest stage in the job for exactly that reason: not a quiet stage, an unmeasured one. And pacing the sort while the mapper wrote unpaced left the original failure mode reachable, because the resource notice macOS filed was against the process, not against a stage. Move PacedFile, the byte counter and ResourceWatch into navigator-resource, a leaf crate both halves depend on. A counter only means something if there is exactly one of it. navigator-app takes it directly rather than reaching through navigator-analysis, since neither half is the owner. Also, from reading the same write path: - Every one of those writers sat behind BufWriter's 8 KB default while the encoders above hand down 64 KB BGZF blocks and multi-MB CRAM containers, so the buffer coalesced nothing. Now 1 MB, matching bamio. - Each stage output is synced once at the end. This matters most for mapped.bam, whose BGZF end-of-file block is what a resumed run reads to decide the file can be trusted — trusting a marker that was still a page-cache promise is how 59 GB was destroyed during phase 5 — and for the .mmi, whose atomic rename otherwise publishes contents the disk has not acknowledged. - The CRAM encoders took build_from_path, which opens the file themselves. build_from_writer, so the writer is ours to wrap. navigator-align carries the regression test: write a BAM through AlignmentWriter, assert the shared counter moved. It would have failed before this change. finalize.rs's .bai is left unpaced on purpose — a few MB, where the syscalls and the log noise would buy nothing. 984 tests pass (983 + the new one); clippy and fmt clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
The write pacing and byte accounting that came out of the 2026-08-13 WindowServer teardown shipped as a module inside
navigator-analysis. That crate holds the revert, sort, markdup and CRAM stages — but not the mapper, which lives innavigator-align, a leaf crate that cannot depend on it and should not.So half the pipeline was covered. Unpaced and uncounted:
mapped.bam.mmi)The phase-5 run log reads
0 MB/sstraight through the longest stage in the job for exactly that reason — not a quiet stage, an unmeasured one. And pacing the sort while the mapper wrote unpaced left the original failure mode reachable, because the resource notice macOS filed was against the process, not against a stage. Fixing half the writers was closer to no fix than it looked.The change
navigator-resource, a new leaf crate holdingPacedFile, the process-wide byte counter, andResourceWatch, depended on by both halves of the pipeline. It has to be a crate rather than a module: a counter only means something if there is exactly one of it.navigator-apptakes it directly rather than reaching throughnavigator-analysis, since neither half is the owner. The move itself is agit mv— 89% similarity, doc changes only, no logic.Both CRAM encoders were taking
build_from_path, which opens the file themselves;build_from_writermakes the writer ours to wrap.Two things from reading the same write path
BufWriter's 8 KB default while the encoders above hand down 64 KB BGZF blocks and multi-MB CRAM containers, so the buffer was coalescing nothing. Now 1 MB, matchingbamio.mapped.bam: its BGZF end-of-file block is precisely what a resumed run reads to decide the file can be trusted, and trusting a marker that was still only a page-cache promise is how 59 GB was destroyed during phase 5. Also the.mmi, whose atomic rename otherwise publishes contents the disk has not acknowledged.Test
navigator-aligncarries the regression test — write a BAM throughAlignmentWriter, assert the shared counter moved. Greater-than rather than an exact figure, since the counter is shared with anything else in the test binary; the claim under test is that these bytes are counted at all. It would have failed before this change.984 pass, 0 fail (983 + the new one). Clippy clean under
-D warnings, fmt clean.Deliberately not done
finalize.rs's.baiis left unpaced — a few MB, where the syscalls and log noise would buy nothing.NAVIGATOR_SORT_MBstill defaults to 512 MB (688 spill runs on a 128 GB machine).🤖 Generated with Claude Code