diff --git a/crates/qbz-core/src/system_capabilities.rs b/crates/qbz-core/src/system_capabilities.rs index c9e773a87..aa272bfab 100644 --- a/crates/qbz-core/src/system_capabilities.rs +++ b/crates/qbz-core/src/system_capabilities.rs @@ -43,6 +43,11 @@ pub struct MemoryProfile { /// (44.1 kHz / 16-bit FLAC) so each cached track stays under ~15 MB /// instead of ~60 MB. pub allow_hires_prefetch: bool, + /// Upper bound for the L1 (in-memory) audio cache. The default + /// 400 MB cap is sized for Normal-class desktops; on a Pi 3B (1 GB + /// total) that single subsystem could consume 40 % of RAM, which + /// guarantees swap thrash before the watchdog can react. + pub audio_cache_l1_max_bytes: usize, } impl MemoryProfile { @@ -61,6 +66,7 @@ impl MemoryProfile { max_initial_buffer_bytes: 2 * 1024 * 1024, max_concurrent_prefetch: 2, allow_hires_prefetch: true, + audio_cache_l1_max_bytes: 400 * 1024 * 1024, } } else { Self { @@ -70,6 +76,7 @@ impl MemoryProfile { max_initial_buffer_bytes: 256 * 1024, max_concurrent_prefetch: 1, allow_hires_prefetch: false, + audio_cache_l1_max_bytes: 50 * 1024 * 1024, } } } @@ -238,6 +245,17 @@ SwapTotal: 2097152 kB assert_eq!(profile.max_concurrent_prefetch, 1); assert!(!profile.allow_hires_prefetch); assert!(profile.max_initial_buffer_bytes <= 256 * 1024); + // L1 cap must be a small fraction of total RAM — at most ~10 % + // of a Pi 3B, so we don't reserve four-tenths of memory for one + // subsystem on a 1 GB host. + assert!(profile.audio_cache_l1_max_bytes <= 100 * 1024 * 1024); + } + + #[test] + fn audio_cache_cap_is_significantly_smaller_on_low_memory() { + let normal = MemoryProfile::from_total_kb(8 * 1024 * 1024); + let low = MemoryProfile::from_total_kb(938196); + assert!(low.audio_cache_l1_max_bytes < normal.audio_cache_l1_max_bytes); } #[test] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e9be222d9..c7dfa6545 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -134,9 +134,13 @@ impl AppState { } }; - // Create audio cache (L1 - memory, 400MB) with optional disk spillover + // Create audio cache (L1 - memory) with optional disk spillover. + // The L1 cap is sized per host: Normal hosts get the historical + // 400 MB; LowMemory hosts (issue #331) drop to 50 MB so the cache + // alone can never claim 40 % of a 1 GB Pi. + let l1_cap = qbz_core::system_capabilities::memory_profile().audio_cache_l1_max_bytes; let audio_cache = if let Some(pc) = playback_cache { - Arc::new(AudioCache::with_playback_cache(400 * 1024 * 1024, pc)) + Arc::new(AudioCache::with_playback_cache(l1_cap, pc)) } else { Arc::new(AudioCache::default()) };