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] ## [Unreleased]
### Added ### 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 - **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. request coalescing and explicit invalidation after voice mutations.
- **Frontend Namespace Bridge.** A stable `window.TTVC` migration surface now exposes Audiobook, Rehearser, and job - **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. invalidation after library metadata/folder mutations; the SQLite voice index remains authoritative.
- [x] Lazy-load profile images and concept art. - [x] Lazy-load profile images and concept art.
- [ ] Update only affected table rows instead of rebuilding entire tables. - [ ] 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; - [x] Add an LRU cache for decoded Rehearser audio and remove old entries automatically. Persisted line-audio is
image-cache eviction remains. 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 - [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. 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. - [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 ───────────────────────────────────────────────────────────────── // ── 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 = { const rehState = {
lines: [], lines: [],
cast: {}, cast: {},
@ -76,7 +101,7 @@ const rehState = {
playing: false, playing: false,
repeat: false, repeat: false,
savedId: null, savedId: null,
synthCache: new Map(), synthCache: new RehAudioCache(),
staleLines: new Set(), // lines that were cached but had their tone changed staleLines: new Set(), // lines that were cached but had their tone changed
synthCancelled: false, synthCancelled: false,
synthRunning: false, synthRunning: false,
@ -2977,7 +3002,7 @@ function _reindexLineState(d) {
} }
return out; return out;
}; };
rehState.synthCache = shift(rehState.synthCache, true); rehState.synthCache = new RehAudioCache(shift(rehState.synthCache, true));
rehState.staleLines = shift(rehState.staleLines, false); rehState.staleLines = shift(rehState.staleLines, false);
const sel = shift(rehState.bulkSel, false); const sel = shift(rehState.bulkSel, false);
rehState.bulkSel.clear(); sel.forEach(v => rehState.bulkSel.add(v)); 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); 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);
});