Skip to main content

medical_cache/
config.rs

1//! Study Cache Configuration
2//!
3//! Provides adaptive cache configuration based on hardware profiles.
4//! The cache system uses a three-tier architecture:
5//! - Hot Tier: GPU memory for active series (instant access)
6//! - Warm Tier: RAM cache for metadata/thumbnails (fast access)
7//! - Cold Tier: Disk/lazy loading (on-demand)
8
9use serde::{Deserialize, Serialize};
10
11/// Performance profile tier for cache settings.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum PerformanceProfile {
15    /// High-end system (discrete GPU, many cores, 32GB+ RAM)
16    High,
17    /// Mid-range system (modern iGPU, 16GB RAM)
18    Medium,
19    /// Budget system (older CPU, 8GB RAM)
20    Low,
21    /// Very old or embedded hardware (4GB RAM)
22    UltraLow,
23}
24
25/// Prefetch strategy based on hardware capabilities
26#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum PrefetchStrategy {
29    /// No prefetching - load on demand only
30    None,
31    /// Only prefetch thumbnails (256×256)
32    ThumbnailsOnly,
33    /// Prefetch metadata and mid-resolution images (512×512)
34    #[default]
35    MetadataAndMidRes,
36    /// Full prefetching - load everything ahead of time
37    Full,
38}
39
40impl PrefetchStrategy {
41    /// Get display name for UI
42    pub fn display_name(&self) -> &'static str {
43        match self {
44            Self::None => "On-Demand Only",
45            Self::ThumbnailsOnly => "Thumbnails Only",
46            Self::MetadataAndMidRes => "Balanced (Thumbnails + Preview)",
47            Self::Full => "Aggressive (Full Quality)",
48        }
49    }
50
51    /// Get description for UI
52    pub fn description(&self) -> &'static str {
53        match self {
54            Self::None => "Load images only when requested. Slowest but lowest memory usage.",
55            Self::ThumbnailsOnly => "Preload small previews. Good for very low memory systems.",
56            Self::MetadataAndMidRes => "Preload medium quality previews. Best balance of speed and memory.",
57            Self::Full => "Preload full quality images. Fastest but requires more memory.",
58        }
59    }
60}
61
62/// Cache retention policy - what to keep cached per study
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct CacheRetentionPolicy {
65    /// Always keep thumbnails (256×256) in memory
66    pub retain_thumbnails: bool,
67    /// Keep mid-resolution (512×512) in warm cache
68    pub retain_mid_res: bool,
69    /// Keep full-resolution in hot cache
70    pub retain_full_res: bool,
71}
72
73impl Default for CacheRetentionPolicy {
74    fn default() -> Self {
75        Self {
76            retain_thumbnails: true,
77            retain_mid_res: true,
78            retain_full_res: false,
79        }
80    }
81}
82
83impl CacheRetentionPolicy {
84    /// High-end systems: keep everything
85    pub fn aggressive() -> Self {
86        Self {
87            retain_thumbnails: true,
88            retain_mid_res: true,
89            retain_full_res: true,
90        }
91    }
92
93    /// Mid-range systems: thumbnails and mid-res
94    pub fn balanced() -> Self {
95        Self {
96            retain_thumbnails: true,
97            retain_mid_res: true,
98            retain_full_res: false,
99        }
100    }
101
102    /// Low-end systems: thumbnails only, mid-res on demand
103    pub fn conservative() -> Self {
104        Self {
105            retain_thumbnails: true,
106            retain_mid_res: true,
107            retain_full_res: false,
108        }
109    }
110
111    /// Ultra-low systems: minimal caching
112    pub fn minimal() -> Self {
113        Self {
114            retain_thumbnails: true,
115            retain_mid_res: false,
116            retain_full_res: false,
117        }
118    }
119}
120
121/// Image quality level for progressive loading
122/// Order: Thumbnail < MidRes < Full (for comparison operators)
123#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
124#[serde(rename_all = "snake_case")]
125pub enum ImageQuality {
126    /// 256×256 thumbnail - instant feedback
127    Thumbnail,
128    /// 512×512 mid-resolution - acceptable quality
129    MidRes,
130    /// Original resolution - diagnostic quality
131    Full,
132}
133
134impl ImageQuality {
135    /// Get target dimension for this quality level
136    pub fn target_dimension(&self) -> usize {
137        match self {
138            Self::Thumbnail => 256,
139            Self::MidRes => 512,
140            Self::Full => 0, // 0 = original size
141        }
142    }
143
144    /// Get display name
145    pub fn display_name(&self) -> &'static str {
146        match self {
147            Self::Thumbnail => "Preview",
148            Self::MidRes => "Loading HD...",
149            Self::Full => "HD Ready",
150        }
151    }
152
153    /// Is this quality sufficient for diagnosis?
154    pub fn is_diagnostic(&self) -> bool {
155        matches!(self, Self::Full)
156    }
157}
158
159/// Study cache configuration based on hardware profile
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct StudyCacheConfig {
162    /// Total memory budget in MB
163    pub total_budget_mb: usize,
164    /// Hot tier (GPU memory) budget in MB
165    pub hot_tier_mb: usize,
166    /// Warm tier (RAM) budget in MB
167    pub warm_tier_mb: usize,
168    /// Maximum number of studies to keep in memory
169    pub max_studies: usize,
170    /// Maximum studies with full-resolution data
171    pub max_full_res_studies: usize,
172    /// Prefetch strategy
173    pub prefetch_strategy: PrefetchStrategy,
174    /// Eviction timeout in seconds (how long before inactive studies are evicted)
175    pub eviction_timeout_secs: u64,
176    /// Cache retention policy
177    pub retention: CacheRetentionPolicy,
178}
179
180impl Default for StudyCacheConfig {
181    fn default() -> Self {
182        Self::for_profile(PerformanceProfile::Medium)
183    }
184}
185
186impl StudyCacheConfig {
187    /// Create cache configuration for a performance profile
188    pub fn for_profile(profile: PerformanceProfile) -> Self {
189        match profile {
190            PerformanceProfile::High => Self::high_profile(),
191            PerformanceProfile::Medium => Self::medium_profile(),
192            PerformanceProfile::Low => Self::low_profile(),
193            PerformanceProfile::UltraLow => Self::ultra_low_profile(),
194        }
195    }
196
197    /// High-end system configuration
198    /// Target: RTX 3060+, 32GB+ RAM, 8+ cores
199    fn high_profile() -> Self {
200        Self {
201            total_budget_mb: 4096,        // 4 GB total
202            hot_tier_mb: 1024,            // 1 GB GPU
203            warm_tier_mb: 3072,           // 3 GB RAM
204            max_studies: 10,
205            max_full_res_studies: 5,
206            prefetch_strategy: PrefetchStrategy::Full,
207            eviction_timeout_secs: 1800,  // 30 minutes
208            retention: CacheRetentionPolicy::aggressive(),
209        }
210    }
211
212    /// Medium system configuration
213    /// Target: Intel Iris Xe, Apple M1, 16GB RAM
214    fn medium_profile() -> Self {
215        Self {
216            total_budget_mb: 2048,        // 2 GB total
217            hot_tier_mb: 512,             // 512 MB GPU
218            warm_tier_mb: 1536,           // 1.5 GB RAM
219            max_studies: 5,
220            max_full_res_studies: 2,
221            prefetch_strategy: PrefetchStrategy::MetadataAndMidRes,
222            eviction_timeout_secs: 900,   // 15 minutes
223            retention: CacheRetentionPolicy::balanced(),
224        }
225    }
226
227    /// Low-end system configuration
228    /// Target: Intel UHD 630, 8GB RAM, 4-6 cores (2019 systems)
229    fn low_profile() -> Self {
230        Self {
231            total_budget_mb: 800,         // 800 MB total (~10% of 8GB)
232            hot_tier_mb: 256,             // 256 MB GPU
233            warm_tier_mb: 544,            // 544 MB RAM
234            max_studies: 3,
235            max_full_res_studies: 1,      // Only active study
236            prefetch_strategy: PrefetchStrategy::MetadataAndMidRes,
237            eviction_timeout_secs: 600,   // 10 minutes
238            retention: CacheRetentionPolicy::conservative(),
239        }
240    }
241
242    /// Ultra-low system configuration
243    /// Target: Intel HD 4000, 4GB RAM, 2 cores (2012-2015 systems)
244    fn ultra_low_profile() -> Self {
245        Self {
246            total_budget_mb: 400,         // 400 MB total
247            hot_tier_mb: 128,             // 128 MB GPU
248            warm_tier_mb: 272,            // 272 MB RAM
249            max_studies: 2,
250            max_full_res_studies: 1,
251            prefetch_strategy: PrefetchStrategy::ThumbnailsOnly,
252            eviction_timeout_secs: 300,   // 5 minutes
253            retention: CacheRetentionPolicy::minimal(),
254        }
255    }
256
257    /// Calculate cache budget based on RAM size
258    ///
259    /// Formula:
260    /// - High: 3% of RAM, max 4096 MB
261    /// - Medium: 2% of RAM, max 2048 MB
262    /// - Low: 10% of RAM, max 1024 MB
263    /// - UltraLow: 10% of RAM, max 512 MB
264    pub fn calculate_budget(ram_gb: u32, profile: PerformanceProfile) -> usize {
265        let (percentage, max_cap_mb) = match profile {
266            PerformanceProfile::High => (0.03, 4096),
267            PerformanceProfile::Medium => (0.02, 2048),
268            PerformanceProfile::Low => (0.10, 1024),
269            PerformanceProfile::UltraLow => (0.10, 512),
270        };
271
272        let calculated = ((ram_gb as f64) * percentage * 1024.0) as usize;
273        calculated.min(max_cap_mb)
274    }
275
276    /// Get description of this configuration
277    pub fn description(&self) -> String {
278        format!(
279            "{}MB total ({} MB hot + {} MB warm), {} studies max, {} prefetch",
280            self.total_budget_mb,
281            self.hot_tier_mb,
282            self.warm_tier_mb,
283            self.max_studies,
284            self.prefetch_strategy.display_name()
285        )
286    }
287
288    /// Get cold tier budget (remaining from total after hot+warm)
289    pub fn cold_tier_mb(&self) -> usize {
290        self.total_budget_mb.saturating_sub(self.hot_tier_mb + self.warm_tier_mb)
291    }
292
293    /// Check if full-res caching is enabled
294    pub fn caches_full_res(&self) -> bool {
295        self.retention.retain_full_res
296    }
297
298    /// Check if mid-res caching is enabled
299    pub fn caches_mid_res(&self) -> bool {
300        self.retention.retain_mid_res
301    }
302}
303
304/// Expected performance characteristics for a cache configuration
305#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct ExpectedPerformance {
307    /// Initial study load time in ms
308    pub initial_load_ms: u32,
309    /// Cached (hot) to active switch time in ms
310    pub cached_switch_ms: u32,
311    /// Warm to active switch time in ms
312    pub warm_switch_ms: u32,
313    /// Cold to active load time in ms
314    pub cold_load_ms: u32,
315    /// MPR render time in ms
316    pub mpr_render_ms: u32,
317    /// 3D volume render time in ms
318    pub volume_render_ms: u32,
319    /// Target scroll FPS
320    pub scroll_fps: u32,
321}
322
323impl ExpectedPerformance {
324    /// Get expected performance for a profile
325    pub fn for_profile(profile: PerformanceProfile) -> Self {
326        match profile {
327            PerformanceProfile::High => Self {
328                initial_load_ms: 800,
329                cached_switch_ms: 50,
330                warm_switch_ms: 100,
331                cold_load_ms: 800,
332                mpr_render_ms: 200,
333                volume_render_ms: 500,
334                scroll_fps: 60,
335            },
336            PerformanceProfile::Medium => Self {
337                initial_load_ms: 1200,
338                cached_switch_ms: 80,
339                warm_switch_ms: 300,
340                cold_load_ms: 1200,
341                mpr_render_ms: 400,
342                volume_render_ms: 1000,
343                scroll_fps: 30,
344            },
345            PerformanceProfile::Low => Self {
346                initial_load_ms: 2000,
347                cached_switch_ms: 50,
348                warm_switch_ms: 450,
349                cold_load_ms: 2000,
350                mpr_render_ms: 800,
351                volume_render_ms: 2000,
352                scroll_fps: 30,
353            },
354            PerformanceProfile::UltraLow => Self {
355                initial_load_ms: 4000,
356                cached_switch_ms: 100,
357                warm_switch_ms: 900,
358                cold_load_ms: 4000,
359                mpr_render_ms: 2000,
360                volume_render_ms: 5000,
361                scroll_fps: 15,
362            },
363        }
364    }
365
366    /// Get UX rating (1-5 stars)
367    pub fn ux_rating(&self) -> u8 {
368        if self.cached_switch_ms <= 50 && self.scroll_fps >= 60 {
369            5 // Excellent
370        } else if self.cached_switch_ms <= 100 && self.scroll_fps >= 30 {
371            4 // Good
372        } else if self.cached_switch_ms <= 200 && self.scroll_fps >= 20 {
373            3 // Acceptable
374        } else if self.scroll_fps >= 15 {
375            2 // Slow but works
376        } else {
377            1 // Problematic
378        }
379    }
380}
381
382/// Cache tier for tracking where data is stored
383#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
384#[serde(rename_all = "snake_case")]
385pub enum CacheTier {
386    /// GPU memory - instant access
387    Hot,
388    /// RAM cache - fast access
389    Warm,
390    /// Disk/lazy - needs loading
391    Cold,
392}
393
394impl CacheTier {
395    /// Get expected access time in ms
396    pub fn expected_access_ms(&self) -> u32 {
397        match self {
398            Self::Hot => 0,
399            Self::Warm => 100,
400            Self::Cold => 1000,
401        }
402    }
403
404    /// Get display name
405    pub fn display_name(&self) -> &'static str {
406        match self {
407            Self::Hot => "GPU Memory (Instant)",
408            Self::Warm => "RAM Cache (Fast)",
409            Self::Cold => "Disk (Loading...)",
410        }
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417
418    #[test]
419    fn test_cache_config_for_profiles() {
420        let high = StudyCacheConfig::for_profile(PerformanceProfile::High);
421        let medium = StudyCacheConfig::for_profile(PerformanceProfile::Medium);
422        let low = StudyCacheConfig::for_profile(PerformanceProfile::Low);
423        let ultra_low = StudyCacheConfig::for_profile(PerformanceProfile::UltraLow);
424
425        // High should have more resources than medium
426        assert!(high.total_budget_mb > medium.total_budget_mb);
427        assert!(high.max_studies > medium.max_studies);
428
429        // Medium should have more resources than low
430        assert!(medium.total_budget_mb > low.total_budget_mb);
431
432        // Low should have more resources than ultra-low
433        assert!(low.total_budget_mb > ultra_low.total_budget_mb);
434    }
435
436    #[test]
437    fn test_budget_calculation() {
438        // 32GB RAM, High profile
439        let budget = StudyCacheConfig::calculate_budget(32, PerformanceProfile::High);
440        assert_eq!(budget, 983); // 32 * 0.03 * 1024 = 983, capped at 4096
441
442        // 16GB RAM, Medium profile
443        let budget = StudyCacheConfig::calculate_budget(16, PerformanceProfile::Medium);
444        assert_eq!(budget, 327); // 16 * 0.02 * 1024 = 327
445
446        // 8GB RAM, Low profile
447        let budget = StudyCacheConfig::calculate_budget(8, PerformanceProfile::Low);
448        assert_eq!(budget, 819); // 8 * 0.10 * 1024 = 819
449
450        // 4GB RAM, UltraLow profile
451        let budget = StudyCacheConfig::calculate_budget(4, PerformanceProfile::UltraLow);
452        assert_eq!(budget, 409); // 4 * 0.10 * 1024 = 409
453    }
454
455    #[test]
456    fn test_prefetch_strategy() {
457        assert_eq!(PrefetchStrategy::Full.display_name(), "Aggressive (Full Quality)");
458        assert_eq!(PrefetchStrategy::None.display_name(), "On-Demand Only");
459    }
460
461    #[test]
462    fn test_image_quality_dimensions() {
463        assert_eq!(ImageQuality::Thumbnail.target_dimension(), 256);
464        assert_eq!(ImageQuality::MidRes.target_dimension(), 512);
465        assert_eq!(ImageQuality::Full.target_dimension(), 0);
466    }
467
468    #[test]
469    fn test_expected_performance() {
470        let high_perf = ExpectedPerformance::for_profile(PerformanceProfile::High);
471        let low_perf = ExpectedPerformance::for_profile(PerformanceProfile::Low);
472
473        // High profile should be faster
474        assert!(high_perf.initial_load_ms < low_perf.initial_load_ms);
475        assert!(high_perf.scroll_fps > low_perf.scroll_fps);
476
477        // UX rating should reflect performance
478        assert!(high_perf.ux_rating() >= low_perf.ux_rating());
479    }
480
481    #[test]
482    fn test_cache_tier_access_times() {
483        assert_eq!(CacheTier::Hot.expected_access_ms(), 0);
484        assert!(CacheTier::Warm.expected_access_ms() < CacheTier::Cold.expected_access_ms());
485    }
486
487    #[test]
488    fn test_retention_policies() {
489        let aggressive = CacheRetentionPolicy::aggressive();
490        assert!(aggressive.retain_full_res);
491        assert!(aggressive.retain_mid_res);
492        assert!(aggressive.retain_thumbnails);
493
494        let minimal = CacheRetentionPolicy::minimal();
495        assert!(!minimal.retain_full_res);
496        assert!(!minimal.retain_mid_res);
497        assert!(minimal.retain_thumbnails);
498    }
499}