Skip to content

refactor: drop Transcript::read_seq, a public field nothing reads - #170

Open
BenjaminDEMAILLE wants to merge 2 commits into
scverse:mainfrom
BenjaminDEMAILLE:bd/drop-dead-read-seq
Open

refactor: drop Transcript::read_seq, a public field nothing reads#170
BenjaminDEMAILLE wants to merge 2 commits into
scverse:mainfrom
BenjaminDEMAILLE:bd/drop-dead-read-seq

Conversation

@BenjaminDEMAILLE

Copy link
Copy Markdown
Contributor

Transcript::read_seq is filled with a full copy of the read at every finalised alignment, and nothing in the crate reads it.

How I know nothing reads it

Not from grepping. I deleted the field and asked the compiler. All 97 resulting errors are struct literals or assignments:

error[E0560]: struct `Transcript` has no field named `read_seq`
  --> src/align/stitch.rs:2158:9    read_seq: read_seq.to_vec(),
error[E0609]: no field `read_seq` on type `Transcript`
  --> src/align/stitch.rs:2698:28   transcript.read_seq = read_seq.to_vec();
  --> src/align/read_align.rs:892   t1.read_seq = mate1_seq.to_vec();
  ...

Not one read site, in src or in tests. The three places that looked like readers on a grep are something else: io/sam.rs:265 uses a local parameter of the same name, and chimeric/output.rs:148,157 read ChimericAlignment::read_seq, a different struct filled from its own parameter in chimeric/detect.rs.

What it costs

Measured on 200 000 real reads (ERR12389696, yeast) at 8 threads, with the global allocator wrapped in a counter:

allocations bytes
before 200 093 070 401 267 330 431
after 184 966 576 397 400 813 793

15.1 million allocations removed, 7.6% of the total, and 3.87 GB of copying. About 50 transcripts are finalised per read, each copying the whole read.

Wall clock: no change, and that is the interesting part

Six interleaved rounds, 2M reads, --runThreadN 16, 87-92% CPU idle:

median
before 20.90 s
after 20.65 s

Direction mixed, inside the run-to-run spread. One round had a clear outlier which I have not excluded from the medians.

So removing 7.6% of all allocations moves nothing measurable. mimalloc is fast, and it is already tuned in src/main.rs. That calibrates #168 downward: I estimated an ~11% ceiling for allocation work from the profile's mimalloc share, and this says that estimate is too generous. I would not spend a new dependency on the rest of it without a workload that shows the cost, and I have updated #168 accordingly.

The reason to take this change is that the field is dead weight, which CONTRIBUTING.md rules out on its own terms. The allocations are a bonus, not the argument.

One thing needing your call

Transcript::read_seq is pub, so this is an API removal. Nothing outside the crate can be relying on its contents, since it is only ever written, but the field disappearing is still a breaking change for anyone naming it. Say the word if you would rather keep it as a deprecated always-empty field for a release.

Verification

Output-neutral: SAM byte-identical on 200 000 real reads against origin/main, whole file including header.

Gate: 560 lib + 26 integration tests, cargo clippy --all-targets -- -D warnings, cargo fmt --check, all green.

BenjaminDEMAILLE and others added 2 commits July 30, 2026 23:52
The field was filled with a full copy of the read at every finalised
transcript and assigned again in four more places, and no code in the
crate reads it. Not "reads it rarely": the compiler was asked, and after
deleting the field every one of the 97 resulting errors is a struct
literal or an assignment. There is no read site, in `src` or in tests.

Measured on 200k real reads at 8 threads: about 50 transcripts are
finalised per read, so removing it takes **15.1 million allocations off a
200 million total** (7.6%) and 3.87 GB of copying.

Wall clock does not move, and that is worth recording rather than hiding:
six interleaved rounds at 87-92% CPU idle give medians 20.90 s against
20.65 s with the direction mixed, inside the run-to-run spread. mimalloc
is fast enough that seventy-five small allocations per read do not
surface. The reason to remove it is that it is dead weight, not that it
is slow.

That number also calibrates scverse#168 downward: if removing 7.6% of the
allocations changes nothing measurable, the rest of the allocation
programme is unlikely to be worth a new dependency.

`Transcript::read_seq` is `pub`, so this is an API removal and needs
sign-off. Nothing outside the crate can be relying on its contents being
meaningful, though, since it is only ever written.

Output-neutral: SAM byte-identical on 200k real reads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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