diff --git a/Cargo.toml b/Cargo.toml index 04cddcf..c725066 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "ndbioimage" version = "0.2.0" edition = "2024" rust-version = "1.94.0" -authors = ["Wim Pomp ", "opencode"] +authors = ["Wim Pomp "] license = "MIT OR Apache-2.0" description = "Read bio image formats using the bio-formats java package." homepage = "https://git.pomppervova.nl/wim/ndbioimage/src/branch/rs" @@ -24,7 +24,6 @@ color-eyre = { version = "0.6", optional = true } console = { version = "0.16", optional = true } downloader = { version = "0.2", optional = true, default-features = false, features = ["rustls-tls"] } ffmpeg-sidecar = { version = "2", optional = true } -#ffmpreg = { version = "0.1", optional = true } itertools = "0.15" indexmap = { version = "2", features = ["serde"] } indicatif = { version = "0.18", features = ["rayon"], optional = true } diff --git a/src/cache.rs b/src/cache.rs deleted file mode 100644 index c725c1f..0000000 --- a/src/cache.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::hash::{Hash, Hasher}; -use std::path::{Path, PathBuf}; -use std::sync::{LazyLock, RwLock}; -use indexmap::{Equivalent, IndexMap}; -use ndarray::IxDyn; -use crate::error::Error; -use crate::readers::ArrayT; - -static CACHE: LazyLock>>> = - LazyLock::new(|| RwLock::new(IndexMap::new())); - -#[derive(Debug, Clone, Eq, PartialEq)] -struct ViewHash { - reader: String, - path: PathBuf, - series: usize, - position: usize, - c: isize, - z: isize, - t: isize, -} - -impl Hash for ViewHash { - fn hash(&self, state: &mut H) { - self.reader.as_str().hash(state); - self.path.as_path().hash(state); - self.series.hash(state); - self.position.hash(state); - self.c.hash(state); - self.z.hash(state); - self.t.hash(state); - } -} - -impl ViewHash { - fn new(reader: String, path: PathBuf, series: usize, position: usize, c: isize, z: isize, t: isize) -> Self { - Self { reader, path, series, position, c, z, t } - } -} - -struct ViewEquiv<'a> { - reader: &'a str, - path: &'a Path, - series: usize, - position: usize, - c: isize, - z: isize, - t: isize, -} - -impl Hash for ViewEquiv<'_> { - fn hash(&self, state: &mut H) { - self.reader.hash(state); - self.path.hash(state); - self.series.hash(state); - self.position.hash(state); - self.c.hash(state); - self.z.hash(state); - self.t.hash(state); - } -} - -impl<'a> ViewEquiv<'a> { - fn new(reader: &'a str, path: &'a Path, series: usize, position: usize, c: isize, z: isize, t: isize) -> ViewEquiv<'a> { - Self { reader, path, series, position, c, z, t } - } -} - -impl Equivalent> for ViewHash { - fn equivalent(&self, key: &ViewEquiv) -> bool { - (self.reader == key.reader) && (self.path == key.path) && (self.series == key.series) && (self.position == key.position) && (self.c == key.c) && (self.z == key.z) && (self.t == key.t) - } -} - -impl Equivalent for ViewEquiv<'_> { - fn equivalent(&self, key: &ViewHash) -> bool { - key.equivalent(self) - } -} - -pub fn cache_get_or_insert(f: &dyn Fn() -> ArrayT, reader: &str, path: &Path, series: usize, position: usize, c: isize, z: isize, t: isize) -> Result, Error> { - if let Some(frame) = CACHE.read().unwrap().get(&ViewEquiv::new(reader, path, series, position, c, z, t)) { - Ok(frame.clone()) - } else { - let a = f(); - let mut cache = CACHE.write().unwrap(); - cache.insert(ViewHash::new(reader.to_string(), path.to_path_buf(), series, position, c, z, t), a.clone()); - // TODO: find an IndexMapDeque to pop efficiently at the other end - while cache.len() > 1024 { - cache.pop(); - } - Ok(a) - } -} \ No newline at end of file diff --git a/src/py.rs b/src/py.rs index f80425a..fc38a33 100644 --- a/src/py.rs +++ b/src/py.rs @@ -246,7 +246,7 @@ impl PyView { #[getter] fn reader_name(&self) -> String { - self.view.reader_name() + self.view.reader_name().to_string() } #[allow(unused_variables)] diff --git a/src/readers.rs b/src/readers.rs index 2b594ab..9cd7591 100644 --- a/src/readers.rs +++ b/src/readers.rs @@ -143,8 +143,8 @@ pub trait Reader: Clone + Sized + Debug + Send + Hash + Into { where P: AsRef; - fn reader_name(&self) -> String { - type_name::().to_string() + fn reader_name(&self) -> &'static str { + type_name::() } // TODO: read from file if present @@ -257,83 +257,77 @@ impl_frame_cast! { isize: INT32 } -impl TryInto> for ArrayT -where - D: Dimension, - T: FromPrimitive + Zero + 'static, -{ - type Error = Error; - - fn try_into(self) -> Result, Self::Error> { +macro_rules! try_into_array_body { + ($self:expr, $map:ident) => {{ let mut err = Ok(()); - let arr = match self { - ArrayT::I8(v) => v.mapv_into_any(|x| { + let arr = match $self { + ArrayT::I8(v) => v.$map(|x| { T::from_i8(x).unwrap_or_else(|| { err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), - ArrayT::U8(v) => v.mapv_into_any(|x| { + ArrayT::U8(v) => v.$map(|x| { T::from_u8(x).unwrap_or_else(|| { err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), - ArrayT::I16(v) => v.mapv_into_any(|x| { + ArrayT::I16(v) => v.$map(|x| { T::from_i16(x).unwrap_or_else(|| { err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), - ArrayT::U16(v) => v.mapv_into_any(|x| { + ArrayT::U16(v) => v.$map(|x| { T::from_u16(x).unwrap_or_else(|| { err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), - ArrayT::I32(v) => v.mapv_into_any(|x| { + ArrayT::I32(v) => v.$map(|x| { T::from_i32(x).unwrap_or_else(|| { err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), - ArrayT::U32(v) => v.mapv_into_any(|x| { + ArrayT::U32(v) => v.$map(|x| { T::from_u32(x).unwrap_or_else(|| { err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), - ArrayT::F32(v) => v.mapv_into_any(|x| { + ArrayT::F32(v) => v.$map(|x| { T::from_f32(x).unwrap_or_else(|| { err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), - ArrayT::F64(v) | ArrayT::F128(v) => v.mapv_into_any(|x| { + ArrayT::F64(v) | ArrayT::F128(v) => v.$map(|x| { T::from_f64(x).unwrap_or_else(|| { err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), - ArrayT::I64(v) => v.mapv_into_any(|x| { + ArrayT::I64(v) => v.$map(|x| { T::from_i64(x).unwrap_or_else(|| { err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), - ArrayT::U64(v) => v.mapv_into_any(|x| { + ArrayT::U64(v) => v.$map(|x| { T::from_u64(x).unwrap_or_else(|| { err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), - ArrayT::I128(v) => v.mapv_into_any(|x| { + ArrayT::I128(v) => v.$map(|x| { T::from_i128(x).unwrap_or_else(|| { err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), - ArrayT::U128(v) => v.mapv_into_any(|x| { + ArrayT::U128(v) => v.$map(|x| { T::from_u128(x).unwrap_or_else(|| { err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() @@ -344,6 +338,30 @@ where Err(err) => Err(err), Ok(()) => Ok(arr), } + }}; +} + +impl TryInto> for ArrayT +where + D: Dimension, + T: FromPrimitive + Zero + 'static, +{ + type Error = Error; + + fn try_into(self) -> Result, Self::Error> { + try_into_array_body!(self, mapv_into_any) + } +} + +impl TryInto> for &ArrayT +where + D: Dimension, + T: Clone + FromPrimitive + Zero + 'static, +{ + type Error = Error; + + fn try_into(self) -> Result, Self::Error> { + try_into_array_body!(self, mapv) } } @@ -398,8 +416,8 @@ impl Reader for DynReader { )) } - fn reader_name(&self) -> String { - let name = match self { + fn reader_name(&self) -> &'static str { + match self { #[cfg(feature = "tiff")] DynReader::Tiff(r) => r.reader_name(), #[cfg(feature = "tiffseq")] @@ -412,8 +430,7 @@ impl Reader for DynReader { DynReader::BioFormatsJava(r) => r.reader_name(), #[allow(unreachable_patterns)] _ => unreachable!(), - }; - format!("DynReader<{}>", name) + } } fn metadata(&self) -> Result { diff --git a/src/view.rs b/src/view.rs index 5673550..044bf6f 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,9 +1,9 @@ use crate::axes::{Ax, Axis, Operation, Shape, Slice, SliceInfoElemDef, slice_info}; use crate::error::Error; use crate::metadata::Metadata; -use crate::readers::{Dimensions, DynReader, Reader}; +use crate::readers::{Dimensions, DynReader, Frame, Reader}; use crate::stats::MinMax; -use indexmap::IndexMap; +use indexmap::{Equivalent, IndexMap}; use itertools::{Itertools, iproduct}; use ndarray::{ Array, Array0, Array1, Array2, ArrayD, Dimension, IntoDimension, Ix0, Ix1, Ix2, Ix5, IxDyn, @@ -15,12 +15,13 @@ use serde::{Deserialize, Serialize}; use serde_with::serde_as; use std::any::type_name; use std::collections::{HashMap, HashSet}; -use std::fmt::{Display, Formatter}; +use std::fmt::{Debug, Display, Formatter}; use std::hash::{Hash, Hasher}; use std::iter::Sum; use std::marker::PhantomData; use std::ops::{AddAssign, Deref, Div}; -use std::path::Path; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex, OnceLock}; fn idx_bnd(idx: isize, bnd: isize) -> Result { if idx < -bnd { @@ -62,9 +63,157 @@ impl Number for T where { } +/// maximum number of frames held in the cache +const DEFAULT_FRAME_CACHE_SIZE: usize = 128; + +/// identity of the reader a frame was read from +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct ReaderKey { + name: String, + path: PathBuf, + series: usize, + position: usize, +} + +type FrameKey = (ReaderKey, usize, usize, usize); + +/// borrowed view of [`FrameKey`] for cache lookups without allocation. +/// hashes byte-identically to [`FrameKey`] (str/String and Path/PathBuf hash +/// the same), so it only matches the same reader identity. +#[derive(Debug, PartialEq, Eq)] +struct FrameKeyRef<'a> { + name: &'a str, + path: &'a Path, + series: usize, + position: usize, + c: usize, + z: usize, + t: usize, +} + +impl Hash for FrameKeyRef<'_> { + fn hash(&self, state: &mut H) { + self.name.hash(state); + self.path.hash(state); + self.series.hash(state); + self.position.hash(state); + self.c.hash(state); + self.z.hash(state); + self.t.hash(state); + } +} + +impl Equivalent for FrameKeyRef<'_> { + fn equivalent(&self, key: &FrameKey) -> bool { + let (rk, c, z, t) = key; + self.name == rk.name + && self.path == rk.path.as_path() + && self.series == rk.series + && self.position == rk.position + && self.c == *c + && self.z == *z + && self.t == *t + } +} + +impl FrameKeyRef<'_> { + fn to_owned(&self) -> FrameKey { + ( + ReaderKey { + name: self.name.to_string(), + path: self.path.to_path_buf(), + series: self.series, + position: self.position, + }, + self.c, + self.z, + self.t, + ) + } +} + +/// process-wide LRU cache of frames, shared between all views and threads +static GLOBAL_FRAME_CACHE: OnceLock = OnceLock::new(); + +/// thread-safe LRU cache of frames read from the underlying reader +struct FrameCache { + inner: Mutex, +} + +struct FrameCacheInner { + map: IndexMap>, + capacity: usize, +} + +impl Default for FrameCache { + fn default() -> Self { + Self::new(DEFAULT_FRAME_CACHE_SIZE) + } +} + +impl FrameCache { + fn new(capacity: usize) -> Self { + Self { + inner: Mutex::new(FrameCacheInner { + map: IndexMap::with_capacity(capacity), + capacity, + }), + } + } + + fn global() -> &'static FrameCache { + GLOBAL_FRAME_CACHE.get_or_init(FrameCache::default) + } + + fn capacity(&self) -> usize { + self.inner.lock().unwrap().capacity + } + + fn set_capacity(&self, capacity: usize) { + let mut inner = self.inner.lock().unwrap(); + inner.capacity = capacity; + while inner.map.len() > inner.capacity { + inner.map.shift_remove_index(0); + } + } + + fn get(&self, key: &Q) -> Option> + where + Q: ?Sized + Hash + Equivalent, + { + let mut inner = self.inner.lock().unwrap(); + if let Some(idx) = inner.map.get_index_of(key) { + let (key, frame) = inner.map.shift_remove_index(idx).unwrap(); + inner.map.insert(key, frame.clone()); + Some(frame) + } else { + None + } + } + + fn insert(&self, key: FrameKey, frame: Arc) { + let mut inner = self.inner.lock().unwrap(); + inner.map.insert(key, frame); + while inner.map.len() > inner.capacity { + inner.map.shift_remove_index(0); + } + } + + #[cfg(test)] + fn len(&self) -> usize { + self.inner.lock().unwrap().map.len() + } +} + +impl Debug for FrameCache { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FrameCache").finish_non_exhaustive() + } +} + /// sliceable view on an image file #[serde_as] -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct View { reader: R, /// same order as axes @@ -176,6 +325,18 @@ impl View { self.reader.series() } + /// set the maximum number of frames the process-wide cache may hold, + /// evicting the least recently used frames + pub fn with_cache_capacity(self, capacity: usize) -> Self { + FrameCache::global().set_capacity(capacity); + self + } + + /// the maximum number of frames the process-wide cache may hold + pub fn cache_capacity(&self) -> usize { + FrameCache::global().capacity() + } + fn with_operations(mut self, operations: IndexMap) -> Self { self.operations = operations; self @@ -744,13 +905,13 @@ impl View { if let Some(i) = axes_out_idx[2] { slice[i] = SliceInfoElem::Index(t) }; - let frame = self.reader.get_frame( + let frame = self.get_cached_frame( (c % shape.c as isize) as usize, (z % shape.z as isize) as usize, (t % shape.t as isize) as usize, )?; - let arr_frame: Array2 = frame.try_into()?; + let arr_frame: Array2 = frame.as_ref().try_into()?; let arr_frame = match xy_dim { 0 => { if op_xy.contains_key(&Axis::X) && op_xy.contains_key(&Axis::Y) { @@ -888,6 +1049,28 @@ impl View { .collect()) } + fn frame_key(&self, c: usize, z: usize, t: usize) -> FrameKeyRef<'_> { + FrameKeyRef { + name: self.reader.reader_name(), + path: self.reader.path(), + series: self.reader.series(), + position: self.reader.position(), + c, + z, + t, + } + } + + fn get_cached_frame(&self, c: usize, z: usize, t: usize) -> Result, Error> { + let key = self.frame_key(c, z, t); + if let Some(frame) = FrameCache::global().get(&key) { + return Ok(frame); + } + let frame = Arc::new(self.reader.get_frame(c, z, t)?); + FrameCache::global().insert(key.to_owned(), frame.clone()); + Ok(frame) + } + /// retrieve a single frame at czt, sliced accordingly pub fn get_frame(&self, c: N, z: N, t: N) -> Result, Error> where @@ -1116,11 +1299,13 @@ to_bytes_vec_impl!( mod tests { use crate::axes::Axis; use crate::error::Error; - use crate::readers::{DynReader, Reader}; + use crate::readers::{DynReader, Frame, Reader}; use crate::stats::MinMax; - use crate::view::Item; + use crate::view::{FrameCache, Item, ReaderKey}; use ndarray::{Array, Array4, Array5, NewAxis}; use ndarray::{Array2, s}; + use std::path::PathBuf; + use std::sync::Arc; fn open(file: &str) -> Result { let path = std::env::current_dir()? @@ -1388,4 +1573,60 @@ mod tests { assert_eq!(a.shape(), [1280, 1280]); Ok(()) } + + #[test] + fn frame_cache() -> Result<(), Error> { + let cache = FrameCache::new(2); + let key = |c: usize, z: usize, t: usize| { + ( + ReaderKey { + name: "test".to_string(), + path: PathBuf::from("test.tif"), + series: 0, + position: 0, + }, + c, + z, + t, + ) + }; + cache.insert( + key(0, 0, 0), + Arc::new(Frame::from(Array2::from_elem((2, 2), 1u16))), + ); + cache.insert( + key(0, 0, 1), + Arc::new(Frame::from(Array2::from_elem((2, 2), 2u16))), + ); + assert!(cache.get(&key(0, 0, 0)).is_some()); + cache.insert( + key(0, 0, 2), + Arc::new(Frame::from(Array2::from_elem((2, 2), 3u16))), + ); + assert!(cache.get(&key(0, 0, 0)).is_some()); + assert!(cache.get(&key(0, 0, 1)).is_none()); + assert!(cache.get(&key(0, 0, 2)).is_some()); + assert_eq!(cache.len(), 2); + Ok(()) + } + + #[test] + fn as_array_caches_frames() -> Result<(), Error> { + let file = "tiffseq/YTL1841B2-2-1_1hr_DMSO_galinduction_1"; + let reader = open(file)?; + let view = reader.view(); + let shape = view.shape(); + let (c, z, t) = (shape[0] - 1, shape[1] - 1, shape[2] - 1); + let cache = FrameCache::global(); + let a: Array5 = view.clone().try_into()?; + assert!(cache.get(&view.frame_key(c, z, t)).is_some()); + let b: Array5 = view.clone().try_into()?; + assert_eq!(a, b); + // a fresh reader on the same file shares the same cache key + let view2 = open(file)?.view(); + assert_eq!(view.frame_key(c, z, t), view2.frame_key(c, z, t)); + let c2: Array5 = view2.clone().try_into()?; + assert_eq!(a, c2); + Ok(()) + } }