Skip to main content

dspk_io/core/
cached.rs

1//! Keeps recently decoded chunks of a chunked source in memory.
2//!
3//! Reading part of a compressed chunk decodes the whole chunk, so views drawing the same region
4//! (traces and heatmap), hover readouts and repeated frames over it (panning, resizing) would
5//! decode it again and again. [`CachedRecording`] decodes each storage chunk once, all channels,
6//! and serves reads from the most recently used chunks within a memory budget. Whole-recording
7//! passes ([`RecordingSource::read_native`]) bypass it so they do not evict what views use.
8
9use std::collections::VecDeque;
10use std::ops::Range;
11use std::sync::{Arc, Mutex};
12
13use dspk_core::recording::check_read;
14use dspk_core::{DspResult, MemoryOrder, RecordingInfo, RecordingSource};
15
16pub struct CachedRecording {
17    inner: Box<dyn RecordingSource>,
18    /// Samples per chunk.
19    chunk: u64,
20    /// Chunks kept.
21    capacity: usize,
22    /// Decoded chunks, most recently used first: (chunk index, channel-major µV of all channels).
23    chunks: Mutex<VecDeque<(u64, Arc<Vec<f32>>)>>,
24}
25
26impl CachedRecording {
27    /// Wraps `inner` when it is stored in chunks, keeping up to `budget_bytes` of decoded chunks
28    /// (at least one); other sources are returned unchanged.
29    pub fn wrap(inner: Box<dyn RecordingSource>, budget_bytes: usize) -> Box<dyn RecordingSource> {
30        match inner.chunk_samples().filter(|&c| c > 0) {
31            Some(chunk) => {
32                let bytes = chunk as usize * inner.info().channel_count().max(1) * 4;
33                let capacity = (budget_bytes / bytes).max(1);
34                Box::new(Self { inner, chunk, capacity, chunks: Mutex::new(VecDeque::new()) })
35            }
36            None => inner,
37        }
38    }
39
40    /// Chunk `i` decoded, from memory or read now.
41    fn chunk(&self, i: u64) -> DspResult<Arc<Vec<f32>>> {
42        {
43            let mut chunks = self.chunks.lock().unwrap();
44            if let Some(at) = chunks.iter().position(|(c, _)| *c == i) {
45                let hit = chunks.remove(at).expect("position is in range");
46                chunks.push_front(hit.clone());
47                return Ok(hit.1);
48            }
49        }
50        // Decoded outside the lock so other readers are not held up
51        let info = self.inner.info();
52        let range = i * self.chunk..((i + 1) * self.chunk).min(info.samples);
53        let all: Vec<usize> = (0..info.channel_count()).collect();
54        let mut data = vec![0.0f32; all.len() * (range.end - range.start) as usize];
55        self.inner.read(&all, range, &mut data)?;
56        let data = Arc::new(data);
57        let mut chunks = self.chunks.lock().unwrap();
58        chunks.retain(|(c, _)| *c != i);
59        chunks.push_front((i, data.clone()));
60        chunks.truncate(self.capacity);
61        Ok(data)
62    }
63}
64
65impl RecordingSource for CachedRecording {
66    fn info(&self) -> &RecordingInfo {
67        self.inner.info()
68    }
69
70    fn chunk_samples(&self) -> Option<u64> {
71        Some(self.chunk)
72    }
73
74    fn read(&self, channels: &[usize], samples: Range<u64>, out: &mut [f32]) -> DspResult<()> {
75        let n = check_read(self.info(), channels, &samples, out.len())?;
76        if n == 0 || channels.is_empty() {
77            return Ok(());
78        }
79        let total = self.info().samples;
80        for i in samples.start / self.chunk..samples.end.div_ceil(self.chunk) {
81            let data = self.chunk(i)?;
82            let (c0, c1) = (i * self.chunk, ((i + 1) * self.chunk).min(total));
83            let len = (c1 - c0) as usize;
84            let (s0, s1) = (samples.start.max(c0), samples.end.min(c1));
85            let (src, dst, count) = ((s0 - c0) as usize, (s0 - samples.start) as usize, (s1 - s0) as usize);
86            for (row, &ch) in out.chunks_exact_mut(n).zip(channels) {
87                row[dst..dst + count].copy_from_slice(&data[ch * len + src..ch * len + src + count]);
88            }
89        }
90        Ok(())
91    }
92
93    fn read_native(&self, samples: Range<u64>, out: &mut [f32]) -> DspResult<MemoryOrder> {
94        self.inner.read_native(samples, out)
95    }
96
97    fn read_stored(&self, channels: &[usize], samples: Range<u64>, out: &mut [u8]) -> DspResult<()> {
98        self.inner.read_stored(channels, samples, out)
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use dspk_core::MemoryRecording;
106    use std::sync::atomic::{AtomicUsize, Ordering};
107
108    /// Memory source reporting 100-sample chunks and counting reads.
109    struct Chunked(MemoryRecording, Arc<AtomicUsize>);
110
111    impl RecordingSource for Chunked {
112        fn info(&self) -> &RecordingInfo {
113            self.0.info()
114        }
115        fn chunk_samples(&self) -> Option<u64> {
116            Some(100)
117        }
118        fn read(&self, channels: &[usize], samples: Range<u64>, out: &mut [f32]) -> DspResult<()> {
119            self.1.fetch_add(1, Ordering::Relaxed);
120            self.0.read(channels, samples, out)
121        }
122    }
123
124    #[test]
125    fn test_reads_match_the_source_and_reuse_chunks() {
126        let data: Vec<f32> = (0..3 * 1050).map(|v| v as f32).collect();
127        let plain = MemoryRecording::new("m", data.clone(), 3, 1000.0).unwrap();
128        let reads = Arc::new(AtomicUsize::new(0));
129        // Room for 4 chunks of 3 channels
130        let cached = CachedRecording::wrap(Box::new(Chunked(MemoryRecording::new("m", data, 3, 1000.0).unwrap(), reads.clone())), 4 * 100 * 3 * 4);
131
132        for (channels, range) in [(vec![2, 0], 50u64..330), (vec![1], 1000..1050), (vec![0, 1, 2], 120..180)] {
133            let (mut a, mut b) = (vec![0.0; channels.len() * (range.end - range.start) as usize], vec![]);
134            b.resize(a.len(), 0.0);
135            cached.read(&channels, range.clone(), &mut a).unwrap();
136            plain.read(&channels, range.clone(), &mut b).unwrap();
137            assert_eq!(a, b, "{channels:?} {range:?}");
138        }
139        // Chunks 0-3, then 10, then 1 again (kept): 5 decodes
140        assert_eq!(reads.load(Ordering::Relaxed), 5);
141        let mut one = [0.0];
142        cached.read(&[0], 0..1, &mut one).unwrap();
143        assert_eq!(reads.load(Ordering::Relaxed), 6, "chunk 0 was evicted by the 4-chunk budget");
144    }
145}