Skip to main content

wsi_rs/core/
read_control.rs

1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::{Arc, Mutex};
3use std::time::Duration;
4
5use crate::WsiError;
6
7/// Mapping selected by the validated fast DICOM encapsulated-frame indexer.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum DicomIndexMapping {
11    /// Extended offsets directly described one Item per frame.
12    ExtendedOffsetTableDirect,
13    /// Extended offsets were resolved after scanning Item headers.
14    ExtendedOffsetTableItems,
15    /// Basic offsets were resolved after scanning Item headers.
16    BasicOffsetTableItems,
17    /// A single frame owns every scanned fragment.
18    SingleFrameItems,
19    /// An empty table was safely resolved as one fragment per frame.
20    OneFragmentPerFrame,
21}
22
23/// Outcome of one DICOM encapsulated-frame index operation.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum DicomIndexOutcome {
27    /// The validated seek-based indexer completed successfully.
28    BuiltFast { mapping: DicomIndexMapping },
29    /// The seek-based path could not safely handle the input and yielded to
30    /// the token parser.
31    FastPathFallback,
32    /// The token parser completed the index after a fast-path fallback.
33    TokenFallback,
34    /// A previously completed index was reused.
35    Reused,
36}
37
38/// Typed timing emitted for one DICOM encapsulated-frame index operation.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40#[non_exhaustive]
41pub struct DicomIndexDiagnostic {
42    pub outcome: DicomIndexOutcome,
43    pub elapsed: Duration,
44}
45
46impl DicomIndexDiagnostic {
47    #[must_use]
48    pub const fn new(outcome: DicomIndexOutcome, elapsed: Duration) -> Self {
49        Self { outcome, elapsed }
50    }
51}
52
53/// Receives opt-in controlled-read diagnostics.
54pub type ReadDiagnosticSink = dyn Fn(DicomIndexDiagnostic) + Send + Sync;
55
56/// Cloneable cooperative cancellation signal for controlled reads.
57#[derive(Debug, Default)]
58struct ReadCancellationState {
59    cancelled: AtomicBool,
60    publication_gate: Mutex<()>,
61}
62
63#[derive(Debug, Clone, Default)]
64pub struct ReadCancellationToken {
65    state: Arc<ReadCancellationState>,
66}
67
68impl ReadCancellationToken {
69    #[must_use]
70    pub fn new() -> Self {
71        Self::default()
72    }
73
74    pub fn cancel(&self) {
75        let _publication = self
76            .state
77            .publication_gate
78            .lock()
79            .unwrap_or_else(|error| error.into_inner());
80        self.state.cancelled.store(true, Ordering::Release);
81    }
82
83    #[must_use]
84    pub fn is_cancelled(&self) -> bool {
85        self.state.cancelled.load(Ordering::Acquire)
86    }
87}
88
89/// Cooperative controls applied to a tile read without changing legacy APIs.
90#[derive(Clone, Default)]
91pub struct ReadControl {
92    cancellation: ReadCancellationToken,
93    diagnostic_sink: Option<Arc<ReadDiagnosticSink>>,
94}
95
96pub(crate) struct DeferredReadDiagnostics {
97    sink: Option<Arc<ReadDiagnosticSink>>,
98    diagnostics: Option<Arc<Mutex<Vec<DicomIndexDiagnostic>>>>,
99}
100
101impl DeferredReadDiagnostics {
102    pub(crate) fn flush(self) {
103        let Some(buffered) = self.diagnostics else {
104            return;
105        };
106        let diagnostics = {
107            let mut guard = buffered.lock().unwrap_or_else(|error| error.into_inner());
108            std::mem::take(&mut *guard)
109        };
110        if let Some(sink) = self.sink {
111            for diagnostic in diagnostics {
112                sink(diagnostic);
113            }
114        }
115    }
116}
117
118impl std::fmt::Debug for ReadControl {
119    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        formatter
121            .debug_struct("ReadControl")
122            .field("cancellation", &self.cancellation)
123            .field("diagnostics_enabled", &self.diagnostic_sink.is_some())
124            .finish()
125    }
126}
127
128impl ReadControl {
129    #[must_use]
130    pub const fn new(cancellation: ReadCancellationToken) -> Self {
131        Self {
132            cancellation,
133            diagnostic_sink: None,
134        }
135    }
136
137    /// Attaches an opt-in diagnostic sink while preserving the cancellation
138    /// token and controlled-read behavior.
139    #[must_use]
140    pub fn with_diagnostic_sink(mut self, sink: Arc<ReadDiagnosticSink>) -> Self {
141        self.diagnostic_sink = Some(sink);
142        self
143    }
144
145    #[must_use]
146    pub fn cancellation(&self) -> &ReadCancellationToken {
147        &self.cancellation
148    }
149
150    /// Returns whether this control has an attached diagnostic sink.
151    #[must_use]
152    pub fn diagnostics_enabled(&self) -> bool {
153        self.diagnostic_sink.is_some()
154    }
155
156    /// Records one diagnostic when a sink is attached. This is a no-op for
157    /// the default control.
158    pub fn record_diagnostic(&self, diagnostic: DicomIndexDiagnostic) {
159        if let Some(sink) = &self.diagnostic_sink {
160            sink(diagnostic);
161        }
162    }
163
164    pub(crate) fn defer_diagnostics(&self) -> (Self, DeferredReadDiagnostics) {
165        let Some(sink) = self.diagnostic_sink.as_ref() else {
166            return (
167                self.clone(),
168                DeferredReadDiagnostics {
169                    sink: None,
170                    diagnostics: None,
171                },
172            );
173        };
174        let diagnostics = Arc::new(Mutex::new(Vec::new()));
175        let diagnostic_sink = {
176            let diagnostics = Arc::clone(&diagnostics);
177            Some(Arc::new(move |diagnostic: DicomIndexDiagnostic| {
178                diagnostics
179                    .lock()
180                    .unwrap_or_else(|error| error.into_inner())
181                    .push(diagnostic);
182            }) as Arc<ReadDiagnosticSink>)
183        };
184        let deferred = DeferredReadDiagnostics {
185            sink: Some(Arc::clone(sink)),
186            diagnostics: Some(diagnostics),
187        };
188        (
189            Self {
190                cancellation: self.cancellation.clone(),
191                diagnostic_sink,
192            },
193            deferred,
194        )
195    }
196
197    /// Returns [`WsiError::Cancelled`] when cancellation has been requested.
198    ///
199    /// Format-specific readers can call this at safe cooperative boundaries
200    /// without changing the legacy read APIs.
201    pub fn check_cancelled(&self) -> Result<(), WsiError> {
202        if self.cancellation.is_cancelled() {
203            Err(WsiError::Cancelled)
204        } else {
205            Ok(())
206        }
207    }
208
209    pub(crate) fn publish_if_active<T>(&self, publish: impl FnOnce() -> T) -> Result<T, WsiError> {
210        let _publication = self
211            .cancellation
212            .state
213            .publication_gate
214            .lock()
215            .unwrap_or_else(|error| error.into_inner());
216        self.check_cancelled()?;
217        Ok(publish())
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use std::sync::{Arc, Mutex};
224    use std::time::Duration;
225
226    use super::{DicomIndexDiagnostic, DicomIndexMapping, DicomIndexOutcome, ReadControl};
227
228    #[test]
229    fn diagnostic_sink_is_opt_in_and_receives_typed_index_events() {
230        let disabled = ReadControl::default();
231        assert!(!disabled.diagnostics_enabled());
232
233        let events = Arc::new(Mutex::new(Vec::new()));
234        let captured = Arc::clone(&events);
235        let control = ReadControl::default().with_diagnostic_sink(Arc::new(move |event| {
236            captured.lock().unwrap().push(event);
237        }));
238        assert!(control.diagnostics_enabled());
239
240        let diagnostic = DicomIndexDiagnostic {
241            outcome: DicomIndexOutcome::BuiltFast {
242                mapping: DicomIndexMapping::BasicOffsetTableItems,
243            },
244            elapsed: Duration::from_millis(7),
245        };
246        control.record_diagnostic(diagnostic);
247
248        assert_eq!(events.lock().unwrap().as_slice(), &[diagnostic]);
249    }
250}