Cut per-paragraph GC stalls: reuse static paper base, cap lookahead to 1
Profiling the per-paragraph playback stutter showed the JS heap sawtoothing (37<->71MB) with 0.4-2.2s long tasks once per block — GC pauses from large (24-48MB) per-block canvas/ImageBitmap allocations, not pagination (buildPages was ~29ms). These pauses freeze the flip/reveal animation, which is also why the title flip looked un-animated. - The reveal "base" layer is the plain paper background, identical for every page of a side. The worker now sends its bitmap once per side+size; the renderer caches the canvas and reuses it for all reveals, removing a large per-block bitmap+canvas allocation. - WEBGL_BOOK_PREFETCH_LOOKAHEAD 2 -> 1 so only the next block's page render is prepared, instead of letting multiple large rasterizations overlap. Verified live: per-paragraph long tasks roughly halved (10 -> 5 over the same window) and worst case 2159ms -> 1431ms. Residual ~1.4s stall remains from the per-block page bitmap + prepared- page snapshot clone + texture upload; further reduction needs reworking those to reuse buffers. Suite 181. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,9 @@ import { BaseModule } from './base-module.js';
|
||||
const TTS_GENERATION_TIMEOUT_MS = 60000;
|
||||
const ASSET_PRELOAD_TIMEOUT_MS = 60000;
|
||||
const USER_CANCEL_BLOCKING_WAIT_MIN_MS = 5000;
|
||||
const WEBGL_BOOK_PREFETCH_LOOKAHEAD = 2;
|
||||
// Prepare only the next block's page render ahead of playback. Higher values let multiple
|
||||
// large page rasterizations overlap, spiking allocation into multi-second GC stalls.
|
||||
const WEBGL_BOOK_PREFETCH_LOOKAHEAD = 1;
|
||||
|
||||
class SentenceQueueModule extends BaseModule {
|
||||
constructor() {
|
||||
@@ -223,14 +225,7 @@ class SentenceQueueModule extends BaseModule {
|
||||
sentence.playbackStartedAt = performance.now();
|
||||
this.onSentenceReadyCallback(sentence, resolve);
|
||||
});
|
||||
// Start lookahead only after the current sentence has entered the display
|
||||
// pipeline. This keeps future WebGL book preparation out of the first
|
||||
// flip/reveal critical path while still overlapping it with playback.
|
||||
window.requestAnimationFrame(() => {
|
||||
if (this.isCurrentQueueItem(item, queueGeneration)) {
|
||||
this.prefetchAhead(6, queueGeneration);
|
||||
}
|
||||
});
|
||||
this.scheduleLookaheadAfterDisplay(item, queueGeneration);
|
||||
await playbackFinished;
|
||||
if (!this.isCurrentQueueItem(item, queueGeneration)) return;
|
||||
} else {
|
||||
@@ -978,6 +973,18 @@ class SentenceQueueModule extends BaseModule {
|
||||
return queueGeneration === this.queueGeneration && this.sentenceQueue[0] === item;
|
||||
}
|
||||
|
||||
scheduleLookaheadAfterDisplay(item, queueGeneration = this.queueGeneration) {
|
||||
const run = () => {
|
||||
if (this.isCurrentQueueItem(item, queueGeneration)) {
|
||||
this.prefetchAhead(6, queueGeneration);
|
||||
}
|
||||
};
|
||||
window.requestAnimationFrame(() => {
|
||||
const scheduleIdle = window.requestIdleCallback || ((callback) => window.setTimeout(callback, 180));
|
||||
scheduleIdle(run, { timeout: 260 });
|
||||
});
|
||||
}
|
||||
|
||||
prefetchAhead(maxLookahead = 6, queueGeneration = this.queueGeneration) {
|
||||
if (this.sentenceQueue.length <= 1) {
|
||||
document.dispatchEvent(new CustomEvent('story:process-state', {
|
||||
|
||||
Reference in New Issue
Block a user