perf(qc): compress chopper output with bgzip (8x faster QC stage) + wait on every pipeline stage - #2
perf(qc): compress chopper output with bgzip (8x faster QC stage) + wait on every pipeline stage#2sanjaynagi-eit wants to merge 2 commits into
Conversation
The chopper QC chain is gunzip -c | chopper --threads N | gzip. Only the compressor is parallel-unaware, and it dominated: on a 300 MB ONT fastq the whole chain took 47.7s, of which gunzip+chopper was 5.4s and gzip was 42s. Use bgzip -@ threads instead. bgzip ships with htslib/samtools, already a hard plassembler dependency, so this adds nothing to the environment; BGZF is a valid gzip stream, so flye, minimap2 and chopper read the result unchanged. It is also slightly smaller here (277 vs 293 MiB). Falls back to gzip if bgzip is somehow missing. gunzip | chopper (no compression) 5.4s ... | gzip (before) 47.7s 293.4 MiB ... | bgzip -@ 8 (after) 5.9s 276.7 MiB Verified content-identical: with chopper --threads 1 (chopper is not order-deterministic above 1 thread) the decompressed output of the two chains is byte-for-byte equal. Also fixes the process handling. Only gzip_proc was waited on, so a failing gunzip or chopper was silently swallowed - the bare 'except: logger.error' never saw it - and left zombies behind. Every stage is now waited on and a non-zero exit is reported with the stage name and log path. The parent also closes its copy of each upstream pipe read end, and plain (uncompressed) input is handed to chopper directly instead of through a pointless 'cat' process. gzip_file, used by --skip_qc, went through python's gzip module, which is slower still; it now uses the same compressor with a python fallback.
4cc79b8 to
53f72f0
Compare
|
Behaviour change worth calling out explicitly, found while testing this against real data. The process-handling fix in this PR is not only cosmetic. I had a staged ONT FASTQ that turned out to be a truncated gzip file (
So this changes corrupt input from "quietly produces a plausible-looking wrong answer" to "fails". I think that is clearly correct, but it is a real behaviour change: batches containing corrupt reads that previously appeared to succeed will now report failures. |
tests/test_data/end_to_end/input_half.fastq.gz is 778 lines - 194.5 FASTQ records. It ends mid-record, with a header and a sequence line but no + or quality line, so chopper rejects it (IncompleteRecord) and exits 101 after emitting 193 complete reads. That has been true since the fixture was committed; test_plassembler_case_depth_filter_some passed only because the chopper failure was swallowed. With this PR's process handling it is detected, and the test fails. Drop the two orphan lines. chopper then exits 0 and emits the same 193 records, so nothing the test exercises changes - and the test asserts only that the command exits 0, so no expectation depends on the fixture's exact contents. The full suite (including slow) is 169 passed / 1 failed before this commit and 170 passed after.
|
Follow-up: this PR surfaced a pre-existing fault in the repository's own test data. Running the full suite (including
That has presumably been true since the fixture was committed. The test passed only because the chopper failure was swallowed — which is exactly the bug this PR fixes. I have repaired the fixture by dropping the two orphan lines. chopper then exits 0 and emits the same 193 records, so nothing the test exercises changes, and that test asserts only that the command exits 0 — no expectation depends on the fixture's exact contents. Full suite: 169 passed / 1 failed before, 170 passed after. If you would rather keep the fixture as-is and handle malformed input differently (warn-and-continue rather than fail), that is a reasonable alternative policy — but it should be a deliberate choice, not the accident of an unwaited-on process. Happy to switch it if you prefer. |
Problem
chopper()builds the chaingunzip -c reads.gz | chopper --threads N | gzip. Every stage is parallel-aware except the last one, andgzipdominated the stage completely.Measured on a real 300 MB ONT fastq, 8 threads:
gunzip | chopper(no compression)... | gzip(current)... | bgzip -@ 8So ~42 of the 47.7 seconds were serial gzip. On the same isolate the whole
plassembler longrun is 499 s, making this ~8% of total wall clock for free.Change
Use
bgzip -@ threadsas the compressor.check_dependencies.gunzipand python'sgzipmodule all read it unchanged.chopper_long_reads.fastq.gzis in any case a purely internal intermediate thatremove_intermediate_filesdeletes.gzipif bgzip is somehow absent.Verified content-identical: with
chopper --threads 1the decompressed output of the old and new chains is byte-for-byte equal. (Above 1 thread chopper does not preserve read order, so only the single-threaded comparison is meaningful — that is pre-existing chopper behaviour, not something this PR changes.)gzip_file(), used by--skip_qc, went through python'sgzipmodule — slower still — and now uses the same compressor with a python fallback.Bug fix in the same function
Only
gzip_procwas ever waited on.source_procandchopper_procwere neverwait()ed and the parent never closed its copies of their pipe read ends, so:gunziporchopperwas silently swallowed (the bareexcept:never saw it, because the exception never happened — the process just exited non-zero), leaving an empty or truncated read file that the rest of the run happily consumed;Every stage is now waited on and a non-zero exit is reported with the stage name and the log path.
test_chopper_reports_a_failing_stagecovers this: feeding plain fastq withgzip_flag=Trueused to pass silently and now raises.The uncompressed-input branch also no longer spawns a
catprocess just to copy the file into a pipe — the file is handed to chopper's stdin directly.Tests
New
tests/test_qc.py: compressor selection and fallback,gzip_fileround-trip through python's gzip reader, chopper output well-formedness for both gzipped and plain input, and the failing-stage regression above. All existing tests still pass.