From 89c0d65285ef2c6a6038fc688710d866ceec9adb Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Fri, 24 Apr 2026 19:45:38 -0600 Subject: [PATCH] feat(memory): scale L1 audio cache cap to host memory profile (#331) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add audio_cache_l1_max_bytes to MemoryProfile and wire it into AppState's AudioCache constructor. Normal hosts keep the historical 400 MB cap; LowMemory hosts drop to 50 MB. Without this, a Pi 3B (1 GB total) starts with the L1 cache cap set at ~40 % of physical RAM. Even with the Feature #3 prefetch caps already in place, the runtime watchdog from the previous commit ends up firing more often than necessary because nothing prevents a single L1 fill cycle from claiming 400 MB. Capping at 50 MB caps how much the cache can grow before either: (a) eviction picks up naturally, or (b) the watchdog steps in if the rest of the system is also under pressure. Pure refactor in lib.rs — replaces a hard-coded 400 * 1024 * 1024 with a profile field read. The default-constructed AudioCache fallback (when disk-spillover init fails) keeps its own crate default; no need to thread the profile there since the failure path is rare. Two tests added: a presence-bound assertion in the existing Pi 3B case, and an explicit comparison that LowMemory's cap is strictly smaller than Normal's. cc --- crates/qbz-core/src/system_capabilities.rs | 18 ++++++++++++++++++ src-tauri/src/lib.rs | 8 ++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) 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()) };