Skip to main content

koth_ms/io/
prefetch.rs

1//! Background-thread prefetching for streaming spectrum sources.
2//!
3//! mzML reading is decompression + XML parsing bound (~half of a typical
4//! run is spent inside `miniz_oxide::inflate` and `quick_xml`), and it runs
5//! on the same thread that consumes the spectra for hill detection. Those two
6//! costs are independent, so serializing them wastes wall-clock equal to the
7//! smaller of the two.
8//!
9//! [`prefetch`] moves the source iterator onto a dedicated reader thread that
10//! decodes spectra into a bounded channel while the caller runs `process_scan`
11//! on the previously-decoded one. Spectra are yielded in the exact same order
12//! (single producer → single consumer FIFO), so hill detection stays
13//! byte-for-byte deterministic — this only overlaps the two stages in time.
14
15use std::sync::mpsc::{sync_channel, Receiver};
16use std::thread::JoinHandle;
17
18/// Run a fallible producer on a background thread. Returning `false` from the
19/// supplied emitter means the consumer has stopped; producers must stop too.
20/// Initialization and decode errors are delivered through the same bounded
21/// channel as the data, so they cannot silently truncate a successful result.
22pub fn try_prefetch<T, E, F>(producer: F, capacity: usize) -> Prefetch<Result<T, E>>
23where
24    T: Send + 'static,
25    E: Send + 'static,
26    F: FnOnce(&mut dyn FnMut(T) -> bool) -> Result<(), E> + Send + 'static,
27{
28    let (tx, rx) = sync_channel(capacity);
29    let handle = std::thread::Builder::new()
30        .name("koth-reader".into())
31        .spawn(move || {
32            if let Err(error) = producer(&mut |item| tx.send(Ok(item)).is_ok()) {
33                let _ = tx.send(Err(error));
34            }
35        })
36        .expect("failed to spawn reader thread");
37    Prefetch {
38        rx: Some(rx),
39        handle: Some(handle),
40    }
41}
42
43/// Run `source` on a dedicated background thread, buffering up to `capacity`
44/// items in a bounded channel. The returned iterator yields the same items in
45/// the same order.
46///
47/// The channel is bounded so a fast reader can't run away with memory: once
48/// `capacity` spectra are queued the reader blocks until the consumer drains
49/// one. A producer panic is re-raised on the consumer thread (see
50/// [`Prefetch::next`]) so parse failures still fail loud.
51pub fn prefetch<I>(source: I, capacity: usize) -> Prefetch<I::Item>
52where
53    I: Iterator + Send + 'static,
54    I::Item: Send + 'static,
55{
56    let (tx, rx) = sync_channel(capacity);
57    let handle = std::thread::Builder::new()
58        .name("koth-prefetch".into())
59        .spawn(move || {
60            for item in source {
61                // Err means the consumer was dropped — stop reading early.
62                if tx.send(item).is_err() {
63                    break;
64                }
65            }
66        })
67        .expect("failed to spawn prefetch thread");
68    Prefetch {
69        rx: Some(rx),
70        handle: Some(handle),
71    }
72}
73
74/// Iterator over spectra produced by a background reader thread.
75pub struct Prefetch<T> {
76    rx: Option<Receiver<T>>,
77    handle: Option<JoinHandle<()>>,
78}
79
80impl<T> Prefetch<T> {
81    /// Drop the receiver (signalling the producer to stop) and join the reader
82    /// thread, re-raising its panic if it panicked.
83    fn finish(&mut self) {
84        // Drop the receiver first so a producer blocked on a full channel
85        // unblocks (its `send` returns Err); otherwise the join would deadlock.
86        self.rx = None;
87        if let Some(h) = self.handle.take() {
88            if let Err(panic) = h.join() {
89                std::panic::resume_unwind(panic);
90            }
91        }
92    }
93}
94
95impl<T> Iterator for Prefetch<T> {
96    type Item = T;
97
98    fn next(&mut self) -> Option<T> {
99        match self.rx.as_ref().and_then(|rx| rx.recv().ok()) {
100            Some(item) => Some(item),
101            None => {
102                // Sender dropped → source exhausted (or the reader panicked).
103                // Join so a producer panic surfaces here instead of silently
104                // truncating the spectrum stream.
105                self.finish();
106                None
107            }
108        }
109    }
110}
111
112impl<T> Drop for Prefetch<T> {
113    fn drop(&mut self) {
114        // Best-effort cleanup if the consumer abandons iteration early. Ignore
115        // a producer panic here — re-raising during an unwind would abort.
116        self.rx = None;
117        if let Some(h) = self.handle.take() {
118            let _ = h.join();
119        }
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use std::sync::{
127        atomic::{AtomicUsize, Ordering},
128        Arc,
129    };
130    use std::time::Duration;
131
132    #[test]
133    fn fallible_producer_is_bounded_and_cancels_on_drop() {
134        let count = Arc::new(AtomicUsize::new(0));
135        let produced = count.clone();
136        let (ready_tx, ready_rx) = std::sync::mpsc::channel();
137        let stream = try_prefetch::<usize, (), _>(
138            move |emit| {
139                for i in 0..1000 {
140                    produced.fetch_add(1, Ordering::SeqCst);
141                    if i == 4 {
142                        ready_tx.send(()).unwrap();
143                    }
144                    if !emit(i) {
145                        break;
146                    }
147                }
148                Ok(())
149            },
150            4,
151        );
152        ready_rx.recv_timeout(Duration::from_secs(5)).unwrap();
153        // Four queued values and one blocked send, independent of run length.
154        assert_eq!(count.load(Ordering::SeqCst), 5);
155        drop(stream); // must unblock the send and join the producer
156        assert_eq!(count.load(Ordering::SeqCst), 5);
157    }
158
159    #[test]
160    fn fallible_producer_delivers_ordered_values_then_error() {
161        let mut stream = try_prefetch(
162            |emit| {
163                assert!(emit(1));
164                assert!(emit(2));
165                Err("decode failure")
166            },
167            1,
168        );
169        assert_eq!(stream.next(), Some(Ok(1)));
170        assert_eq!(stream.next(), Some(Ok(2)));
171        assert_eq!(stream.next(), Some(Err("decode failure")));
172        assert_eq!(stream.next(), None);
173        assert_eq!(stream.next(), None);
174    }
175
176    #[test]
177    fn producer_panic_reaches_consumer() {
178        let mut stream = try_prefetch::<(), (), _>(|_| panic!("reader panic"), 1);
179        assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| stream.next())).is_err());
180    }
181}