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
/// 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]