perf: bound rehearser audio cache

This commit is contained in:
mARTin-B78 2026-09-04 10:11:44 +02:00
parent bf186387f9
commit 0f1d93c9df
6 changed files with 47 additions and 8 deletions

View File

@ -8,6 +8,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
## [Unreleased]
### Added
- **Bounded Rehearser Audio Cache.** Decoded/synthesized line audio now uses a 128-entry LRU cache so long scripts
cannot grow browser memory without limit.
- **Shared Voice-Library Cache.** Library, Benchmark, and Rehearser now share a bounded session cache with in-flight
request coalescing and explicit invalidation after voice mutations.
- **Frontend Namespace Bridge.** A stable `window.TTVC` migration surface now exposes Audiobook, Rehearser, and job

View File

@ -127,8 +127,8 @@ the first block before investing in architecture or polish.
invalidation after library metadata/folder mutations; the SQLite voice index remains authoritative.
- [x] Lazy-load profile images and concept art.
- [ ] Update only affected table rows instead of rebuilding entire tables.
- [ ] Add an LRU cache for audio and images and remove old entries automatically. Persisted line-audio is now bounded;
image-cache eviction remains.
- [x] Add an LRU cache for decoded Rehearser audio and remove old entries automatically. Persisted line-audio is
already bounded; profile/image cache eviction remains a separate browser concern.
- [x] Revoke discarded object URLs with `URL.revokeObjectURL(objectUrl)`; completed synthesized voice previews and
character/Rehearser local auditions now release their Blob URLs when playback ends or is replaced.
- [x] Add low-memory mode using `navigator.deviceMemory` where available; shared worker pools cap at two tasks on devices reporting approximately 4 GB RAM or less.

View File

@ -1 +1 @@
1.24.13
1.24.14

File diff suppressed because one or more lines are too long

View File

@ -66,6 +66,31 @@ function stripMarkdown(text) {
}
// ── State ─────────────────────────────────────────────────────────────────
class RehAudioCache extends Map {
static MAX_ENTRIES = 128;
get(key) {
if (!super.has(key)) return undefined;
const value = super.get(key);
super.delete(key); // Map insertion order is the LRU order.
super.set(key, value);
return value;
}
set(key, value) {
super.delete(key);
super.set(key, value);
while (this.size > RehAudioCache.MAX_ENTRIES) {
let oldest = this.keys().next().value;
const current = typeof rehState !== 'undefined' ? rehState.lineIndex : null;
if (oldest === current && this.size > 1) {
const iterator = this.keys();
iterator.next();
oldest = iterator.next().value;
}
super.delete(oldest);
}
return this;
}
}
const rehState = {
lines: [],
cast: {},
@ -76,7 +101,7 @@ const rehState = {
playing: false,
repeat: false,
savedId: null,
synthCache: new Map(),
synthCache: new RehAudioCache(),
staleLines: new Set(), // lines that were cached but had their tone changed
synthCancelled: false,
synthRunning: false,
@ -2977,7 +3002,7 @@ function _reindexLineState(d) {
}
return out;
};
rehState.synthCache = shift(rehState.synthCache, true);
rehState.synthCache = new RehAudioCache(shift(rehState.synthCache, true));
rehState.staleLines = shift(rehState.staleLines, false);
const sel = shift(rehState.bulkSel, false);
rehState.bulkSel.clear(); sel.forEach(v => rehState.bulkSel.add(v));

View File

@ -85,3 +85,15 @@ test('voice library cache reuses fresh data and honors explicit refresh', async
});
expect(result).toBe(2);
});
test('Rehearser audio cache stays bounded with LRU eviction', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => window.rehState?.synthCache, null, { timeout: 15_000 });
const size = await page.evaluate(() => {
const cache = window.rehState.synthCache;
for (let index = 0; index < 180; index += 1) cache.set(index, new Blob(['audio-' + index]));
cache.get(179);
return cache.size;
});
expect(size).toBeLessThanOrEqual(128);
});