perf(qc): compress chopper output with bgzip (8x faster QC stage) + wait on every pipeline stage - #88
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.
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.
|
Thanks for this, looks like a big cleanup and runtime improvements. Looks like it will be used on Linux only given the dependency tree - I am reticent to add some dependency origami to make it pinned for macOS too, it will introduce fragility. There's also a new chopper parameter warning that the review detected, but I will fix that on a separate PR I've had LLM-augmented review of your PR - looks good to me. The main thing is one revert needed in the test data if Thanks again mate George ClaudeWhat I verifiedThe speedup is real — I measured a bit more than you did. 304 MiB ONT fastq, 8 cores:
~10x, and smaller output. I also confirmed the BGZF output passes The process-handling fix is the more valuable half of this PR, and I don't think the title gives it enough credit. Running chopper directly on thread '<unnamed>' panicked at src/main.rs:443:45:
ERROR: problem parsing fastq record: IncompleteRecord
exit code: 101Because only Your fixture repair was correct. I checked: One thing to revert
I initially assumed a test had clobbered it, since Nothing reads it as an input so it broke nothing, but it drops 100 KB of real ONT reads. I have the revert plus one small follow-up on a branch — happy to push them to your branch, or you can just Heads-up on the bgzip dependency
Almost — but it's transitive, and it doesn't hold on Apple Silicon. bioconda's
Two consequences worth knowing: your benchmark won't reproduce on an M-series Mac (you'll silently get the I looked at just adding One nit (already applied on my branch)In the Separately (not yours)While testing I noticed chopper 0.13.0 warns |
Hey @gbouras13 ! thank you for plassembler! I've been using it recently in a pipeline which I'm trying to optimise. I noticed the below and wondered whether you'd like to incorporate it, seemed worthwhile to me :)
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.Suggested 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 acatprocess just to copy the file into a pipe — the file is handed to chopper's stdin directly.