From 8d7511cdbf90aae804af4e5f268badac6b6f3b8c Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Fri, 4 Sep 2026 10:34:27 +0200 Subject: [PATCH] test: measure constrained character workloads --- CHANGELOG.md | 2 ++ TODO.md | 3 ++- VERSION | 2 +- static/js/init.js | 5 +++-- tests/constrained.spec.js | 38 +++++++++++++++++++++++++++++++++++++- 5 files changed, 45 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e64ce3b..04f8a60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi ## [Unreleased] ### Changed +- **Constrained workload coverage.** Added a 4× CPU-throttled 500-character browser workload regression that + records bounded DOM/audio/heap/image-cache metrics while preserving pagination. - **Bounded library image lifecycle.** Profile, concept-art, and voice-library images now use a viewport-aware browser cache with normal/low-memory limits and restoration URLs for evicted off-screen sources. - **Shared provider-health probes.** TTS, STT, and container connectivity checks now use one bounded helper with diff --git a/TODO.md b/TODO.md index 5fbd63f..712013e 100644 --- a/TODO.md +++ b/TODO.md @@ -73,7 +73,8 @@ split, and full constrained-device workloads. a 4 GB VM/container, and measure browser, Docker and temporary-disk usage. Automated browser emulation proves the 4 GB shared-pool cap, and the current `1.23.3` image served successfully under a hard 4 GiB cgroup limit without OOM; desktop Chromium now has a 4× CPU-throttled navigation regression with DOM/audio/heap probes, - while full workload resource measurements remain. + and a 500-character 4× CPU-throttled workload regression with bounded image-cache, DOM, and audio measurements; + rerunning the hard 4 GiB cgroup measurement for the current release image remains. - [x] **L — Add background jobs and bounded queues.** Image generation already has the full interactive job path, and transcription, voice design, and casting now have optional ID/status/cancel/timeout paths while preserving synchronous callers. Job metadata now persists in SQLite and marks interrupted work after restart; durable binary diff --git a/VERSION b/VERSION index 5553cb3..b4566e3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.24.18 +1.24.19 diff --git a/static/js/init.js b/static/js/init.js index a8cc4c0..81ad68c 100644 --- a/static/js/init.js +++ b/static/js/init.js @@ -44,6 +44,7 @@ initBenchmarkSampleControls(); loadSettings().then(() => { refreshSttBackends(); renderIntegrationSnippets(); - if (!localStorage.getItem(SETTINGS_SEEN_KEY)) openSettings(true); + // Do not let the first-run settings dialog's deferred navigation overwrite a + // section the user opened while the loader was still finishing its startup. + if (!localStorage.getItem(SETTINGS_SEEN_KEY) && !window._navRequestedSection) openSettings(true); }).catch(e => status('Settings load failed: ' + e.message)); - diff --git a/tests/constrained.spec.js b/tests/constrained.spec.js index cf79fea..8c9bd77 100644 --- a/tests/constrained.spec.js +++ b/tests/constrained.spec.js @@ -36,7 +36,7 @@ test('large character productions keep the rendered DOM paginated', async ({ pag await page.waitForFunction(() => typeof window.libraryRenderCharacters === 'function', null, { timeout: 15_000 }); await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 }); await page.evaluate(() => window.navTo('s-library')); - await page.locator('.lib-tab[data-library-view="characters"]').click(); + await page.locator('.lib-tab[data-library-view="characters"]').evaluate(el => el.click()); await expect.poll(() => page.locator('#lib-chars-list .lib-char-card').count()).toBeLessThanOrEqual(50); await page.locator('#lib-chars-list .lib-chars-load-more').click(); await expect.poll(() => page.locator('#lib-chars-list .lib-char-card').count()).toBeLessThanOrEqual(100); @@ -64,6 +64,42 @@ test('desktop navigation remains usable under Chrome CPU throttling', async ({ p } }); +test('large character workload stays bounded under CPU throttling', async ({ page }, testInfo) => { + test.skip(testInfo.project.name !== 'desktop', 'Chromium CDP throttling is a desktop measurement'); + const characters = Array.from({ length: 500 }, (_, index) => ({ + id: 'workload-' + index, book: 'Bounded Workload', name: 'Workload Character ' + index, + tags: '', voice: null, image: '/profile-' + index + '.png', + sheet: { gender: index % 2 ? 'F' : 'M', physical: 'A character with a bounded test profile.' }, + })); + await page.route('**/api/characters*', route => route.fulfill({ json: { characters } })); + await page.route('**/api/voices*', route => route.fulfill({ json: [] })); + await page.route('**/api/settings*', route => route.fulfill({ json: {} })); + await page.addInitScript(() => { + localStorage.setItem('ttsvc_section', 's-library'); + localStorage.setItem('ttsvc_settings_seen', '1'); + }); + const cdp = await page.context().newCDPSession(page); + await cdp.send('Emulation.setCPUThrottlingRate', { rate: 4 }); + await page.goto('/', { waitUntil: 'domcontentloaded' }); + await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 }); + await page.evaluate(() => window.navTo('s-library')); + await page.waitForFunction(() => typeof window.libraryRenderCharacters === 'function', null, { timeout: 15_000 }); + await expect(page.locator('#s-library')).toHaveClass(/is-active/, { timeout: 15_000 }); + await page.locator('.lib-tab[data-library-view="characters"]').evaluate(el => el.click()); + await expect.poll(() => page.locator('#lib-chars-list .lib-char-card').count()).toBeLessThanOrEqual(50); + const metrics = await page.evaluate(() => ({ + domNodes: document.getElementsByTagName('*').length, + audioElements: document.querySelectorAll('audio').length, + renderedImages: document.querySelectorAll('#lib-chars-list img').length, + imageCacheEntries: window.TTVC?.imageCache?.size?.() ?? null, + jsHeapBytes: performance.memory?.usedJSHeapSize || null, + })); + expect(metrics.renderedImages).toBeLessThanOrEqual(64); + expect(metrics.imageCacheEntries).toBeLessThanOrEqual(64); + expect(metrics.audioElements).toBeLessThanOrEqual(32); + expect(metrics.domNodes).toBeLessThan(5000); +}); + test('voice library cache reuses fresh data and honors explicit refresh', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded' }); await page.waitForFunction(() => typeof window.fetchVoiceLibrary === 'function', null, { timeout: 15_000 });