Skip to content

Commit 9b06b6d

Browse files
lefarcenaudit
andauthored
perf(bake): bound the deck slide-walk so huge-DOM decks don't run long (#4029)
A deck that renders every slide side-by-side in one giant rail (#deck{width:10000vw}) has thousands of DOM nodes. deckSignal called getBoundingClientRect + getComputedStyle on ALL of them to fingerprint the current slide, which is O(n) layout reads — fast locally but ~3s/scan in CI, dragging guizang's walk (and its clip) out to 21.7s while every other deck stayed <=10s. Cap deckSignal's scan to the first 600 elements (the slide rail/track is a structural node near the top of the DOM, so this still detects slide changes — verified guizang still walks to its later slides), and add an 8s wall-time backstop on the walk so a clip can never run long even if signal reads are slow. BAKE_VERSION -> 3 re-bakes everything. Co-authored-by: audit <a@b.c>
1 parent 0ba68c8 commit 9b06b6d

1 file changed

Lines changed: 27 additions & 9 deletions

File tree

scripts/bake-plugin-previews.mjs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,12 @@ import path from 'node:path';
4242

4343
// Bump when the bake recipe changes (capture geometry, timing, encoder, waits…)
4444
// so every plugin re-bakes even though its page content is byte-identical.
45-
// v2: deck mode (16:9 + slide-walk for fixed-viewport PPT/slideshow pages) +
45+
// v2: deck mode (slide-walk for fixed-viewport PPT/slideshow pages) +
4646
// force-load webfonts before capture (CJK templates were baking tofu).
47-
const BAKE_VERSION = 2;
47+
// v3: bound the slide-walk — cap deckSignal's DOM scan + a wall-time cap — so a
48+
// deck that renders every slide in one giant rail (#deck{width:10000vw})
49+
// no longer drags the walk, and the clip, out to 20s+ in CI.
50+
const BAKE_VERSION = 3;
4851

4952
// ---- config ---------------------------------------------------------------
5053
const BASE_URL = process.env.BASE_URL || 'http://127.0.0.1:17579';
@@ -59,6 +62,13 @@ const DECK_W = 1760;
5962
const DECK_H = 1344; // 1760/1344 = 1.31, the tile aspect
6063
const SLIDE_MS = 1150; // deck per-slide dwell: ~.9s CSS transition + settle
6164
const MAX_SLIDES = 6; // advance budget; HOLD + slides stays ~<=9s
65+
const MAX_WALK_MS = 8000; // hard wall-time cap on the walk: even when signal
66+
// reads are slow (huge-DOM decks), the clip stays
67+
// bounded (HOLD + walk ~<=10.5s)
68+
const DECK_SCAN_CAP = 600; // deckSignal scans only the first N elements — the
69+
// slide rail/track is a structural element near the
70+
// DOM top, so this stays cheap on decks that render
71+
// every slide in one giant rail
6272
const OUT_W = Number(process.env.PREVIEW_W || 640); // small — tile renders ~393px
6373
const FPS = Number(process.env.PREVIEW_FPS || 30); // 30 is smooth for a gentle pan, half the bytes of 60
6474
const VELOCITY = 0.30; // px/ms — base pan pace (snappy but readable)
@@ -116,9 +126,16 @@ async function discoverIds() {
116126
// offset of a horizontal track, and any active-slide marker. Deliberately does
117127
// NOT read canvas pixels, so a continuously-animating WebGL background doesn't
118128
// look like a slide change. Runs in the page; must stay self-contained.
119-
function deckSignal() {
129+
function deckSignal(cap) {
120130
const parts = [location.hash];
121-
for (const el of document.querySelectorAll('*')) {
131+
// getBoundingClientRect + getComputedStyle force layout/style per element, so
132+
// scanning ALL of them is pathological on a deck that renders every slide in
133+
// one rail (thousands of nodes). The rail/track is structural and near the top
134+
// of the DOM, so the first `cap` elements in document order suffice.
135+
const els = document.querySelectorAll('*');
136+
const n = Math.min(els.length, cap || 600);
137+
for (let i = 0; i < n; i += 1) {
138+
const el = els[i];
122139
const r = el.getBoundingClientRect();
123140
if (r.width > window.innerWidth * 1.5 || r.height > window.innerHeight * 1.5) {
124141
parts.push(getComputedStyle(el).transform);
@@ -155,10 +172,11 @@ async function walkSlides(page, driver) {
155172
let moved = 0;
156173
const t0 = Date.now();
157174
for (let s = 0; s < MAX_SLIDES; s += 1) {
158-
const before = await page.evaluate(deckSignal);
175+
if (Date.now() - t0 > MAX_WALK_MS) break; // backstop so the clip never runs long
176+
const before = await page.evaluate(deckSignal, DECK_SCAN_CAP);
159177
await driveDeck(page, driver);
160178
await sleep(SLIDE_MS);
161-
if ((await page.evaluate(deckSignal)) === before) break; // reached the last slide
179+
if ((await page.evaluate(deckSignal, DECK_SCAN_CAP)) === before) break; // last slide
162180
moved += 1;
163181
}
164182
return Math.max(SLIDE_MS, Date.now() - t0);
@@ -183,14 +201,14 @@ async function bakeOne(browser, id, hash) {
183201
// advances the deck, so decks reload to reset to slide 1 before capturing.
184202
let deckDriver = null;
185203
{
186-
const sig0 = await page.evaluate(deckSignal);
204+
const sig0 = await page.evaluate(deckSignal, DECK_SCAN_CAP);
187205
await driveDeck(page, 'arrow');
188206
await sleep(900);
189-
if ((await page.evaluate(deckSignal)) !== sig0) deckDriver = 'arrow';
207+
if ((await page.evaluate(deckSignal, DECK_SCAN_CAP)) !== sig0) deckDriver = 'arrow';
190208
else {
191209
await driveDeck(page, 'wheel');
192210
await sleep(900);
193-
if ((await page.evaluate(deckSignal)) !== sig0) deckDriver = 'wheel';
211+
if ((await page.evaluate(deckSignal, DECK_SCAN_CAP)) !== sig0) deckDriver = 'wheel';
194212
}
195213
}
196214
const vScrollable = await page.evaluate(

0 commit comments

Comments
 (0)