perf: speed up per-read validation hot loops (~25% less CPU) - #2
Draft
corneliusroemer-agent wants to merge 1 commit into
Draft
Conversation
Two hot-loop changes in the FASTQ validation path, no behaviour change. InsdcReadsValidator: the per-base IUPAC check ran effectiveBases.toUpperCase().toCharArray() and then looked each base up in a HashSet<Character>. That is two allocations per read plus a boxed HashMap lookup per base. Replaced with two case-folded boolean[128] tables indexed by the raw char. Bases containing non-ASCII fall back to the original String.toUpperCase() path, so exotic case folding (U+017F uppercases to 'S', a valid IUPAC code) still behaves exactly as before. PairedFastqReadsValidator: the read name and the pair index were derived from the same regex match but computed by two separate methods, so every read name was matched twice by both patterns. Match once and read both groups. Measured ~25% less CPU in steady state (~21% on a cold JVM) on 100k-read paired FASTQ. 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.
Not for merging. This is a record of an experiment, opened so the measurements and the patch don't get lost. It shows that a small, behaviour-preserving patch makes readtools' FASTQ validation meaningfully faster — about 25% less CPU — in case that's useful later, either here or upstream in
enasequence/readtools.Context: Loculus runs
java -jar readtools.jar <mate1> <mate2> --format FASTQonce per submitted entry in its raw-reads processing service, and that step is one of the larger per-entry costs. I profiled it to find out where the time actually goes, and two things stood out as cheap to fix.What's slow
Profiled with JFR on a warm JVM, 100k-read paired FASTQ (quick mode's cap), 540 samples, 99.8% attributed:
InsdcReadsValidator.validate— the per-base IUPAC checkFastqReaderline readingFastqReadsValidator.validateQualityScoresPairedFastqReadsValidatorread-name regexesWhat this changes
InsdcReadsValidator— the per-base check was:with
iupacSetaHashSet<Character>. That's two allocations per read (an uppercasedStringand achar[]), plus a boxed lookup per base. At 64bp × 100k reads × 2 mates that's ~12.8M boxedHashMaplookups —HashMap.getNodewas the single hottest leaf frame in the profile.Replaced with two case-folded
boolean[128]tables indexed by the raw char.PairedFastqReadsValidator—getNonCasavaReadNameWithoutIndex()andgetNonCasavaReadIndex()were called back to back on the same read name, and each ran both patterns. So every read name was matched twice over. Now matched once, with both capture groups read from the one match. The four helpers this made dead are removed.Behaviour
No intended behaviour change, including on the rejection path.
One case needed care.
String.toUpperCase()means U+017F (long s,ſ) folds toS, which is a valid IUPAC code — so the original accepts it. A naive lookup table rejects it, which would fail submissions ENA's own webin-cli accepts, since it runs this same code. So reads containing any non-ASCII character fall back to the originaltoUpperCase()path. Pure-ASCII reads — i.e. all real data — take the fast path.Verified byte-identical CLI output against the released
v1.0.0jar across 20 comparisons: 1-read / 5MB / 50MB / 500MB inputs, gzipped and plain, four distinct 100k-read pairs, single-end, and edge cases covering lowercase bases, mixed case, the full IUPAC alphabet, an invalid base (rejected identically), and the non-ASCII case above. The 227 tests in this repo pass.Numbers
Measured as steady-state CPU time rather than wall clock — the test box had other JVMs on it and wall clock was too noisy to trust. Four distinct 100k-read pairs rotated through one JVM, pinned to a single core, JDK 21:
~25% less CPU in steady state, ~21% on a cold JVM.
Two caveats on those numbers: they're from an aarch64 container, not the deployment target, so treat the ratio as the transferable part; and they're a validation-call-level measurement, not end-to-end submission latency.
Things found along the way that aren't in this PR
quick = true, with--no-quickmarkedhidden), so this isn't a Loculus-specific shortcut.mainis only ~0.036s, but a cold call costs ~0.7-0.85s more than a warm one. Keeping a JVM warm is worth roughly 50% per call on top of this patch, which is a much bigger change than this one and isn't attempted here.