1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum PerformanceProfile {
15 High,
17 Medium,
19 Low,
21 UltraLow,
23}
24
25#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum PrefetchStrategy {
29 None,
31 ThumbnailsOnly,
33 #[default]
35 MetadataAndMidRes,
36 Full,
38}
39
40impl PrefetchStrategy {
41 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct CacheRetentionPolicy {
65 pub retain_thumbnails: bool,
67 pub retain_mid_res: bool,
69 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 pub fn aggressive() -> Self {
86 Self {
87 retain_thumbnails: true,
88 retain_mid_res: true,
89 retain_full_res: true,
90 }
91 }
92
93 pub fn balanced() -> Self {
95 Self {
96 retain_thumbnails: true,
97 retain_mid_res: true,
98 retain_full_res: false,
99 }
100 }
101
102 pub fn conservative() -> Self {
104 Self {
105 retain_thumbnails: true,
106 retain_mid_res: true,
107 retain_full_res: false,
108 }
109 }
110
111 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
124#[serde(rename_all = "snake_case")]
125pub enum ImageQuality {
126 Thumbnail,
128 MidRes,
130 Full,
132}
133
134impl ImageQuality {
135 pub fn target_dimension(&self) -> usize {
137 match self {
138 Self::Thumbnail => 256,
139 Self::MidRes => 512,
140 Self::Full => 0, }
142 }
143
144 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 pub fn is_diagnostic(&self) -> bool {
155 matches!(self, Self::Full)
156 }
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct StudyCacheConfig {
162 pub total_budget_mb: usize,
164 pub hot_tier_mb: usize,
166 pub warm_tier_mb: usize,
168 pub max_studies: usize,
170 pub max_full_res_studies: usize,
172 pub prefetch_strategy: PrefetchStrategy,
174 pub eviction_timeout_secs: u64,
176 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 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 fn high_profile() -> Self {
200 Self {
201 total_budget_mb: 4096, hot_tier_mb: 1024, warm_tier_mb: 3072, max_studies: 10,
205 max_full_res_studies: 5,
206 prefetch_strategy: PrefetchStrategy::Full,
207 eviction_timeout_secs: 1800, retention: CacheRetentionPolicy::aggressive(),
209 }
210 }
211
212 fn medium_profile() -> Self {
215 Self {
216 total_budget_mb: 2048, hot_tier_mb: 512, warm_tier_mb: 1536, max_studies: 5,
220 max_full_res_studies: 2,
221 prefetch_strategy: PrefetchStrategy::MetadataAndMidRes,
222 eviction_timeout_secs: 900, retention: CacheRetentionPolicy::balanced(),
224 }
225 }
226
227 fn low_profile() -> Self {
230 Self {
231 total_budget_mb: 800, hot_tier_mb: 256, warm_tier_mb: 544, max_studies: 3,
235 max_full_res_studies: 1, prefetch_strategy: PrefetchStrategy::MetadataAndMidRes,
237 eviction_timeout_secs: 600, retention: CacheRetentionPolicy::conservative(),
239 }
240 }
241
242 fn ultra_low_profile() -> Self {
245 Self {
246 total_budget_mb: 400, hot_tier_mb: 128, warm_tier_mb: 272, max_studies: 2,
250 max_full_res_studies: 1,
251 prefetch_strategy: PrefetchStrategy::ThumbnailsOnly,
252 eviction_timeout_secs: 300, retention: CacheRetentionPolicy::minimal(),
254 }
255 }
256
257 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 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 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 pub fn caches_full_res(&self) -> bool {
295 self.retention.retain_full_res
296 }
297
298 pub fn caches_mid_res(&self) -> bool {
300 self.retention.retain_mid_res
301 }
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct ExpectedPerformance {
307 pub initial_load_ms: u32,
309 pub cached_switch_ms: u32,
311 pub warm_switch_ms: u32,
313 pub cold_load_ms: u32,
315 pub mpr_render_ms: u32,
317 pub volume_render_ms: u32,
319 pub scroll_fps: u32,
321}
322
323impl ExpectedPerformance {
324 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 pub fn ux_rating(&self) -> u8 {
368 if self.cached_switch_ms <= 50 && self.scroll_fps >= 60 {
369 5 } else if self.cached_switch_ms <= 100 && self.scroll_fps >= 30 {
371 4 } else if self.cached_switch_ms <= 200 && self.scroll_fps >= 20 {
373 3 } else if self.scroll_fps >= 15 {
375 2 } else {
377 1 }
379 }
380}
381
382#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
384#[serde(rename_all = "snake_case")]
385pub enum CacheTier {
386 Hot,
388 Warm,
390 Cold,
392}
393
394impl CacheTier {
395 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 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 assert!(high.total_budget_mb > medium.total_budget_mb);
427 assert!(high.max_studies > medium.max_studies);
428
429 assert!(medium.total_budget_mb > low.total_budget_mb);
431
432 assert!(low.total_budget_mb > ultra_low.total_budget_mb);
434 }
435
436 #[test]
437 fn test_budget_calculation() {
438 let budget = StudyCacheConfig::calculate_budget(32, PerformanceProfile::High);
440 assert_eq!(budget, 983); let budget = StudyCacheConfig::calculate_budget(16, PerformanceProfile::Medium);
444 assert_eq!(budget, 327); let budget = StudyCacheConfig::calculate_budget(8, PerformanceProfile::Low);
448 assert_eq!(budget, 819); let budget = StudyCacheConfig::calculate_budget(4, PerformanceProfile::UltraLow);
452 assert_eq!(budget, 409); }
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 assert!(high_perf.initial_load_ms < low_perf.initial_load_ms);
475 assert!(high_perf.scroll_fps > low_perf.scroll_fps);
476
477 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}