feat(memory): scale L1 audio cache cap to host memory profile (#331)

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
This commit is contained in:
vicrodh committed 2026-04-24 19:45:38 -06:00
1 parent c6fc01961f
commit 89c0d65285
2 files changed
+24 -2

No files matched your search

@@ -43,6 +43,11 @@ pub struct MemoryProfile {
/// (44.1 kHz / 16-bit FLAC) so each cached track stays under ~15 MB /// (44.1 kHz / 16-bit FLAC) so each cached track stays under ~15 MB
/// instead of ~60 MB. /// instead of ~60 MB.
pub allow_hires_prefetch: bool, 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 { impl MemoryProfile {
@@ -61,6 +66,7 @@ impl MemoryProfile {
max_initial_buffer_bytes: 2 * 1024 * 1024, max_initial_buffer_bytes: 2 * 1024 * 1024,
max_concurrent_prefetch: 2, max_concurrent_prefetch: 2,
allow_hires_prefetch: true, allow_hires_prefetch: true,
audio_cache_l1_max_bytes: 400 * 1024 * 1024,
} }
} else { } else {
Self { Self {
@@ -70,6 +76,7 @@ impl MemoryProfile {
max_initial_buffer_bytes: 256 * 1024, max_initial_buffer_bytes: 256 * 1024,
max_concurrent_prefetch: 1, max_concurrent_prefetch: 1,
allow_hires_prefetch: false, 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_eq!(profile.max_concurrent_prefetch, 1);
assert!(!profile.allow_hires_prefetch); assert!(!profile.allow_hires_prefetch);
assert!(profile.max_initial_buffer_bytes <= 256 * 1024); 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] #[test]
+6 -2
View File
@@ -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 { 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 { } else {
Arc::new(AudioCache::default()) Arc::new(AudioCache::default())
}; };