use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::WsiError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DicomIndexMapping {
ExtendedOffsetTableDirect,
ExtendedOffsetTableItems,
BasicOffsetTableItems,
SingleFrameItems,
OneFragmentPerFrame,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DicomIndexOutcome {
BuiltFast { mapping: DicomIndexMapping },
FastPathFallback,
TokenFallback,
Reused,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct DicomIndexDiagnostic {
pub outcome: DicomIndexOutcome,
pub elapsed: Duration,
}
impl DicomIndexDiagnostic {
#[must_use]
pub const fn new(outcome: DicomIndexOutcome, elapsed: Duration) -> Self {
Self { outcome, elapsed }
}
}
pub type ReadDiagnosticSink = dyn Fn(DicomIndexDiagnostic) + Send + Sync;
#[derive(Debug, Default)]
struct ReadCancellationState {
cancelled: AtomicBool,
publication_gate: Mutex<()>,
}
#[derive(Debug, Clone, Default)]
pub struct ReadCancellationToken {
state: Arc<ReadCancellationState>,
}
impl ReadCancellationToken {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn cancel(&self) {
let _publication = self
.state
.publication_gate
.lock()
.unwrap_or_else(|error| error.into_inner());
self.state.cancelled.store(true, Ordering::Release);
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.state.cancelled.load(Ordering::Acquire)
}
}
#[derive(Clone, Default)]
pub struct ReadControl {
cancellation: ReadCancellationToken,
diagnostic_sink: Option<Arc<ReadDiagnosticSink>>,
}
pub(crate) struct DeferredReadDiagnostics {
sink: Option<Arc<ReadDiagnosticSink>>,
diagnostics: Option<Arc<Mutex<Vec<DicomIndexDiagnostic>>>>,
}
impl DeferredReadDiagnostics {
pub(crate) fn flush(self) {
let Some(buffered) = self.diagnostics else {
return;
};
let diagnostics = {
let mut guard = buffered.lock().unwrap_or_else(|error| error.into_inner());
std::mem::take(&mut *guard)
};
if let Some(sink) = self.sink {
for diagnostic in diagnostics {
sink(diagnostic);
}
}
}
}
impl std::fmt::Debug for ReadControl {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ReadControl")
.field("cancellation", &self.cancellation)
.field("diagnostics_enabled", &self.diagnostic_sink.is_some())
.finish()
}
}
impl ReadControl {
#[must_use]
pub const fn new(cancellation: ReadCancellationToken) -> Self {
Self {
cancellation,
diagnostic_sink: None,
}
}
#[must_use]
pub fn with_diagnostic_sink(mut self, sink: Arc<ReadDiagnosticSink>) -> Self {
self.diagnostic_sink = Some(sink);
self
}
#[must_use]
pub fn cancellation(&self) -> &ReadCancellationToken {
&self.cancellation
}
#[must_use]
pub fn diagnostics_enabled(&self) -> bool {
self.diagnostic_sink.is_some()
}
pub fn record_diagnostic(&self, diagnostic: DicomIndexDiagnostic) {
if let Some(sink) = &self.diagnostic_sink {
sink(diagnostic);
}
}
pub(crate) fn defer_diagnostics(&self) -> (Self, DeferredReadDiagnostics) {
let Some(sink) = self.diagnostic_sink.as_ref() else {
return (
self.clone(),
DeferredReadDiagnostics {
sink: None,
diagnostics: None,
},
);
};
let diagnostics = Arc::new(Mutex::new(Vec::new()));
let diagnostic_sink = {
let diagnostics = Arc::clone(&diagnostics);
Some(Arc::new(move |diagnostic: DicomIndexDiagnostic| {
diagnostics
.lock()
.unwrap_or_else(|error| error.into_inner())
.push(diagnostic);
}) as Arc<ReadDiagnosticSink>)
};
let deferred = DeferredReadDiagnostics {
sink: Some(Arc::clone(sink)),
diagnostics: Some(diagnostics),
};
(
Self {
cancellation: self.cancellation.clone(),
diagnostic_sink,
},
deferred,
)
}
pub fn check_cancelled(&self) -> Result<(), WsiError> {
if self.cancellation.is_cancelled() {
Err(WsiError::Cancelled)
} else {
Ok(())
}
}
pub(crate) fn publish_if_active<T>(&self, publish: impl FnOnce() -> T) -> Result<T, WsiError> {
let _publication = self
.cancellation
.state
.publication_gate
.lock()
.unwrap_or_else(|error| error.into_inner());
self.check_cancelled()?;
Ok(publish())
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use std::time::Duration;
use super::{DicomIndexDiagnostic, DicomIndexMapping, DicomIndexOutcome, ReadControl};
#[test]
fn diagnostic_sink_is_opt_in_and_receives_typed_index_events() {
let disabled = ReadControl::default();
assert!(!disabled.diagnostics_enabled());
let events = Arc::new(Mutex::new(Vec::new()));
let captured = Arc::clone(&events);
let control = ReadControl::default().with_diagnostic_sink(Arc::new(move |event| {
captured.lock().unwrap().push(event);
}));
assert!(control.diagnostics_enabled());
let diagnostic = DicomIndexDiagnostic {
outcome: DicomIndexOutcome::BuiltFast {
mapping: DicomIndexMapping::BasicOffsetTableItems,
},
elapsed: Duration::from_millis(7),
};
control.record_diagnostic(diagnostic);
assert_eq!(events.lock().unwrap().as_slice(), &[diagnostic]);
}
}