max / audiofiles
1 file changed,
+58 insertions,
-11 deletions
| @@ -32,32 +32,74 @@ | |||
| 32 | 32 | use std::sync::atomic::{AtomicBool, Ordering}; | |
| 33 | 33 | use std::sync::{mpsc, Arc, Mutex}; | |
| 34 | 34 | use std::thread; | |
| 35 | + | use std::time::Duration; | |
| 36 | + | ||
| 37 | + | /// Bound on the per-worker event channel. The channel is the one place an | |
| 38 | + | /// unbounded queue could balloon — the analysis batch fans out over rayon and | |
| 39 | + | /// emits a per-sample event, which on a 100k-sample import outruns a slow GUI | |
| 40 | + | /// drain. A bounded channel applies backpressure (the producer waits) instead of | |
| 41 | + | /// growing without limit; the cap is generous so a normally-draining GUI never | |
| 42 | + | /// blocks a worker. | |
| 43 | + | const EVENT_CHANNEL_CAP: usize = 4096; | |
| 35 | 44 | ||
| 36 | 45 | /// Sink handed to a worker step so it can emit zero or more events per command. | |
| 37 | 46 | /// | |
| 38 | - | /// Cloneable and `Sync` (it wraps an `mpsc::Sender`), so a step may share it | |
| 39 | - | /// across rayon worker threads (e.g. the analysis batch fan-out). | |
| 47 | + | /// Cloneable and `Sync` (it wraps an `mpsc::SyncSender`), so a step may share it | |
| 48 | + | /// across rayon worker threads (e.g. the analysis batch fan-out). [`emit`](Self::emit) | |
| 49 | + | /// applies bounded backpressure but never blocks past cancellation. | |
| 40 | 50 | pub struct EventSink<Ev> { | |
| 41 | - | tx: mpsc::Sender<Ev>, | |
| 51 | + | tx: mpsc::SyncSender<Ev>, | |
| 52 | + | cancel: Arc<AtomicBool>, | |
| 42 | 53 | } | |
| 43 | 54 | ||
| 44 | 55 | impl<Ev> Clone for EventSink<Ev> { | |
| 45 | 56 | fn clone(&self) -> Self { | |
| 46 | - | Self { tx: self.tx.clone() } | |
| 57 | + | Self { | |
| 58 | + | tx: self.tx.clone(), | |
| 59 | + | cancel: Arc::clone(&self.cancel), | |
| 60 | + | } | |
| 47 | 61 | } | |
| 48 | 62 | } | |
| 49 | 63 | ||
| 50 | 64 | impl<Ev> EventSink<Ev> { | |
| 51 | - | /// Emit one event back to the handle. Silently dropped if the handle is gone. | |
| 65 | + | /// Emit one event back to the handle. | |
| 66 | + | /// | |
| 67 | + | /// The event channel is bounded, so this applies backpressure when the GUI | |
| 68 | + | /// hasn't drained yet: it retries rather than dropping the event or growing | |
| 69 | + | /// the queue. It must never block *past cancellation*, though — a worker stuck | |
| 70 | + | /// here while the handle is being dropped would hang the join on quit — so once | |
| 71 | + | /// the cancel flag is set it drops the event and returns. Also returns if the | |
| 72 | + | /// receiver is gone (handle dropped). | |
| 52 | 73 | pub fn emit(&self, ev: Ev) { | |
| 53 | - | let _ = self.tx.send(ev); | |
| 74 | + | let mut pending = ev; | |
| 75 | + | loop { | |
| 76 | + | match self.tx.try_send(pending) { | |
| 77 | + | Ok(()) => return, | |
| 78 | + | Err(mpsc::TrySendError::Full(returned)) => { | |
| 79 | + | if self.cancel.load(Ordering::Acquire) { | |
| 80 | + | return; // shutting down — drop the event rather than hang | |
| 81 | + | } | |
| 82 | + | pending = returned; | |
| 83 | + | thread::sleep(Duration::from_millis(1)); | |
| 84 | + | } | |
| 85 | + | Err(mpsc::TrySendError::Disconnected(_)) => return, | |
| 86 | + | } | |
| 87 | + | } | |
| 54 | 88 | } | |
| 55 | 89 | ||
| 56 | 90 | /// Create a sink paired with a receiver. For tests that exercise functions | |
| 57 | - | /// taking an `&EventSink` directly, outside a spawned worker. | |
| 91 | + | /// taking an `&EventSink` directly, outside a spawned worker. The test must | |
| 92 | + | /// stay under [`EVENT_CHANNEL_CAP`] unsent events or drain, since the standalone | |
| 93 | + | /// sink has no cancel flag to release a full-channel wait. | |
| 58 | 94 | pub fn channel() -> (Self, mpsc::Receiver<Ev>) { | |
| 59 | - | let (tx, rx) = mpsc::channel(); | |
| 60 | - | (Self { tx }, rx) | |
| 95 | + | let (tx, rx) = mpsc::sync_channel(EVENT_CHANNEL_CAP); | |
| 96 | + | ( | |
| 97 | + | Self { | |
| 98 | + | tx, | |
| 99 | + | cancel: Arc::new(AtomicBool::new(false)), | |
| 100 | + | }, | |
| 101 | + | rx, | |
| 102 | + | ) | |
| 61 | 103 | } | |
| 62 | 104 | } | |
| 63 | 105 | ||
| @@ -179,8 +221,10 @@ where | |||
| 179 | 221 | State: 'static, | |
| 180 | 222 | InitErr: 'static, | |
| 181 | 223 | { | |
| 224 | + | // Commands stay unbounded (low-volume, one per user action — blocking the GUI | |
| 225 | + | // on a command send would be worse than a tiny queue). Only events are bounded. | |
| 182 | 226 | let (cmd_tx, cmd_rx) = mpsc::channel::<Cmd>(); | |
| 183 | - | let (event_tx, event_rx) = mpsc::channel::<Ev>(); | |
| 227 | + | let (event_tx, event_rx) = mpsc::sync_channel::<Ev>(EVENT_CHANNEL_CAP); | |
| 184 | 228 | let cancel = Arc::new(AtomicBool::new(false)); | |
| 185 | 229 | let cancel_worker = Arc::clone(&cancel); | |
| 186 | 230 | ||
| @@ -188,7 +232,10 @@ where | |||
| 188 | 232 | .name(name.to_string()) | |
| 189 | 233 | .spawn(move || { | |
| 190 | 234 | let ctx = WorkerCtx { | |
| 191 | - | sink: EventSink { tx: event_tx }, | |
| 235 | + | sink: EventSink { | |
| 236 | + | tx: event_tx, | |
| 237 | + | cancel: Arc::clone(&cancel_worker), | |
| 238 | + | }, | |
| 192 | 239 | cancel: cancel_worker, | |
| 193 | 240 | }; | |
| 194 | 241 | let mut state = match init() { |